diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Permissions.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Permissions.cs deleted file mode 100644 index 8248077deaf50..0000000000000 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Permissions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal static partial class Interop -{ - internal static partial class Sys - { - [Flags] - internal enum Permissions - { - Mask = S_IRWXU | S_IRWXG | S_IRWXO, - - S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR, - S_IRUSR = 0x100, - S_IWUSR = 0x80, - S_IXUSR = 0x40, - - S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP, - S_IRGRP = 0x20, - S_IWGRP = 0x10, - S_IXGRP = 0x8, - - S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH, - S_IROTH = 0x4, - S_IWOTH = 0x2, - S_IXOTH = 0x1, - - S_IXUGO = S_IXUSR | S_IXGRP | S_IXOTH, - } - } -} diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 3de13e8cdc662..527292b461da5 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -276,8 +276,6 @@ Link="Common\Interop\Unix\System.Native\Interop.Access.cs" /> - (() => Process.Start(path)); Assert.NotEqual(0, e.NativeErrorCode); @@ -520,7 +520,7 @@ public void TestStartOnUnixWithBadFormat() { string path = GetTestFilePath(); File.Create(path).Dispose(); - ChMod(path, "744"); // set x-bit + File.SetUnixFileMode(path, ExecutablePermissions); Win32Exception e = Assert.Throws(() => Process.Start(path)); Assert.NotEqual(0, e.NativeErrorCode); @@ -936,14 +936,6 @@ private static int GetWaitStateReferenceCount(object waitState) return (int)referenCountField.GetValue(waitState); } - [DllImport("libc")] - private static extern int chmod(string path, int mode); - - private static void ChMod(string filename, string mode) - { - Assert.Equal(0, chmod(filename, Convert.ToInt32(mode, 8))); - } - [DllImport("libc")] private static extern uint geteuid(); [DllImport("libc")] @@ -1001,7 +993,7 @@ private string WriteScriptFile(string directory, string name, int returnValue) { string filename = Path.Combine(directory, name); File.WriteAllText(filename, $"#!/bin/sh\nexit {returnValue}\n"); - ChMod(filename, "744"); // set x-bit + File.SetUnixFileMode(filename, ExecutablePermissions); return filename; } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs index 5a8a0dc448bba..6fb91c0f4e113 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Windows.cs @@ -15,8 +15,5 @@ private string WriteScriptFile(string directory, string name, int returnValue) File.WriteAllText(filename, $"exit {returnValue}"); return filename; } - - private static void ChMod(string filename, string mode) - => throw new PlatformNotSupportedException(); } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs index 3d6500fc7eb07..6a6ffef0d7c57 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs @@ -23,6 +23,10 @@ namespace System.Diagnostics.Tests { public partial class ProcessTests : ProcessTestBase { + const UnixFileMode ExecutablePermissions = UnixFileMode.UserRead | UnixFileMode.UserExecute | UnixFileMode.UserWrite | + UnixFileMode.GroupRead | UnixFileMode.GroupExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupExecute; + private class FinalizingProcess : Process { public static volatile bool WasFinalized; @@ -2193,7 +2197,7 @@ public void LongProcessNamesAreSupported() // Instead of using sleep directly, we wrap it with a script. sleepPath = GetTestFilePath(); File.WriteAllText(sleepPath, $"#!/bin/sh\nsleep 600\n"); // sleep 10 min. - ChMod(sleepPath, "744"); + File.SetUnixFileMode(sleepPath, ExecutablePermissions); } else { diff --git a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj index 867940f737573..f6f39c8cd9853 100644 --- a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj +++ b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj @@ -57,13 +57,11 @@ - - diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs index bd492019fbc53..73f15a4996a7c 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Microsoft.Win32.SafeHandles; +using System.IO; namespace System.Formats.Tar { @@ -53,7 +54,7 @@ partial void SetModeOnFile(SafeFileHandle handle, string destinationFileName) // If the permissions weren't set at all, don't write the file's permissions. if (permissions != 0) { - Interop.CheckIo(Interop.Sys.FChMod(handle, permissions), destinationFileName); + File.SetUnixFileMode(handle, (UnixFileMode)permissions); } } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System.IO.Compression.ZipFile.csproj b/src/libraries/System.IO.Compression.ZipFile/src/System.IO.Compression.ZipFile.csproj index f00f15bf5a445..3287498e587c1 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System.IO.Compression.ZipFile.csproj +++ b/src/libraries/System.IO.Compression.ZipFile/src/System.IO.Compression.ZipFile.csproj @@ -31,8 +31,6 @@ Link="Common\Interop\Unix\Interop.Libraries.cs" /> - - diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs index 91b6b4baf5e32..2bca01e0c5c00 100644 --- a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs +++ b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs @@ -100,7 +100,7 @@ private static string[] CreateFiles(string folderPath, string[] testPermissions) string filename = Path.Combine(folderPath, $"{permissions}.txt"); File.WriteAllText(filename, "contents"); - Assert.Equal(0, Interop.Sys.ChMod(filename, Convert.ToInt32(permissions, 8))); + File.SetUnixFileMode(filename, (UnixFileMode)Convert.ToInt32(permissions, 8)); // In some environments, the file mode may be modified by the OS. // See the Rationale section of https://linux.die.net/man/3/chmod. diff --git a/src/libraries/System.IO.FileSystem/tests/Base/AllGetSetAttributes.cs b/src/libraries/System.IO.FileSystem/tests/Base/AllGetSetAttributes.cs index ab667d4ebe7e6..e58a3f89fe25f 100644 --- a/src/libraries/System.IO.FileSystem/tests/Base/AllGetSetAttributes.cs +++ b/src/libraries/System.IO.FileSystem/tests/Base/AllGetSetAttributes.cs @@ -59,7 +59,15 @@ public void SymLinksReflectSymLinkAttributes() try { Assert.Equal(FileAttributes.ReadOnly, FileAttributes.ReadOnly & GetAttributes(path)); - Assert.NotEqual(FileAttributes.ReadOnly, FileAttributes.ReadOnly & GetAttributes(linkPath)); + if (OperatingSystem.IsWindows()) + { + Assert.NotEqual(FileAttributes.ReadOnly, FileAttributes.ReadOnly & GetAttributes(linkPath)); + } + else + { + // On Unix, Get/SetAttributes FileAttributes.ReadOnly operates on the target of the link. + Assert.Equal(FileAttributes.ReadOnly, FileAttributes.ReadOnly & GetAttributes(linkPath)); + } } finally { diff --git a/src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetUnixFileMode.cs b/src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetUnixFileMode.cs new file mode 100644 index 0000000000000..2429777390c2a --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/BaseGetSetUnixFileMode.cs @@ -0,0 +1,217 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.IO.Tests +{ + public abstract class BaseGetSetUnixFileMode : FileSystemTest + { + protected abstract UnixFileMode GetMode(string path); + protected abstract void SetMode(string path, UnixFileMode mode); + + // When false, the Get API returns (UnixFileMode)(-1) when the file doesn't exist. + protected virtual bool GetThrowsWhenDoesntExist => false; + + // The SafeFileHandle APIs require a readable file to open the handle. + protected virtual bool GetModeNeedsReadableFile => false; + + // When false, the Get API returns (UnixFileMode)(-1) when the platform is not supported (Windows). + protected virtual bool GetModeThrowsPNSE => true; + + // Determines if the derived Test class is for directories or files. + protected virtual bool IsDirectory => false; + + // On OSX, directories created under /tmp have same group as /tmp. + // Because that group is different from the test user's group, chmod + // returns EPERM when trying to setgid on directories, and for files + // chmod filters out the bit. + // We skip the tests with setgid. + private bool CanSetGroup => !PlatformDetection.IsBsdLike; + + private string CreateTestItem(string path = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) + { + path = path ?? GetTestFilePath(null, memberName, lineNumber); + if (IsDirectory) + { + Directory.CreateDirectory(path); + } + else + { + File.Create(path).Dispose(); + } + return path; + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public void Get() + { + string path = CreateTestItem(); + + UnixFileMode mode = GetMode(path); // Doesn't throw. + + Assert.NotEqual((UnixFileMode)(-1), mode); + + UnixFileMode required = UnixFileMode.UserRead | UnixFileMode.UserWrite; + if (IsDirectory) + { + required |= UnixFileMode.UserExecute; + } + Assert.True((mode & required) == required); + + if (!PlatformDetection.IsBrowser) + { + // The umask should prevent this file from being writable by others. + Assert.Equal(UnixFileMode.None, mode & UnixFileMode.OtherWrite); + } + } + + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] + [Theory] + [MemberData(nameof(TestUnixFileModes))] + public void SetThenGet(UnixFileMode mode) + { + if (!CanSetGroup && (mode & UnixFileMode.SetGroup) != 0) + { + return; // Skip + } + if (GetModeNeedsReadableFile) + { + // Ensure the file remains readable. + mode |= UnixFileMode.UserRead; + } + + string path = CreateTestItem(); + + SetMode(path, mode); + + Assert.Equal(mode, GetMode(path)); + } + + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] + [Theory] + [MemberData(nameof(TestUnixFileModes))] + public void SetThenGet_SymbolicLink(UnixFileMode mode) + { + if (!CanSetGroup && (mode & UnixFileMode.SetGroup) != 0) + { + return; // Skip + } + if (GetModeNeedsReadableFile) + { + // Ensure the file remains readable. + mode |= UnixFileMode.UserRead; + } + + string path = CreateTestItem(); + + string linkPath = GetTestFilePath(); + File.CreateSymbolicLink(linkPath, path); + + SetMode(linkPath, mode); + + Assert.Equal(mode, GetMode(linkPath)); + Assert.Equal(mode, GetMode(path)); + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public void FileDoesntExist() + { + string path = GetTestFilePath(); + + if (GetThrowsWhenDoesntExist) + { + Assert.Throws(() => GetMode(path)); + } + else + { + Assert.Equal((UnixFileMode)(-1), GetMode(path)); + } + Assert.Throws(() => SetMode(path, AllAccess)); + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public void FileDoesntExist_SymbolicLink() + { + string path = GetTestFilePath(); + string linkPath = GetTestFilePath(); + File.CreateSymbolicLink(linkPath, path); + + Assert.Throws(() => SetMode(linkPath, AllAccess)); + + if (GetThrowsWhenDoesntExist) + { + Assert.Throws(() => GetMode(path)); + } + else + { + Assert.Equal((UnixFileMode)(-1), GetMode(path)); + } + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public void ParentDirDoesntExist() + { + string path = Path.Combine(GetTestFilePath(), "dir", "file"); + + if (GetThrowsWhenDoesntExist) + { + Assert.Throws(() => GetMode(path)); + } + else + { + Assert.Equal((UnixFileMode)(-1), GetMode(path)); + } + Assert.Throws(() => SetMode(path, AllAccess)); + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public void NullPath() + { + Assert.Throws(() => GetMode(null)); + Assert.Throws(() => SetMode(null, AllAccess)); + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public void InvalidPath() + { + Assert.Throws(() => GetMode(string.Empty)); + Assert.Throws(() => SetMode(string.Empty, AllAccess)); + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Theory] + [InlineData((UnixFileMode)(1 << 12))] + public void InvalidMode(UnixFileMode mode) + { + string path = CreateTestItem(); + + Assert.Throws(() => SetMode(path, mode)); + } + + [PlatformSpecific(TestPlatforms.Windows)] + [Fact] + public void Unsupported() + { + string path = CreateTestItem(); + + Assert.Throws(() => SetMode(path, AllAccess)); + + if (GetModeThrowsPNSE) + { + Assert.Throws(() => GetMode(path)); + } + else + { + Assert.Equal((UnixFileMode)(-1), GetMode(path)); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/CreateDirectory_UnixFileMode.Unix.cs b/src/libraries/System.IO.FileSystem/tests/Directory/CreateDirectory_UnixFileMode.Unix.cs new file mode 100644 index 0000000000000..602483c349144 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Directory/CreateDirectory_UnixFileMode.Unix.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Xunit; + +namespace System.IO.Tests +{ + public class CreateDirectoryWithUnixFileMode : Directory_CreateDirectory + { + // Runs base class tests using CreateDirectory method that takes a UnixFileMode. + public override DirectoryInfo Create(string path) + { + return Directory.CreateDirectory(path, AllAccess); + } + + [Theory] + [MemberData(nameof(TestUnixFileModes))] + public void CreateWithUnixFileMode(UnixFileMode mode) + { + string path = GetRandomDirPath(); + DirectoryInfo dir = Directory.CreateDirectory(path, mode); + + // under Linux the created directory gets mode (mode & ~umask & 01777). + // under OSX, it gets (mode & ~umask & 0777). + UnixFileMode platformFilter = UnixFileMode.SetGroup | UnixFileMode.SetUser | (PlatformDetection.IsBsdLike ? UnixFileMode.StickyBit : UnixFileMode.None); + UnixFileMode expectedMode = mode & ~GetUmask() & ~platformFilter; + Assert.Equal(expectedMode, dir.UnixFileMode); + } + + [Fact] + public void CreateDoesntChangeExistingMode() + { + string path = GetRandomDirPath(); + DirectoryInfo dir = Directory.CreateDirectory(path, AllAccess); + UnixFileMode initialMode = dir.UnixFileMode; + + DirectoryInfo sameDir = Directory.CreateDirectory(path, UnixFileMode.UserRead); + Assert.Equal(initialMode, sameDir.UnixFileMode); + } + + [Theory] + [InlineData((UnixFileMode)(1 << 12), false)] + [InlineData((UnixFileMode)(1 << 12), true)] + public void InvalidModeThrows(UnixFileMode mode, bool alreadyExists) + { + string path = GetRandomDirPath(); + + if (alreadyExists) + { + Directory.CreateDirectory(path); + } + + Assert.Throws(() => Directory.CreateDirectory(path, mode)); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/CreateDirectory_UnixFileMode.Windows.cs b/src/libraries/System.IO.FileSystem/tests/Directory/CreateDirectory_UnixFileMode.Windows.cs new file mode 100644 index 0000000000000..d20b94f0a0d1e --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Directory/CreateDirectory_UnixFileMode.Windows.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.IO.Tests +{ + public class CreateDirectoryWithUnixFileMode : FileSystemTest + { + [Fact] + public void NotSupported() + { + string path = GetRandomDirPath(); + Assert.Throws(() => Directory.CreateDirectory(path, UnixFileMode.UserRead)); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/GetSetUnixFileMode.cs b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/GetSetUnixFileMode.cs new file mode 100644 index 0000000000000..1a0d6fd1dbbe1 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/GetSetUnixFileMode.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.IO.Tests +{ + public class DirectoryInfo_GetSetUnixFileMode : BaseGetSetUnixFileMode + { + protected override bool GetModeThrowsPNSE => false; + + protected override bool IsDirectory => true; + + protected override UnixFileMode GetMode(string path) + => new DirectoryInfo(path).UnixFileMode; + + protected override void SetMode(string path, UnixFileMode mode) + => new DirectoryInfo(path).UnixFileMode = mode; + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/File/GetSetUnixFileMode.cs b/src/libraries/System.IO.FileSystem/tests/File/GetSetUnixFileMode.cs new file mode 100644 index 0000000000000..50366d6e208e0 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/File/GetSetUnixFileMode.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Tests +{ + public class File_GetSetUnixFileMode : BaseGetSetUnixFileMode + { + protected override bool GetThrowsWhenDoesntExist => true; + + protected override UnixFileMode GetMode(string path) + => File.GetUnixFileMode(path); + + protected override void SetMode(string path, UnixFileMode mode) + => File.SetUnixFileMode(path, mode); + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/File/GetSetUnixFileMode_SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/File/GetSetUnixFileMode_SafeFileHandle.cs new file mode 100644 index 0000000000000..6aaf607caea6a --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/File/GetSetUnixFileMode_SafeFileHandle.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; + +namespace System.IO.Tests +{ + public class File_GetSetUnixFileMode_SafeFileHandle : BaseGetSetUnixFileMode + { + protected override bool GetThrowsWhenDoesntExist => true; + protected override bool GetModeNeedsReadableFile => true; + + protected override UnixFileMode GetMode(string path) + { + using SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + return File.GetUnixFileMode(fileHandle); + } + + protected override void SetMode(string path, UnixFileMode mode) + { + using SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + File.SetUnixFileMode(fileHandle, mode); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/File/Open.cs b/src/libraries/System.IO.FileSystem/tests/File/Open.cs index bd9b38b121b88..91030270ce5f7 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/Open.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/Open.cs @@ -86,6 +86,18 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA PreallocationSize = preallocationSize }); } + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize, UnixFileMode unixFileMode) + => File.Open(path, + new FileStreamOptions { + Mode = mode, + Access = access, + Share = share, + BufferSize = bufferSize, + Options = options, + PreallocationSize = preallocationSize, + UnixCreateMode = unixFileMode + }); } public class File_OpenSpecial : FileStream_ctor_str_fm_fa_fs diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/GetSetUnixFileMode.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/GetSetUnixFileMode.cs new file mode 100644 index 0000000000000..1ca1d6040a05a --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/GetSetUnixFileMode.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Tests +{ + public class FileInfo_GetSetUnixFileMode : BaseGetSetUnixFileMode + { + protected override bool GetModeThrowsPNSE => false; + + protected override UnixFileMode GetMode(string path) + => new FileInfo(path).UnixFileMode; + + protected override void SetMode(string path, UnixFileMode mode) + => new FileInfo(path).UnixFileMode = mode; + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs index 36fd39a600056..eef2fcf372eae 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/Open.cs @@ -118,6 +118,18 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA PreallocationSize = preallocationSize }); } + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize, UnixFileMode unixFileMode) + => new FileInfo(path).Open( + new FileStreamOptions { + Mode = mode, + Access = access, + Share = share, + BufferSize = bufferSize, + Options = options, + PreallocationSize = preallocationSize, + UnixCreateMode = unixFileMode + }); } public class FileInfo_OpenSpecial : FileStream_ctor_str_fm_fa_fs diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamOptions.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamOptions.cs index 8dd7f87b84340..2ad939d9933c5 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamOptions.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamOptions.cs @@ -198,5 +198,31 @@ static void Validate(FileStream fs, string expectedPath, bool expectedAsync, boo } } } + + [Fact] + public void UnixCreateModeDefaultsToNull() + { + Assert.Null(new FileStreamOptions().UnixCreateMode); + } + + [PlatformSpecific(TestPlatforms.Windows)] + [Fact] + public void UnixCreateMode_Unsupported() + { + Assert.Throws(() => new FileStreamOptions { UnixCreateMode = null }); + Assert.Throws(() => new FileStreamOptions { UnixCreateMode = UnixFileMode.None }); + Assert.Throws(() => new FileStreamOptions { UnixCreateMode = UnixFileMode.UserRead }); + } + + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public void UnixCreateMode_Supported() + { + Assert.Null(new FileStreamOptions { UnixCreateMode = null }.UnixCreateMode); + Assert.Equal(UnixFileMode.None, new FileStreamOptions { UnixCreateMode = UnixFileMode.None }.UnixCreateMode); + Assert.Equal(UnixFileMode.UserRead, new FileStreamOptions { UnixCreateMode = UnixFileMode.UserRead }.UnixCreateMode); + + Assert.Throws(() => new FileStreamOptions { UnixCreateMode = (UnixFileMode)(1 << 12) }); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Unix.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Unix.cs index 60a2af22d4911..f2555faaacf93 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Unix.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.Unix.cs @@ -3,11 +3,70 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using Xunit; namespace System.IO.Tests { public partial class FileStream_ctor_options { + [Theory] + [MemberData(nameof(TestUnixFileModes))] + public void CreateWithUnixFileMode(UnixFileMode mode) + { + string filename = GetTestFilePath(); + FileStream fs = CreateFileStream(filename, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 0, mode); + fs.Dispose(); + + UnixFileMode platformFilter = PlatformDetection.IsBsdLike + ? (UnixFileMode.SetGroup | UnixFileMode.SetUser | UnixFileMode.StickyBit) + : UnixFileMode.None; + UnixFileMode expectedMode = mode & ~GetUmask() & ~platformFilter; + UnixFileMode actualMode = File.GetUnixFileMode(filename); + Assert.Equal(expectedMode, actualMode); + } + + [Theory] + [InlineData(FileMode.Append)] + [InlineData(FileMode.Create)] + [InlineData(FileMode.OpenOrCreate)] + public void CreateDoesntChangeExistingUnixFileMode(FileMode fileMode) + { + // Create file as writable for user only. + const UnixFileMode mode = UnixFileMode.UserWrite; + string filename = GetTestFilePath(); + CreateFileStream(filename, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 0, mode).Dispose(); + + // Now open with AllAccess. + using FileStream fs = CreateFileStream(filename, fileMode, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 0, AllAccess); + UnixFileMode actualMode = File.GetUnixFileMode(filename); + Assert.Equal(mode, actualMode); + } + + [Theory] + [InlineData(FileMode.Append, true)] + [InlineData(FileMode.Create, true)] + [InlineData(FileMode.CreateNew, true)] + [InlineData(FileMode.OpenOrCreate, true)] + [InlineData(FileMode.Truncate, false)] + [InlineData(FileMode.Open, false)] + public void UnixCreateModeThrowsForNonCreatingFileModes(FileMode fileMode, bool canSetUnixCreateMode) + { + const UnixFileMode unixMode = UnixFileMode.UserWrite; + string filename = GetTestFilePath(); + + if (canSetUnixCreateMode) + { + CreateFileStream(filename, fileMode, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 0, unixMode).Dispose(); + + UnixFileMode actualMode = File.GetUnixFileMode(filename); + Assert.Equal(unixMode, actualMode); + } + else + { + Assert.Throws(() => CreateFileStream(filename, fileMode, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.None, preallocationSize: 0, unixMode)); + } + } + private static long GetAllocatedSize(FileStream fileStream) { bool isOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.cs index bbae2fbb0e240..99b6d246da646 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options.cs @@ -54,6 +54,19 @@ protected virtual FileStream CreateFileStream(string path, FileMode mode, FileAc PreallocationSize = preallocationSize }); + protected virtual FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize, UnixFileMode unixFileMode) + => new FileStream(path, + new FileStreamOptions + { + Mode = mode, + Access = access, + Share = share, + BufferSize = bufferSize, + Options = options, + PreallocationSize = preallocationSize, + UnixCreateMode = unixFileMode + }); + [Fact] public virtual void NegativePreallocationSizeThrows() { diff --git a/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs b/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs index 3d309194df649..cc12a3f26d907 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using Microsoft.DotNet.XUnitExtensions; using Xunit; @@ -108,5 +109,52 @@ protected static bool GetIsCaseSensitiveByProbing(string probingDirectory) return !File.Exists(lowerCased); } } + + protected const UnixFileMode AllAccess = + UnixFileMode.UserRead | + UnixFileMode.UserWrite | + UnixFileMode.UserExecute | + UnixFileMode.GroupRead | + UnixFileMode.GroupWrite | + UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | + UnixFileMode.OtherWrite | + UnixFileMode.OtherExecute; + + public static IEnumerable TestUnixFileModes + { + get + { + // Make combinations of the enum with 0, 1 and 2 bits set. + UnixFileMode[] modes = Enum.GetValues(); + for (int i = 0; i < modes.Length; i++) + { + for (int j = i; j < modes.Length; j++) + { + yield return new object[] { modes[i] | modes[j] }; + } + } + } + } + + private static UnixFileMode s_umask = (UnixFileMode)(-1); + + protected static UnixFileMode GetUmask() + { + if (s_umask == (UnixFileMode)(-1)) + { + // The umask can't be retrieved without changing it. + // We launch a child process to get its value. + using Process px = Process.Start(new ProcessStartInfo + { + FileName = "/bin/sh", + ArgumentList = { "-c", "umask" }, + RedirectStandardOutput = true + }); + string stdout = px.StandardOutput.ReadToEnd().Trim(); + s_umask = (UnixFileMode)Convert.ToInt32(stdout, 8); + } + return s_umask; + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index fbbe5ff070949..1db5a016e3685 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -10,6 +10,7 @@ + @@ -21,7 +22,9 @@ + + @@ -32,6 +35,8 @@ + + @@ -75,9 +80,11 @@ + + diff --git a/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj b/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj index cd8e39c645a8f..9be9a376620fd 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj +++ b/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj @@ -99,8 +99,6 @@ Link="Common\Interop\Unix\Interop.Open.cs" /> - - = 0); return handle; } - internal static SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, - Interop.Sys.Permissions openPermissions = DefaultOpenPermissions, + internal static SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode = null, Func? createOpenException = null) { - return Open(fullPath, mode, access, share, options, preallocationSize, openPermissions, out _, out _, createOpenException); + return Open(fullPath, mode, access, share, options, preallocationSize, unixCreateMode ?? DefaultCreateMode, out _, out _, createOpenException); } - private static SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, - Interop.Sys.Permissions openPermissions, - out long fileLength, - out Interop.Sys.Permissions filePermissions, + private static SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode openPermissions, + out long fileLength, out UnixFileMode filePermissions, Func? createOpenException = null) { // Translate the arguments into arguments for an open call. @@ -305,7 +316,7 @@ private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mo } private bool Init(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, - out long fileLength, out Interop.Sys.Permissions filePermissions) + out long fileLength, out UnixFileMode filePermissions) { Interop.Sys.FileStatus status = default; bool statusHasValue = false; @@ -334,7 +345,7 @@ private bool Init(string path, FileMode mode, FileAccess access, FileShare share } fileLength = status.Size; - filePermissions = (Interop.Sys.Permissions)(status.Mode & (int)Interop.Sys.Permissions.Mask); + filePermissions = ((UnixFileMode)status.Mode) & PermissionMask; } IsAsync = (options & FileOptions.Asynchronous) != 0; diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index d6bbfdd166bf0..66218ec2c61a8 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -35,8 +35,10 @@ internal bool TryGetCachedLength(out long cachedLength) return _lengthCanBeCached && cachedLength >= 0; } - internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode = null) { + Debug.Assert(!unixCreateMode.HasValue); + using (DisableMediaInsertionPrompt.Create()) { // we don't use NtCreateFile as there is no public and reliable way diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 3caedf01be957..63668ee8412d7 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -1098,6 +1098,9 @@ Preallocation size can be requested only for new files. + + UnixCreateMode can be requested only for modes that can create new files. + Type of argument is not compatible with the generic comparer. @@ -3091,6 +3094,9 @@ Strong-name signing is not supported on this platform. + + Unix file modes are not supported on this platform. + This API is specific to the way in which Windows handles asynchronous I/O, and is not supported on this platform. @@ -3760,6 +3766,9 @@ Invalid File or Directory attributes value. + + Invalid UnixFileMode value. + Second path fragment must not be a drive or UNC name. diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 1154e3ccab4b3..6a4a0e062698c 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -462,6 +462,7 @@ + @@ -1890,8 +1891,10 @@ + + @@ -2009,6 +2012,9 @@ Common\Interop\Unix\System.Native\Interop.UnixFileSystemTypes.cs + + Common\Interop\Unix\System.Native\Interop.FChMod.cs + Common\Interop\Unix\System.Native\Interop.FLock.cs @@ -2093,9 +2099,6 @@ Common\Interop\Unix\System.Native\Interop.PathConf.cs - - Common\Interop\Unix\System.Native\Interop.Permissions.cs - Common\Interop\Unix\System.Native\Interop.PosixFAdvise.cs @@ -2192,6 +2195,8 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Unix.cs new file mode 100644 index 0000000000000..d874d1c8c9bc3 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Unix.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO +{ + public static partial class Directory + { + private static DirectoryInfo CreateDirectoryCore(string path, UnixFileMode unixCreateMode) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + if ((unixCreateMode & ~FileSystem.ValidUnixFileModes) != 0) + { + throw new ArgumentException(SR.Arg_InvalidUnixFileMode, nameof(unixCreateMode)); + } + + string fullPath = Path.GetFullPath(path); + + FileSystem.CreateDirectory(fullPath, unixCreateMode); + + return new DirectoryInfo(path, fullPath, isNormalized: true); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Windows.cs new file mode 100644 index 0000000000000..0f96417902ffc --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Windows.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO +{ + public static partial class Directory + { + private static DirectoryInfo CreateDirectoryCore(string path, UnixFileMode unixCreateMode) + => throw new PlatformNotSupportedException(SR.PlatformNotSupported_UnixFileMode); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs index 8e85f3a74e2cc..0087ecfef8f9b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Enumeration; +using System.Runtime.Versioning; namespace System.IO { @@ -34,6 +35,23 @@ public static DirectoryInfo CreateDirectory(string path) return new DirectoryInfo(path, fullPath, isNormalized: true); } + /// + /// Creates all directories and subdirectories in the specified path with the specified permissions unless they already exist. + /// + /// The directory to create. + /// Unix file mode used to create directories. + /// An object that represents the directory at the specified path. This object is returned regardless of whether a directory at the specified path already exists. + /// is a zero-length string, or contains one or more invalid characters. You can query for invalid characters by using the method. + /// is . + /// The caller attempts to use an invalid file mode. + /// The caller does not have the required permission. + /// The specified path exceeds the system-defined maximum length. + /// is a file. + /// A component of the is not a directory. + [UnsupportedOSPlatform("windows")] + public static DirectoryInfo CreateDirectory(string path, UnixFileMode unixCreateMode) + => CreateDirectoryCore(path, unixCreateMode); + // Tests if the given path refers to an existing DirectoryInfo on disk. public static bool Exists([NotNullWhen(true)] string? path) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.Unix.cs new file mode 100644 index 0000000000000..3a2f1cf1a4f1f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.Unix.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + public static partial class File + { + private static UnixFileMode GetUnixFileModeCore(string path) + => FileSystem.GetUnixFileMode(Path.GetFullPath(path)); + + private static UnixFileMode GetUnixFileModeCore(SafeFileHandle fileHandle) + => FileSystem.GetUnixFileMode(fileHandle); + + private static void SetUnixFileModeCore(string path, UnixFileMode mode) + => FileSystem.SetUnixFileMode(Path.GetFullPath(path), mode); + + private static void SetUnixFileModeCore(SafeFileHandle fileHandle, UnixFileMode mode) + => FileSystem.SetUnixFileMode(fileHandle, mode); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.Windows.cs new file mode 100644 index 0000000000000..70ee2e2c142c5 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.Windows.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + public static partial class File + { + private static UnixFileMode GetUnixFileModeCore(string path) + => throw new PlatformNotSupportedException(SR.PlatformNotSupported_UnixFileMode); + + private static UnixFileMode GetUnixFileModeCore(SafeFileHandle fileHandle) + => throw new PlatformNotSupportedException(SR.PlatformNotSupported_UnixFileMode); + + private static void SetUnixFileModeCore(string path, UnixFileMode mode) + => throw new PlatformNotSupportedException(SR.PlatformNotSupported_UnixFileMode); + + private static void SetUnixFileModeCore(SafeFileHandle fileHandle, UnixFileMode mode) + => throw new PlatformNotSupportedException(SR.PlatformNotSupported_UnixFileMode); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index de14b513cfccc..9c8916f8090f2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -17,7 +17,7 @@ namespace System.IO { // Class for creating FileStream objects, and some basic file management // routines such as Delete, etc. - public static class File + public static partial class File { private const int ChunkSize = 8192; private static Encoding? s_UTF8NoBOM; @@ -220,6 +220,51 @@ public static FileAttributes GetAttributes(string path) public static void SetAttributes(string path, FileAttributes fileAttributes) => FileSystem.SetAttributes(Path.GetFullPath(path), fileAttributes); + /// Gets the of the file on the path. + /// The path to the file. + /// The of the file on the path. + /// is a zero-length string, or contains one or more invalid characters. You can query for invalid characters by using the method. + /// is . + /// The caller does not have the required permission. + /// The specified path exceeds the system-defined maximum length. + /// A component of the is not a directory. + /// The file cannot be found. + [UnsupportedOSPlatform("windows")] + public static UnixFileMode GetUnixFileMode(string path) + => GetUnixFileModeCore(path); + + /// Gets the of the specified file handle. + /// The file handle. + /// The of the file handle. + /// The file is closed. + [UnsupportedOSPlatform("windows")] + public static UnixFileMode GetUnixFileMode(SafeFileHandle fileHandle) + => GetUnixFileModeCore(fileHandle); + + /// Sets the specified of the file on the specified path. + /// The path to the file. + /// The unix file mode. + /// is a zero-length string, or contains one or more invalid characters. You can query for invalid characters by using the method. + /// is . + /// The caller attempts to use an invalid file mode. + /// The caller does not have the required permission. + /// The specified path exceeds the system-defined maximum length. + /// A component of the is not a directory. + /// The file cannot be found. + [UnsupportedOSPlatform("windows")] + public static void SetUnixFileMode(string path, UnixFileMode mode) + => SetUnixFileModeCore(path, mode); + + /// Sets the specified of the specified file handle. + /// The file handle. + /// The unix file mode. + /// The caller attempts to use an invalid file mode. + /// The caller does not have the required permission. + /// The file is closed. + [UnsupportedOSPlatform("windows")] + public static void SetUnixFileMode(SafeFileHandle fileHandle, UnixFileMode mode) + => SetUnixFileModeCore(fileHandle, mode); + public static FileStream OpenRead(string path) => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs index e90408a7481e2..0621f3f82cf6e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStatus.Unix.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; namespace System.IO { @@ -10,13 +11,14 @@ internal partial struct FileStatus { private const int NanosecondsPerTick = 100; + private const int InitializedExistsBrokenLink = -4; // target is link with no target. private const int InitializedExistsDir = -3; // target is directory. private const int InitializedExistsFile = -2; // target is file. private const int InitializedNotExists = -1; // entry does not exist. private const int Uninitialized = 0; // uninitialized, '0' to make default(FileStatus) uninitialized. // Tracks the initialization state. - // < 0 : initialized succesfully. Value is InitializedNotExists, InitializedExistsFile or InitializedExistsDir. + // < 0 : initialized succesfully. Value is InitializedNotExists, InitializedExistsFile, InitializedExistsDir or InitializedExistsBrokenLink. // 0 : uninitialized. // > 0 : initialized with error. Value is raw errno. private int _state; @@ -29,6 +31,8 @@ internal partial struct FileStatus private bool IsDir => _state == InitializedExistsDir; + private bool IsBrokenLink => _state == InitializedExistsBrokenLink; + // Check if the main path (without following symlinks) has the hidden attribute set. private bool HasHiddenFlag { @@ -47,15 +51,15 @@ private bool HasReadOnlyFlag { Debug.Assert(_state != Uninitialized); // Use this after EnsureCachesInitialized has been called. - if (!EntryExists) + if (!EntryExists || IsBrokenLink) { return false; } #if TARGET_BROWSER - var mode = (Interop.Sys.Permissions)(_fileCache.Mode & (int)Interop.Sys.Permissions.Mask); - bool isUserReadOnly = (mode & Interop.Sys.Permissions.S_IRUSR) != 0 && // has read permission - (mode & Interop.Sys.Permissions.S_IWUSR) == 0; // but not write permission + var mode = ((UnixFileMode)_fileCache.Mode & FileSystem.ValidUnixFileModes); + bool isUserReadOnly = (mode & UnixFileMode.UserRead) != 0 && // has read permission + (mode & UnixFileMode.UserWrite) == 0; // but not write permission return isUserReadOnly; #else if (_isReadOnlyCache == 0) @@ -83,14 +87,14 @@ private bool HasReadOnlyFlag private bool IsModeReadOnlyCore() { - var mode = (Interop.Sys.Permissions)(_fileCache.Mode & (int)Interop.Sys.Permissions.Mask); + var mode = ((UnixFileMode)_fileCache.Mode & FileSystem.ValidUnixFileModes); - bool isUserReadOnly = (mode & Interop.Sys.Permissions.S_IRUSR) != 0 && // has read permission - (mode & Interop.Sys.Permissions.S_IWUSR) == 0; // but not write permission - bool isGroupReadOnly = (mode & Interop.Sys.Permissions.S_IRGRP) != 0 && // has read permission - (mode & Interop.Sys.Permissions.S_IWGRP) == 0; // but not write permission - bool isOtherReadOnly = (mode & Interop.Sys.Permissions.S_IROTH) != 0 && // has read permission - (mode & Interop.Sys.Permissions.S_IWOTH) == 0; // but not write permission + bool isUserReadOnly = (mode & UnixFileMode.UserRead) != 0 && // has read permission + (mode & UnixFileMode.UserWrite) == 0; // but not write permission + bool isGroupReadOnly = (mode & UnixFileMode.GroupRead) != 0 && // has read permission + (mode & UnixFileMode.GroupWrite) == 0; // but not write permission + bool isOtherReadOnly = (mode & UnixFileMode.OtherRead) != 0 && // has read permission + (mode & UnixFileMode.OtherWrite) == 0; // but not write permission // If they are all the same, no need to check user/group. if ((isUserReadOnly == isGroupReadOnly) && (isGroupReadOnly == isOtherReadOnly)) @@ -241,20 +245,21 @@ internal void SetAttributes(string path, FileAttributes attributes, bool asDirec // The only thing we can reasonably change is whether the file object is readonly by changing permissions. - int newMode = _fileCache.Mode; + int oldMode = _fileCache.Mode & (int)FileSystem.ValidUnixFileModes; + int newMode = oldMode; if ((attributes & FileAttributes.ReadOnly) != 0) { // Take away all write permissions from user/group/everyone - newMode &= ~(int)(Interop.Sys.Permissions.S_IWUSR | Interop.Sys.Permissions.S_IWGRP | Interop.Sys.Permissions.S_IWOTH); + newMode &= ~(int)(UnixFileMode.UserWrite | UnixFileMode.GroupWrite | UnixFileMode.OtherWrite); } - else if ((newMode & (int)Interop.Sys.Permissions.S_IRUSR) != 0) + else if ((newMode & (int)UnixFileMode.UserRead) != 0) { // Give write permission to the owner if the owner has read permission - newMode |= (int)Interop.Sys.Permissions.S_IWUSR; + newMode |= (int)UnixFileMode.UserWrite; } // Change the permissions on the file - if (newMode != _fileCache.Mode) + if (newMode != oldMode) { Interop.CheckIo(Interop.Sys.ChMod(path, newMode), path, asDirectory); } @@ -399,17 +404,69 @@ internal long GetLength(ReadOnlySpan path, bool continueOnError = false) return EntryExists ? _fileCache.Size : 0; } + internal UnixFileMode GetUnixFileMode(ReadOnlySpan path, bool continueOnError = false) + => GetUnixFileMode(handle: null, path, continueOnError); + + internal UnixFileMode GetUnixFileMode(SafeFileHandle handle, bool continueOnError = false) + => GetUnixFileMode(handle, handle.Path, continueOnError); + + private UnixFileMode GetUnixFileMode(SafeFileHandle? handle, ReadOnlySpan path, bool continueOnError = false) + { + EnsureCachesInitialized(handle, path, continueOnError); + + if (!EntryExists || IsBrokenLink) + return (UnixFileMode)(-1); + + return (UnixFileMode)(_fileCache.Mode & (int)FileSystem.ValidUnixFileModes); + } + + internal void SetUnixFileMode(string path, UnixFileMode mode) + => SetUnixFileMode(handle: null, path, mode); + + internal void SetUnixFileMode(SafeFileHandle handle, UnixFileMode mode) + => SetUnixFileMode(handle, handle.Path, mode); + + private void SetUnixFileMode(SafeFileHandle? handle, string? path, UnixFileMode mode) + { + if ((mode & ~FileSystem.ValidUnixFileModes) != 0) + { + throw new ArgumentException(SR.Arg_InvalidUnixFileMode, nameof(UnixFileMode)); + } + + // Use ThrowNotFound to throw the appropriate exception when the file doesn't exist. + if (handle is null && path is not null) + { + EnsureCachesInitialized(path); + + if (!EntryExists || IsBrokenLink) + FileSystemInfo.ThrowNotFound(path); + } + + // Linux does not support link permissions. + // To have consistent cross-platform behavior we operate on the link target. + int rv = handle is not null ? Interop.Sys.FChMod(handle, (int)mode) + : Interop.Sys.ChMod(path!, (int)mode); + Interop.CheckIo(rv, path); + + InvalidateCaches(); + } + + internal void RefreshCaches(ReadOnlySpan path) + => RefreshCaches(handle: null, path); + // Tries to refresh the lstat cache (_fileCache). // This method should not throw. Instead, we store the results, and we will throw when the user attempts to access any of the properties when there was a failure - internal void RefreshCaches(ReadOnlySpan path) + internal void RefreshCaches(SafeFileHandle? handle, ReadOnlySpan path) { - path = Path.TrimEndingDirectorySeparator(path); + Debug.Assert(handle is not null || path.Length > 0); #if !TARGET_BROWSER _isReadOnlyCache = -1; #endif + int rv = handle is not null ? + Interop.Sys.FStat(handle, out _fileCache) : + Interop.Sys.LStat(Path.TrimEndingDirectorySeparator(path), out _fileCache); - int rv = Interop.Sys.LStat(path, out _fileCache); if (rv < 0) { Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); @@ -430,21 +487,37 @@ internal void RefreshCaches(ReadOnlySpan path) // Check if the main path is a directory, or a link to a directory. int fileType = _fileCache.Mode & Interop.Sys.FileTypes.S_IFMT; - bool isDirectory = fileType == Interop.Sys.FileTypes.S_IFDIR || - (fileType == Interop.Sys.FileTypes.S_IFLNK - && Interop.Sys.Stat(path, out Interop.Sys.FileStatus target) == 0 - && (target.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR); + bool isDirectory = fileType == Interop.Sys.FileTypes.S_IFDIR; + + if (fileType == Interop.Sys.FileTypes.S_IFLNK) + { + if (Interop.Sys.Stat(path, out Interop.Sys.FileStatus target) == 0) + { + isDirectory = (target.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR; + + // Make GetUnixFileMode return target permissions. + _fileCache.Mode = Interop.Sys.FileTypes.S_IFLNK | (target.Mode & (int)FileSystem.ValidUnixFileModes); + } + else + { + _state = InitializedExistsBrokenLink; + return; + } + } _state = isDirectory ? InitializedExistsDir : InitializedExistsFile; } + internal void EnsureCachesInitialized(ReadOnlySpan path, bool continueOnError = false) + => EnsureCachesInitialized(handle: null, path, continueOnError); + // Checks if the file cache is uninitialized and refreshes it's value. // If it failed, and continueOnError is set to true, this method will throw. - internal void EnsureCachesInitialized(ReadOnlySpan path, bool continueOnError = false) + internal void EnsureCachesInitialized(SafeFileHandle? handle, ReadOnlySpan path, bool continueOnError = false) { if (_state == Uninitialized) { - RefreshCaches(path); + RefreshCaches(handle, path); } if (!continueOnError) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index b86ba855be3b7..83b406d4eec90 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -200,17 +200,26 @@ public FileStream(string path, FileStreamOptions options) FileStreamHelpers.ValidateArgumentsForPreallocation(options.Mode, options.Access); } + if (options.UnixCreateMode.HasValue) + { + // Only allow UnixCreateMode for file modes that can create a new file. + if (options.Mode == FileMode.Truncate || options.Mode == FileMode.Open) + { + throw new ArgumentException(SR.Argument_InvalidUnixCreateMode, nameof(options)); + } + } + FileStreamHelpers.SerializationGuard(options.Access); _strategy = FileStreamHelpers.ChooseStrategy( - this, path, options.Mode, options.Access, options.Share, options.BufferSize, options.Options, options.PreallocationSize); + this, path, options.Mode, options.Access, options.Share, options.BufferSize, options.Options, options.PreallocationSize, options.UnixCreateMode); } private FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize, options, preallocationSize); - _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options, preallocationSize); + _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options, preallocationSize, unixCreateMode: null); } [Obsolete("FileStream.Handle has been deprecated. Use FileStream's SafeFileHandle property instead.")] diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs index 491d739de40f5..56f5c545b0ef6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.Versioning; + namespace System.IO { public sealed class FileStreamOptions @@ -11,6 +13,7 @@ public sealed class FileStreamOptions private FileOptions _options; private long _preallocationSize; private int _bufferSize = FileStream.DefaultBufferSize; + private UnixFileMode? _unixCreateMode; /// /// One of the enumeration values that determines how to open or create the file. @@ -109,5 +112,32 @@ public int BufferSize get => _bufferSize; set => _bufferSize = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum); } + + /// + /// Unix file mode used when a new file is created. + /// + /// When is an invalid file mode. + public UnixFileMode? UnixCreateMode + { + get + { + return _unixCreateMode; + } + [UnsupportedOSPlatform("windows")] + set + { + if (OperatingSystem.IsWindows()) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_UnixFileMode); + } + + if (value.HasValue && ((value & ~FileSystem.ValidUnixFileModes) != 0)) + { + throw new ArgumentException(SR.Arg_InvalidUnixFileMode, nameof(UnixCreateMode)); + } + + _unixCreateMode = value; + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs index 751a59f9eab94..a2aa1b070f227 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs @@ -16,13 +16,25 @@ internal static partial class FileSystem // See: https://man7.org/linux/man-pages/man7/path_resolution.7.html private const int MaxFollowedLinks = 40; + // This gets filtered by umask. + internal const UnixFileMode DefaultUnixCreateDirectoryMode = + UnixFileMode.UserRead | + UnixFileMode.UserWrite | + UnixFileMode.UserExecute | + UnixFileMode.GroupRead | + UnixFileMode.GroupWrite | + UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | + UnixFileMode.OtherWrite | + UnixFileMode.OtherExecute; + public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite) { long fileLength; - Interop.Sys.Permissions filePermissions; + UnixFileMode filePermissions; using SafeFileHandle src = SafeFileHandle.OpenReadOnly(sourceFullPath, FileOptions.None, out fileLength, out filePermissions); using SafeFileHandle dst = SafeFileHandle.Open(destFullPath, overwrite ? FileMode.Create : FileMode.CreateNew, - FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize: 0, openPermissions: filePermissions, + FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize: 0, filePermissions, (Interop.ErrorInfo error, Interop.Sys.OpenFlags flags, string path) => CreateOpenException(error, flags, path)); Interop.CheckIo(Interop.Sys.CopyFile(src, dst, fileLength)); @@ -279,6 +291,9 @@ public static void DeleteFile(string fullPath) } public static void CreateDirectory(string fullPath) + => CreateDirectory(fullPath, DefaultUnixCreateDirectoryMode); + + public static void CreateDirectory(string fullPath, UnixFileMode unixCreateMode) { // The argument is a full path, which means it is an absolute path that // doesn't contain "//", "/./", and "/../". @@ -290,7 +305,7 @@ public static void CreateDirectory(string fullPath) return; // fullPath is '/'. } - int result = Interop.Sys.MkDir(fullPath, (int)Interop.Sys.Permissions.Mask); + int result = Interop.Sys.MkDir(fullPath, (int)unixCreateMode); if (result == 0) { return; // Created directory. @@ -303,7 +318,7 @@ public static void CreateDirectory(string fullPath) } else if (errorInfo.Error == Interop.Error.ENOENT) // Some parts of the path don't exist yet. { - CreateParentsAndDirectory(fullPath); + CreateParentsAndDirectory(fullPath, unixCreateMode); } else { @@ -311,7 +326,7 @@ public static void CreateDirectory(string fullPath) } } - private static void CreateParentsAndDirectory(string fullPath) + private static void CreateParentsAndDirectory(string fullPath, UnixFileMode unixCreateMode) { // Try create parents bottom to top and track those that could not // be created due to missing parents. Then create them top to bottom. @@ -334,7 +349,7 @@ private static void CreateParentsAndDirectory(string fullPath) } ReadOnlySpan mkdirPath = fullPath.AsSpan(0, i); - int result = Interop.Sys.MkDir(mkdirPath, (int)Interop.Sys.Permissions.Mask); + int result = Interop.Sys.MkDir(mkdirPath, (int)unixCreateMode); if (result == 0) { break; // Created parent. @@ -366,7 +381,7 @@ private static void CreateParentsAndDirectory(string fullPath) for (i = stackDir.Length - 1; i >= 0; i--) { ReadOnlySpan mkdirPath = fullPath.AsSpan(0, stackDir[i]); - int result = Interop.Sys.MkDir(mkdirPath, (int)Interop.Sys.Permissions.Mask); + int result = Interop.Sys.MkDir(mkdirPath, (int)unixCreateMode); if (result < 0) { Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); @@ -588,6 +603,25 @@ public static FileAttributes GetAttributes(string fullPath) public static void SetAttributes(string fullPath, FileAttributes attributes) => default(FileStatus).SetAttributes(fullPath, attributes, asDirectory: false); + public static UnixFileMode GetUnixFileMode(string fullPath) + { + UnixFileMode mode = default(FileStatus).GetUnixFileMode(fullPath); + + if (mode == (UnixFileMode)(-1)) + FileSystemInfo.ThrowNotFound(fullPath); + + return mode; + } + + public static UnixFileMode GetUnixFileMode(SafeFileHandle fileHandle) + => default(FileStatus).GetUnixFileMode(fileHandle); + + public static void SetUnixFileMode(string fullPath, UnixFileMode mode) + => default(FileStatus).SetUnixFileMode(fullPath, mode); + + public static void SetUnixFileMode(SafeFileHandle fileHandle, UnixFileMode mode) + => default(FileStatus).SetUnixFileMode(fileHandle, mode); + public static DateTimeOffset GetCreationTime(string fullPath) => default(FileStatus).GetCreationTime(fullPath).UtcDateTime; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs index aee8736cd8321..392791fd462b6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs @@ -5,6 +5,20 @@ namespace System.IO { internal static partial class FileSystem { + internal const UnixFileMode ValidUnixFileModes = + UnixFileMode.UserRead | + UnixFileMode.UserWrite | + UnixFileMode.UserExecute | + UnixFileMode.GroupRead | + UnixFileMode.GroupWrite | + UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | + UnixFileMode.OtherWrite | + UnixFileMode.OtherExecute | + UnixFileMode.StickyBit | + UnixFileMode.SetGroup | + UnixFileMode.SetUser; + internal static void VerifyValidPath(string path, string argName) { ArgumentException.ThrowIfNullOrEmpty(path, argName); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs index f74e3c411dd6f..a1113a1fd10e8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs @@ -63,6 +63,12 @@ internal DateTimeOffset LastWriteTimeCore internal long LengthCore => _fileStatus.GetLength(FullPath); + internal UnixFileMode UnixFileModeCore + { + get => _fileStatus.GetUnixFileMode(FullPath); + set => _fileStatus.SetUnixFileMode(FullPath, value); + } + public void Refresh() { _linkTargetIsValid = false; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs index cadd69ce7e67a..9145e94dc0097 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs @@ -130,6 +130,14 @@ internal long LengthCore } } +#pragma warning disable CA1822 + internal UnixFileMode UnixFileModeCore + { + get => (UnixFileMode)(-1); + set => throw new PlatformNotSupportedException(SR.PlatformNotSupported_UnixFileMode); + } +#pragma warning restore CA1822 + private void EnsureDataInitialized() { if (_dataInitialized == -1) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs index 26a06b20986f8..b5ddf7282c72b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.Serialization; +using System.Runtime.Versioning; namespace System.IO { @@ -130,6 +131,27 @@ public string? LinkTarget } } + /// Gets or sets the Unix file mode for the current file or directory. + /// of the current . + /// The caller attempts to set an invalid file mode. + /// The caller does not have the required permission. + /// The specified path exceeds the system-defined maximum length. + /// The specified path is invalid. Only thrown when setting the property value. + /// The specified file doesn't exist. Only thrown when setting the property value. + /// cannot initialize the data. + /// + /// + /// The value may be cached when either the value itself or other properties are accessed. To get the latest value, call the method. + /// + /// If the path doesn't exist as of the last cached state, the return value is `(UnixFileMode)(-1)`. or can only be thrown when setting the value. + /// + public UnixFileMode UnixFileMode + { + get => UnixFileModeCore; + [UnsupportedOSPlatform("windows")] + set => UnixFileModeCore = value; + } + /// /// Creates a symbolic link located in that points to the specified . /// diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs index 82ed7659b8363..0ed73e5e74cc7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs @@ -14,8 +14,8 @@ internal AsyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access { } - internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) - : base(path, mode, access, share, options, preallocationSize) + internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode) + : base(path, mode, access, share, options, preallocationSize, unixCreateMode) { } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs index 3df3aa692f64c..534a49217408c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs @@ -13,8 +13,8 @@ private static OSFileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, Fi new UnixFileStreamStrategy(handle, access); #pragma warning restore IDE0060 - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) => - new UnixFileStreamStrategy(path, mode, access, share, options, preallocationSize); + private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode) => + new UnixFileStreamStrategy(path, mode, access, share, options, preallocationSize, unixCreateMode); internal static long CheckFileCall(long result, string? path, bool ignoreNotSupported = false) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs index 3e90bf225385c..4cc2b56008c58 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs @@ -31,10 +31,10 @@ private static OSFileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, Fi new AsyncWindowsFileStreamStrategy(handle, access) : new SyncWindowsFileStreamStrategy(handle, access); - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) => + private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode) => (options & FileOptions.Asynchronous) != 0 ? - new AsyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize) : - new SyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize); + new AsyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize, unixCreateMode) : + new SyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize, unixCreateMode); internal static void FlushToDisk(SafeFileHandle handle) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs index 5a067fd76ee46..e0aeb3da747e5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs @@ -19,10 +19,10 @@ internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFil return WrapIfDerivedType(fileStream, strategy); } - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) + internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode) { FileStreamStrategy strategy = - EnableBufferingIfNeeded(ChooseStrategyCore(path, mode, access, share, options, preallocationSize), bufferSize); + EnableBufferingIfNeeded(ChooseStrategyCore(path, mode, access, share, options, preallocationSize, unixCreateMode), bufferSize); return WrapIfDerivedType(fileStream, strategy); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs index 9502490f59079..43f7152167cd3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs @@ -37,13 +37,13 @@ internal OSFileStreamStrategy(SafeFileHandle handle, FileAccess access) _fileHandle = handle; } - internal OSFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + internal OSFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode) { string fullPath = Path.GetFullPath(path); _access = access; - _fileHandle = SafeFileHandle.Open(fullPath, mode, access, share, options, preallocationSize); + _fileHandle = SafeFileHandle.Open(fullPath, mode, access, share, options, preallocationSize, unixCreateMode); try { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs index 5e2513252a510..6166df51ad83e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs @@ -11,8 +11,8 @@ internal SyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access) { } - internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) - : base(path, mode, access, share, options, preallocationSize) + internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode) + : base(path, mode, access, share, options, preallocationSize, unixCreateMode) { } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/UnixFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/UnixFileStreamStrategy.cs index 3c54f1b23a476..0ab7d5cb7f086 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/UnixFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/UnixFileStreamStrategy.cs @@ -11,8 +11,8 @@ internal UnixFileStreamStrategy(SafeFileHandle handle, FileAccess access) : base { } - internal UnixFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) : - base(path, mode, access, share, options, preallocationSize) + internal UnixFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize, UnixFileMode? unixCreateMode) : + base(path, mode, access, share, options, preallocationSize, unixCreateMode) { } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/UnixFileMode.cs b/src/libraries/System.Private.CoreLib/src/System/IO/UnixFileMode.cs new file mode 100644 index 0000000000000..457f1cff529c5 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/UnixFileMode.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO; + +/// +/// Represents the Unix filesystem permissions. +/// This enumeration supports a bitwise combination of its member values. +/// +[Flags] +public enum UnixFileMode +{ + /// + /// No permissions. + /// + None = 0, + /// + /// Execute permission for others. + /// + OtherExecute = 1, + /// + /// Write permission for others. + /// + OtherWrite = 2, + /// + /// Read permission for others. + /// + OtherRead = 4, + /// + /// Execute permission for group. + /// + GroupExecute = 8, + /// + /// Write permission for group. + /// + GroupWrite = 16, + /// + /// Read permission for group. + /// + GroupRead = 32, + /// + /// Execute permission for owner. + /// + UserExecute = 64, + /// + /// Write permission for owner. + /// + UserWrite = 128, + /// + /// Read permission for owner. + /// + UserRead = 256, + /// + /// Sticky bit permission. + /// + StickyBit = 512, + /// + /// Set Group permission. + /// + SetGroup = 1024, + /// + /// Set User permission. + /// + SetUser = 2048, +} diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 4d592b8f1f628..27ab63296fb7c 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -9292,6 +9292,8 @@ public override void WriteByte(byte value) { } public static partial class Directory { public static System.IO.DirectoryInfo CreateDirectory(string path) { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public static System.IO.DirectoryInfo CreateDirectory(string path, System.IO.UnixFileMode unixCreateMode) { throw null; } public static System.IO.FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) { throw null; } public static void Delete(string path) { } public static void Delete(string path, bool recursive) { } @@ -9435,6 +9437,10 @@ public static void Encrypt(string path) { } public static System.DateTime GetLastAccessTimeUtc(string path) { throw null; } public static System.DateTime GetLastWriteTime(string path) { throw null; } public static System.DateTime GetLastWriteTimeUtc(string path) { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public static System.IO.UnixFileMode GetUnixFileMode(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public static System.IO.UnixFileMode GetUnixFileMode(string path) { throw null; } public static void Move(string sourceFileName, string destFileName) { } public static void Move(string sourceFileName, string destFileName, bool overwrite) { } public static System.IO.FileStream Open(string path, System.IO.FileMode mode) { throw null; } @@ -9469,6 +9475,10 @@ public static void SetLastAccessTime(string path, System.DateTime lastAccessTime public static void SetLastAccessTimeUtc(string path, System.DateTime lastAccessTimeUtc) { } public static void SetLastWriteTime(string path, System.DateTime lastWriteTime) { } public static void SetLastWriteTimeUtc(string path, System.DateTime lastWriteTimeUtc) { } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public static void SetUnixFileMode(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle, System.IO.UnixFileMode mode) { } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public static void SetUnixFileMode(string path, System.IO.UnixFileMode mode) { } public static void WriteAllBytes(string path, byte[] bytes) { } public static System.Threading.Tasks.Task WriteAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static void WriteAllLines(string path, System.Collections.Generic.IEnumerable contents) { } @@ -9675,6 +9685,7 @@ public FileStreamOptions() { } public System.IO.FileOptions Options { get { throw null; } set { } } public long PreallocationSize { get { throw null; } set { } } public System.IO.FileShare Share { get { throw null; } set { } } + public System.IO.UnixFileMode? UnixCreateMode { get { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] set { } } } public abstract partial class FileSystemInfo : System.MarshalByRefObject, System.Runtime.Serialization.ISerializable { @@ -9694,6 +9705,7 @@ protected FileSystemInfo(System.Runtime.Serialization.SerializationInfo info, Sy public System.DateTime LastWriteTimeUtc { get { throw null; } set { } } public string? LinkTarget { get { throw null; } } public abstract string Name { get; } + public System.IO.UnixFileMode UnixFileMode { get { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] set { } } public void CreateAsSymbolicLink(string pathToTarget) { } public abstract void Delete(); public virtual void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } @@ -10151,6 +10163,23 @@ public virtual void WriteLine(ulong value) { } public virtual System.Threading.Tasks.Task WriteLineAsync(string? value) { throw null; } public virtual System.Threading.Tasks.Task WriteLineAsync(System.Text.StringBuilder? value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } + [System.FlagsAttribute] + public 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, + } public partial class UnmanagedMemoryStream : System.IO.Stream { protected UnmanagedMemoryStream() { } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj index 50d8fa1b5989c..547a33c182cae 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj @@ -91,8 +91,6 @@ - - - - - - /// Checks the file has the correct permissions and attempts to modify them if they're inappropriate. - /// - /// - /// The file stream to check. - /// - /// - /// The current userId from GetEUid(). - /// - private static void EnsureFilePermissions(FileStream stream, uint userId) - { - // Verify that we're creating files with u+rw and g-rw, o-rw. - const Interop.Sys.Permissions requiredPermissions = - Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR; - - const Interop.Sys.Permissions forbiddenPermissions = - Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP | - Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; - - Interop.Sys.FileStatus stat; - if (Interop.Sys.FStat(stream.SafeFileHandle, out stat) != 0) - { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - throw new CryptographicException( - SR.Cryptography_FileStatusError, - new IOException(error.GetErrorMessage(), error.RawErrno)); - } - - if (stat.Uid != userId) - { - throw new CryptographicException(SR.Format(SR.Cryptography_OwnerNotCurrentUser, stream.Name)); - } - - if ((stat.Mode & (int)requiredPermissions) != (int)requiredPermissions || - (stat.Mode & (int)forbiddenPermissions) != 0) - { - if (Interop.Sys.FChMod(stream.SafeFileHandle, (int)requiredPermissions) < 0) - { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - throw new CryptographicException( - SR.Format(SR.Cryptography_InvalidFilePermissions, stream.Name), - new IOException(error.GetErrorMessage(), error.RawErrno)); - } - - // Verify the chmod applied. - if (Interop.Sys.FStat(stream.SafeFileHandle, out stat) != 0) - { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - throw new CryptographicException( - SR.Cryptography_FileStatusError, - new IOException(error.GetErrorMessage(), error.RawErrno)); - } - - if ((stat.Mode & (int)requiredPermissions) != (int)requiredPermissions || - (stat.Mode & (int)forbiddenPermissions) != 0) - { - throw new CryptographicException(SR.Format(SR.Cryptography_InvalidFilePermissions, stream.Name)); - } - } - } - internal static IStorePal OpenDisallowedStore(OpenFlags openFlags) { string storePath = GetStorePath(X509Store.DisallowedStoreName);