-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Expose the ExternalAttributes field in ZipArchive #18565
Conversation
The ExternalFileAttributes field of a ZipArchiveEntry is currently hidden within the implementation and not accessible. This has been fine in the Windows world since it's not used but it is used by Unix's zip/unzip programs to store file permissions of an entry. As it currently stands, there is no way to carry Unix RWX permissions within a zip, so at unzip time the unzipper has to guess at permissions and manually chmod the entries. This PR simply exposes the raw field so that it may be used in any circumstance that requires access to the field. Tests are added to ensure that this field is set, read, and roundtrips properly with zips created from other common programs which use the field.
@@ -59,7 +59,7 @@ | |||
<Version>2.5.0</Version> | |||
</PackageReference> | |||
<PackageReference Include="System.IO.Compression.TestData"> | |||
<Version>1.0.4-prerelease</Version> | |||
<Version>1.0.5-prerelease</Version> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you just manually upload this data when you need to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. The TestData package doesn't need to be changed very often so it's not too much of a hassle.
@@ -39,6 +39,7 @@ public partial class ZipArchiveEntry | |||
private bool _currentlyOpenForWrite; | |||
private bool _everOpenedForWrite; | |||
private Stream _outstandingWriteStream; | |||
private uint _externalFileAttr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it still correct to use uint
internally? Does it matter?
It's a bit odd to use it and then test with int.MaxValue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't really matter. We could use an int internally instead, I just like the uint since you don't have to worry about bit carrying when doing shifts and it's already being using in the ZipCentralDirectoryFileHeader reader. Both could be changed to ints easily though.
@@ -527,7 +543,7 @@ internal void WriteCentralDirectoryFileHeader() | |||
writer.Write(_fileComment != null ? (ushort)_fileComment.Length : (ushort)0); // file comment length | |||
writer.Write((ushort)0); // disk number start | |||
writer.Write((ushort)0); // internal file attributes | |||
writer.Write((uint)0); // external file attributes | |||
writer.Write(_externalFileAttr); // external file attributes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this line actually getting hit by your tests? Seems you're just reading zips you already made, and testing the set/get pairs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not, really. Testing that the zips written work correctly with other programs requires manual testing (which FWIW I did).
As @eerhardt mentioned below, I can add a test to do a more complete roundtripping test to hit this line.
[InlineData(int.MaxValue)] | ||
[InlineData(int.MinValue)] | ||
[InlineData(0)] | ||
public static async Task RoundTrips_UnixFilePermissions(int expectedAttr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we should have a test that actually writes the ExternalAttributes to a file, and then reads the file in and makes sure the ExternalAttributes are preserved in a full "round-trip".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll add one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added. There isn't any reason to write the stream to a file, so I closed the ZipArchive then reopened it to ensure the attribute was written to the stream correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Just reading it back out of the stream works for me. Thanks.
@@ -45,8 +49,11 @@ | |||
<Link>Common\System\IO\Compression\ZipTestHelper.cs</Link> | |||
</Compile> | |||
</ItemGroup> | |||
<ItemGroup Condition="'$(TargetGroup)' == 'netcoreapp'"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this new property not going to be supported in netstandard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
netstandard
configuration means you run on netstandard. Adding the parameter is fine. I forgot to ask why you have to add a netcoreapp configuration here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would have to be added to full .net to be a part of netstandard, yes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but this has a netstandard
configuration which indicates it works on top of netstandard. It can expose any API it wants. I don't think you need any new configuraiotn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disregard the above --this is a test configuration. I believe you did it right. My bad.
@@ -4,6 +4,8 @@ | |||
<BuildConfigurations> | |||
netstandard-Unix; | |||
netstandard-Windows_NT; | |||
netcoreapp-Unix; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean we will build and run all the tests twice (with the exception of the newly added tests, which will only run once on netcoreapp)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not certain. As netstandard should be a subset of netcoreapp, it wouldn't make much sense to run a netstandard build alongside a netcoreapp build. My assumption is that when running tests against netcoreapp via build-tests.cmd, only the most specific Configuration is ran e.g. netcoreapp if available otherwise netstandard, or netfx where available otherwise netstandard. @karajas probably knows more.
Regardless, the important bit is that the new tests will run only on netcoreapp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ianhays is correct. We have https:/dotnet/corefx/issues/15667 to try and run explicitly the netstandard test configurations as well but in general those should be included in the netcoreapp configurations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than Eric's feedback, LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pushing forward with this @ianhays. This gets us closer to where we need to be to fully support non-Windows scenarios.
[InlineData(int.MaxValue)] | ||
[InlineData(int.MinValue)] | ||
[InlineData(0)] | ||
public static async Task RoundTrips_UnixFilePermissions(int expectedAttr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Just reading it back out of the stream works for me. Thanks.
Glad to help @eerhardt! |
.NET Standard: .NET Core |
@aventurella that is correct. This is a new API which is added to .NET Core first and will be considered in a future version of .NET Standard. The standard versions at a slower rate then .NET Core. |
The ExternalFileAttributes field of a ZipArchiveEntry is currently hidden within the implementation and not accessible. This has been fine in the Windows world since it's not used but it is used by Unix's zip/unzip programs to store file permissions of an entry. As it currently stands, there is no way to carry Unix RWX permissions within a zip, so at unzip time the unzipper has to guess at permissions and manually chmod the entries. This PR simply exposes the raw field so that it may be used in any circumstance that requires access to the field. Tests are added to ensure that this field is set, read, and roundtrips properly with zips created from other common programs which use the field.
Note: Unix
unzip
doesn't apply permissions in the external file attributes field of a zip produced on Windows. So if you modify the externalfileattributes of a zip on Windows,unzip
will use the defaults instead of what is set in the field. This isn't easily resolvable on our side since the platformmadeby byte determines a number of other things that we don't want to mess with (e.g. file name validity), so it would be unwise to lie about the platform on which the zip was written.resolves https:/dotnet/corefx/issues/17067
cc: @eerhardt @danmosemsft @stephentoub