Skip to content

Commit

Permalink
Limit using the GNU numeric format to TarEntryFormat.Gnu.
Browse files Browse the repository at this point in the history
  • Loading branch information
tmds committed May 23, 2024
1 parent 64f7ef4 commit 9ca577e
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public int DeviceMajor
}

ArgumentOutOfRangeException.ThrowIfNegative(value);
if (FormatIsOctalOnly)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 0x1FFFFF); // 7777777 in octal
}

_header._devMajor = value;
}
Expand All @@ -84,6 +88,10 @@ public int DeviceMinor
}

ArgumentOutOfRangeException.ThrowIfNegative(value);
if (FormatIsOctalOnly)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 0x1FFFFF); // 7777777 in octal
}

_header._devMinor = value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public abstract partial class TarEntry
// Used to access the data section of this entry in an unseekable file
private TarReader? _readerOfOrigin;

// These formats have a limited numeric range due to the octal number representation.
protected bool FormatIsOctalOnly => _header._format is TarEntryFormat.V7 or TarEntryFormat.Ustar;

// Constructor called when reading a TarEntry from a TarReader.
internal TarEntry(TarHeader header, TarReader readerOfOrigin, TarEntryFormat format)
{
Expand Down Expand Up @@ -98,6 +101,10 @@ public DateTimeOffset ModificationTime
get => _header._mTime;
set
{
if (FormatIsOctalOnly)
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, DateTimeOffset.UnixEpoch);
}
_header._mTime = value;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1053,14 +1053,10 @@ private int FormatNumeric(int value, Span<byte> destination)
{
Debug.Assert(destination.Length == 8, "8 byte field expected.");

// Prefer the octal format. For non-PAX, use GNU format to widen the range.
bool useOctal = (value >= 0 && value <= Octal8ByteFieldMaxValue) || _format is TarEntryFormat.Pax;
// Use GNU format to widen the range.
bool useGnuFormat = (value < 0 || value > Octal8ByteFieldMaxValue) && _format == TarEntryFormat.Gnu;

if (useOctal)
{
return FormatOctal(value, destination);
}
else
if (useGnuFormat)
{
// GNU format: store negative numbers in big endian format with leading '0xff' byte.
// store positive numbers in big endian format with leading '0x80' byte.
Expand All @@ -1069,28 +1065,32 @@ private int FormatNumeric(int value, Span<byte> destination)
BinaryPrimitives.WriteInt64BigEndian(destination, destinationValue);
return Checksum(destination);
}
else
{
return FormatOctal(value, destination);
}
}

private int FormatNumeric(long value, Span<byte> destination)
{
Debug.Assert(destination.Length == 12, "12 byte field expected.");
const int Offset = 4; // 4 bytes before the long.

// Prefer the octal format. For non-PAX, use GNU format to widen the range.
bool useOctal = (value >= 0 && value <= Octal12ByteFieldMaxValue) || _format is TarEntryFormat.Pax;
// Use GNU format to widen the range.
bool useGnuFormat = (value < 0 || value > Octal12ByteFieldMaxValue) && _format == TarEntryFormat.Gnu;

if (useOctal)
{
return FormatOctal(value, destination);
}
else
if (useGnuFormat)
{
// GNU format: store negative numbers in big endian format with leading '0xff' byte.
// store positive numbers in big endian format with leading '0x80' byte.
BinaryPrimitives.WriteUInt32BigEndian(destination, value < 0 ? 0xffffffff : 1U << 31);
BinaryPrimitives.WriteInt64BigEndian(destination.Slice(Offset), value);
return Checksum(destination);
}
else
{
return FormatOctal(value, destination);
}
}

