未验证 提交 620bc761 编写于 作者: D David Cantú 提交者: GitHub

Use UTF8 encoding on Tar string fields (#75902)

* Use UTF8 encoding on Tar string fields

* Slice destination on Checksum

* Use Encoding.GetByteCount as fast path

* Use escape sequences on hardcoded UTF8 characters

* Fix ustar prefix logic and throw if name would be truncated

* Address feedback

* Fix truncation and prefix logic

* Fix nits

* Add async tests

* Add tests for unseekable streams

* Address feedback
上级 a45eef28
......@@ -17,6 +17,7 @@ internal static partial class PathInternal
internal const string DirectorySeparatorCharAsString = "/";
internal const string ParentDirectoryPrefix = @"../";
internal const string DirectorySeparators = DirectorySeparatorCharAsString;
internal static ReadOnlySpan<byte> Utf8DirectorySeparators => "/"u8;
internal static int GetRootLength(ReadOnlySpan<char> path)
{
......
......@@ -55,6 +55,7 @@ internal static partial class PathInternal
internal const string DevicePathPrefix = @"\\.\";
internal const string ParentDirectoryPrefix = @"..\";
internal const string DirectorySeparators = @"\/";
internal static ReadOnlySpan<byte> Utf8DirectorySeparators => @"\/"u8;
internal const int MaxShortPath = 260;
internal const int MaxShortDirectoryPath = 248;
......
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
......@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
......@@ -261,4 +261,7 @@
<data name="TarInvalidNumber" xml:space="preserve">
<value>Unable to parse number.</value>
</data>
</root>
<data name="TarEntryFieldExceedsMaxLength" xml:space="preserve">
<value>The field '{0}' exceeds the maximum allowed length for this format.</value>
</data>
</root>
\ No newline at end of file
......@@ -65,6 +65,7 @@
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.MkFifo.cs" Link="Common\Interop\Unix\System.Native\Interop.MkFifo.cs" />
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs" Link="Common\Interop\Unix\Interop.Stat.cs" />
<Compile Include="$(CommonPath)System\IO\Archiving.Utils.Unix.cs" Link="Common\System\IO\Archiving.Utils.Unix.cs" />
<Compile Include="$(CommonPath)System\IO\PathInternal.Unix.cs" Link="Common\System\IO\PathInternal.Unix.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Collections" />
......
......@@ -517,8 +517,8 @@ private void ReadVersionAttribute(Span<byte> buffer)
private void ReadPosixAndGnuSharedAttributes(Span<byte> buffer)
{
// Convert the byte arrays
_uName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.UName, FieldLengths.UName));
_gName = TarHelpers.GetTrimmedAsciiString(buffer.Slice(FieldLocations.GName, FieldLengths.GName));
_uName = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.UName, FieldLengths.UName));
_gName = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.GName, FieldLengths.GName));
// DevMajor and DevMinor only have values with character devices and block devices.
// For all other typeflags, the values in these fields are irrelevant.
......
......@@ -5,9 +5,7 @@
using System.Buffers.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
......@@ -25,6 +23,7 @@ internal sealed partial class TarHeader
// Predefined text for the Name field of a GNU long metadata entry. Applies for both LongPath ('L') and LongLink ('K').
private const string GnuLongMetadataName = "././@LongLink";
private const string ArgNameEntry = "entry";
// Writes the current header as a V7 entry into the archive stream.
internal void WriteAsV7(Stream archiveStream, Span<byte> buffer)
......@@ -101,7 +100,7 @@ private long WriteUstarFieldsToBuffer(Span<byte> buffer)
long actualLength = GetTotalDataBytesToWrite();
TarEntryType actualEntryType = TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar, _typeFlag);
int tmpChecksum = WritePosixName(buffer);
int tmpChecksum = WriteUstarName(buffer);
tmpChecksum += WriteCommonFields(buffer, actualLength, actualEntryType);
tmpChecksum += WritePosixMagicAndVersion(buffer);
tmpChecksum += WritePosixAndGnuSharedFields(buffer);
......@@ -178,7 +177,7 @@ internal async Task WriteAsPaxAsync(Stream archiveStream, Memory<byte> buffer, C
internal void WriteAsGnu(Stream archiveStream, Span<byte> buffer)
{
// First, we determine if we need a preceding LongLink, and write it if needed
if (_linkName?.Length > FieldLengths.LinkName)
if (_linkName != null && Encoding.UTF8.GetByteCount(_linkName) > FieldLengths.LinkName)
{
TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, _linkName);
longLinkHeader.WriteAsGnuInternal(archiveStream, buffer);
......@@ -186,7 +185,7 @@ internal void WriteAsGnu(Stream archiveStream, Span<byte> buffer)
}
// Second, we determine if we need a preceding LongPath, and write it if needed
if (_name.Length > FieldLengths.Name)
if (Encoding.UTF8.GetByteCount(_name) > FieldLengths.Name)
{
TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, _name);
longPathHeader.WriteAsGnuInternal(archiveStream, buffer);
......@@ -204,7 +203,7 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory<byte> buffer, C
cancellationToken.ThrowIfCancellationRequested();
// First, we determine if we need a preceding LongLink, and write it if needed
if (_linkName?.Length > FieldLengths.LinkName)
if (_linkName != null && Encoding.UTF8.GetByteCount(_linkName) > FieldLengths.LinkName)
{
TarHeader longLinkHeader = GetGnuLongMetadataHeader(TarEntryType.LongLink, _linkName);
await longLinkHeader.WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false);
......@@ -212,7 +211,7 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory<byte> buffer, C
}
// Second, we determine if we need a preceding LongPath, and write it if needed
if (_name.Length > FieldLengths.Name)
if (Encoding.UTF8.GetByteCount(_name) > FieldLengths.Name)
{
TarHeader longPathHeader = GetGnuLongMetadataHeader(TarEntryType.LongPath, _name);
await longPathHeader.WriteAsGnuInternalAsync(archiveStream, buffer, cancellationToken).ConfigureAwait(false);
......@@ -226,8 +225,7 @@ internal async Task WriteAsGnuAsync(Stream archiveStream, Memory<byte> buffer, C
// Creates and returns a GNU long metadata header, with the specified long text written into its data stream.
private static TarHeader GetGnuLongMetadataHeader(TarEntryType entryType, string longText)
{
Debug.Assert((entryType is TarEntryType.LongPath && longText.Length > FieldLengths.Name) ||
(entryType is TarEntryType.LongLink && longText.Length > FieldLengths.LinkName));
Debug.Assert(entryType is TarEntryType.LongPath or TarEntryType.LongLink);
TarHeader longMetadataHeader = new(TarEntryFormat.Gnu);
......@@ -350,7 +348,7 @@ private void WriteAsPaxSharedInternal(Span<byte> buffer, out long actualLength)
{
actualLength = GetTotalDataBytesToWrite();
int tmpChecksum = WritePosixName(buffer);
int tmpChecksum = WriteName(buffer);
tmpChecksum += WriteCommonFields(buffer, actualLength, TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.Pax, _typeFlag));
tmpChecksum += WritePosixMagicAndVersion(buffer);
tmpChecksum += WritePosixAndGnuSharedFields(buffer);
......@@ -358,31 +356,93 @@ private void WriteAsPaxSharedInternal(Span<byte> buffer, out long actualLength)
_checksum = WriteChecksum(tmpChecksum, buffer);
}
// All formats save in the name byte array only the ASCII bytes that fit.
// Gnu and pax save in the name byte array only the UTF8 bytes that fit.
// V7 does not support more than 100 bytes so it throws.
private int WriteName(Span<byte> buffer)
{
ReadOnlySpan<char> src = _name.AsSpan(0, Math.Min(_name.Length, FieldLengths.Name));
Span<byte> dest = buffer.Slice(FieldLocations.Name, FieldLengths.Name);
int encoded = Encoding.ASCII.GetBytes(src, dest);
return Checksum(dest.Slice(0, encoded));
ReadOnlySpan<char> name = _name;
int encodedLength = GetUtf8TextLength(name);
if (encodedLength > FieldLengths.Name)
{
if (_format is TarEntryFormat.V7)
{
throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, nameof(TarEntry.Name)), ArgNameEntry);
}
int utf16NameTruncatedLength = GetUtf16TruncatedTextLength(name, FieldLengths.Name);
name = name.Slice(0, utf16NameTruncatedLength);
}
return WriteAsUtf8String(name, buffer.Slice(FieldLocations.Name, FieldLengths.Name));
}
// Ustar and PAX save in the name byte array only the ASCII bytes that fit, and the rest of that string is saved in the prefix field.
private int WritePosixName(Span<byte> buffer)
// 'https://www.freebsd.org/cgi/man.cgi?tar(5)'
// If the path name is too long to fit in the 100 bytes provided by the standard format,
// it can be split at any / character with the first portion going into the prefix field.
private int WriteUstarName(Span<byte> buffer)
{
int checksum = WriteName(buffer);
// We can have a path name as big as 256, prefix + '/' + name,
// the separator in between can be neglected as the reader will append it when it joins both fields.
const int MaxPathName = FieldLengths.Prefix + 1 + FieldLengths.Name;
if (_name.Length > FieldLengths.Name)
if (GetUtf8TextLength(_name) > MaxPathName)
{
int prefixBytesLength = Math.Min(_name.Length - FieldLengths.Name, FieldLengths.Prefix);
Span<byte> remaining = stackalloc byte[prefixBytesLength];
int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining);
Debug.Assert(encoded == remaining.Length);
throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, nameof(TarEntry.Name)), ArgNameEntry);
}
checksum += WriteLeftAlignedBytesAndGetChecksum(remaining, buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix));
Span<byte> encodingBuffer = stackalloc byte[MaxPathName];
int encoded = Encoding.UTF8.GetBytes(_name, encodingBuffer);
ReadOnlySpan<byte> pathNameBytes = encodingBuffer.Slice(0, encoded);
// If the pathname is able to fit in Name, we can write it down there and avoid calculating Prefix.
if (pathNameBytes.Length <= FieldLengths.Name)
{
return WriteLeftAlignedBytesAndGetChecksum(pathNameBytes, buffer.Slice(FieldLocations.Name, FieldLengths.Name));
}
return checksum;
int lastIdx = pathNameBytes.LastIndexOfAny(PathInternal.Utf8DirectorySeparators);
scoped ReadOnlySpan<byte> name;
scoped ReadOnlySpan<byte> prefix;
if (lastIdx < 1) // splitting at the root is not allowed.
{
name = pathNameBytes;
prefix = default;
}
else
{
name = pathNameBytes.Slice(lastIdx + 1);
prefix = pathNameBytes.Slice(0, lastIdx);
}
// At this point path name is > 100.
// Attempt to split it in a way it can use prefix.
while (prefix.Length - name.Length > FieldLengths.Prefix)
{
lastIdx = prefix.LastIndexOfAny(PathInternal.Utf8DirectorySeparators);
if (lastIdx < 1)
{
break;
}
name = pathNameBytes.Slice(lastIdx + 1);
prefix = pathNameBytes.Slice(0, lastIdx);
}
if (prefix.Length <= FieldLengths.Prefix && name.Length <= FieldLengths.Name)
{
Debug.Assert(prefix.Length != 1 || !PathInternal.Utf8DirectorySeparators.Contains(prefix[0]));
int checksum = WriteLeftAlignedBytesAndGetChecksum(prefix, buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix));
checksum += WriteLeftAlignedBytesAndGetChecksum(name, buffer.Slice(FieldLocations.Name, FieldLengths.Name));
return checksum;
}
else
{
throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, nameof(TarEntry.Name)), ArgNameEntry);
}
}
// Writes all the common fields shared by all formats into the specified spans.
......@@ -423,7 +483,20 @@ private int WriteCommonFields(Span<byte> buffer, long actualLength, TarEntryType
if (!string.IsNullOrEmpty(_linkName))
{
checksum += WriteAsAsciiString(_linkName, buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName));
ReadOnlySpan<char> linkName = _linkName;
if (GetUtf8TextLength(linkName) > FieldLengths.LinkName)
{
if (_format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu)
{
throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, nameof(TarEntry.LinkName)), ArgNameEntry);
}
int truncatedLength = GetUtf16TruncatedTextLength(linkName, FieldLengths.LinkName);
linkName = linkName.Slice(0, truncatedLength);
}
checksum += WriteAsUtf8String(linkName, buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName));
}
return checksum;
......@@ -467,12 +540,38 @@ private int WritePosixAndGnuSharedFields(Span<byte> buffer)
if (!string.IsNullOrEmpty(_uName))
{
checksum += WriteAsAsciiString(_uName, buffer.Slice(FieldLocations.UName, FieldLengths.UName));
ReadOnlySpan<char> uName = _uName;
if (GetUtf8TextLength(uName) > FieldLengths.UName)
{
if (_format is not TarEntryFormat.Pax)
{
throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, nameof(PaxTarEntry.UserName)), ArgNameEntry);
}
int truncatedLength = GetUtf16TruncatedTextLength(uName, FieldLengths.UName);
uName = uName.Slice(0, truncatedLength);
}
checksum += WriteAsUtf8String(uName, buffer.Slice(FieldLocations.UName, FieldLengths.UName));
}
if (!string.IsNullOrEmpty(_gName))
{
checksum += WriteAsAsciiString(_gName, buffer.Slice(FieldLocations.GName, FieldLengths.GName));
ReadOnlySpan<char> gName = _gName;
if (GetUtf8TextLength(gName) > FieldLengths.GName)
{
if (_format is not TarEntryFormat.Pax)
{
throw new ArgumentException(SR.Format(SR.TarEntryFieldExceedsMaxLength, nameof(PaxTarEntry.GroupName)), ArgNameEntry);
}
int truncatedLength = GetUtf16TruncatedTextLength(gName, FieldLengths.GName);
gName = gName.Slice(0, truncatedLength);
}
checksum += WriteAsUtf8String(gName, buffer.Slice(FieldLocations.GName, FieldLengths.GName));
}
if (_devMajor > 0)
......@@ -766,11 +865,11 @@ private static int WriteAsTimestamp(DateTimeOffset timestamp, Span<byte> destina
return FormatOctal(unixTimeSeconds, destination);
}
// Writes the specified text as an ASCII string aligned to the left, and returns its checksum.
private static int WriteAsAsciiString(string str, Span<byte> buffer)
// Writes the specified text as an UTF8 string aligned to the left, and returns its checksum.
private static int WriteAsUtf8String(ReadOnlySpan<char> text, Span<byte> buffer)
{
byte[] bytes = Encoding.ASCII.GetBytes(str);
return WriteLeftAlignedBytesAndGetChecksum(bytes.AsSpan(), buffer);
int encoded = Encoding.UTF8.GetBytes(text, buffer);
return WriteLeftAlignedBytesAndGetChecksum(buffer.Slice(0, encoded), buffer);
}
// Gets the special name for the 'name' field in an extended attribute entry.
......@@ -819,5 +918,32 @@ private static string GenerateGlobalExtendedAttributeName(int globalExtendedAttr
return result;
}
private static int GetUtf8TextLength(ReadOnlySpan<char> text)
=> Encoding.UTF8.GetByteCount(text);
// Returns the text's utf16 length truncated at the specified utf8 max length.
private static int GetUtf16TruncatedTextLength(ReadOnlySpan<char> text, int utf8MaxLength)
{
Debug.Assert(GetUtf8TextLength(text) > utf8MaxLength);
int utf8Length = 0;
int utf16TruncatedLength = 0;
foreach (Rune rune in text.EnumerateRunes())
{
utf8Length += rune.Utf8SequenceLength;
if (utf8Length <= utf8MaxLength)
{
utf16TruncatedLength += rune.Utf16SequenceLength;
}
else
{
break;
}
}
return utf16TruncatedLength;
}
}
}
......@@ -47,9 +47,11 @@
<Compile Include="TarTestsBase.Posix.cs" />
<Compile Include="TarTestsBase.Ustar.cs" />
<Compile Include="TarTestsBase.V7.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntry.Entry.Roundtrip.Tests.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntryAsync.File.Tests.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntry.Base.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntryAsync.Tests.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntryAsync.Entry.Roundtrip.Tests.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntryAsync.Entry.Ustar.Tests.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntryAsync.Entry.V7.Tests.cs" />
<Compile Include="TarWriter\TarWriter.WriteEntryAsync.Entry.Pax.Tests.cs" />
......
// Licensed to the .NET Foundation under one or more agreements.
// 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;
......@@ -89,6 +89,11 @@ public abstract partial class TarTestsBase : FileCleanupTestBase
protected const string PaxEaSize = "size";
protected const string PaxEaDevMajor = "devmajor";
protected const string PaxEaDevMinor = "devminor";
internal const char OneByteCharacter = 'a';
internal const char TwoBytesCharacter = '\u00F6';
internal const string FourBytesCharacter = "\uD83D\uDE12";
internal const char Separator = '/';
internal const int MaxPathComponent = 255;
private static readonly string[] V7TestCaseNames = new[]
{
......@@ -622,5 +627,174 @@ public static IEnumerable<object[]> GetPaxAndGnuTestCaseNames()
yield return new object[] { name };
}
}
private static List<string> GetPrefixes()
{
List<string> prefixes = new() { "", "/a/", "./", "../" };
if (OperatingSystem.IsWindows())
prefixes.Add("C:/");
return prefixes;
}
internal static IEnumerable<string> GetNamesPrefixedTestData(NameCapabilities max)
{
Assert.True(Enum.IsDefined(max));
List<string> prefixes = GetPrefixes();
foreach (string prefix in prefixes)
{
// prefix + name of length 100
int nameLength = 100 - prefix.Length;
yield return prefix + Repeat(OneByteCharacter, nameLength);
yield return prefix + Repeat(OneByteCharacter, nameLength - 2) + TwoBytesCharacter;
yield return prefix + Repeat(OneByteCharacter, nameLength - 4) + FourBytesCharacter;
// prefix alone
if (prefix != string.Empty)
yield return prefix;
}
if (max == NameCapabilities.Name)
yield break;
// maxed out name.
foreach (string prefix in prefixes)
{
yield return prefix + Repeat(OneByteCharacter, 100);
yield return prefix + Repeat(OneByteCharacter, 100 - 2) + TwoBytesCharacter;
yield return prefix + Repeat(OneByteCharacter, 100 - 4) + FourBytesCharacter;
}
// maxed out prefix and name.
foreach (string prefix in prefixes)
{
int directoryLength = 155 - prefix.Length;
yield return prefix + Repeat(OneByteCharacter, directoryLength) + Separator + Repeat(OneByteCharacter, 100);
yield return prefix + Repeat(OneByteCharacter, directoryLength - 2) + TwoBytesCharacter + Separator + Repeat(OneByteCharacter, 100);
yield return prefix + Repeat(OneByteCharacter, directoryLength - 4) + FourBytesCharacter + Separator + Repeat(OneByteCharacter, 100);
}
if (max == NameCapabilities.NameAndPrefix)
yield break;
foreach (string prefix in prefixes)
{
int directoryLength = MaxPathComponent - prefix.Length;
yield return prefix + Repeat(OneByteCharacter, directoryLength) + Separator + Repeat(OneByteCharacter, MaxPathComponent);
yield return prefix + Repeat(OneByteCharacter, directoryLength - 2) + TwoBytesCharacter + Separator + Repeat(OneByteCharacter, MaxPathComponent);
yield return prefix + Repeat(OneByteCharacter, directoryLength - 4) + FourBytesCharacter + Separator + Repeat(OneByteCharacter, MaxPathComponent);
}
}
internal static IEnumerable<string> GetNamesNonAsciiTestData(NameCapabilities max)
{
Assert.True(Enum.IsDefined(max));
yield return Repeat(OneByteCharacter, 100);
yield return Repeat(TwoBytesCharacter, 100 / 2);
yield return Repeat(OneByteCharacter, 2) + Repeat(TwoBytesCharacter, 49);
yield return Repeat(FourBytesCharacter, 100 / 4);
yield return Repeat(OneByteCharacter, 4) + Repeat(FourBytesCharacter, 24);
if (max == NameCapabilities.Name)
yield break;
// prefix + name
// this is 256 but is supported because prefix is not required to end in separator.
yield return Repeat(OneByteCharacter, 155) + Separator + Repeat(OneByteCharacter, 100);
// non-ascii prefix + name
yield return Repeat(TwoBytesCharacter, 155 / 2) + Separator + Repeat(OneByteCharacter, 100);
yield return Repeat(FourBytesCharacter, 155 / 4) + Separator + Repeat(OneByteCharacter, 100);
// prefix + non-ascii name
yield return Repeat(OneByteCharacter, 155) + Separator + Repeat(TwoBytesCharacter, 100 / 2);
yield return Repeat(OneByteCharacter, 155) + Separator + Repeat(FourBytesCharacter, 100 / 4);
// non-ascii prefix + non-ascii name
yield return Repeat(TwoBytesCharacter, 155 / 2) + Separator + Repeat(TwoBytesCharacter, 100 / 2);
yield return Repeat(FourBytesCharacter, 155 / 4) + Separator + Repeat(FourBytesCharacter, 100 / 4);
if (max == NameCapabilities.NameAndPrefix)
yield break;
// Pax and Gnu support unlimited paths.
yield return Repeat(OneByteCharacter, MaxPathComponent);
yield return Repeat(TwoBytesCharacter, MaxPathComponent / 2);
yield return Repeat(FourBytesCharacter, MaxPathComponent / 4);
yield return Repeat(OneByteCharacter, MaxPathComponent) + Separator + Repeat(OneByteCharacter, MaxPathComponent);
yield return Repeat(TwoBytesCharacter, MaxPathComponent / 2) + Separator + Repeat(TwoBytesCharacter, MaxPathComponent / 2);
yield return Repeat(FourBytesCharacter, MaxPathComponent / 4) + Separator + Repeat(FourBytesCharacter, MaxPathComponent / 4);
}
internal static IEnumerable<string> GetTooLongNamesTestData(NameCapabilities max)
{
Assert.True(max is NameCapabilities.Name or NameCapabilities.NameAndPrefix);
// root directory can't be saved as prefix
yield return "/" + Repeat(OneByteCharacter, 100);
List<string> prefixes = GetPrefixes();
// 1. non-ascii last character doesn't fit in name.
foreach (string prefix in prefixes)
{
// 1.1. last character doesn't fit fully.
yield return prefix + Repeat(OneByteCharacter, 100 + 1);
yield return prefix + Repeat(OneByteCharacter, 100 - 2) + Repeat(TwoBytesCharacter, 2);
yield return prefix + Repeat(OneByteCharacter, 100 - 4) + Repeat(FourBytesCharacter, 2);
// 1.2. last character doesn't fit by one byte.
yield return prefix + Repeat(OneByteCharacter, 100 - 2 + 1) + Repeat(TwoBytesCharacter, 1);
yield return prefix + Repeat(OneByteCharacter, 100 - 4 + 1) + Repeat(FourBytesCharacter, 1);
}
// 2. non-ascii last character doesn't fit in prefix.
string maxedOutName = Repeat(OneByteCharacter, 100);
// 2.1. last char doesn't fit fully.
yield return Repeat(OneByteCharacter, 155 + 1) + Separator + maxedOutName;
yield return Repeat(OneByteCharacter, 155 - 2) + Repeat(TwoBytesCharacter, 2) + Separator + maxedOutName;
yield return Repeat(OneByteCharacter, 155 - 4) + Repeat(FourBytesCharacter, 2) + Separator + maxedOutName;
// 2.2 last char doesn't fit by one byte.
yield return Repeat(OneByteCharacter, 155 - 2 + 1) + TwoBytesCharacter + Separator + maxedOutName;
yield return Repeat(OneByteCharacter, 155 - 4 + 1) + FourBytesCharacter + Separator + maxedOutName;
if (max is NameCapabilities.NameAndPrefix)
yield break;
// Next cases only apply for V7 which only allows 100 length names.
foreach (string prefix in prefixes)
{
if (prefix.Length == 0)
continue;
yield return prefix + Repeat(OneByteCharacter, 100);
yield return prefix + Repeat(TwoBytesCharacter, 100 / 2);
yield return prefix + Repeat(FourBytesCharacter, 100 / 4);
}
}
internal static string Repeat(char c, int count)
{
return new string(c, count);
}
internal static string Repeat(string c, int count)
{
return string.Concat(Enumerable.Repeat(c, count));
}
internal enum NameCapabilities
{
Name,
NameAndPrefix,
Unlimited
}
}
}
// 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.IO;
using Xunit;
......@@ -174,7 +173,7 @@ private TarEntry CreateTarEntryAndGetExpectedChecksum(TarEntryFormat format, Tar
if (entryType is TarEntryType.SymbolicLink)
{
expectedChecksum += GetLinkChecksum(longLink, out string linkName);
expectedChecksum += GetLinkChecksum(format, longLink, out string linkName);
entry.LinkName = linkName;
}
......@@ -195,23 +194,26 @@ private int GetNameChecksum(TarEntryFormat format, bool longPath, out string ent
}
else
{
entryName = new string('a', 150);
// 100 * 97 = 9700 (first 100 bytes go into 'name' field)
expectedChecksum += 9700;
entryName = new string('a', 100);
expectedChecksum += 9700; // 100 * 97 = 9700 (first 100 bytes go into 'name' field)
// V7 does not support name fields larger than 100
if (format is not TarEntryFormat.V7)
{
entryName += "/" + new string('a', 50);
}
// - V7 does not support name fields larger than 100, writes what it can
// - Gnu writes first 100 bytes in 'name' field, then the full name is written in a LonPath entry
// that precedes this one.
if (format is TarEntryFormat.Ustar or TarEntryFormat.Pax)
// Gnu and Pax writes first 100 bytes in 'name' field, then the full name is written in a metadata entry that precedes this one.
if (format is TarEntryFormat.Ustar)
{
// 50 * 97 = 4850 (rest of bytes go into 'prefix' field)
expectedChecksum += 4850;
// Ustar can write the directory into prefix.
expectedChecksum += 4850; // 50 * 97 = 4850
}
}
return expectedChecksum;
}
private int GetLinkChecksum(bool longLink, out string linkName)
private int GetLinkChecksum(TarEntryFormat format, bool longLink, out string linkName)
{
int expectedChecksum = 0;
if (!longLink)
......@@ -222,12 +224,16 @@ private int GetLinkChecksum(bool longLink, out string linkName)
}
else
{
linkName = new string('a', 150);
// 100 * 97 = 9700 (first 100 bytes go into 'linkName' field)
linkName = new string('a', 100); // 100 * 97 = 9700 (first 100 bytes go into 'linkName' field)
expectedChecksum += 9700;
// - V7 and Ustar ignore the rest of the bytes
// - Pax and Gnu write first 100 bytes in 'linkName' field, then the full link name is written in the
// V7 and Ustar does not support name fields larger than 100
// Pax and Gnu write first 100 bytes in 'linkName' field, then the full link name is written in the
// preceding metadata entry (extended attributes for PAX, LongLink for GNU).
if (format is not TarEntryFormat.V7 and not TarEntryFormat.Ustar)
{
linkName += "/" + new string('a', 50);
}
}
return expectedChecksum;
......
// 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.IO;
using System.Linq;
using Xunit;
namespace System.Formats.Tar.Tests
{
public class TarWriter_WriteEntry_Roundtrip_Tests : TarTestsBase
{
public static IEnumerable<object[]> NameRoundtripsTheoryData()
{
foreach (bool unseekableStream in new[] { false, true })
{
foreach (TarEntryType entryType in new[] { TarEntryType.RegularFile, TarEntryType.Directory })
{
foreach (string name in GetNamesNonAsciiTestData(NameCapabilities.Name).Concat(GetNamesPrefixedTestData(NameCapabilities.Name)))
{
TarEntryType v7EntryType = entryType is TarEntryType.RegularFile ? TarEntryType.V7RegularFile : entryType;
yield return new object[] { TarEntryFormat.V7, v7EntryType, unseekableStream, name };
}
foreach (string name in GetNamesNonAsciiTestData(NameCapabilities.NameAndPrefix).Concat(GetNamesPrefixedTestData(NameCapabilities.NameAndPrefix)))
{
yield return new object[] { TarEntryFormat.Ustar, entryType, unseekableStream, name };
}
foreach (string name in GetNamesNonAsciiTestData(NameCapabilities.Unlimited).Concat(GetNamesPrefixedTestData(NameCapabilities.Unlimited)))
{
yield return new object[] { TarEntryFormat.Pax, entryType, unseekableStream, name };
yield return new object[] { TarEntryFormat.Gnu, entryType, unseekableStream, name };
}
}
}
}
[Theory]
[MemberData(nameof(NameRoundtripsTheoryData))]
public void NameRoundtrips(TarEntryFormat entryFormat, TarEntryType entryType, bool unseekableStream, string name)
{
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, name);
entry.Name = name;
MemoryStream ms = new();
Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms;
using (TarWriter writer = new(s, leaveOpen: true))
{
writer.WriteEntry(entry);
}
ms.Position = 0;
using TarReader reader = new(s);
entry = reader.GetNextEntry();
Assert.Null(reader.GetNextEntry());
Assert.Equal(name, entry.Name);
}
public static IEnumerable<object[]> LinkNameRoundtripsTheoryData()
{
foreach (bool unseekableStream in new[] { false, true })
{
foreach (TarEntryType entryType in new[] { TarEntryType.SymbolicLink, TarEntryType.HardLink })
{
foreach (string name in GetNamesNonAsciiTestData(NameCapabilities.Name).Concat(GetNamesPrefixedTestData(NameCapabilities.Name)))
{
yield return new object[] { TarEntryFormat.V7, entryType, unseekableStream, name };
yield return new object[] { TarEntryFormat.Ustar, entryType, unseekableStream, name };
}
foreach (string name in GetNamesNonAsciiTestData(NameCapabilities.Unlimited).Concat(GetNamesPrefixedTestData(NameCapabilities.Unlimited)))
{
yield return new object[] { TarEntryFormat.Pax, entryType, unseekableStream, name };
yield return new object[] { TarEntryFormat.Gnu, entryType, unseekableStream, name };
}
}
}
}
[Theory]
[MemberData(nameof(LinkNameRoundtripsTheoryData))]
public void LinkNameRoundtrips(TarEntryFormat entryFormat, TarEntryType entryType, bool unseekableStream, string linkName)
{
string name = "foo";
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, name);
entry.LinkName = linkName;
MemoryStream ms = new();
Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms;
using (TarWriter writer = new(s, leaveOpen: true))
{
writer.WriteEntry(entry);
}
ms.Position = 0;
using TarReader reader = new(s);
entry = reader.GetNextEntry();
Assert.Null(reader.GetNextEntry());
Assert.Equal(name, entry.Name);
Assert.Equal(linkName, entry.LinkName);
}
public static IEnumerable<object[]> UserNameGroupNameRoundtripsTheoryData()
{
foreach (bool unseekableStream in new[] { false, true })
{
foreach (TarEntryFormat entryFormat in new[] { TarEntryFormat.Ustar, TarEntryFormat.Pax, TarEntryFormat.Gnu })
{
yield return new object[] { entryFormat, unseekableStream, Repeat(OneByteCharacter, 32) };
yield return new object[] { entryFormat, unseekableStream, Repeat(TwoBytesCharacter, 32 / 2) };
yield return new object[] { entryFormat, unseekableStream, Repeat(FourBytesCharacter, 32 / 4) };
}
}
}
[Theory]
[MemberData(nameof(UserNameGroupNameRoundtripsTheoryData))]
public void UserNameGroupNameRoundtrips(TarEntryFormat entryFormat, bool unseekableStream, string userGroupName)
{
string name = "foo";
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, TarEntryType.RegularFile, name);
PosixTarEntry posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
posixEntry.UserName = userGroupName;
posixEntry.GroupName = userGroupName;
MemoryStream ms = new();
Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms;
using (TarWriter writer = new(s, leaveOpen: true))
{
writer.WriteEntry(posixEntry);
}
ms.Position = 0;
using TarReader reader = new(s);
entry = reader.GetNextEntry();
posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
Assert.Null(reader.GetNextEntry());
Assert.Equal(name, posixEntry.Name);
Assert.Equal(userGroupName, posixEntry.UserName);
Assert.Equal(userGroupName, posixEntry.GroupName);
}
}
}
......@@ -301,8 +301,6 @@ public void WriteTimestampsBeyondOctalLimit(TarEntryFormat format)
}
[Theory]
[InlineData(TarEntryFormat.V7)]
// [InlineData(TarEntryFormat.Ustar)] https://github.com/dotnet/runtime/issues/75360
[InlineData(TarEntryFormat.Pax)]
[InlineData(TarEntryFormat.Gnu)]
public void WriteLongName(TarEntryFormat format)
......@@ -355,5 +353,102 @@ string GetExpectedNameForFormat(TarEntryFormat format, string expectedName)
return expectedName;
}
}
public static IEnumerable<object[]> WriteEntry_TooLongName_Throws_TheoryData()
{
foreach (TarEntryType entryType in new[] { TarEntryType.RegularFile, TarEntryType.Directory })
{
foreach (string name in GetTooLongNamesTestData(NameCapabilities.Name))
{
TarEntryType v7EntryType = entryType is TarEntryType.RegularFile ? TarEntryType.V7RegularFile : entryType;
yield return new object[] { TarEntryFormat.V7, v7EntryType, name };
}
foreach (string name in GetTooLongNamesTestData(NameCapabilities.NameAndPrefix))
{
yield return new object[] { TarEntryFormat.Ustar, entryType, name };
}
}
}
[Theory]
[MemberData(nameof(WriteEntry_TooLongName_Throws_TheoryData))]
public void WriteEntry_TooLongName_Throws(TarEntryFormat entryFormat, TarEntryType entryType, string name)
{
using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, name);
Assert.Throws<ArgumentException>("entry", () => writer.WriteEntry(entry));
}
public static IEnumerable<object[]> WriteEntry_TooLongLinkName_Throws_TheoryData()
{
foreach (TarEntryType entryType in new[] { TarEntryType.SymbolicLink, TarEntryType.HardLink })
{
foreach (string name in GetTooLongNamesTestData(NameCapabilities.Name))
{
yield return new object[] { TarEntryFormat.V7, entryType, name };
}
foreach (string name in GetTooLongNamesTestData(NameCapabilities.NameAndPrefix))
{
yield return new object[] { TarEntryFormat.Ustar, entryType, name };
}
}
}
[Theory]
[MemberData(nameof(WriteEntry_TooLongLinkName_Throws_TheoryData))]
public void WriteEntry_TooLongLinkName_Throws(TarEntryFormat entryFormat, TarEntryType entryType, string linkName)
{
using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, "foo");
entry.LinkName = linkName;
Assert.Throws<ArgumentException>("entry", () => writer.WriteEntry(entry));
}
public static IEnumerable<object[]> WriteEntry_TooLongUserGroupName_Throws_TheoryData()
{
// Not testing Pax as it supports unlimited size uname/gname.
foreach (TarEntryFormat entryFormat in new[] { TarEntryFormat.Ustar, TarEntryFormat.Gnu })
{
// Last character doesn't fit fully.
yield return new object[] { entryFormat, Repeat(OneByteCharacter, 32 + 1) };
yield return new object[] { entryFormat, Repeat(TwoBytesCharacter, 32 / 2 + 1) };
yield return new object[] { entryFormat, Repeat(FourBytesCharacter, 32 / 4 + 1) };
// Last character doesn't fit by one byte.
yield return new object[] { entryFormat, Repeat(TwoBytesCharacter, 32 - 2 + 1) + TwoBytesCharacter };
yield return new object[] { entryFormat, Repeat(FourBytesCharacter, 32 - 4 + 1) + FourBytesCharacter };
}
}
[Theory]
[MemberData(nameof(WriteEntry_TooLongUserGroupName_Throws_TheoryData))]
public void WriteEntry_TooLongUserName_Throws(TarEntryFormat entryFormat, string userName)
{
using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, TarEntryType.RegularFile, "foo");
PosixTarEntry posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
posixEntry.UserName = userName;
Assert.Throws<ArgumentException>("entry", () => writer.WriteEntry(entry));
}
[Theory]
[MemberData(nameof(WriteEntry_TooLongUserGroupName_Throws_TheoryData))]
public void WriteEntry_TooLongGroupName_Throws(TarEntryFormat entryFormat, string groupName)
{
using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, TarEntryType.RegularFile, "foo");
PosixTarEntry posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
posixEntry.GroupName = groupName;
Assert.Throws<ArgumentException>("entry", () => writer.WriteEntry(entry));
}
}
}
// 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.IO;
using System.Threading.Tasks;
using Xunit;
namespace System.Formats.Tar.Tests
{
public class TarWriter_WriteEntryAsync_Roundtrip_Tests : TarTestsBase
{
public static IEnumerable<object[]> NameRoundtripsAsyncTheoryData()
=> TarWriter_WriteEntry_Roundtrip_Tests.NameRoundtripsTheoryData();
[Theory]
[MemberData(nameof(NameRoundtripsAsyncTheoryData))]
public async Task NameRoundtripsAsync(TarEntryFormat entryFormat, TarEntryType entryType, bool unseekableStream, string name)
{
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, name);
entry.Name = name;
MemoryStream ms = new();
Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms;
await using (TarWriter writer = new(s, leaveOpen: true))
{
await writer.WriteEntryAsync(entry);
}
ms.Position = 0;
await using TarReader reader = new(s);
entry = await reader.GetNextEntryAsync();
Assert.Null(await reader.GetNextEntryAsync());
Assert.Equal(name, entry.Name);
}
public static IEnumerable<object[]> LinkNameRoundtripsAsyncTheoryData()
=> TarWriter_WriteEntry_Roundtrip_Tests.LinkNameRoundtripsTheoryData();
[Theory]
[MemberData(nameof(LinkNameRoundtripsAsyncTheoryData))]
public async Task LinkNameRoundtripsAsync(TarEntryFormat entryFormat, TarEntryType entryType, bool unseekableStream, string linkName)
{
string name = "foo";
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, name);
entry.LinkName = linkName;
MemoryStream ms = new();
Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms;
await using (TarWriter writer = new(s, leaveOpen: true))
{
await writer.WriteEntryAsync(entry);
}
ms.Position = 0;
await using TarReader reader = new(s);
entry = await reader.GetNextEntryAsync();
Assert.Null(await reader.GetNextEntryAsync());
Assert.Equal(name, entry.Name);
Assert.Equal(linkName, entry.LinkName);
}
public static IEnumerable<object[]> UserNameGroupNameRoundtripsAsyncTheoryData()
=> TarWriter_WriteEntry_Roundtrip_Tests.UserNameGroupNameRoundtripsTheoryData();
[Theory]
[MemberData(nameof(UserNameGroupNameRoundtripsAsyncTheoryData))]
public async Task UserNameGroupNameRoundtripsAsync(TarEntryFormat entryFormat, bool unseekableStream, string userGroupName)
{
string name = "foo";
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, TarEntryType.RegularFile, name);
PosixTarEntry posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
posixEntry.UserName = userGroupName;
posixEntry.GroupName = userGroupName;
MemoryStream ms = new();
Stream s = unseekableStream ? new WrappedStream(ms, ms.CanRead, ms.CanWrite, canSeek: false) : ms;
await using (TarWriter writer = new(s, leaveOpen: true))
{
await writer.WriteEntryAsync(posixEntry);
}
ms.Position = 0;
await using TarReader reader = new(s);
entry = await reader.GetNextEntryAsync();
posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
Assert.Null(await reader.GetNextEntryAsync());
Assert.Equal(name, posixEntry.Name);
Assert.Equal(userGroupName, posixEntry.UserName);
Assert.Equal(userGroupName, posixEntry.GroupName);
}
}
}
......@@ -322,5 +322,62 @@ await using (TarReader reader = new TarReader(archiveStream))
}
}
}
public static IEnumerable<object[]> WriteEntry_TooLongName_Throws_Async_TheoryData()
=> TarWriter_WriteEntry_Tests.WriteEntry_TooLongName_Throws_TheoryData();
[Theory]
[MemberData(nameof(WriteEntry_TooLongName_Throws_Async_TheoryData))]
public async Task WriteEntry_TooLongName_Throws_Async(TarEntryFormat entryFormat, TarEntryType entryType, string name)
{
await using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, name);
await Assert.ThrowsAsync<ArgumentException>("entry", () => writer.WriteEntryAsync(entry));
}
public static IEnumerable<object[]> WriteEntry_TooLongLinkName_Throws_Async_TheoryData()
=> TarWriter_WriteEntry_Tests.WriteEntry_TooLongLinkName_Throws_TheoryData();
[Theory]
[MemberData(nameof(WriteEntry_TooLongLinkName_Throws_Async_TheoryData))]
public async Task WriteEntry_TooLongLinkName_Throws_Async(TarEntryFormat entryFormat, TarEntryType entryType, string linkName)
{
await using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, entryType, "foo");
entry.LinkName = linkName;
await Assert.ThrowsAsync<ArgumentException>("entry", () => writer.WriteEntryAsync(entry));
}
public static IEnumerable<object[]> WriteEntry_TooLongUserGroupName_Throws_Async_TheoryData()
=> TarWriter_WriteEntry_Tests.WriteEntry_TooLongUserGroupName_Throws_TheoryData();
[Theory]
[MemberData(nameof(WriteEntry_TooLongUserGroupName_Throws_Async_TheoryData))]
public async Task WriteEntry_TooLongUserName_Throws_Async(TarEntryFormat entryFormat, string userName)
{
await using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, TarEntryType.RegularFile, "foo");
PosixTarEntry posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
posixEntry.UserName = userName;
await Assert.ThrowsAsync<ArgumentException>("entry", () => writer.WriteEntryAsync(entry));
}
[Theory]
[MemberData(nameof(WriteEntry_TooLongUserGroupName_Throws_Async_TheoryData))]
public async Task WriteEntry_TooLongGroupName_Throws_Async(TarEntryFormat entryFormat, string groupName)
{
await using TarWriter writer = new(new MemoryStream());
TarEntry entry = InvokeTarEntryCreationConstructor(entryFormat, TarEntryType.RegularFile, "foo");
PosixTarEntry posixEntry = Assert.IsAssignableFrom<PosixTarEntry>(entry);
posixEntry.GroupName = groupName;
await Assert.ThrowsAsync<ArgumentException>("entry", () => writer.WriteEntryAsync(entry));
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册