diff --git a/apiCount.include.md b/apiCount.include.md index 200d7720..bf6bbc26 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,28 +1,28 @@ -**API count: 1016** +**API count: 1020** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 963 | -| `net462` | 963 | -| `net47` | 962 | -| `net471` | 961 | -| `net472` | 957 | -| `net48` | 957 | -| `net481` | 957 | -| `netstandard2.0` | 959 | -| `netstandard2.1` | 811 | -| `netcoreapp2.0` | 880 | -| `netcoreapp2.1` | 822 | -| `netcoreapp2.2` | 822 | -| `netcoreapp3.0` | 769 | -| `netcoreapp3.1` | 768 | -| `net5.0` | 640 | -| `net6.0` | 545 | -| `net7.0` | 392 | -| `net8.0` | 273 | -| `net9.0` | 179 | -| `net10.0` | 126 | -| `net11.0` | 58 | -| `uap10.0` | 949 | +| `net461` | 967 | +| `net462` | 967 | +| `net47` | 966 | +| `net471` | 965 | +| `net472` | 961 | +| `net48` | 961 | +| `net481` | 961 | +| `netstandard2.0` | 963 | +| `netstandard2.1` | 815 | +| `netcoreapp2.0` | 884 | +| `netcoreapp2.1` | 826 | +| `netcoreapp2.2` | 826 | +| `netcoreapp3.0` | 773 | +| `netcoreapp3.1` | 772 | +| `net5.0` | 644 | +| `net6.0` | 549 | +| `net7.0` | 396 | +| `net8.0` | 277 | +| `net9.0` | 183 | +| `net10.0` | 130 | +| `net11.0` | 62 | +| `uap10.0` | 953 | diff --git a/api_list.include.md b/api_list.include.md index a1d1ab73..ec671003 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -1505,3 +1505,11 @@ #### UnreachableException +#### StringStream + +#### ReadOnlyMemoryStream + +#### WritableMemoryStream + +#### ReadOnlySequenceStream + diff --git a/assemblySize.include.md b/assemblySize.include.md index 8bab779b..27bcf822 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -2,51 +2,51 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 342.5KB | +334.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netstandard2.1 | 8.5KB | 297.0KB | +288.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net461 | 8.5KB | 341.0KB | +332.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 345.0KB | +338.0KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | -| net47 | 7.0KB | 344.5KB | +337.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net471 | 8.5KB | 343.5KB | +335.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 342.5KB | +334.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| net48 | 8.5KB | 342.5KB | +334.0KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | -| net481 | 8.5KB | 342.5KB | +334.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.0 | 9.0KB | 318.5KB | +309.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 299.5KB | +290.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.2 | 9.0KB | 299.5KB | +290.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.0 | 9.5KB | 292.0KB | +282.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 290.0KB | +280.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| net5.0 | 9.5KB | 254.0KB | +244.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net6.0 | 10.0KB | 195.0KB | +185.0KB | +10.0KB | +7.0KB | +1.0KB | +3.5KB | -| net7.0 | 10.0KB | 157.5KB | +147.5KB | +9.5KB | +5.5KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 129.0KB | +119.5KB | +8.5KB | | +512bytes | +3.5KB | -| net9.0 | 9.5KB | 82.5KB | +73.0KB | +8.5KB | | +512bytes | +3.5KB | -| net10.0 | 10.0KB | 60.0KB | +50.0KB | +9.0KB | | +512bytes | +3.5KB | -| net11.0 | 10.0KB | 20.5KB | +10.5KB | +9.0KB | | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 347.5KB | +339.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netstandard2.1 | 8.5KB | 302.0KB | +293.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net461 | 8.5KB | 346.5KB | +338.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net462 | 7.0KB | 350.0KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net47 | 7.0KB | 349.5KB | +342.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net471 | 8.5KB | 349.0KB | +340.5KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB | +| net472 | 8.5KB | 347.5KB | +339.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 347.5KB | +339.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net481 | 8.5KB | 347.5KB | +339.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 323.5KB | +314.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 304.5KB | +295.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 304.5KB | +295.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 297.0KB | +287.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp3.1 | 9.5KB | 295.5KB | +286.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net5.0 | 9.5KB | 259.0KB | +249.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net6.0 | 10.0KB | 201.0KB | +191.0KB | +10.0KB | +6.5KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 163.5KB | +153.5KB | +9.0KB | +5.5KB | +512bytes | +3.0KB | +| net8.0 | 9.5KB | 135.0KB | +125.5KB | +8.0KB | | +512bytes | +3.0KB | +| net9.0 | 9.5KB | 88.5KB | +79.0KB | +8.5KB | | +512bytes | +3.5KB | +| net10.0 | 10.0KB | 66.0KB | +56.0KB | +9.0KB | | +512bytes | +3.5KB | +| net11.0 | 10.0KB | 27.0KB | +17.0KB | +9.0KB | | +1.0KB | +3.5KB | ### Assembly Sizes with EmbedUntrackedSources | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 498.3KB | +490.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netstandard2.1 | 8.5KB | 427.1KB | +418.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net461 | 8.5KB | 497.8KB | +489.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 501.8KB | +494.8KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | -| net47 | 7.0KB | 501.1KB | +494.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net471 | 8.5KB | 499.7KB | +491.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 497.7KB | +489.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| net48 | 8.5KB | 497.7KB | +489.2KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | -| net481 | 8.5KB | 497.7KB | +489.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.0 | 9.0KB | 463.7KB | +454.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 433.3KB | +424.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.2 | 9.0KB | 433.3KB | +424.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.0 | 9.5KB | 417.0KB | +407.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 415.0KB | +405.5KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net5.0 | 9.5KB | 360.8KB | +351.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net6.0 | 10.0KB | 281.8KB | +271.8KB | +17.7KB | +8.7KB | +1.6KB | +4.2KB | -| net7.0 | 10.0KB | 225.8KB | +215.8KB | +17.1KB | +6.9KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 182.2KB | +172.7KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | -| net9.0 | 9.5KB | 114.7KB | +105.2KB | +16.0KB | | +1.1KB | +4.2KB | -| net10.0 | 10.0KB | 83.9KB | +73.9KB | +16.5KB | | +1.1KB | +4.2KB | -| net11.0 | 10.0KB | 30.3KB | +20.3KB | +16.5KB | | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 507.8KB | +499.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netstandard2.1 | 8.5KB | 436.6KB | +428.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net461 | 8.5KB | 507.8KB | +499.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net462 | 7.0KB | 511.3KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net47 | 7.0KB | 510.6KB | +503.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net471 | 8.5KB | 509.7KB | +501.2KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB | +| net472 | 8.5KB | 507.2KB | +498.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 507.2KB | +498.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net481 | 8.5KB | 507.2KB | +498.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 473.2KB | +464.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 442.8KB | +433.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 442.8KB | +433.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 426.5KB | +417.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp3.1 | 9.5KB | 425.0KB | +415.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net5.0 | 9.5KB | 370.3KB | +360.8KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net6.0 | 10.0KB | 292.3KB | +282.3KB | +17.7KB | +8.2KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 236.2KB | +226.2KB | +16.6KB | +6.9KB | +1.1KB | +3.7KB | +| net8.0 | 9.5KB | 192.7KB | +183.2KB | +15.5KB | +299bytes | +1.1KB | +3.7KB | +| net9.0 | 9.5KB | 125.2KB | +115.7KB | +16.0KB | | +1.1KB | +4.2KB | +| net10.0 | 10.0KB | 94.4KB | +84.4KB | +16.5KB | | +1.1KB | +4.2KB | +| net11.0 | 10.0KB | 41.3KB | +31.3KB | +16.5KB | | +1.6KB | +4.2KB | diff --git a/readme.md b/readme.md index 63ae2db0..f3a2578c 100644 --- a/readme.md +++ b/readme.md @@ -13,34 +13,34 @@ The package targets `netstandard2.0` and is designed to support the following ru * `uap10` -**API count: 1016** +**API count: 1020** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 963 | -| `net462` | 963 | -| `net47` | 962 | -| `net471` | 961 | -| `net472` | 957 | -| `net48` | 957 | -| `net481` | 957 | -| `netstandard2.0` | 959 | -| `netstandard2.1` | 811 | -| `netcoreapp2.0` | 880 | -| `netcoreapp2.1` | 822 | -| `netcoreapp2.2` | 822 | -| `netcoreapp3.0` | 769 | -| `netcoreapp3.1` | 768 | -| `net5.0` | 640 | -| `net6.0` | 545 | -| `net7.0` | 392 | -| `net8.0` | 273 | -| `net9.0` | 179 | -| `net10.0` | 126 | -| `net11.0` | 58 | -| `uap10.0` | 949 | +| `net461` | 967 | +| `net462` | 967 | +| `net47` | 966 | +| `net471` | 965 | +| `net472` | 961 | +| `net48` | 961 | +| `net481` | 961 | +| `netstandard2.0` | 963 | +| `netstandard2.1` | 815 | +| `netcoreapp2.0` | 884 | +| `netcoreapp2.1` | 826 | +| `netcoreapp2.2` | 826 | +| `netcoreapp3.0` | 773 | +| `netcoreapp3.1` | 772 | +| `net5.0` | 644 | +| `net6.0` | 549 | +| `net7.0` | 396 | +| `net8.0` | 277 | +| `net9.0` | 183 | +| `net10.0` | 130 | +| `net11.0` | 62 | +| `uap10.0` | 953 | @@ -96,54 +96,54 @@ This project uses features from the newest stable SDK and C# language. As such c | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 342.5KB | +334.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netstandard2.1 | 8.5KB | 297.0KB | +288.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net461 | 8.5KB | 341.0KB | +332.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 345.0KB | +338.0KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | -| net47 | 7.0KB | 344.5KB | +337.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net471 | 8.5KB | 343.5KB | +335.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 342.5KB | +334.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| net48 | 8.5KB | 342.5KB | +334.0KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | -| net481 | 8.5KB | 342.5KB | +334.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.0 | 9.0KB | 318.5KB | +309.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 299.5KB | +290.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.2 | 9.0KB | 299.5KB | +290.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.0 | 9.5KB | 292.0KB | +282.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 290.0KB | +280.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| net5.0 | 9.5KB | 254.0KB | +244.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net6.0 | 10.0KB | 195.0KB | +185.0KB | +10.0KB | +7.0KB | +1.0KB | +3.5KB | -| net7.0 | 10.0KB | 157.5KB | +147.5KB | +9.5KB | +5.5KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 129.0KB | +119.5KB | +8.5KB | | +512bytes | +3.5KB | -| net9.0 | 9.5KB | 82.5KB | +73.0KB | +8.5KB | | +512bytes | +3.5KB | -| net10.0 | 10.0KB | 60.0KB | +50.0KB | +9.0KB | | +512bytes | +3.5KB | -| net11.0 | 10.0KB | 20.5KB | +10.5KB | +9.0KB | | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 347.5KB | +339.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netstandard2.1 | 8.5KB | 302.0KB | +293.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net461 | 8.5KB | 346.5KB | +338.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net462 | 7.0KB | 350.0KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net47 | 7.0KB | 349.5KB | +342.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net471 | 8.5KB | 349.0KB | +340.5KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB | +| net472 | 8.5KB | 347.5KB | +339.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 347.5KB | +339.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net481 | 8.5KB | 347.5KB | +339.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 323.5KB | +314.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 304.5KB | +295.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 304.5KB | +295.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 297.0KB | +287.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp3.1 | 9.5KB | 295.5KB | +286.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net5.0 | 9.5KB | 259.0KB | +249.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net6.0 | 10.0KB | 201.0KB | +191.0KB | +10.0KB | +6.5KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 163.5KB | +153.5KB | +9.0KB | +5.5KB | +512bytes | +3.0KB | +| net8.0 | 9.5KB | 135.0KB | +125.5KB | +8.0KB | | +512bytes | +3.0KB | +| net9.0 | 9.5KB | 88.5KB | +79.0KB | +8.5KB | | +512bytes | +3.5KB | +| net10.0 | 10.0KB | 66.0KB | +56.0KB | +9.0KB | | +512bytes | +3.5KB | +| net11.0 | 10.0KB | 27.0KB | +17.0KB | +9.0KB | | +1.0KB | +3.5KB | ### Assembly Sizes with EmbedUntrackedSources | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 498.3KB | +490.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netstandard2.1 | 8.5KB | 427.1KB | +418.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net461 | 8.5KB | 497.8KB | +489.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 501.8KB | +494.8KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | -| net47 | 7.0KB | 501.1KB | +494.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net471 | 8.5KB | 499.7KB | +491.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 497.7KB | +489.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| net48 | 8.5KB | 497.7KB | +489.2KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | -| net481 | 8.5KB | 497.7KB | +489.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.0 | 9.0KB | 463.7KB | +454.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 433.3KB | +424.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.2 | 9.0KB | 433.3KB | +424.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.0 | 9.5KB | 417.0KB | +407.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 415.0KB | +405.5KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net5.0 | 9.5KB | 360.8KB | +351.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net6.0 | 10.0KB | 281.8KB | +271.8KB | +17.7KB | +8.7KB | +1.6KB | +4.2KB | -| net7.0 | 10.0KB | 225.8KB | +215.8KB | +17.1KB | +6.9KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 182.2KB | +172.7KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | -| net9.0 | 9.5KB | 114.7KB | +105.2KB | +16.0KB | | +1.1KB | +4.2KB | -| net10.0 | 10.0KB | 83.9KB | +73.9KB | +16.5KB | | +1.1KB | +4.2KB | -| net11.0 | 10.0KB | 30.3KB | +20.3KB | +16.5KB | | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 507.8KB | +499.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netstandard2.1 | 8.5KB | 436.6KB | +428.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net461 | 8.5KB | 507.8KB | +499.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net462 | 7.0KB | 511.3KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net47 | 7.0KB | 510.6KB | +503.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net471 | 8.5KB | 509.7KB | +501.2KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB | +| net472 | 8.5KB | 507.2KB | +498.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 507.2KB | +498.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net481 | 8.5KB | 507.2KB | +498.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 473.2KB | +464.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 442.8KB | +433.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 442.8KB | +433.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 426.5KB | +417.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp3.1 | 9.5KB | 425.0KB | +415.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net5.0 | 9.5KB | 370.3KB | +360.8KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net6.0 | 10.0KB | 292.3KB | +282.3KB | +17.7KB | +8.2KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 236.2KB | +226.2KB | +16.6KB | +6.9KB | +1.1KB | +3.7KB | +| net8.0 | 9.5KB | 192.7KB | +183.2KB | +15.5KB | +299bytes | +1.1KB | +3.7KB | +| net9.0 | 9.5KB | 125.2KB | +115.7KB | +16.0KB | | +1.1KB | +4.2KB | +| net10.0 | 10.0KB | 94.4KB | +84.4KB | +16.5KB | | +1.1KB | +4.2KB | +| net11.0 | 10.0KB | 41.3KB | +31.3KB | +16.5KB | | +1.6KB | +4.2KB | @@ -2035,6 +2035,14 @@ The class `Polyfill` includes the following extension methods: #### TaskCompletionSource #### UnreachableException + +#### StringStream + +#### ReadOnlyMemoryStream + +#### WritableMemoryStream + +#### ReadOnlySequenceStream @@ -2233,7 +2241,7 @@ void ObjectDisposedExceptionExample(bool isDisposed) ObjectDisposedException.ThrowIf(isDisposed, typeof(Consume)); } ``` -snippet source | anchor +snippet source | anchor @@ -2252,7 +2260,7 @@ void EnsureExample(Order order, Customer customer, string customerId, string ema this.quantity = Ensure.NotNegativeOrZero(quantity); } ``` -snippet source | anchor +snippet source | anchor diff --git a/src/ApiBuilderTests/BuildApiTest.cs b/src/ApiBuilderTests/BuildApiTest.cs index 047b78a3..77d97df2 100644 --- a/src/ApiBuilderTests/BuildApiTest.cs +++ b/src/ApiBuilderTests/BuildApiTest.cs @@ -23,6 +23,10 @@ public Task RunWithRoslyn() WriteHelper("Utf16", writer, ref count); WriteType(nameof(TaskCompletionSource), writer, ref count); WriteType(nameof(UnreachableException), writer, ref count); + WriteType("StringStream", writer, ref count); + WriteType("ReadOnlyMemoryStream", writer, ref count); + WriteType("WritableMemoryStream", writer, ref count); + WriteType("ReadOnlySequenceStream", writer, ref count); count += Directory.EnumerateFiles(polyfillDir, "*Attribute.cs", SearchOption.AllDirectories).Count(); // Index and Range diff --git a/src/ApiBuilderTests/StreamWrapperBclDetectionTests.cs b/src/ApiBuilderTests/StreamWrapperBclDetectionTests.cs new file mode 100644 index 00000000..fe4bda33 --- /dev/null +++ b/src/ApiBuilderTests/StreamWrapperBclDetectionTests.cs @@ -0,0 +1,84 @@ +using System.Text; + +/// +/// The Stream wrapper types from dotnet/runtime#126669 (StringStream, ReadOnlyMemoryStream, +/// WritableMemoryStream, ReadOnlySequenceStream) were approved/merged for net11 but are NOT in the +/// current net11 preview/RC. Polyfill therefore ships them as ACTIVE on net11 too. +/// +/// This project (ApiBuilderTests) targets net11.0 and does NOT compile the polyfill source into +/// itself, so it keeps building once the BCL gains these types. This test runs on the net11 runtime +/// and fails the moment the real types appear, telling you exactly which guards to flip. +/// +public class StreamWrapperBclDetectionTests +{ + static readonly (string FullName, string File)[] pending = + [ + ("System.IO.StringStream", "StringStream.cs"), + ("System.IO.ReadOnlyMemoryStream", "ReadOnlyMemoryStream.cs"), + ("System.IO.WritableMemoryStream", "WritableMemoryStream.cs"), + ("System.Buffers.ReadOnlySequenceStream", "ReadOnlySequenceStream.cs"), + ]; + + [Test] + public void StreamWrapperPolyfillsStillNeededOnNet11() + { + var present = new List<(string FullName, string File, string Assembly)>(); + foreach (var (fullName, file) in pending) + { + var type = FindBclType(fullName); + if (type != null) + { + present.Add((fullName, file, type.Assembly.GetName().Name!)); + } + } + + if (present.Count == 0) + { + return; + } + + var builder = new StringBuilder(); + builder.AppendLine($"{present.Count} Stream wrapper type(s) from dotnet/runtime#126669 are now provided by the BCL"); + builder.AppendLine($"on this runtime ({System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription})."); + builder.AppendLine("The matching Polyfill source now collides with the real type and must be retired for net11."); + builder.AppendLine("For EACH type below, edit the listed file under src/Polyfill/:"); + builder.AppendLine(); + + foreach (var (fullName, file, assembly) in present) + { + builder.AppendLine($" {fullName} (now in {assembly})"); + builder.AppendLine($" file: src/Polyfill/{file}"); + builder.AppendLine(" 1. change `#if FeatureMemory` to `#if FeatureMemory && !NET11_0_OR_GREATER`"); + builder.AppendLine(" 2. append after the final #endif:"); + builder.AppendLine(" #if NET11_0_OR_GREATER"); + builder.AppendLine($" [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof({fullName}))]"); + builder.AppendLine(" #endif"); + builder.AppendLine(); + } + + builder.AppendLine("Then re-run ApiBuilderTests in Debug to regenerate Split + api_list, and drop any now-redundant"); + builder.AppendLine("usage from src/Consume/Consume.cs."); + + throw new(builder.ToString()); + } + + static System.Type? FindBclType(string fullName) + { + foreach (var assembly in new[] + { + "System.Private.CoreLib", + "System.Runtime", + "System.Memory", + "mscorlib" + }) + { + var type = System.Type.GetType($"{fullName}, {assembly}", throwOnError: false); + if (type is { IsPublic: true }) + { + return type; + } + } + + return null; + } +} diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index 443df782..cc72a8e8 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -258,6 +258,39 @@ void Base64UrlUsage() } #endif +#if FeatureMemory + void StreamWrappersUsage() + { + var bytes = new byte[10]; + + using (Stream stringStream = new StringStream("text", Encoding.UTF8)) + { + stringStream.Read(bytes, 0, bytes.Length); + } + + using (var stringStream = new StringStream("text".AsMemory(), Encoding.UTF8)) + { + Encoding encoding = stringStream.Encoding; + stringStream.ReadByte(); + } + + using (MemoryStream readOnly = new ReadOnlyMemoryStream(new ReadOnlyMemory(bytes))) + { + readOnly.Read(bytes, 0, bytes.Length); + } + + using (MemoryStream writable = new WritableMemoryStream(new Memory(bytes))) + { + writable.Write(bytes, 0, bytes.Length); + } + + using (Stream sequenceStream = new System.Buffers.ReadOnlySequenceStream(new System.Buffers.ReadOnlySequence(bytes))) + { + sequenceStream.Read(bytes, 0, bytes.Length); + } + } +#endif + #if FeatureMemory && !RefsBclMemory void Base64Usage() { diff --git a/src/Polyfill/ReadOnlyMemoryStream.cs b/src/Polyfill/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..13bb791b --- /dev/null +++ b/src/Polyfill/ReadOnlyMemoryStream.cs @@ -0,0 +1,74 @@ +#nullable enable + +// === Polyfill target window ===================================================================== +// System.IO.ReadOnlyMemoryStream was approved and merged into dotnet/runtime for net11 +// (https://github.com/dotnet/runtime/pull/126669) but is NOT in the current net11 preview/RC SDK, +// so this polyfill is intentionally ACTIVE on net11 as well. +// When you upgrade to a net11 SDK that actually ships this type, the build will collide (CS0436) +// and the ApiBuilderTests "StreamWrapperBclDetectionTests" test FAILS with these exact steps. +// To retire this polyfill for net11: +// 1. change `#if FeatureMemory` below to `#if FeatureMemory && !NET11_0_OR_GREATER` +// 2. add at the bottom of this file (after the final #endif): +// #if NET11_0_OR_GREATER +// [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.IO.ReadOnlyMemoryStream))] +// #endif +// 3. re-run ApiBuilderTests in Debug to regenerate Split + api_list. +// ================================================================================================ +#if FeatureMemory + +#pragma warning disable + +namespace System.IO; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.readonlymemorystream?view=net-11.0 +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.readonlymemorystream.-ctor?view=net-11.0 + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} + +#endif diff --git a/src/Polyfill/ReadOnlySequenceStream.cs b/src/Polyfill/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..be82092c --- /dev/null +++ b/src/Polyfill/ReadOnlySequenceStream.cs @@ -0,0 +1,236 @@ +#nullable enable + +// === Polyfill target window ===================================================================== +// System.Buffers.ReadOnlySequenceStream was approved and merged into dotnet/runtime for net11 +// (https://github.com/dotnet/runtime/pull/126669) but is NOT in the current net11 preview/RC SDK, +// so this polyfill is intentionally ACTIVE on net11 as well. +// When you upgrade to a net11 SDK that actually ships this type, the build will collide (CS0436) +// and the ApiBuilderTests "StreamWrapperBclDetectionTests" test FAILS with these exact steps. +// To retire this polyfill for net11: +// 1. change `#if FeatureMemory` below to `#if FeatureMemory && !NET11_0_OR_GREATER` +// 2. add at the bottom of this file (after the final #endif): +// #if NET11_0_OR_GREATER +// [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Buffers.ReadOnlySequenceStream))] +// #endif +// 3. re-run ApiBuilderTests in Debug to regenerate Split + api_list. +// ================================================================================================ +#if FeatureMemory + +#pragma warning disable + +namespace System.Buffers; + +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequencestream?view=net-11.0 +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + + /// + /// Initializes a new instance of the class over the specified . + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequencestream.-ctor?view=net-11.0 + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + + /// + public override bool CanRead => !disposed; + + /// + public override bool CanSeek => !disposed; + + /// + public override bool CanWrite => false; + + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + position = value; + } + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + + if (position >= sequence.Length) + { + return 0; + } + + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + + /// + public override int ReadByte() + { + ThrowIfDisposed(); + + if (position >= sequence.Length) + { + return -1; + } + + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + + position = newPosition; + return position; + } + + /// + public override void Flush() + { + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} + +#endif diff --git a/src/Polyfill/StringStream.cs b/src/Polyfill/StringStream.cs new file mode 100644 index 00000000..87cd6090 --- /dev/null +++ b/src/Polyfill/StringStream.cs @@ -0,0 +1,241 @@ +#nullable enable + +// === Polyfill target window ===================================================================== +// System.IO.StringStream was approved and merged into dotnet/runtime for net11 +// (https://github.com/dotnet/runtime/pull/126669) but is NOT in the current net11 preview/RC SDK, +// so this polyfill is intentionally ACTIVE on net11 as well. +// When you upgrade to a net11 SDK that actually ships this type, the build will collide (CS0436) +// and the ApiBuilderTests "StreamWrapperBclDetectionTests" test FAILS with these exact steps. +// To retire this polyfill for net11: +// 1. change `#if FeatureMemory` below to `#if FeatureMemory && !NET11_0_OR_GREATER` +// 2. add at the bottom of this file (after the final #endif): +// #if NET11_0_OR_GREATER +// [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.IO.StringStream))] +// #endif +// 3. re-run ApiBuilderTests in Debug to regenerate Split + api_list. +// ================================================================================================ +#if FeatureMemory + +#pragma warning disable + +namespace System.IO; + +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.stringstream?view=net-11.0 +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.stringstream.-ctor?view=net-11.0#system-io-stringstream-ctor(system-string-system-text-encoding) + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + this.text = text.AsMemory(); + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.stringstream.-ctor?view=net-11.0#system-io-stringstream-ctor(system-readonlymemory((system-char))-system-text-encoding) + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + this.text = text; + this.encoding = encoding; + } + + /// + /// Gets the encoding used by this stream. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.stringstream.encoding?view=net-11.0 + public Encoding Encoding => encoding; + + /// + public override bool CanRead => !disposed; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + + return encoded; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + + /// + public override int ReadByte() + { + ThrowIfDisposed(); + + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + + return -1; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + + /// + public override void Flush() + { + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} + +#endif diff --git a/src/Polyfill/WritableMemoryStream.cs b/src/Polyfill/WritableMemoryStream.cs new file mode 100644 index 00000000..5189f658 --- /dev/null +++ b/src/Polyfill/WritableMemoryStream.cs @@ -0,0 +1,75 @@ +#nullable enable + +// === Polyfill target window ===================================================================== +// System.IO.WritableMemoryStream was approved and merged into dotnet/runtime for net11 +// (https://github.com/dotnet/runtime/pull/126669) but is NOT in the current net11 preview/RC SDK, +// so this polyfill is intentionally ACTIVE on net11 as well. +// When you upgrade to a net11 SDK that actually ships this type, the build will collide (CS0436) +// and the ApiBuilderTests "StreamWrapperBclDetectionTests" test FAILS with these exact steps. +// To retire this polyfill for net11: +// 1. change `#if FeatureMemory` below to `#if FeatureMemory && !NET11_0_OR_GREATER` +// 2. add at the bottom of this file (after the final #endif): +// #if NET11_0_OR_GREATER +// [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.IO.WritableMemoryStream))] +// #endif +// 3. re-run ApiBuilderTests in Debug to regenerate Split + api_list. +// ================================================================================================ +#if FeatureMemory + +#pragma warning disable + +namespace System.IO; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.writablememorystream?view=net-11.0 +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.writablememorystream.-ctor?view=net-11.0 + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + // The base constructor reports the whole region as content; reset to an empty, ready-to-write stream. + base.SetLength(0); + + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} + +#endif diff --git a/src/Split/net10.0/ReadOnlyMemoryStream.cs b/src/Split/net10.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net10.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net10.0/ReadOnlySequenceStream.cs b/src/Split/net10.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net10.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net10.0/StringStream.cs b/src/Split/net10.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net10.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net10.0/WritableMemoryStream.cs b/src/Split/net10.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net10.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net11.0/ReadOnlyMemoryStream.cs b/src/Split/net11.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net11.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net11.0/ReadOnlySequenceStream.cs b/src/Split/net11.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net11.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net11.0/StringStream.cs b/src/Split/net11.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net11.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net11.0/WritableMemoryStream.cs b/src/Split/net11.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net11.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net461/ReadOnlyMemoryStream.cs b/src/Split/net461/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net461/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net461/ReadOnlySequenceStream.cs b/src/Split/net461/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net461/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net461/StringStream.cs b/src/Split/net461/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net461/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net461/WritableMemoryStream.cs b/src/Split/net461/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net461/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net462/ReadOnlyMemoryStream.cs b/src/Split/net462/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net462/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net462/ReadOnlySequenceStream.cs b/src/Split/net462/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net462/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net462/StringStream.cs b/src/Split/net462/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net462/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net462/WritableMemoryStream.cs b/src/Split/net462/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net462/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net47/ReadOnlyMemoryStream.cs b/src/Split/net47/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net47/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net47/ReadOnlySequenceStream.cs b/src/Split/net47/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net47/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net47/StringStream.cs b/src/Split/net47/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net47/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net47/WritableMemoryStream.cs b/src/Split/net47/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net47/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net471/ReadOnlyMemoryStream.cs b/src/Split/net471/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net471/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net471/ReadOnlySequenceStream.cs b/src/Split/net471/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net471/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net471/StringStream.cs b/src/Split/net471/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net471/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net471/WritableMemoryStream.cs b/src/Split/net471/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net471/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net472/ReadOnlyMemoryStream.cs b/src/Split/net472/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net472/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net472/ReadOnlySequenceStream.cs b/src/Split/net472/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net472/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net472/StringStream.cs b/src/Split/net472/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net472/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net472/WritableMemoryStream.cs b/src/Split/net472/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net472/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net48/ReadOnlyMemoryStream.cs b/src/Split/net48/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net48/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net48/ReadOnlySequenceStream.cs b/src/Split/net48/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net48/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net48/StringStream.cs b/src/Split/net48/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net48/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net48/WritableMemoryStream.cs b/src/Split/net48/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net48/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net481/ReadOnlyMemoryStream.cs b/src/Split/net481/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net481/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net481/ReadOnlySequenceStream.cs b/src/Split/net481/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net481/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net481/StringStream.cs b/src/Split/net481/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net481/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net481/WritableMemoryStream.cs b/src/Split/net481/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net481/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net5.0/ReadOnlyMemoryStream.cs b/src/Split/net5.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net5.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net5.0/ReadOnlySequenceStream.cs b/src/Split/net5.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net5.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net5.0/StringStream.cs b/src/Split/net5.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net5.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net5.0/WritableMemoryStream.cs b/src/Split/net5.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net5.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net6.0/ReadOnlyMemoryStream.cs b/src/Split/net6.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net6.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net6.0/ReadOnlySequenceStream.cs b/src/Split/net6.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net6.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net6.0/StringStream.cs b/src/Split/net6.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net6.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net6.0/WritableMemoryStream.cs b/src/Split/net6.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net6.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net7.0/ReadOnlyMemoryStream.cs b/src/Split/net7.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net7.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net7.0/ReadOnlySequenceStream.cs b/src/Split/net7.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net7.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net7.0/StringStream.cs b/src/Split/net7.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net7.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net7.0/WritableMemoryStream.cs b/src/Split/net7.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net7.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net8.0/ReadOnlyMemoryStream.cs b/src/Split/net8.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net8.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net8.0/ReadOnlySequenceStream.cs b/src/Split/net8.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net8.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net8.0/StringStream.cs b/src/Split/net8.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net8.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net8.0/WritableMemoryStream.cs b/src/Split/net8.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net8.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/net9.0/ReadOnlyMemoryStream.cs b/src/Split/net9.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/net9.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/net9.0/ReadOnlySequenceStream.cs b/src/Split/net9.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/net9.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net9.0/StringStream.cs b/src/Split/net9.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/net9.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/net9.0/WritableMemoryStream.cs b/src/Split/net9.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/net9.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/netcoreapp2.0/ReadOnlyMemoryStream.cs b/src/Split/netcoreapp2.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/netcoreapp2.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/netcoreapp2.0/ReadOnlySequenceStream.cs b/src/Split/netcoreapp2.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/netcoreapp2.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp2.0/StringStream.cs b/src/Split/netcoreapp2.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/netcoreapp2.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp2.0/WritableMemoryStream.cs b/src/Split/netcoreapp2.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/netcoreapp2.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/netcoreapp2.1/ReadOnlyMemoryStream.cs b/src/Split/netcoreapp2.1/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/netcoreapp2.1/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/netcoreapp2.1/ReadOnlySequenceStream.cs b/src/Split/netcoreapp2.1/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/netcoreapp2.1/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp2.1/StringStream.cs b/src/Split/netcoreapp2.1/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/netcoreapp2.1/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp2.1/WritableMemoryStream.cs b/src/Split/netcoreapp2.1/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/netcoreapp2.1/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/netcoreapp2.2/ReadOnlyMemoryStream.cs b/src/Split/netcoreapp2.2/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/netcoreapp2.2/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/netcoreapp2.2/ReadOnlySequenceStream.cs b/src/Split/netcoreapp2.2/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/netcoreapp2.2/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp2.2/StringStream.cs b/src/Split/netcoreapp2.2/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/netcoreapp2.2/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp2.2/WritableMemoryStream.cs b/src/Split/netcoreapp2.2/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/netcoreapp2.2/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/netcoreapp3.0/ReadOnlyMemoryStream.cs b/src/Split/netcoreapp3.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/netcoreapp3.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/netcoreapp3.0/ReadOnlySequenceStream.cs b/src/Split/netcoreapp3.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/netcoreapp3.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp3.0/StringStream.cs b/src/Split/netcoreapp3.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/netcoreapp3.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp3.0/WritableMemoryStream.cs b/src/Split/netcoreapp3.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/netcoreapp3.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/netcoreapp3.1/ReadOnlyMemoryStream.cs b/src/Split/netcoreapp3.1/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/netcoreapp3.1/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/netcoreapp3.1/ReadOnlySequenceStream.cs b/src/Split/netcoreapp3.1/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/netcoreapp3.1/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp3.1/StringStream.cs b/src/Split/netcoreapp3.1/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/netcoreapp3.1/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netcoreapp3.1/WritableMemoryStream.cs b/src/Split/netcoreapp3.1/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/netcoreapp3.1/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/netstandard2.0/ReadOnlyMemoryStream.cs b/src/Split/netstandard2.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/netstandard2.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/netstandard2.0/ReadOnlySequenceStream.cs b/src/Split/netstandard2.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/netstandard2.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netstandard2.0/StringStream.cs b/src/Split/netstandard2.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/netstandard2.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netstandard2.0/WritableMemoryStream.cs b/src/Split/netstandard2.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/netstandard2.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/netstandard2.1/ReadOnlyMemoryStream.cs b/src/Split/netstandard2.1/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/netstandard2.1/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/netstandard2.1/ReadOnlySequenceStream.cs b/src/Split/netstandard2.1/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/netstandard2.1/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netstandard2.1/StringStream.cs b/src/Split/netstandard2.1/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/netstandard2.1/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/netstandard2.1/WritableMemoryStream.cs b/src/Split/netstandard2.1/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/netstandard2.1/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Split/uap10.0/ReadOnlyMemoryStream.cs b/src/Split/uap10.0/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..7a03bfdb --- /dev/null +++ b/src/Split/uap10.0/ReadOnlyMemoryStream.cs @@ -0,0 +1,52 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The stream cannot be written to. always returns . +/// throws and returns . +/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it; +/// the BCL always wraps the memory without copying. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlyMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlyMemoryStream(ReadOnlyMemory source) + : base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false) + { + } + static byte[] GetArray(ReadOnlyMemory source, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray(source, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + var array = source.ToArray(); + offset = 0; + count = array.Length; + return array; + } +} +#endif diff --git a/src/Split/uap10.0/ReadOnlySequenceStream.cs b/src/Split/uap10.0/ReadOnlySequenceStream.cs new file mode 100644 index 00000000..1b9bd979 --- /dev/null +++ b/src/Split/uap10.0/ReadOnlySequenceStream.cs @@ -0,0 +1,185 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.Buffers; +using System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a seekable, read-only over a . +/// +/// +/// The underlying sequence is not copied; reads are served directly from its segments. +/// The stream cannot be written to. always returns . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ReadOnlySequenceStream : + Stream +{ + ReadOnlySequence sequence; + long position; + bool disposed; + /// + /// Initializes a new instance of the class over the specified . + /// + public ReadOnlySequenceStream(ReadOnlySequence source) => + sequence = source; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => !disposed; + /// + public override bool CanWrite => false; + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return sequence.Length; + } + } + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return position; + } + set + { + ThrowIfDisposed(); + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + position = value; + } + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return 0; + } + var remaining = sequence.Slice(position); + var toRead = (int)Math.Min(remaining.Length, count); + if (toRead <= 0) + { + return 0; + } + remaining.Slice(0, toRead).CopyTo(buffer.AsSpan(offset, toRead)); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + if (position >= sequence.Length) + { + return -1; + } + var result = sequence.Slice(position, 1).First.Span[0]; + position++; + return result; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + var basePosition = origin switch + { + SeekOrigin.Begin => 0L, + SeekOrigin.Current => position, + SeekOrigin.End => sequence.Length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(origin)) + }; + var newPosition = basePosition + offset; + if (newPosition < 0) + { + throw new IOException("An attempt was made to move the position before the beginning of the stream."); + } + position = newPosition; + return position; + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + sequence = default; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/uap10.0/StringStream.cs b/src/Split/uap10.0/StringStream.cs new file mode 100644 index 00000000..b4cd3943 --- /dev/null +++ b/src/Split/uap10.0/StringStream.cs @@ -0,0 +1,186 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// The polyfill encodes the whole text into a single buffer on first read rather than encoding +/// on-the-fly; this is a non-observable performance difference from the BCL implementation. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class StringStream : + Stream +{ + ReadOnlyMemory text; + Encoding encoding; + byte[]? encoded; + int position; + bool disposed; + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + public StringStream(string text, Encoding encoding) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text.AsMemory(); + this.encoding = encoding; + } + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + this.text = text; + this.encoding = encoding; + } + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => encoding; + /// + public override bool CanRead => !disposed; + /// + public override bool CanSeek => false; + /// + public override bool CanWrite => false; + /// + public override long Length => throw new NotSupportedException("Stream does not support seeking."); + /// + public override long Position + { + get => throw new NotSupportedException("Stream does not support seeking."); + set => throw new NotSupportedException("Stream does not support seeking."); + } + byte[] GetEncoded() + { + if (encoded == null) + { + var chars = text.ToArray(); + encoded = encoding.GetBytes(chars, 0, chars.Length); + } + return encoded; + } + /// + public override int Read(byte[] buffer, int offset, int count) + { + GuardRange(buffer, offset, count); + ThrowIfDisposed(); + var all = GetEncoded(); + var remaining = all.Length - position; + if (remaining <= 0 || + count == 0) + { + return 0; + } + var toRead = Math.Min(remaining, count); + Array.Copy(all, position, buffer, offset, toRead); + position += toRead; + return toRead; + } + /// + public override int ReadByte() + { + ThrowIfDisposed(); + var all = GetEncoded(); + if (position < all.Length) + { + return all[position++]; + } + return -1; + } + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + GuardRange(buffer, offset, count); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + /// + public override void Flush() + { + } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Stream does not support seeking."); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Stream does not support writing."); + /// + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Stream does not support writing."); + /// + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } + void ThrowIfDisposed() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + static void GuardRange(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (buffer.Length - offset < count) + { + throw new ArgumentException("The sum of offset and count is larger than the buffer length."); + } + } +} +#endif diff --git a/src/Split/uap10.0/WritableMemoryStream.cs b/src/Split/uap10.0/WritableMemoryStream.cs new file mode 100644 index 00000000..f8e0ecb6 --- /dev/null +++ b/src/Split/uap10.0/WritableMemoryStream.cs @@ -0,0 +1,51 @@ +// +#pragma warning disable +#nullable enable +#if FeatureMemory +#pragma warning disable +namespace System.IO; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Provides a seekable, read-write over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// throws and returns . +/// The polyfill requires an array-backed ; memory backed by native (or other non-array) +/// buffers is not supported and the constructor throws . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class WritableMemoryStream : + MemoryStream +{ + /// + /// Initializes a new instance of the class over the specified . + /// + public WritableMemoryStream(Memory buffer) + : base(GetArray(buffer, out var offset, out var count), offset, count, writable: true, publiclyVisible: false) => + base.SetLength(0); + /// + public override void SetLength(long value) => + throw new NotSupportedException("Memory stream is not expandable."); + static byte[] GetArray(Memory buffer, out int offset, out int count) + { + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out var segment) && + segment.Array != null) + { + offset = segment.Offset; + count = segment.Count; + return segment.Array; + } + throw new NotSupportedException("WritableMemoryStream polyfill requires an array-backed Memory."); + } +} +#endif diff --git a/src/Tests/PolyfillTests_ReadOnlyMemoryStream.cs b/src/Tests/PolyfillTests_ReadOnlyMemoryStream.cs new file mode 100644 index 00000000..6f6a2035 --- /dev/null +++ b/src/Tests/PolyfillTests_ReadOnlyMemoryStream.cs @@ -0,0 +1,113 @@ +#if FeatureMemory +using System.IO; + +partial class PolyfillTests +{ + [Test] + public async Task ReadOnlyMemoryStream_IsAMemoryStream() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3 }); + + await Assert.That(stream).IsAssignableTo(); + } + + [Test] + public async Task ReadOnlyMemoryStream_Capabilities() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3 }); + + await Assert.That(stream.CanRead).IsTrue(); + await Assert.That(stream.CanSeek).IsTrue(); + await Assert.That(stream.CanWrite).IsFalse(); + await Assert.That(stream.Length).IsEqualTo(3L); + } + + [Test] + public async Task ReadOnlyMemoryStream_ReadsAllBytes() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 10, 20, 30, 40, 50 }); + + var buffer = new byte[5]; + var read = stream.Read(buffer, 0, 5); + + await Assert.That(read).IsEqualTo(5); + await Assert.That(buffer).IsEquivalentTo(new byte[] { 10, 20, 30, 40, 50 }); + } + + [Test] + public async Task ReadOnlyMemoryStream_WorksWithSlicedMemory() + { + var largeBuffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + var slice = largeBuffer.AsMemory(3, 4); + using var stream = new ReadOnlyMemoryStream(slice); + + await Assert.That(stream.Length).IsEqualTo(4L); + + var result = new byte[4]; + var read = stream.Read(result, 0, 4); + + await Assert.That(read).IsEqualTo(4); + await Assert.That(result).IsEquivalentTo(new byte[] { 3, 4, 5, 6 }); + } + + [Test] + public async Task ReadOnlyMemoryStream_SeekAndPosition() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + + stream.Seek(2, SeekOrigin.Begin); + await Assert.That(stream.Position).IsEqualTo(2L); + await Assert.That(stream.ReadByte()).IsEqualTo(3); + + stream.Position = 0; + await Assert.That(stream.ReadByte()).IsEqualTo(1); + } + + [Test] + public async Task ReadOnlyMemoryStream_GetBufferThrows_TryGetBufferFalse() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3 }); + + await Assert.That(() => stream.GetBuffer()).Throws(); + await Assert.That(stream.TryGetBuffer(out var segment)).IsFalse(); + await Assert.That(segment.Array).IsNull(); + } + + [Test] + public async Task ReadOnlyMemoryStream_WritingThrows() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3 }); + + await Assert.That(() => stream.Write(new byte[1], 0, 1)).Throws(); + await Assert.That(() => stream.WriteByte(1)).Throws(); + } + + [Test] + public async Task ReadOnlyMemoryStream_ToArrayReturnsContent() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 9, 8, 7 }); + + await Assert.That(stream.ToArray()).IsEquivalentTo(new byte[] { 9, 8, 7 }); + } + + [Test] + public async Task ReadOnlyMemoryStream_CopyTo() + { + using var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3, 4 }); + using var target = new MemoryStream(); + + stream.CopyTo(target); + + await Assert.That(target.ToArray()).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); + } + + [Test] + public async Task ReadOnlyMemoryStream_Empty() + { + using var stream = new ReadOnlyMemoryStream(ReadOnlyMemory.Empty); + + await Assert.That(stream.Length).IsEqualTo(0L); + await Assert.That(stream.Read(new byte[4], 0, 4)).IsEqualTo(0); + } +} +#endif diff --git a/src/Tests/PolyfillTests_ReadOnlySequenceStream.cs b/src/Tests/PolyfillTests_ReadOnlySequenceStream.cs new file mode 100644 index 00000000..66cc44d8 --- /dev/null +++ b/src/Tests/PolyfillTests_ReadOnlySequenceStream.cs @@ -0,0 +1,143 @@ +#if FeatureMemory +using System.Buffers; +using System.IO; + +partial class PolyfillTests +{ + [Test] + public async Task ReadOnlySequenceStream_Capabilities() + { + using var stream = new ReadOnlySequenceStream(new ReadOnlySequence(new byte[] { 1, 2, 3 })); + + await Assert.That(stream.CanRead).IsTrue(); + await Assert.That(stream.CanSeek).IsTrue(); + await Assert.That(stream.CanWrite).IsFalse(); + await Assert.That(stream.Length).IsEqualTo(3L); + } + + [Test] + public async Task ReadOnlySequenceStream_SingleSegment_ReadsAll() + { + using var stream = new ReadOnlySequenceStream(new ReadOnlySequence(new byte[] { 1, 2, 3, 4, 5 })); + + var result = ReadToEnd(stream); + + await Assert.That(result).IsEquivalentTo(new byte[] { 1, 2, 3, 4, 5 }); + } + + [Test] + public async Task ReadOnlySequenceStream_MultiSegment_ReadsAcrossSegments() + { + var sequence = CreateMultiSegment(new byte[] { 1, 2, 3 }, new byte[] { 4, 5 }, new byte[] { 6, 7, 8, 9 }); + using var stream = new ReadOnlySequenceStream(sequence); + + await Assert.That(stream.Length).IsEqualTo(9L); + + // Read in small chunks so reads straddle segment boundaries. + using var accumulator = new MemoryStream(); + var buffer = new byte[2]; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + accumulator.Write(buffer, 0, read); + } + + await Assert.That(accumulator.ToArray()).IsEquivalentTo(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + } + + [Test] + public async Task ReadOnlySequenceStream_ReadByte_AcrossSegments() + { + var sequence = CreateMultiSegment(new byte[] { 1 }, new byte[] { 2, 3 }); + using var stream = new ReadOnlySequenceStream(sequence); + + await Assert.That(stream.ReadByte()).IsEqualTo(1); + await Assert.That(stream.ReadByte()).IsEqualTo(2); + await Assert.That(stream.ReadByte()).IsEqualTo(3); + await Assert.That(stream.ReadByte()).IsEqualTo(-1); + } + + [Test] + public async Task ReadOnlySequenceStream_Seek() + { + using var stream = new ReadOnlySequenceStream(new ReadOnlySequence(new byte[] { 1, 2, 3, 4, 5 })); + + stream.Seek(3, SeekOrigin.Begin); + await Assert.That(stream.Position).IsEqualTo(3L); + await Assert.That(stream.ReadByte()).IsEqualTo(4); + + stream.Seek(-2, SeekOrigin.Current); + await Assert.That(stream.ReadByte()).IsEqualTo(3); + + stream.Seek(-1, SeekOrigin.End); + await Assert.That(stream.ReadByte()).IsEqualTo(5); + } + + [Test] + public async Task ReadOnlySequenceStream_SeekBeforeBeginThrows() + { + using var stream = new ReadOnlySequenceStream(new ReadOnlySequence(new byte[] { 1, 2, 3 })); + + await Assert.That(() => stream.Seek(-1, SeekOrigin.Begin)).Throws(); + } + + [Test] + public async Task ReadOnlySequenceStream_WritingThrows() + { + using var stream = new ReadOnlySequenceStream(new ReadOnlySequence(new byte[] { 1, 2, 3 })); + + await Assert.That(() => stream.Write(new byte[1], 0, 1)).Throws(); + await Assert.That(() => stream.SetLength(1)).Throws(); + } + + [Test] + public async Task ReadOnlySequenceStream_ReadAsync() + { + using var stream = new ReadOnlySequenceStream(new ReadOnlySequence(new byte[] { 1, 2, 3, 4 })); + + var buffer = new byte[4]; + var read = await stream.ReadAsync(buffer, 0, 4); + + await Assert.That(read).IsEqualTo(4); + await Assert.That(buffer).IsEquivalentTo(new byte[] { 1, 2, 3, 4 }); + } + + [Test] + public async Task ReadOnlySequenceStream_Empty() + { + using var stream = new ReadOnlySequenceStream(ReadOnlySequence.Empty); + + await Assert.That(stream.Length).IsEqualTo(0L); + await Assert.That(stream.Read(new byte[4], 0, 4)).IsEqualTo(0); + } + + static ReadOnlySequence CreateMultiSegment(params byte[][] parts) + { + var first = new ReadOnlySequenceStreamSegment(parts[0]); + var last = first; + for (var index = 1; index < parts.Length; index++) + { + last = last.Append(parts[index]); + } + + return new(first, 0, last, last.Memory.Length); + } + + class ReadOnlySequenceStreamSegment : + ReadOnlySequenceSegment + { + public ReadOnlySequenceStreamSegment(ReadOnlyMemory memory) => + Memory = memory; + + public ReadOnlySequenceStreamSegment Append(ReadOnlyMemory memory) + { + var segment = new ReadOnlySequenceStreamSegment(memory) + { + RunningIndex = RunningIndex + Memory.Length + }; + Next = segment; + return segment; + } + } +} +#endif diff --git a/src/Tests/PolyfillTests_StringStream.cs b/src/Tests/PolyfillTests_StringStream.cs new file mode 100644 index 00000000..c75e79b7 --- /dev/null +++ b/src/Tests/PolyfillTests_StringStream.cs @@ -0,0 +1,173 @@ +#if FeatureMemory +using System.IO; +using System.Text; + +partial class PolyfillTests +{ + [Test] + public async Task StringStream_ReadsExpectedUtf8Bytes() + { + var expected = Encoding.UTF8.GetBytes("Hello, World!"); + using var stream = new StringStream("Hello, World!", Encoding.UTF8); + + var actual = ReadToEnd(stream); + + await Assert.That(actual).IsEquivalentTo(expected); + } + + [Test] + public async Task StringStream_FromReadOnlyMemory_ReadsExpectedBytes() + { + var expected = Encoding.UTF8.GetBytes("chars"); + using var stream = new StringStream("chars".AsMemory(), Encoding.UTF8); + + var actual = ReadToEnd(stream); + + await Assert.That(actual).IsEquivalentTo(expected); + } + + [Test] + public async Task StringStream_Unicode_RoundTripsThroughEncoding() + { + const string input = "Unicode: 你好世界 🌍"; + var expected = Encoding.UTF8.GetBytes(input); + using var stream = new StringStream(input, Encoding.UTF8); + + var actual = ReadToEnd(stream); + + await Assert.That(actual).IsEquivalentTo(expected); + } + + [Test] + public async Task StringStream_ReadInSmallChunks_ReconstructsOutput() + { + var expected = Encoding.UTF8.GetBytes("The quick brown fox"); + using var stream = new StringStream("The quick brown fox", Encoding.UTF8); + + using var accumulator = new MemoryStream(); + var buffer = new byte[3]; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + accumulator.Write(buffer, 0, read); + } + + await Assert.That(accumulator.ToArray()).IsEquivalentTo(expected); + } + + [Test] + public async Task StringStream_ReadByte_ReturnsBytesThenMinusOne() + { + using var stream = new StringStream("AB", Encoding.ASCII); + + var first = stream.ReadByte(); + var second = stream.ReadByte(); + var third = stream.ReadByte(); + + await Assert.That(first).IsEqualTo((int)'A'); + await Assert.That(second).IsEqualTo((int)'B'); + await Assert.That(third).IsEqualTo(-1); + } + + [Test] + public async Task StringStream_Empty_ReadsZero() + { + using var stream = new StringStream("", Encoding.UTF8); + + var buffer = new byte[8]; + var read = stream.Read(buffer, 0, buffer.Length); + + await Assert.That(read).IsEqualTo(0); + } + + [Test] + public async Task StringStream_NeverEmitsBom() + { + // UTF8 with BOM preamble: StringStream must not write the preamble. + var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); + using var stream = new StringStream("x", encoding); + + var actual = ReadToEnd(stream); + + await Assert.That(actual).IsEquivalentTo(new byte[] { (byte)'x' }); + } + + [Test] + public async Task StringStream_ReadAsync_ReadsExpectedBytes() + { + var expected = Encoding.UTF8.GetBytes("async"); + using var stream = new StringStream("async", Encoding.UTF8); + + var buffer = new byte[expected.Length]; + var total = 0; + int read; + while (total < buffer.Length && + (read = await stream.ReadAsync(buffer, total, buffer.Length - total)) > 0) + { + total += read; + } + + await Assert.That(total).IsEqualTo(expected.Length); + await Assert.That(buffer).IsEquivalentTo(expected); + } + + [Test] + public async Task StringStream_Capabilities() + { + using var stream = new StringStream("data", Encoding.UTF8); + + await Assert.That(stream.CanRead).IsTrue(); + await Assert.That(stream.CanSeek).IsFalse(); + await Assert.That(stream.CanWrite).IsFalse(); + await Assert.That(stream.Encoding).IsEqualTo((Encoding)Encoding.UTF8); + } + + [Test] + public async Task StringStream_SeekingMembersThrow() + { + using var stream = new StringStream("data", Encoding.UTF8); + + await Assert.That(() => stream.Length).Throws(); + await Assert.That(() => stream.Position).Throws(); + await Assert.That(() => stream.Seek(0, SeekOrigin.Begin)).Throws(); + } + + [Test] + public async Task StringStream_WritingMembersThrow() + { + using var stream = new StringStream("data", Encoding.UTF8); + + await Assert.That(() => stream.Write(new byte[1], 0, 1)).Throws(); + await Assert.That(() => stream.SetLength(1)).Throws(); + } + + [Test] + public async Task StringStream_NullArgumentsThrow() + { + await Assert.That(() => new StringStream((string)null!, Encoding.UTF8)).Throws(); + await Assert.That(() => new StringStream("x", null!)).Throws(); + } + + [Test] + public async Task StringStream_AfterDispose_CanReadIsFalse() + { + var stream = new StringStream("data", Encoding.UTF8); + stream.Dispose(); + + await Assert.That(stream.CanRead).IsFalse(); + } + + static byte[] ReadToEnd(Stream stream) + { + using var memory = new MemoryStream(); + var buffer = new byte[16]; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + memory.Write(buffer, 0, read); + } + + return memory.ToArray(); + } +} +#endif diff --git a/src/Tests/PolyfillTests_WritableMemoryStream.cs b/src/Tests/PolyfillTests_WritableMemoryStream.cs new file mode 100644 index 00000000..c0f14262 --- /dev/null +++ b/src/Tests/PolyfillTests_WritableMemoryStream.cs @@ -0,0 +1,100 @@ +#if FeatureMemory +using System.IO; + +partial class PolyfillTests +{ + [Test] + public async Task WritableMemoryStream_IsAMemoryStream() + { + using var stream = new WritableMemoryStream(new byte[4]); + + await Assert.That(stream).IsAssignableTo(); + } + + [Test] + public async Task WritableMemoryStream_StartsEmptyWithFixedCapacity() + { + using var stream = new WritableMemoryStream(new byte[8]); + + await Assert.That(stream.Length).IsEqualTo(0L); + await Assert.That(stream.Position).IsEqualTo(0L); + await Assert.That(stream.CanWrite).IsTrue(); + await Assert.That(stream.CanRead).IsTrue(); + await Assert.That(stream.CanSeek).IsTrue(); + await Assert.That(stream.Capacity).IsEqualTo(8); + } + + [Test] + public async Task WritableMemoryStream_WritesIntoBackingArray() + { + var backing = new byte[5]; + using var stream = new WritableMemoryStream(backing); + + stream.Write(new byte[] { 1, 2, 3 }, 0, 3); + + await Assert.That(stream.Length).IsEqualTo(3L); + await Assert.That(backing).IsEquivalentTo(new byte[] { 1, 2, 3, 0, 0 }); + } + + [Test] + public async Task WritableMemoryStream_WritesIntoSlicedBackingMemory() + { + var backing = new byte[8]; + using var stream = new WritableMemoryStream(backing.AsMemory(2, 3)); + + stream.WriteByte(7); + stream.WriteByte(8); + + await Assert.That(backing).IsEquivalentTo(new byte[] { 0, 0, 7, 8, 0, 0, 0, 0 }); + } + + [Test] + public async Task WritableMemoryStream_ReadBackWhatWasWritten() + { + using var stream = new WritableMemoryStream(new byte[16]); + stream.Write(new byte[] { 4, 5, 6 }, 0, 3); + + stream.Position = 0; + var buffer = new byte[3]; + var read = stream.Read(buffer, 0, 3); + + await Assert.That(read).IsEqualTo(3); + await Assert.That(buffer).IsEquivalentTo(new byte[] { 4, 5, 6 }); + } + + [Test] + public async Task WritableMemoryStream_WritingBeyondCapacityThrows() + { + using var stream = new WritableMemoryStream(new byte[3]); + + await Assert.That(() => stream.Write(new byte[] { 1, 2, 3, 4 }, 0, 4)).Throws(); + } + + [Test] + public async Task WritableMemoryStream_SetLengthThrows() + { + using var stream = new WritableMemoryStream(new byte[4]); + + await Assert.That(() => stream.SetLength(2)).Throws(); + } + + [Test] + public async Task WritableMemoryStream_GetBufferThrows_TryGetBufferFalse() + { + using var stream = new WritableMemoryStream(new byte[4]); + + await Assert.That(() => stream.GetBuffer()).Throws(); + await Assert.That(stream.TryGetBuffer(out var segment)).IsFalse(); + await Assert.That(segment.Array).IsNull(); + } + + [Test] + public async Task WritableMemoryStream_ToArrayReturnsWrittenContent() + { + using var stream = new WritableMemoryStream(new byte[16]); + stream.Write(new byte[] { 1, 2, 3 }, 0, 3); + + await Assert.That(stream.ToArray()).IsEquivalentTo(new byte[] { 1, 2, 3 }); + } +} +#endif