// Writes the specified decimal number as a right-aligned octal number and returns its checksum.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ internal static T ParseNumeric<T>(ReadOnlySpan<byte> buffer) where T : struct, I
// This limits the range of values that can be stored in the fields.
// To increase the supported range, a GNU extension defines that when the leading byte is
// '0xff'/'0x80' the remaining bytes are a negative/positive big formatted endian value.
// Like the 'tar' tool we are permissive when encountering this representation in non GNU formats.
byte leadingByte = buffer[0];
if (leadingByte == 0xff)
{
Expand Down
5 changes: 4 additions & 1 deletion src/libraries/System.Formats.Tar/tests/Manual/ManualTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ public static IEnumerable<object[]> WriteEntry_LongFileSize_TheoryData()
foreach (TarEntryFormat entryFormat in new[] { TarEntryFormat.V7, TarEntryFormat.Ustar, TarEntryFormat.Gnu, TarEntryFormat.Pax })
{
yield return new object[] { entryFormat, LegacyMaxFileSize, unseekableStream };
yield return new object[] { entryFormat, LegacyMaxFileSize + 1, unseekableStream };
}

// Pax and Gnu supports unlimited size files.
yield return new object[] { TarEntryFormat.Pax, LegacyMaxFileSize + 1, unseekableStream };
yield return new object[] { TarEntryFormat.Gnu, LegacyMaxFileSize + 1, unseekableStream };
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/libraries/System.Formats.Tar/tests/TarTestsBase.Gnu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,33 @@ protected void SetFifo(GnuTarEntry fifo)

protected void SetGnuProperties(GnuTarEntry entry)
{
// The octal format limits the representable range.
bool formatIsOctalOnly = entry.Format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu;

DateTimeOffset approxNow = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(6));

// ATime: Verify the default value was approximately "now"
Assert.True(entry.AccessTime > approxNow);
if (formatIsOctalOnly)
{
Assert.Throws<ArgumentOutOfRangeException>(() => entry.AccessTime = DateTimeOffset.MinValue);
}
else
{
entry.AccessTime = DateTimeOffset.MinValue;
}
entry.AccessTime = TestAccessTime;

// CTime: Verify the default value was approximately "now"
Assert.True(entry.ChangeTime > approxNow);
if (formatIsOctalOnly)
{
Assert.Throws<ArgumentOutOfRangeException>(() => entry.ChangeTime = DateTimeOffset.MinValue);
}
else
{
entry.ChangeTime = DateTimeOffset.MinValue;
}
entry.ChangeTime = TestChangeTime;
}

Expand Down
38 changes: 38 additions & 0 deletions src/libraries/System.Formats.Tar/tests/TarTestsBase.Posix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ protected void SetPosixProperties(PosixTarEntry entry)

private void SetBlockDeviceProperties(PosixTarEntry device)
{
// The octal format limits the representable range.
bool formatIsOctalOnly = device.Format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu;

Assert.NotNull(device);
Assert.Equal(TarEntryType.BlockDevice, device.EntryType);
SetCommonProperties(device);
Expand All @@ -27,16 +30,35 @@ private void SetBlockDeviceProperties(PosixTarEntry device)
// DeviceMajor
Assert.Equal(DefaultDeviceMajor, device.DeviceMajor);
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMajor = -1);
if (formatIsOctalOnly)
{
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMajor = 2097152);
}
else
{
device.DeviceMajor = 2097152;
}
device.DeviceMajor = TestBlockDeviceMajor;

// DeviceMinor
Assert.Equal(DefaultDeviceMinor, device.DeviceMinor);
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMinor = -1);
if (formatIsOctalOnly)
{
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMinor = 2097152);
}
else
{
device.DeviceMinor = 2097152;
}
device.DeviceMinor = TestBlockDeviceMinor;
}

private void SetCharacterDeviceProperties(PosixTarEntry device)
{
// The octal format limits the representable range.
bool formatIsOctalOnly = device.Format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu;

Assert.NotNull(device);
Assert.Equal(TarEntryType.CharacterDevice, device.EntryType);
SetCommonProperties(device);
Expand All @@ -45,11 +67,27 @@ private void SetCharacterDeviceProperties(PosixTarEntry device)
// DeviceMajor
Assert.Equal(DefaultDeviceMajor, device.DeviceMajor);
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMajor = -1);
if (formatIsOctalOnly)
{
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMajor = 2097152);
}
else
{
device.DeviceMajor = 2097152;
}
device.DeviceMajor = TestCharacterDeviceMajor;

// DeviceMinor
Assert.Equal(DefaultDeviceMinor, device.DeviceMinor);
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMinor = -1);
if (formatIsOctalOnly)
{
Assert.Throws<ArgumentOutOfRangeException>(() => device.DeviceMinor = 2097152);
}
else
{
device.DeviceMinor = 2097152;
}
device.DeviceMinor = TestCharacterDeviceMinor;
}

Expand Down
11 changes: 11 additions & 0 deletions src/libraries/System.Formats.Tar/tests/TarTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ protected void SetCommonSymbolicLink(TarEntry symbolicLink)

