diff --git a/PolyShim.Tests/Net60/FileTests.cs b/PolyShim.Tests/Net60/FileTests.cs new file mode 100644 index 00000000..8fc6407b --- /dev/null +++ b/PolyShim.Tests/Net60/FileTests.cs @@ -0,0 +1,128 @@ +using System; +using System.IO; +using System.Runtime.Versioning; +using FluentAssertions; +using PolyShim.Tests.Utils.Extensions; +using Xunit; + +namespace PolyShim.Tests.Net60; + +public class FileTests +{ + [Fact] + public void Open_Test() + { + // Arrange + var tempFilePath = Path.GetTempFileName(); + File.WriteAllBytes(tempFilePath, [0x00, 0x01, 0x02]); + + try + { + var options = new FileStreamOptions(); + + // Act + using var stream = File.Open(tempFilePath, options); + + // Assert + stream.CanRead.Should().BeTrue(); + stream.Length.Should().Be(3); + } + finally + { + File.TryDelete(tempFilePath); + } + } + + [Fact] + public void Open_Write_Test() + { + // Arrange + var tempFilePath = Path.GetTempFileName(); + + try + { + var options = new FileStreamOptions + { + Mode = FileMode.Create, + Access = FileAccess.Write, + Share = FileShare.None, + }; + + // Act + using var stream = File.Open(tempFilePath, options); + stream.Write([0x0A, 0x0B, 0x0C], 0, 3); + + // Assert + stream.CanWrite.Should().BeTrue(); + stream.Position.Should().Be(3); + } + finally + { + File.TryDelete(tempFilePath); + } + } + + [Fact] + public void Open_ReadWrite_Test() + { + // Arrange + var tempFilePath = Path.GetTempFileName(); + File.WriteAllBytes(tempFilePath, [0x00, 0x01, 0x02]); + + try + { + var options = new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.ReadWrite, + Share = FileShare.None, + }; + + // Act + using var stream = File.Open(tempFilePath, options); + + // Assert + stream.CanRead.Should().BeTrue(); + stream.CanWrite.Should().BeTrue(); + } + finally + { + File.TryDelete(tempFilePath); + } + } + + [SkippableFact] + [UnsupportedOSPlatform("windows")] + public void Open_UnixFileMode_Test() + { + Skip.If(OperatingSystem.IsWindows()); + + // Arrange + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + try + { + var expectedMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; + + var options = new FileStreamOptions + { + Mode = FileMode.Create, + Access = FileAccess.Write, + Share = FileShare.None, + UnixCreateMode = expectedMode, + }; + + // Act + using (var stream = File.Open(tempFilePath, options)) + stream.Write([0x0A, 0x0B], 0, 2); + + // Assert + var actualMode = File.GetUnixFileMode(tempFilePath); + actualMode.Should().Be(expectedMode); + } + finally + { + File.TryDelete(tempFilePath); + } + } +} diff --git a/PolyShim.Tests/Net70/DirectoryTests.cs b/PolyShim.Tests/Net70/DirectoryTests.cs new file mode 100644 index 00000000..c04d4e70 --- /dev/null +++ b/PolyShim.Tests/Net70/DirectoryTests.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Runtime.Versioning; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net70; + +public class DirectoryTests +{ + [SkippableFact] + [UnsupportedOSPlatform("windows")] + public void CreateDirectory_UnixFileMode_Test() + { + Skip.If(OperatingSystem.IsWindows()); + + // Arrange + var tempDirPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + try + { + var expectedMode = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; + + // Act + var info = Directory.CreateDirectory(tempDirPath, expectedMode); + + // Assert + info.Should().NotBeNull(); + info.Exists.Should().BeTrue(); + File.GetUnixFileMode(tempDirPath).Should().Be(expectedMode); + } + finally + { + if (Directory.Exists(tempDirPath)) + Directory.Delete(tempDirPath); + } + } +} diff --git a/PolyShim.Tests/Net70/FileTests.cs b/PolyShim.Tests/Net70/FileTests.cs index a3dadb46..c0b22c69 100644 --- a/PolyShim.Tests/Net70/FileTests.cs +++ b/PolyShim.Tests/Net70/FileTests.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.IO; +using System.Runtime.Versioning; using System.Threading.Tasks; using FluentAssertions; using PolyShim.Tests.Utils.Extensions; @@ -32,4 +34,30 @@ public async Task ReadLinesAsync_Test() File.TryDelete(tempFilePath); } } + + [SkippableFact] + [UnsupportedOSPlatform("windows")] + public void SetUnixFileMode_Test() + { + Skip.If(OperatingSystem.IsWindows()); + + // Arrange + var tempFilePath = Path.GetTempFileName(); + + try + { + var expectedMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; + + // Act + File.SetUnixFileMode(tempFilePath, expectedMode); + + // Assert + var actualMode = File.GetUnixFileMode(tempFilePath); + actualMode.Should().Be(expectedMode); + } + finally + { + File.TryDelete(tempFilePath); + } + } } diff --git a/PolyShim/Net60/File.cs b/PolyShim/Net60/File.cs new file mode 100644 index 00000000..a4096e03 --- /dev/null +++ b/PolyShim/Net60/File.cs @@ -0,0 +1,62 @@ +#if (NETCOREAPP && !NET6_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +#pragma warning disable CS0436 + +// No file I/O on .NET Standard prior to 1.3 +#if !NETSTANDARD || NETSTANDARD1_3_OR_GREATER + +using System; +using System.IO; +using System.Diagnostics.CodeAnalysis; + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class MemberPolyfills_Net60_File +{ + extension(File) + { + // https://learn.microsoft.com/dotnet/api/system.io.file.open#system-io-file-open(system-string-system-io-filestreamoptions) + public static FileStream Open(string path, FileStreamOptions options) + { + var existed = File.Exists(path); + + var stream = new FileStream( + path, + options.Mode, + options.Access, + options.Share, + options.BufferSize, + options.Options + ); + + try + { +#if !NETFRAMEWORK || NET40_OR_GREATER + if ( + !existed + && !OperatingSystem.IsWindows() + && options.UnixCreateMode is { } unixCreateMode + && options.Mode + is FileMode.CreateNew + or FileMode.Create + or FileMode.OpenOrCreate + or FileMode.Append + ) + { + File.SetUnixFileMode(path, unixCreateMode & File.GetUnixFileMode(path)); + } +#endif + } + catch + { + stream.Dispose(); + throw; + } + + return stream; + } + } +} +#endif +#endif diff --git a/PolyShim/Net60/FileStreamOptions.cs b/PolyShim/Net60/FileStreamOptions.cs new file mode 100644 index 00000000..3ff378ab --- /dev/null +++ b/PolyShim/Net60/FileStreamOptions.cs @@ -0,0 +1,41 @@ +#if (NETCOREAPP && !NET6_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +#pragma warning disable CS0436 + +// No file I/O on .NET Standard prior to 1.3 +#if !NETSTANDARD || NETSTANDARD1_3_OR_GREATER + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace System.IO; + +// https://learn.microsoft.com/dotnet/api/system.io.filestreamoptions +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal class FileStreamOptions +{ + public FileMode Mode { get; set; } = FileMode.Open; + + public FileAccess Access { get; set; } = FileAccess.Read; + + public FileShare Share { get; set; } = FileShare.Read; + + public int BufferSize + { + get; + set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } = 4096; + + public FileOptions Options { get; set; } = FileOptions.None; + + public long PreallocationSize + { + get; + set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } +} + +#endif +#endif diff --git a/PolyShim/Net70/Directory.cs b/PolyShim/Net70/Directory.cs new file mode 100644 index 00000000..1ad71dd0 --- /dev/null +++ b/PolyShim/Net70/Directory.cs @@ -0,0 +1,54 @@ +#if (NETCOREAPP && !NET7_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +#pragma warning disable CS0436 + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Diagnostics.CodeAnalysis; + +// No file I/O on .NET Standard prior to 1.3 +#if !NETSTANDARD || NETSTANDARD1_3_OR_GREATER + +file static class NativeMethods +{ + [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] + public static extern int Chmod(string path, uint mode); +} + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class MemberPolyfills_Net70_Directory +{ + extension(Directory) + { + // https://learn.microsoft.com/dotnet/api/system.io.directory.createdirectory#system-io-directory-createdirectory(system-string-system-io-unixfilemode) + [UnsupportedOSPlatform("windows")] + public static DirectoryInfo CreateDirectory(string path, UnixFileMode unixCreateMode) + { + if (OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException(); + + var existed = Directory.Exists(path); + var info = Directory.CreateDirectory(path); + + if (!existed) + { + var effectiveMode = unixCreateMode & File.GetUnixFileMode(info.FullName); + if (NativeMethods.Chmod(info.FullName, (uint)effectiveMode) != 0) + { + throw new IOException( + $"Could not set Unix file mode for '{path}' (errno={Marshal.GetLastWin32Error()})." + ); + } + } + + return info; + } + } +} + +#endif +#endif diff --git a/PolyShim/Net70/File.cs b/PolyShim/Net70/File.cs index 2140064a..7fc829cb 100644 --- a/PolyShim/Net70/File.cs +++ b/PolyShim/Net70/File.cs @@ -2,22 +2,109 @@ #nullable enable #pragma warning disable CS0436 +using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Diagnostics.CodeAnalysis; +// No file I/O on .NET Standard prior to 1.3 +#if !NETSTANDARD || NETSTANDARD1_3_OR_GREATER + +file static class NativeMethods +{ + // The stat struct on Unix is platform-specific; we read the raw bytes and extract the mode field. + // 256 bytes is enough for any supported platform (Linux x86_64/arm64: 128–144 bytes; macOS: 144 bytes). + [StructLayout(LayoutKind.Sequential)] + public struct StatBuf + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)] + public byte[] Data; + } + + // Linux: stat is in libc.so.6 + [DllImport("libc", EntryPoint = "stat", SetLastError = true)] + public static extern int StatLinux(string path, ref StatBuf buf); + + // macOS: the 64-bit inode variant is exported as stat$INODE64 + [DllImport("libSystem.dylib", EntryPoint = "stat$INODE64", SetLastError = true)] + public static extern int StatMacOs(string path, ref StatBuf buf); + + [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] + public static extern int Chmod(string path, uint mode); + + public static int GetStat(string path, ref StatBuf buf) => + OperatingSystem.IsMacOS() ? StatMacOs(path, ref buf) : StatLinux(path, ref buf); + + // Returns the raw native stat st_mode field. Callers should mask as needed + // (for example, with 0xFFF to extract Unix permission bits only). + public static int ReadStatMode(StatBuf buf) + { + if (OperatingSystem.IsMacOS()) + { + // macOS (all architectures): st_mode is uint16 at byte offset 4 + // struct stat { dev_t st_dev[4]; mode_t st_mode[2]; ... } + return BitConverter.ToUInt16(buf.Data, 4); + } + + // Linux: st_mode is uint32, but the offset differs by architecture. + // x86_64: { dev_t[8] + ino_t[8] + nlink_t[8] + mode_t[4] } → offset 24 + // arm64: { dev_t[8] + ino_t[8] + mode_t[4] + nlink_t[4] } → offset 16 +#if FEATURE_RUNTIMEINFORMATION + var offset = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? 16 : 24; +#else + var offset = 24; // Old targets ran on x86/x64 only +#endif + + return BitConverter.ToInt32(buf.Data, offset); + } +} + #if !POLYSHIM_INCLUDE_COVERAGE [ExcludeFromCodeCoverage] #endif internal static class MemberPolyfills_Net70_File { - // No file I/O on .NET Standard prior to 1.3 -#if !NETSTANDARD || NETSTANDARD1_3_OR_GREATER extension(File) { + // https://learn.microsoft.com/dotnet/api/system.io.file.getunixfilemode + [UnsupportedOSPlatform("windows")] + public static UnixFileMode GetUnixFileMode(string path) + { + if (OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException(); + + // Initialize the array so the marshaler has a non-null source for the in-direction copy + var buf = new NativeMethods.StatBuf { Data = new byte[256] }; + if (NativeMethods.GetStat(path, ref buf) != 0) + { + throw new IOException( + $"Could not get Unix file mode for '{path}' (errno={Marshal.GetLastWin32Error()})." + ); + } + + return (UnixFileMode)(NativeMethods.ReadStatMode(buf) & 0xFFF); + } + + // https://learn.microsoft.com/dotnet/api/system.io.file.setunixfilemode + [UnsupportedOSPlatform("windows")] + public static void SetUnixFileMode(string path, UnixFileMode mode) + { + if (OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException(); + + if (NativeMethods.Chmod(path, (uint)mode) != 0) + { + throw new IOException( + $"Could not set Unix file mode for '{path}' (errno={Marshal.GetLastWin32Error()})." + ); + } + } + #if FEATURE_TASK // https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync#system-io-file-readlinesasync(system-string-system-text-encoding-system-threading-cancellationtoken) public static async IAsyncEnumerable ReadLinesAsync( @@ -75,6 +162,7 @@ public static async IAsyncEnumerable ReadLinesAsync( } #endif } -#endif } + +#endif #endif diff --git a/PolyShim/Net70/FileStreamOptions.cs b/PolyShim/Net70/FileStreamOptions.cs new file mode 100644 index 00000000..993b97eb --- /dev/null +++ b/PolyShim/Net70/FileStreamOptions.cs @@ -0,0 +1,39 @@ +#if (NETCOREAPP && !NET7_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +#pragma warning disable CS0436 + +// No file I/O on .NET Standard prior to 1.3, and ConditionalWeakTable unavailable on .NET Framework 3.5 +#if (!NETSTANDARD || NETSTANDARD1_3_OR_GREATER) && (!NETFRAMEWORK || NET40_OR_GREATER) + +using System.IO; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; + +#if !POLYSHIM_INCLUDE_COVERAGE +[ExcludeFromCodeCoverage] +#endif +internal static class MemberPolyfills_Net70_FileStreamOptions +{ + private sealed class UnixFileModeBox + { + public UnixFileMode? Value; + } + + private static readonly ConditionalWeakTable< + FileStreamOptions, + UnixFileModeBox + > _unixCreateModes = new(); + + extension(FileStreamOptions options) + { + // https://learn.microsoft.com/dotnet/api/system.io.filestreamoptions.unixcreatemode + public UnixFileMode? UnixCreateMode + { + get => _unixCreateModes.TryGetValue(options, out var box) ? box.Value : null; + set => _unixCreateModes.GetOrCreateValue(options).Value = value; + } + } +} + +#endif +#endif diff --git a/PolyShim/Net70/UnixFileMode.cs b/PolyShim/Net70/UnixFileMode.cs new file mode 100644 index 00000000..63f8c255 --- /dev/null +++ b/PolyShim/Net70/UnixFileMode.cs @@ -0,0 +1,28 @@ +#if (NETCOREAPP && !NET7_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +#pragma warning disable CS0436 + +using System; + +namespace System.IO; + +// https://learn.microsoft.com/dotnet/api/system.io.unixfilemode +[Flags] +internal enum UnixFileMode +{ + None = 0, + OtherExecute = 1, + OtherWrite = 2, + OtherRead = 4, + GroupExecute = 8, + GroupWrite = 16, + GroupRead = 32, + UserExecute = 64, + UserWrite = 128, + UserRead = 256, + StickyBit = 512, + SetGroup = 1024, + SetUser = 2048, +} + +#endif diff --git a/Signatures.md b/Signatures.md index 15613792..c688ab52 100644 --- a/Signatures.md +++ b/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 470 -- **Types:** 102 -- **Members:** 368 +- **Total:** 477 +- **Types:** 104 +- **Members:** 373 ___ @@ -91,6 +91,8 @@ ___ - [`int EnsureCapacity(int)`](https://learn.microsoft.com/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity) .NET 5.0 - `DictionaryEntry` - [`void Deconstruct(out object, out object?)`](https://learn.microsoft.com/dotnet/api/system.collections.dictionaryentry.deconstruct) .NET Core 2.0 +- `Directory` + - [`static DirectoryInfo CreateDirectory(string, UnixFileMode)`](https://learn.microsoft.com/dotnet/api/system.io.directory.createdirectory#system-io-directory-createdirectory(system-string-system-io-unixfilemode)) .NET 7.0 - `DisallowNullAttribute` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.disallownullattribute) .NET Core 3.0 - `DoesNotReturnAttribute` @@ -129,6 +131,7 @@ ___ - `FeatureSwitchDefinitionAttribute` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.featureswitchdefinitionattribute) .NET 9.0 - `File` + - [`static FileStream Open(string, FileStreamOptions)`](https://learn.microsoft.com/dotnet/api/system.io.file.open#system-io-file-open(system-string-system-io-filestreamoptions)) .NET 6.0 - [`static IAsyncEnumerable ReadLinesAsync(string, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync#system-io-file-readlinesasync(system-string-system-threading-cancellationtoken)) .NET 7.0 - [`static IAsyncEnumerable ReadLinesAsync(string, Encoding, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync#system-io-file-readlinesasync(system-string-system-text-encoding-system-threading-cancellationtoken)) .NET 7.0 - [`static Task AppendAllBytesAsync(string, byte[], CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.appendallbytesasync#system-io-file-appendallbytesasync(system-string-system-byte()-system-threading-cancellationtoken)) .NET 9.0 @@ -147,9 +150,14 @@ ___ - [`static Task ReadAllTextAsync(string, Encoding, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.readalltextasync#system-io-file-readalltextasync(system-string-system-text-encoding-system-threading-cancellationtoken)) .NET Core 2.0 - [`static Task ReadAllLinesAsync(string, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.readalllinesasync#system-io-file-readalllinesasync(system-string-system-threading-cancellationtoken)) .NET Core 2.0 - [`static Task ReadAllLinesAsync(string, Encoding, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.readalllinesasync#system-io-file-readalllinesasync(system-string-system-text-encoding-system-threading-cancellationtoken)) .NET Core 2.0 + - [`static UnixFileMode GetUnixFileMode(string)`](https://learn.microsoft.com/dotnet/api/system.io.file.getunixfilemode) .NET 7.0 - [`static void AppendAllBytes(string, byte[])`](https://learn.microsoft.com/dotnet/api/system.io.file.appendallbytes#system-io-file-appendallbytes(system-string-system-byte())) .NET 9.0 - [`static void AppendAllBytes(string, ReadOnlySpan)`](https://learn.microsoft.com/dotnet/api/system.io.file.appendallbytes#system-io-file-appendallbytes(system-string-system-readonlyspan((system-byte)))) .NET 9.0 - [`static void Move(string, string, bool)`](https://learn.microsoft.com/dotnet/api/system.io.file.move#system-io-file-move(system-string-system-string-system-boolean)) .NET Core 3.0 + - [`static void SetUnixFileMode(string, UnixFileMode)`](https://learn.microsoft.com/dotnet/api/system.io.file.setunixfilemode) .NET 7.0 +- `FileStreamOptions` + - [**[class]**](https://learn.microsoft.com/dotnet/api/system.io.filestreamoptions) .NET 6.0 + - [`UnixFileMode? UnixCreateMode`](https://learn.microsoft.com/dotnet/api/system.io.filestreamoptions.unixcreatemode) .NET 7.0 - `float` - [`static bool TryParse(ReadOnlySpan, IFormatProvider?, out float)`](https://learn.microsoft.com/dotnet/api/system.single.tryparse#system-single-tryparse(system-readonlyspan((system-char))-system-iformatprovider-system-single@)) .NET 7.0 - [`static bool TryParse(string?, IFormatProvider?, out float)`](https://learn.microsoft.com/dotnet/api/system.single.tryparse#system-single-tryparse(system-string-system-iformatprovider-system-single@)) .NET 7.0 @@ -601,6 +609,8 @@ ___ - [**[class]**](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.unconditionalsuppressmessageattribute) .NET 5.0 - `UnionAttribute` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.unionattribute) .NET 11.0 +- `UnixFileMode` + - [**[enum]**](https://learn.microsoft.com/dotnet/api/system.io.unixfilemode) .NET 7.0 - `UnsupportedOSPlatformAttribute` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.runtime.versioning.unsupportedosplatformattribute) .NET 5.0 - `UnsupportedOSPlatformGuardAttribute`