diff options
author | Eric Erhardt <eric.erhardt@microsoft.com> | 2021-07-14 07:14:24 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-14 07:14:24 +0300 |
commit | 5b0c6dd5756eea63a57b1f69b182f7386da0f631 (patch) | |
tree | 0cadbafde08d75b9993efeb7766bb85df3fc3427 /src | |
parent | 6a7603e4edfd273df69726c8e589a121bd165649 (diff) |
Compression.ZipFile support for Unix Permissions (#55531)
* Compression.ZipFile support for Unix Permissions
When running on Unix, capture the file's permissions on ZipFile Create and write the captured file permissions on ZipFile Extract.
Fix #1548
Diffstat (limited to 'src')
9 files changed, 299 insertions, 8 deletions
diff --git a/src/libraries/System.IO.Compression.ZipFile/src/Resources/Strings.resx b/src/libraries/System.IO.Compression.ZipFile/src/Resources/Strings.resx index 5d25044f7b4..b8f50177b39 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression.ZipFile/src/Resources/Strings.resx @@ -57,10 +57,46 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> + <data name="ArgumentOutOfRange_FileLengthTooBig" xml:space="preserve"> + <value>Specified file length was too large for the file system.</value> + </data> <data name="IO_DirectoryNameWithData" xml:space="preserve"> <value>Zip entry name ends in directory separator character but contains data.</value> </data> <data name="IO_ExtractingResultsInOutside" xml:space="preserve"> <value>Extracting Zip entry would have resulted in a file outside the specified destination directory.</value> </data> -</root>
\ No newline at end of file + <data name="IO_FileExists_Name" xml:space="preserve"> + <value>The file '{0}' already exists.</value> + </data> + <data name="IO_FileNotFound" xml:space="preserve"> + <value>Unable to find the specified file.</value> + </data> + <data name="IO_FileNotFound_FileName" xml:space="preserve"> + <value>Could not find file '{0}'.</value> + </data> + <data name="IO_PathNotFound_NoPathName" xml:space="preserve"> + <value>Could not find a part of the path.</value> + </data> + <data name="IO_PathNotFound_Path" xml:space="preserve"> + <value>Could not find a part of the path '{0}'.</value> + </data> + <data name="IO_PathTooLong" xml:space="preserve"> + <value>The specified file name or path is too long, or a component of the specified path is too long.</value> + </data> + <data name="IO_PathTooLong_Path" xml:space="preserve"> + <value>The path '{0}' is too long, or a component of the specified path is too long.</value> + </data> + <data name="IO_SharingViolation_File" xml:space="preserve"> + <value>The process cannot access the file '{0}' because it is being used by another process.</value> + </data> + <data name="IO_SharingViolation_NoFileName" xml:space="preserve"> + <value>The process cannot access the file because it is being used by another process.</value> + </data> + <data name="UnauthorizedAccess_IODenied_NoPathName" xml:space="preserve"> + <value>Access to the path is denied.</value> + </data> + <data name="UnauthorizedAccess_IODenied_Path" xml:space="preserve"> + <value>Access to the path '{0}' is denied.</value> + </data> +</root> 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 93eb3e71933..2110ae810ea 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 @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> - <TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks> + <TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser</TargetFrameworks> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> @@ -14,10 +14,26 @@ <Compile Include="$(CommonPath)System\IO\PathInternal.CaseSensitivity.cs" Link="Common\System\IO\PathInternal.CaseSensitivity.cs" /> </ItemGroup> + <!-- Unix specific files --> + <ItemGroup Condition=" '$(TargetsUnix)' == 'true' or '$(TargetsBrowser)' == 'true' "> + <Compile Include="System\IO\Compression\ZipFileExtensions.ZipArchive.Create.Unix.cs" /> + <Compile Include="System\IO\Compression\ZipFileExtensions.ZipArchiveEntry.Extract.Unix.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\Interop.IOErrors.cs" + Link="Common\Interop\Unix\Interop.IOErrors.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs" + Link="Common\Interop\Unix\Interop.Libraries.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\Interop.Errors.cs" + Link="Common\Interop\Unix\System.Native\Interop.Errors.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.FChMod.cs" + Link="Common\Interop\Unix\System.Native\Interop.FChMod.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs" + Link="Common\Interop\Unix\System.Native\Interop.Stat.cs" /> + </ItemGroup> <ItemGroup> <Reference Include="System.IO.Compression" /> <Reference Include="System.IO.FileSystem" /> <Reference Include="System.Runtime" /> <Reference Include="System.Runtime.Extensions" /> + <Reference Include="System.Runtime.InteropServices" /> </ItemGroup> </Project> diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Create.Unix.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Create.Unix.cs new file mode 100644 index 00000000000..d2e4f44a675 --- /dev/null +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Create.Unix.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.Compression +{ + public static partial class ZipFileExtensions + { + static partial void SetExternalAttributes(FileStream fs, ZipArchiveEntry entry) + { + Interop.Sys.FileStatus status; + Interop.CheckIo(Interop.Sys.FStat(fs.SafeFileHandle, out status), fs.Name); + + entry.ExternalAttributes |= status.Mode << 16; + } + } +} diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Create.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Create.cs index 00d82c242b0..c9199a97f2a 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Create.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Create.cs @@ -94,7 +94,7 @@ namespace System.IO.Compression // Argument checking gets passed down to FileStream's ctor and CreateEntry - using (Stream fs = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 0x1000, useAsync: false)) + using (FileStream fs = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 0x1000, useAsync: false)) { ZipArchiveEntry entry = compressionLevel.HasValue ? destination.CreateEntry(entryName, compressionLevel.Value) @@ -109,11 +109,15 @@ namespace System.IO.Compression entry.LastWriteTime = lastWrite; + SetExternalAttributes(fs, entry); + using (Stream es = entry.Open()) fs.CopyTo(es); return entry; } } + + static partial void SetExternalAttributes(FileStream fs, ZipArchiveEntry entry); } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.Unix.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.Unix.cs new file mode 100644 index 00000000000..4da8b87b43e --- /dev/null +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.Unix.cs @@ -0,0 +1,29 @@ +// 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.Compression +{ + public static partial class ZipFileExtensions + { + static partial void ExtractExternalAttributes(FileStream fs, ZipArchiveEntry entry) + { + // Only extract USR, GRP, and OTH file permissions, and ignore + // S_ISUID, S_ISGID, and S_ISVTX bits. This matches unzip's default behavior. + // It is off by default because of this comment: + + // "It's possible that a file in an archive could have one of these bits set + // and, unknown to the person unzipping, could allow others to execute the + // file as the user or group. The new option -K bypasses this check." + const int ExtractPermissionMask = 0x1FF; + int permissions = (entry.ExternalAttributes >> 16) & ExtractPermissionMask; + + // If the permissions weren't set at all, don't write the file's permissions, + // since the .zip could have been made using a previous version of .NET, which didn't + // include the permissions, or was made on Windows. + if (permissions != 0) + { + Interop.CheckIo(Interop.Sys.FChMod(fs.SafeFileHandle, permissions), fs.Name); + } + } + } +} diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs index cad1a12c5a4..a74aca915fa 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; - namespace System.IO.Compression { public static partial class ZipFileExtensions @@ -75,15 +73,19 @@ namespace System.IO.Compression // Rely on FileStream's ctor for further checking destinationFileName parameter FileMode fMode = overwrite ? FileMode.Create : FileMode.CreateNew; - using (Stream fs = new FileStream(destinationFileName, fMode, FileAccess.Write, FileShare.None, bufferSize: 0x1000, useAsync: false)) + using (FileStream fs = new FileStream(destinationFileName, fMode, FileAccess.Write, FileShare.None, bufferSize: 0x1000, useAsync: false)) { using (Stream es = source.Open()) es.CopyTo(fs); + + ExtractExternalAttributes(fs, source); } File.SetLastWriteTime(destinationFileName, source.LastWriteTime.DateTime); } + static partial void ExtractExternalAttributes(FileStream fs, ZipArchiveEntry entry); + internal static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryName) => ExtractRelativeToDirectory(source, destinationDirectoryName, overwrite: false); diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/System.IO.Compression.ZipFile.Tests.csproj b/src/libraries/System.IO.Compression.ZipFile/tests/System.IO.Compression.ZipFile.Tests.csproj index bf9e78ab742..9fbde72eef7 100644 --- a/src/libraries/System.IO.Compression.ZipFile/tests/System.IO.Compression.ZipFile.Tests.csproj +++ b/src/libraries/System.IO.Compression.ZipFile/tests/System.IO.Compression.ZipFile.Tests.csproj @@ -1,6 +1,6 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks> + <TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser</TargetFrameworks> </PropertyGroup> <ItemGroup> <Compile Include="ZipFile.Create.cs" /> @@ -23,6 +23,12 @@ <Compile Include="$(CommonTestPath)System\IO\Compression\ZipTestHelper.cs" Link="Common\System\IO\Compression\ZipTestHelper.cs" /> </ItemGroup> + <ItemGroup Condition=" '$(TargetsUnix)' == 'true' or '$(TargetsBrowser)' == 'true' "> + <Compile Include="ZipFile.Unix.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs" Link="Interop\Unix\Interop.Libraries.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.ChMod.cs" Link="Interop\Unix\System.Native\Interop.ChMod.cs" /> + <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs" Link="Interop\Unix\System.Native\Interop.Stat.cs" /> + </ItemGroup> <ItemGroup> <PackageReference Include="System.IO.Compression.TestData" Version="$(SystemIOCompressionTestDataVersion)" /> </ItemGroup> diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.cs index 3ba4081397b..18768b5a592 100644 --- a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.cs +++ b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.cs @@ -450,5 +450,28 @@ namespace System.IO.Compression.Tests } } } + + [Fact] + public void CreateSetsExternalAttributesCorrectly() + { + string folderName = zfolder("normal"); + string filepath = GetTestFilePath(); + ZipFile.CreateFromDirectory(folderName, filepath); + + using (ZipArchive archive = ZipFile.Open(filepath, ZipArchiveMode.Read)) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (OperatingSystem.IsWindows()) + { + Assert.Equal(0, entry.ExternalAttributes); + } + else + { + Assert.NotEqual(0, entry.ExternalAttributes); + } + } + } + } } } diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs new file mode 100644 index 00000000000..ab61ae92443 --- /dev/null +++ b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Xunit; + +namespace System.IO.Compression.Tests +{ + public class ZipFile_Unix : ZipFileTestBase + { + [Fact] + public void UnixCreateSetsPermissionsInExternalAttributes() + { + // '7600' tests that S_ISUID, S_ISGID, and S_ISVTX bits get preserved in ExternalAttributes + string[] testPermissions = new[] { "777", "755", "644", "600", "7600" }; + + using (var tempFolder = new TempDirectory(Path.Combine(GetTestFilePath(), "testFolder"))) + { + foreach (string permission in testPermissions) + { + CreateFile(tempFolder.Path, permission); + } + + string archivePath = GetTestFilePath(); + ZipFile.CreateFromDirectory(tempFolder.Path, archivePath); + + using (ZipArchive archive = ZipFile.OpenRead(archivePath)) + { + Assert.Equal(5, archive.Entries.Count); + + foreach (ZipArchiveEntry entry in archive.Entries) + { + Assert.EndsWith(".txt", entry.Name, StringComparison.Ordinal); + EnsureExternalAttributes(entry.Name.Substring(0, entry.Name.Length - 4), entry); + } + + void EnsureExternalAttributes(string permissions, ZipArchiveEntry entry) + { + Assert.Equal(Convert.ToInt32(permissions, 8), (entry.ExternalAttributes >> 16) & 0xFFF); + } + } + + // test that round tripping the archive has the same file permissions + using (var extractFolder = new TempDirectory(Path.Combine(GetTestFilePath(), "extract"))) + { + ZipFile.ExtractToDirectory(archivePath, extractFolder.Path); + + foreach (string permission in testPermissions) + { + string filename = Path.Combine(extractFolder.Path, permission + ".txt"); + Assert.True(File.Exists(filename)); + + EnsureFilePermissions(filename, permission); + } + } + } + } + + [Fact] + public void UnixExtractSetsFilePermissionsFromExternalAttributes() + { + // '7600' tests that S_ISUID, S_ISGID, and S_ISVTX bits don't get extracted to file permissions + string[] testPermissions = new[] { "777", "755", "644", "754", "7600" }; + byte[] contents = Encoding.UTF8.GetBytes("contents"); + + string archivePath = GetTestFilePath(); + using (FileStream fileStream = new FileStream(archivePath, FileMode.CreateNew)) + using (ZipArchive archive = new ZipArchive(fileStream, ZipArchiveMode.Create)) + { + foreach (string permission in testPermissions) + { + ZipArchiveEntry entry = archive.CreateEntry(permission + ".txt"); + entry.ExternalAttributes = Convert.ToInt32(permission, 8) << 16; + using Stream stream = entry.Open(); + stream.Write(contents); + stream.Flush(); + } + } + + using (var tempFolder = new TempDirectory(GetTestFilePath())) + { + ZipFile.ExtractToDirectory(archivePath, tempFolder.Path); + + foreach (string permission in testPermissions) + { + string filename = Path.Combine(tempFolder.Path, permission + ".txt"); + Assert.True(File.Exists(filename)); + + EnsureFilePermissions(filename, permission); + } + } + } + + private static void CreateFile(string folderPath, string permissions) + { + string filename = Path.Combine(folderPath, $"{permissions}.txt"); + File.WriteAllText(filename, "contents"); + + Assert.Equal(0, Interop.Sys.ChMod(filename, Convert.ToInt32(permissions, 8))); + } + + private static void EnsureFilePermissions(string filename, string permissions) + { + Interop.Sys.FileStatus status; + Assert.Equal(0, Interop.Sys.Stat(filename, out status)); + + // note that we don't extract S_ISUID, S_ISGID, and S_ISVTX bits, + // so only use the last 3 numbers of permissions to verify the file permissions + permissions = permissions.Length > 3 ? permissions.Substring(permissions.Length - 3) : permissions; + Assert.Equal(Convert.ToInt32(permissions, 8), status.Mode & 0xFFF); + } + + [Theory] + [InlineData("sharpziplib.zip", null)] // ExternalAttributes are not set in this .zip, use the system default + [InlineData("Linux_RW_RW_R__.zip", "664")] + [InlineData("Linux_RWXRW_R__.zip", "764")] + [InlineData("OSX_RWXRW_R__.zip", "764")] + public void UnixExtractFilePermissionsCompat(string zipName, string expectedPermissions) + { + expectedPermissions = GetExpectedPermissions(expectedPermissions); + + string zipFileName = compat(zipName); + using (var tempFolder = new TempDirectory(GetTestFilePath())) + { + ZipFile.ExtractToDirectory(zipFileName, tempFolder.Path); + + using ZipArchive archive = ZipFile.Open(zipFileName, ZipArchiveMode.Read); + foreach (ZipArchiveEntry entry in archive.Entries) + { + string filename = Path.Combine(tempFolder.Path, entry.FullName); + Assert.True(File.Exists(filename), $"File '{filename}' should exist"); + + EnsureFilePermissions(filename, expectedPermissions); + } + } + } + + private static string GetExpectedPermissions(string expectedPermissions) + { + if (string.IsNullOrEmpty(expectedPermissions)) + { + // Create a new file, and get its permissions to get the current system default permissions + + using (var tempFolder = new TempDirectory()) + { + string filename = Path.Combine(tempFolder.Path, Path.GetRandomFileName()); + File.WriteAllText(filename, "contents"); + + Interop.Sys.FileStatus status; + Assert.Equal(0, Interop.Sys.Stat(filename, out status)); + + expectedPermissions = Convert.ToString(status.Mode & 0xFFF, 8); + } + } + + return expectedPermissions; + } + } +} |