protected void SetCommonProperties(TarEntry entry, bool isDirectory = false)
{
// The octal format limits the range.
bool formatIsOctalOnly = entry.Format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu;

// Length (Data is checked outside this method)
Assert.Equal(0, entry.Length);

Expand All @@ -329,6 +332,14 @@ protected void SetCommonProperties(TarEntry entry, bool isDirectory = false)
DateTimeOffset approxNow = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(6));
Assert.True(entry.ModificationTime > approxNow);

if (formatIsOctalOnly)
{
Assert.Throws<ArgumentOutOfRangeException>(() => entry.ModificationTime = DateTime.MinValue); // Minimum allowed is UnixEpoch, not MinValue
}
else
{
entry.ModificationTime = DateTimeOffset.MinValue;
}
entry.ModificationTime = TestModificationTime;

// Name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,29 @@ public static IEnumerable<object[]> WriteIntField_TheoryData()
{
// Min value.
yield return new object[] { format, 0 };
// Max value.
yield return new object[] { format, int.MaxValue }; // This doesn't fit an 8-byte field with octal representation.

yield return new object[] { format, 1 };
yield return new object[] { format, 42 };

// Max value octal.
yield return new object[] { format, 0x1FFFFF };

// These values do not fit the octal representation.
bool formatIsOctalOnly = format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu;
if (!formatIsOctalOnly)
{
// Max value property.
yield return new object[] { format, int.MaxValue };
}

}
}

public static IEnumerable<object[]> WriteTimeStampsWithFormats_TheoryData()
{
foreach (TarEntryFormat entryFormat in new[] { TarEntryFormat.V7, TarEntryFormat.Ustar, TarEntryFormat.Gnu, TarEntryFormat.Pax })
{
foreach (DateTimeOffset timestamp in GetWriteTimeStamps())
foreach (DateTimeOffset timestamp in GetWriteTimeStamps(entryFormat))
{
yield return new object[] { entryFormat, timestamp };
}
Expand All @@ -76,31 +86,42 @@ public static IEnumerable<object[]> WriteTimeStampsWithFormats_TheoryData()

public static IEnumerable<object[]> WriteTimeStamps_TheoryData()
{
foreach (DateTimeOffset timestamp in GetWriteTimeStamps())
foreach (DateTimeOffset timestamp in GetWriteTimeStamps(TarEntryFormat.Pax))
{
yield return new object[] { timestamp };
}
}

private static IEnumerable<DateTimeOffset> GetWriteTimeStamps()
private static IEnumerable<DateTimeOffset> GetWriteTimeStamps(TarEntryFormat format)
{
// One second past Y2K38
yield return new DateTimeOffset(2038, 1, 19, 3, 14, 8, TimeSpan.Zero);

// One second past what a 12-byte field can store with octal representation
yield return new DateTimeOffset(2242, 3, 16, 12, 56, 33, TimeSpan.Zero);
// Min value octal
yield return DateTimeOffset.UnixEpoch;

// Min value
yield return DateTimeOffset.MinValue;
// Max value 12-byte octal field.
yield return DateTimeOffset.UnixEpoch + new TimeSpan(0x1FFFFFFFF * TimeSpan.TicksPerSecond);

// Max value. Everything below seconds is set to zero for test equality comparison.
yield return new DateTimeOffset(new DateTime(DateTime.MaxValue.Year,
DateTime.MaxValue.Month,
DateTime.MaxValue.Day,
DateTime.MaxValue.Hour,
DateTime.MaxValue.Minute,
DateTime.MaxValue.Second,
DateTime.MaxValue.Kind), TimeSpan.Zero);
// These values do not fit the octal representation.
bool formatIsOctalOnly = format is not TarEntryFormat.Pax and not TarEntryFormat.Gnu;
if (!formatIsOctalOnly)
{
// Min value property.
yield return DateTimeOffset.MinValue; // This is not representable with the octal format.

// One second past what a 12-byte field can store with octal representation
yield return DateTimeOffset.UnixEpoch + new TimeSpan((0x1FFFFFFFF + 1) * TimeSpan.TicksPerSecond);

// Max value property. Everything below seconds is set to zero for test equality comparison.
yield return new DateTimeOffset(new DateTime(DateTime.MaxValue.Year,
DateTime.MaxValue.Month,
DateTime.MaxValue.Day,
DateTime.MaxValue.Hour,
DateTime.MaxValue.Minute,
DateTime.MaxValue.Second,
DateTime.MaxValue.Kind), TimeSpan.Zero);
}
}
}
}

0 comments on commit 9ca577e

Please sign in to comment.