diff --git a/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs b/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs index 6f319461c35ef4..b5937a946869e3 100644 --- a/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs +++ b/src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs @@ -383,5 +383,69 @@ internal static void AddEntry(ZipArchive archive, string name, string contents, w.WriteLine(contents); } } + + protected const string Utf8SmileyEmoji = "\ud83d\ude04"; + protected const string Utf8LowerCaseOUmlautChar = "\u00F6"; + protected const string Utf8CopyrightChar = "\u00A9"; + protected const string AsciiFileName = "file.txt"; + // The o with umlaut is a character that exists in both latin1 and utf8 + protected const string Utf8AndLatin1FileName = $"{Utf8LowerCaseOUmlautChar}.txt"; + // emojis only make sense in utf8 + protected const string Utf8FileName = $"{Utf8SmileyEmoji}.txt"; + protected static readonly string ALettersUShortMaxValueMinusOne = new string('a', ushort.MaxValue - 1); + protected static readonly string ALettersUShortMaxValue = ALettersUShortMaxValueMinusOne + 'a'; + protected static readonly string ALettersUShortMaxValueMinusOneAndCopyRightChar = ALettersUShortMaxValueMinusOne + Utf8CopyrightChar; + protected static readonly string ALettersUShortMaxValueMinusOneAndTwoCopyRightChars = ALettersUShortMaxValueMinusOneAndCopyRightChar + Utf8CopyrightChar; + + // Returns pairs that are returned the same way by Utf8 and Latin1 + // Returns: originalComment, expectedComment + private static IEnumerable SharedComment_Data() + { + yield return new object[] { null, string.Empty }; + yield return new object[] { string.Empty, string.Empty }; + yield return new object[] { "a", "a" }; + yield return new object[] { Utf8LowerCaseOUmlautChar, Utf8LowerCaseOUmlautChar }; + } + + // Returns pairs as expected by Utf8 + // Returns: originalComment, expectedComment + public static IEnumerable Utf8Comment_Data() + { + string asciiOriginalOverMaxLength = ALettersUShortMaxValue + "aaa"; + + // A smiley emoji code point consists of two characters, + // meaning the whole emoji should be fully truncated + string utf8OriginalALettersAndOneEmojiDoesNotFit = ALettersUShortMaxValueMinusOne + Utf8SmileyEmoji; + + // A smiley emoji code point consists of two characters, + // so it should not be truncated if it's the last character and the total length is not over the limit. + string utf8OriginalALettersAndOneEmojiFits = "aaaaa" + Utf8SmileyEmoji; + + yield return new object[] { asciiOriginalOverMaxLength, ALettersUShortMaxValue }; + yield return new object[] { utf8OriginalALettersAndOneEmojiDoesNotFit, ALettersUShortMaxValueMinusOne }; + yield return new object[] { utf8OriginalALettersAndOneEmojiFits, utf8OriginalALettersAndOneEmojiFits }; + + foreach (object[] e in SharedComment_Data()) + { + yield return e; + } + } + + // Returns pairs as expected by Latin1 + // Returns: originalComment, expectedComment + public static IEnumerable Latin1Comment_Data() + { + // In Latin1, all characters are exactly 1 byte + + string latin1ExpectedALettersAndOneOUmlaut = ALettersUShortMaxValueMinusOne + Utf8LowerCaseOUmlautChar; + string latin1OriginalALettersAndTwoOUmlauts = latin1ExpectedALettersAndOneOUmlaut + Utf8LowerCaseOUmlautChar; + + yield return new object[] { latin1OriginalALettersAndTwoOUmlauts, latin1ExpectedALettersAndOneOUmlaut }; + + foreach (object[] e in SharedComment_Data()) + { + yield return e; + } + } } } diff --git a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs index 80b7286baa5416..3090d4aaba384c 100644 --- a/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs +++ b/src/libraries/System.IO.Compression/ref/System.IO.Compression.cs @@ -94,6 +94,8 @@ public ZipArchive(System.IO.Stream stream) { } public ZipArchive(System.IO.Stream stream, System.IO.Compression.ZipArchiveMode mode) { } public ZipArchive(System.IO.Stream stream, System.IO.Compression.ZipArchiveMode mode, bool leaveOpen) { } public ZipArchive(System.IO.Stream stream, System.IO.Compression.ZipArchiveMode mode, bool leaveOpen, System.Text.Encoding? entryNameEncoding) { } + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Comment { get { throw null; } set { } } public System.Collections.ObjectModel.ReadOnlyCollection Entries { get { throw null; } } public System.IO.Compression.ZipArchiveMode Mode { get { throw null; } } public System.IO.Compression.ZipArchiveEntry CreateEntry(string entryName) { throw null; } @@ -106,6 +108,8 @@ public partial class ZipArchiveEntry { internal ZipArchiveEntry() { } public System.IO.Compression.ZipArchive Archive { get { throw null; } } + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Comment { get { throw null; } set { } } public long CompressedLength { get { throw null; } } [System.CLSCompliantAttribute(false)] public uint Crc32 { get { throw null; } } diff --git a/src/libraries/System.IO.Compression/src/Resources/Strings.resx b/src/libraries/System.IO.Compression/src/Resources/Strings.resx index cac801aa8fcc47..3b51a1cf192939 100644 --- a/src/libraries/System.IO.Compression/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression/src/Resources/Strings.resx @@ -218,8 +218,8 @@ Cannot access entries in Create mode. - - The specified entry name encoding is not supported. + + The specified encoding is not supported for entry names and comments. Entry names cannot require more than 2^16 bits. diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs index 58b65209caf0eb..5d68416b3b25da 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchive.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace System.IO.Compression @@ -27,8 +28,8 @@ public class ZipArchive : IDisposable private uint _numberOfThisDisk; //only valid after ReadCentralDirectory private long _expectedNumberOfEntries; private Stream? _backingStream; - private byte[]? _archiveComment; - private Encoding? _entryNameEncoding; + private byte[] _archiveComment; + private Encoding? _entryNameAndCommentEncoding; #if DEBUG_FORCE_ZIP64 public bool _forceZip64; @@ -121,7 +122,7 @@ public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? if (stream == null) throw new ArgumentNullException(nameof(stream)); - EntryNameEncoding = entryNameEncoding; + EntryNameAndCommentEncoding = entryNameEncoding; Stream? extraTempStream = null; try @@ -173,7 +174,7 @@ public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? _centralDirectoryStart = 0; // invalid until ReadCentralDirectory _isDisposed = false; _numberOfThisDisk = 0; // invalid until ReadCentralDirectory - _archiveComment = null; + _archiveComment = Array.Empty(); switch (mode) { @@ -211,6 +212,20 @@ public ZipArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? } } + /// + /// Gets or sets the optional archive comment. + /// + /// + /// The comment encoding is determined by the entryNameEncoding parameter of the constructor. + /// If the comment byte length is larger than , it will be truncated when disposing the archive. + /// + [AllowNull] + public string Comment + { + get => (EntryNameAndCommentEncoding ?? Encoding.UTF8).GetString(_archiveComment); + set => _archiveComment = ZipHelper.GetEncodedTruncatedBytesFromString(value, EntryNameAndCommentEncoding, ZipEndOfCentralDirectoryBlock.ZipFileCommentMaxLength, out _); + } + /// /// The collection of entries that are currently in the ZipArchive. This may not accurately represent the actual entries that are present in the underlying file or stream. /// @@ -345,9 +360,9 @@ public void Dispose() internal uint NumberOfThisDisk => _numberOfThisDisk; - internal Encoding? EntryNameEncoding + internal Encoding? EntryNameAndCommentEncoding { - get { return _entryNameEncoding; } + get => _entryNameAndCommentEncoding; private set { @@ -370,10 +385,10 @@ private set (value.Equals(Encoding.BigEndianUnicode) || value.Equals(Encoding.Unicode))) { - throw new ArgumentException(SR.EntryNameEncodingNotSupported, nameof(EntryNameEncoding)); + throw new ArgumentException(SR.EntryNameAndCommentEncodingNotSupported, nameof(EntryNameAndCommentEncoding)); } - _entryNameEncoding = value; + _entryNameAndCommentEncoding = value; } } @@ -547,9 +562,7 @@ private void ReadEndOfCentralDirectory() _expectedNumberOfEntries = eocd.NumberOfEntriesInTheCentralDirectory; - // only bother saving the comment if we are in update mode - if (_mode == ZipArchiveMode.Update) - _archiveComment = eocd.ArchiveComment; + _archiveComment = eocd.ArchiveComment; TryReadZip64EndOfCentralDirectory(eocd, eocdStart); diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs index 0d5de3cbac6bd2..f16c193383ae8a 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs @@ -40,10 +40,11 @@ public partial class ZipArchiveEntry // only apply to update mode private List? _cdUnknownExtraFields; private List? _lhUnknownExtraFields; - private readonly byte[]? _fileComment; + private byte[] _fileComment; private readonly CompressionLevel? _compressionLevel; + private bool _hasUnicodeEntryNameOrComment; - // Initializes, attaches it to archive + // Initializes a ZipArchiveEntry instance for an existing archive entry. internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd) { _archive = archive; @@ -72,17 +73,22 @@ internal ZipArchiveEntry(ZipArchive archive, ZipCentralDirectoryFileHeader cd) _everOpenedForWrite = false; _outstandingWriteStream = null; - FullName = DecodeEntryName(cd.Filename); + _storedEntryNameBytes = cd.Filename; + _storedEntryName = (_archive.EntryNameAndCommentEncoding ?? Encoding.UTF8).GetString(_storedEntryNameBytes); + DetectEntryNameVersion(); _lhUnknownExtraFields = null; - // the cd should have these as null if we aren't in Update mode + // the cd should have this as null if we aren't in Update mode _cdUnknownExtraFields = cd.ExtraFields; + _fileComment = cd.FileComment; _compressionLevel = null; + + _hasUnicodeEntryNameOrComment = (_generalPurposeBitFlag & BitFlagValues.UnicodeFileNameAndComment) != 0; } - // Initializes new entry + // Initializes a ZipArchiveEntry instance for a new archive entry with a specified compression level. internal ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel compressionLevel) : this(archive, entryName) { @@ -93,7 +99,7 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName, CompressionLevel } } - // Initializes new entry + // Initializes a ZipArchiveEntry instance for a new archive entry. internal ZipArchiveEntry(ZipArchive archive, string entryName) { _archive = archive; @@ -125,7 +131,8 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName) _cdUnknownExtraFields = null; _lhUnknownExtraFields = null; - _fileComment = null; + + _fileComment = Array.Empty(); _compressionLevel = null; @@ -137,6 +144,8 @@ internal ZipArchiveEntry(ZipArchive archive, string entryName) { _archive.AcquireArchiveStream(this); } + + _hasUnicodeEntryNameOrComment = false; } /// @@ -174,6 +183,24 @@ public int ExternalAttributes } } + /// + /// Gets or sets the optional entry comment. + /// + /// + ///The comment encoding is determined by the entryNameEncoding parameter of the constructor. + /// If the comment byte length is larger than , it will be truncated when disposing the archive. + /// + [AllowNull] + public string Comment + { + get => (_archive.EntryNameAndCommentEncoding ?? Encoding.UTF8).GetString(_fileComment); + set + { + _fileComment = ZipHelper.GetEncodedTruncatedBytesFromString(value, _archive.EntryNameAndCommentEncoding, ushort.MaxValue, out bool isUTF8); + _hasUnicodeEntryNameOrComment |= isUTF8; + } + } + /// /// The relative path of the entry as stored in the Zip archive. Note that Zip archives allow any string to be the path of the entry, including invalid and absolute paths. /// @@ -191,17 +218,13 @@ private set if (value == null) throw new ArgumentNullException(nameof(FullName)); - bool isUTF8; - _storedEntryNameBytes = EncodeEntryName(value, out isUTF8); - _storedEntryName = value; + _storedEntryNameBytes = ZipHelper.GetEncodedTruncatedBytesFromString( + value, _archive.EntryNameAndCommentEncoding, 0 /* No truncation */, out bool hasUnicodeEntryName); - if (isUTF8) - _generalPurposeBitFlag |= BitFlagValues.UnicodeFileName; - else - _generalPurposeBitFlag &= ~BitFlagValues.UnicodeFileName; + _hasUnicodeEntryNameOrComment |= hasUnicodeEntryName; + _storedEntryName = value; - if (ParseFileName(value, _versionMadeByPlatform) == "") - VersionToExtractAtLeast(ZipVersionNeededValues.ExplicitDirectory); + DetectEntryNameVersion(); } } @@ -396,39 +419,6 @@ private CompressionMethodValues CompressionMethod } } - private string DecodeEntryName(byte[] entryNameBytes) - { - Debug.Assert(entryNameBytes != null); - - Encoding readEntryNameEncoding; - if ((_generalPurposeBitFlag & BitFlagValues.UnicodeFileName) == 0) - { - readEntryNameEncoding = _archive == null ? - Encoding.UTF8 : - _archive.EntryNameEncoding ?? Encoding.UTF8; - } - else - { - readEntryNameEncoding = Encoding.UTF8; - } - - return readEntryNameEncoding.GetString(entryNameBytes); - } - - private byte[] EncodeEntryName(string entryName, out bool isUTF8) - { - Debug.Assert(entryName != null); - - Encoding writeEntryNameEncoding; - if (_archive != null && _archive.EntryNameEncoding != null) - writeEntryNameEncoding = _archive.EntryNameEncoding; - else - writeEntryNameEncoding = ZipHelper.RequiresUnicode(entryName) ? Encoding.UTF8 : Encoding.ASCII; - - isUTF8 = writeEntryNameEncoding.Equals(Encoding.UTF8); - return writeEntryNameEncoding.GetBytes(entryName); - } - // does almost everything you need to do to forget about this entry // writes the local header/data, gets rid of all the data, // closes all of the streams except for the very outermost one that @@ -516,6 +506,11 @@ internal void WriteCentralDirectoryFileHeader() extraFieldLength = (ushort)bigExtraFieldLength; } + if (_hasUnicodeEntryNameOrComment) + _generalPurposeBitFlag |= BitFlagValues.UnicodeFileNameAndComment; + else + _generalPurposeBitFlag &= ~BitFlagValues.UnicodeFileNameAndComment; + writer.Write(ZipCentralDirectoryFileHeader.SignatureConstant); // Central directory file header signature (4 bytes) writer.Write((byte)_versionMadeBySpecification); // Version made by Specification (version) (1 byte) writer.Write((byte)CurrentZipPlatform); // Version made by Compatibility (type) (1 byte) @@ -529,10 +524,9 @@ internal void WriteCentralDirectoryFileHeader() writer.Write((ushort)_storedEntryNameBytes.Length); // File Name Length (2 bytes) writer.Write(extraFieldLength); // Extra Field Length (2 bytes) - // This should hold because of how we read it originally in ZipCentralDirectoryFileHeader: - Debug.Assert((_fileComment == null) || (_fileComment.Length <= ushort.MaxValue)); + Debug.Assert(_fileComment.Length <= ushort.MaxValue); - writer.Write(_fileComment != null ? (ushort)_fileComment.Length : (ushort)0); // file comment length + writer.Write((ushort)_fileComment.Length); writer.Write((ushort)0); // disk number start writer.Write((ushort)0); // internal file attributes writer.Write(_externalFileAttr); // external file attributes @@ -546,7 +540,7 @@ internal void WriteCentralDirectoryFileHeader() if (_cdUnknownExtraFields != null) ZipGenericExtraField.WriteAllBlocks(_cdUnknownExtraFields, _archive.ArchiveStream); - if (_fileComment != null) + if (_fileComment.Length > 0) writer.Write(_fileComment); } @@ -596,6 +590,14 @@ internal void ThrowIfNotOpenable(bool needToUncompress, bool needToLoadIntoMemor throw new InvalidDataException(message); } + private void DetectEntryNameVersion() + { + if (ParseFileName(_storedEntryName, _versionMadeByPlatform) == "") + { + VersionToExtractAtLeast(ZipVersionNeededValues.ExplicitDirectory); + } + } + private CheckSumAndSizeWriteStream GetDataCompressor(Stream backingStream, bool leaveBackingStreamOpen, EventHandler? onClose) { // stream stack: backingStream -> DeflateStream -> CheckSumWriteStream @@ -1303,7 +1305,7 @@ protected override void Dispose(bool disposing) } [Flags] - internal enum BitFlagValues : ushort { DataDescriptor = 0x8, UnicodeFileName = 0x800 } + internal enum BitFlagValues : ushort { DataDescriptor = 0x8, UnicodeFileNameAndComment = 0x800 } internal enum CompressionMethodValues : ushort { Stored = 0x0, Deflate = 0x8, Deflate64 = 0x9, BZip2 = 0xC, LZMA = 0xE } } diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs index 058a8617dea61c..64fffd2e1f945d 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs @@ -453,7 +453,7 @@ internal struct ZipCentralDirectoryFileHeader public long RelativeOffsetOfLocalHeader; public byte[] Filename; - public byte[]? FileComment; + public byte[] FileComment; public List? ExtraFields; // if saveExtraFieldsAndComments is false, FileComment and ExtraFields will be null @@ -513,13 +513,7 @@ public static bool TryReadBlock(BinaryReader reader, bool saveExtraFieldsAndComm // of the ExtraField block. Thus we must force the stream's position to the proper place. reader.BaseStream.AdvanceToPosition(endExtraFields); - if (saveExtraFieldsAndComments) - header.FileComment = reader.ReadBytes(header.FileCommentLength); - else - { - reader.BaseStream.Position += header.FileCommentLength; - header.FileComment = null; - } + header.FileComment = reader.ReadBytes(header.FileCommentLength); header.UncompressedSize = zip64.UncompressedSize == null ? uncompressedSizeSmall diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs index 57ff0eafd993b6..1e46185461a294 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace System.IO.Compression { @@ -17,20 +20,19 @@ internal static class ZipHelper private static readonly DateTime s_invalidDateIndicator = new DateTime(ValidZipDate_YearMin, 1, 1, 0, 0, 0); - internal static bool RequiresUnicode(string test) + internal static Encoding GetEncoding(string text) { - Debug.Assert(test != null); - - foreach (char c in test) + foreach (char c in text) { // The Zip Format uses code page 437 when the Unicode bit is not set. This format // is the same as ASCII for characters 32-126 but differs otherwise. If we can fit // the string into CP437 then we treat ASCII as acceptable. if (c > 126 || c < 32) - return true; + { + return Encoding.UTF8; + } } - - return false; + return Encoding.ASCII; } /// @@ -193,5 +195,50 @@ private static bool SeekBackwardsAndRead(Stream stream, byte[] buffer, out int b return true; } } + + // Converts the specified string into bytes using the optional specified encoding. + // If the encoding null, then the encoding is calculated from the string itself. + // If maxBytes is greater than zero, the returned string will be truncated to a total + // number of characters whose bytes do not add up to more than that number. + internal static byte[] GetEncodedTruncatedBytesFromString(string? text, Encoding? encoding, int maxBytes, out bool isUTF8) + { + if (string.IsNullOrEmpty(text)) + { + isUTF8 = false; + return Array.Empty(); + } + + encoding ??= GetEncoding(text); + isUTF8 = encoding.CodePage == 65001; + + if (maxBytes == 0) // No truncation + { + return encoding.GetBytes(text); + } + + byte[] bytes; + if (isUTF8) + { + int totalCodePoints = 0; + foreach (Rune rune in text.EnumerateRunes()) + { + if (totalCodePoints + rune.Utf8SequenceLength > maxBytes) + { + break; + } + totalCodePoints += rune.Utf8SequenceLength; + } + + bytes = encoding.GetBytes(text); + + Debug.Assert(totalCodePoints > 0); + Debug.Assert(totalCodePoints <= bytes.Length); + + return bytes[0..totalCodePoints]; + } + + bytes = encoding.GetBytes(text); + return maxBytes < bytes.Length ? bytes[0..maxBytes] : bytes; + } } } diff --git a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj index 509c7202afb435..adac7a348c6922 100644 --- a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj +++ b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj @@ -20,11 +20,13 @@ + + diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.Comments.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.Comments.cs new file mode 100644 index 00000000000000..d04ab6c6d1d590 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.Comments.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Text; + +namespace System.IO.Compression.Tests +{ + public partial class zip_CreateTests : ZipFileTestBase + { + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Create_Comment_AsciiEntryName_NullEncoding(string originalComment, string expectedComment) => + Create_Comment_EntryName_Encoding_Internal(AsciiFileName, originalComment, expectedComment, null); + + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Create_Comment_AsciiEntryName_Utf8Encoding(string originalComment, string expectedComment) => + Create_Comment_EntryName_Encoding_Internal(AsciiFileName, originalComment, expectedComment, Encoding.UTF8); + + [Theory] + [MemberData(nameof(Latin1Comment_Data))] + public static void Create_Comment_AsciiEntryName_Latin1Encoding(string originalComment, string expectedComment) => + Create_Comment_EntryName_Encoding_Internal(AsciiFileName, originalComment, expectedComment, Encoding.Latin1); + + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Create_Comment_Utf8EntryName_NullEncoding(string originalComment, string expectedComment) => + Create_Comment_EntryName_Encoding_Internal(Utf8FileName, originalComment, expectedComment, null); + + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Create_Comment_Utf8EntryName_Utf8Encoding(string originalComment, string expectedComment) => + Create_Comment_EntryName_Encoding_Internal(Utf8FileName, originalComment, expectedComment, Encoding.UTF8); + + [Theory] + [MemberData(nameof(Latin1Comment_Data))] + public static void Create_Comment_Utf8EntryName_Latin1Encoding(string originalComment, string expectedComment) => + // Emoji not supported by latin1 + Create_Comment_EntryName_Encoding_Internal(Utf8AndLatin1FileName, originalComment, expectedComment, Encoding.Latin1); + + private static void Create_Comment_EntryName_Encoding_Internal(string entryName, string originalComment, string expectedComment, Encoding encoding) + { + using var ms = new MemoryStream(); + + using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true, encoding)) + { + ZipArchiveEntry entry = zip.CreateEntry(entryName, CompressionLevel.NoCompression); + entry.Comment = originalComment; + Assert.Equal(expectedComment, entry.Comment); + } + + using (var zip = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: false, encoding)) + { + foreach (ZipArchiveEntry entry in zip.Entries) + { + Assert.Equal(entryName, entry.Name); + Assert.Equal(expectedComment, entry.Comment); + } + } + } + } +} \ No newline at end of file diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs index f0c36d5967626b..a964f5da7587f1 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_CreateTests.cs @@ -1,14 +1,12 @@ // 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.InteropServices; using System.Threading.Tasks; using Xunit; -using Microsoft.DotNet.XUnitExtensions; namespace System.IO.Compression.Tests { - public class zip_CreateTests : ZipFileTestBase + public partial class zip_CreateTests : ZipFileTestBase { [Fact] public static void CreateModeInvalidOperations() diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.Comments.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.Comments.cs new file mode 100644 index 00000000000000..7195dd504e0755 --- /dev/null +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.Comments.cs @@ -0,0 +1,95 @@ +// 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.Linq; +using System.Text; +using Xunit; + +namespace System.IO.Compression.Tests +{ + public partial class zip_UpdateTests : ZipFileTestBase + { + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Update_Comment_AsciiEntryName_NullEncoding(string originalComment, string expectedComment) => + Update_Comment_EntryName_Encoding_Internal(AsciiFileName, + originalComment, expectedComment, null, + ALettersUShortMaxValueMinusOneAndCopyRightChar, ALettersUShortMaxValueMinusOne); + + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Update_Comment_AsciiEntryName_Utf8Encoding(string originalComment, string expectedComment) => + Update_Comment_EntryName_Encoding_Internal(AsciiFileName, + originalComment, expectedComment, Encoding.UTF8, + ALettersUShortMaxValueMinusOneAndCopyRightChar, ALettersUShortMaxValueMinusOne); + + [Theory] + [MemberData(nameof(Latin1Comment_Data))] + public static void Update_Comment_AsciiEntryName_Latin1Encoding(string originalComment, string expectedComment) => + Update_Comment_EntryName_Encoding_Internal(AsciiFileName, + originalComment, expectedComment, Encoding.Latin1, + ALettersUShortMaxValueMinusOneAndTwoCopyRightChars, ALettersUShortMaxValueMinusOneAndCopyRightChar); + + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Update_Comment_Utf8EntryName_NullEncoding(string originalComment, string expectedComment) => + Update_Comment_EntryName_Encoding_Internal(Utf8FileName, + originalComment, expectedComment, null, + ALettersUShortMaxValueMinusOneAndCopyRightChar, ALettersUShortMaxValueMinusOne); + + [Theory] + [MemberData(nameof(Utf8Comment_Data))] + public static void Update_Comment_Utf8EntryName_Utf8Encoding(string originalComment, string expectedComment) => + Update_Comment_EntryName_Encoding_Internal(Utf8FileName, + originalComment, expectedComment, Encoding.UTF8, + ALettersUShortMaxValueMinusOneAndCopyRightChar, ALettersUShortMaxValueMinusOne); + + [Theory] + [MemberData(nameof(Latin1Comment_Data))] + public static void Update_Comment_Utf8EntryName_Latin1Encoding(string originalComment, string expectedComment) => + // Emoji is not supported/detected in latin1 + Update_Comment_EntryName_Encoding_Internal(Utf8AndLatin1FileName, + originalComment, expectedComment, Encoding.Latin1, + ALettersUShortMaxValueMinusOneAndTwoCopyRightChars, ALettersUShortMaxValueMinusOneAndCopyRightChar); + + private static void Update_Comment_EntryName_Encoding_Internal(string entryName, + string originalCreateComment, string expectedCreateComment, Encoding encoding, + string originalUpdateComment, string expectedUpdateComment) + { + using var ms = new MemoryStream(); + + using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true, encoding)) + { + ZipArchiveEntry entry = zip.CreateEntry(entryName, CompressionLevel.NoCompression); + entry.Comment = originalCreateComment; + Assert.Equal(expectedCreateComment, entry.Comment); + } + + using (var zip = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: true, encoding)) + { + foreach (ZipArchiveEntry entry in zip.Entries) + { + Assert.Equal(expectedCreateComment, entry.Comment); + } + } + + using (var zip = new ZipArchive(ms, ZipArchiveMode.Update, leaveOpen: true, encoding)) + { + foreach (ZipArchiveEntry entry in zip.Entries) + { + entry.Comment = originalUpdateComment; + Assert.Equal(expectedUpdateComment, entry.Comment); + } + } + + using (var zip = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: false, encoding)) + { + foreach (ZipArchiveEntry entry in zip.Entries) + { + Assert.Equal(expectedUpdateComment, entry.Comment); + } + } + } + } +} \ No newline at end of file diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs index 3cb9934fb3a7ca..637d04500d77a8 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_UpdateTests.cs @@ -1,14 +1,15 @@ // 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.InteropServices; +using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; namespace System.IO.Compression.Tests { - public class zip_UpdateTests : ZipFileTestBase + public partial class zip_UpdateTests : ZipFileTestBase { [Theory] [InlineData("normal.zip", "normal")]