Beispiel #1
0
        // Reads attributes specific to the GNU format.
        // Throws if any conversion fails.
        private void ReadGnuAttributes(Span <byte> buffer)
        {
            // Convert byte arrays
            long aTime = TarHelpers.GetTenBaseLongFromOctalAsciiChars(buffer.Slice(FieldLocations.ATime, FieldLengths.ATime));

            _aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(aTime);

            long cTime = TarHelpers.GetTenBaseLongFromOctalAsciiChars(buffer.Slice(FieldLocations.CTime, FieldLengths.CTime));

            _cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(cTime);

            // TODO: Read the bytes of the currently unsupported GNU fields, in case user wants to write this entry into another GNU archive, they need to be preserved. https://github.com/dotnet/runtime/issues/68230
        }
Beispiel #2
0
        // Reads the ustar prefix attribute.
        // Throws if a conversion to an expected data type fails.
        private void ReadUstarAttributes(Span <byte> buffer)
        {
            _prefix = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix));

            // In ustar, Prefix is used to store the *leading* path segments of
            // Name, if the full path did not fit in the Name byte array.
            if (!string.IsNullOrEmpty(_prefix))
            {
                // Prefix never has a leading separator, so we add it
                // it should always  be a forward slash for compatibility
                _name = string.Format(UstarPrefixFormat, _prefix, _name);
            }
        }
Beispiel #3
0
        // Shared checksum and data length calculations for GNU entry writing.
        private void WriteAsGnuSharedInternal(Span <byte> buffer, out long actualLength)
        {
            actualLength = GetTotalDataBytesToWrite();

            int tmpChecksum = WriteName(buffer);

            tmpChecksum += WriteCommonFields(buffer, actualLength, TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.Gnu, _typeFlag));
            tmpChecksum += WriteGnuMagicAndVersion(buffer);
            tmpChecksum += WritePosixAndGnuSharedFields(buffer);
            tmpChecksum += WriteGnuFields(buffer);

            _checksum = WriteChecksum(tmpChecksum, buffer);
        }
Beispiel #4
0
        private const string UstarPrefixFormat = "{0}/{1}"; // "prefix/name"

        // Attempts to read all the fields of the next header.
        // Throws if end of stream is reached or if any data type conversion fails.
        // Returns true if all the attributes were read successfully, false otherwise.
        internal bool TryGetNextHeader(Stream archiveStream, bool copyData)
        {
            // The four supported formats have a header that fits in the default record size
            byte[] rented = ArrayPool <byte> .Shared.Rent(minimumLength : TarHelpers.RecordSize);

            Span <byte> buffer = rented.AsSpan(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger

            buffer.Clear();                                               // Rented arrays aren't clean

            TarHelpers.ReadOrThrow(archiveStream, buffer);

            try
            {
                // Confirms if v7 or pax, or tentatively selects ustar
                if (!TryReadCommonAttributes(buffer))
                {
                    return(false);
                }

                // Confirms if gnu, or tentatively selects ustar
                ReadMagicAttribute(buffer);

                if (_format != TarFormat.V7)
                {
                    // Confirms if gnu
                    ReadVersionAttribute(buffer);

                    // Fields that ustar, pax and gnu share identically
                    ReadPosixAndGnuSharedAttributes(buffer);

                    Debug.Assert(_format is TarFormat.Ustar or TarFormat.Pax or TarFormat.Gnu);
                    if (_format == TarFormat.Ustar)
                    {
                        ReadUstarAttributes(buffer);
                    }
                    else if (_format == TarFormat.Gnu)
                    {
                        ReadGnuAttributes(buffer);
                    }
                    // In PAX, there is nothing to read in this section (empty space)
                }

                ProcessDataBlock(archiveStream, copyData);

                return(true);
            }
            finally
            {
                ArrayPool <byte> .Shared.Return(rented);
            }
        }
Beispiel #5
0
        // Writes the Ustar header fields to the specified buffer, calculates and writes the checksum, then returns the final data length.
        private long WriteUstarFieldsToBuffer(Span <byte> buffer)
        {
            long         actualLength    = GetTotalDataBytesToWrite();
            TarEntryType actualEntryType = TarHelpers.GetCorrectTypeFlagForFormat(TarEntryFormat.Ustar, _typeFlag);

            int tmpChecksum = WritePosixName(buffer);

            tmpChecksum += WriteCommonFields(buffer, actualLength, actualEntryType);
            tmpChecksum += WritePosixMagicAndVersion(buffer);
            tmpChecksum += WritePosixAndGnuSharedFields(buffer);
            _checksum    = WriteChecksum(tmpChecksum, buffer);

            return(actualLength);
        }
Beispiel #6
0
        // Constructor called when the user creates a TarEntry instance from scratch.
        internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat format, bool isGea)
        {
            ArgumentException.ThrowIfNullOrEmpty(entryName);

            Debug.Assert(!isGea || entryType is TarEntryType.GlobalExtendedAttributes);

            if (!isGea)
            {
                TarHelpers.ThrowIfEntryTypeNotSupported(entryType, format);
            }

            // Default values for fields shared by all supported formats
            _header = new TarHeader(format, entryName, TarHelpers.GetDefaultMode(entryType), DateTimeOffset.UtcNow, entryType);
        }
Beispiel #7
0
        // Constructor called when converting an entry to the selected format.
        internal TarEntry(TarEntry other, TarEntryFormat format)
        {
            if (other is PaxGlobalExtendedAttributesTarEntry)
            {
                throw new InvalidOperationException(SR.TarCannotConvertPaxGlobalExtendedAttributesEntry);
            }

            TarEntryType compatibleEntryType = TarHelpers.GetCorrectTypeFlagForFormat(format, other.EntryType);

            TarHelpers.ThrowIfEntryTypeNotSupported(compatibleEntryType, format);

            _readerOfOrigin = other._readerOfOrigin;

            _header = new TarHeader(format, compatibleEntryType, other._header);
        }
Beispiel #8
0
        // Returns a dictionary containing the extended attributes collected from the provided byte buffer.
        private void ReadExtendedAttributesFromBuffer(ReadOnlySpan <byte> buffer, string name)
        {
            string dataAsString = TarHelpers.GetTrimmedUtf8String(buffer);

            using StringReader reader = new(dataAsString);

            while (TryGetNextExtendedAttribute(reader, out string?key, out string?value))
            {
                if (ExtendedAttributes.ContainsKey(key))
                {
                    throw new FormatException(string.Format(SR.TarDuplicateExtendedAttribute, name));
                }
                ExtendedAttributes.Add(key, value);
            }
        }
Beispiel #9
0
        // Writes the current header's data stream into the archive stream.
        private static void WriteData(Stream archiveStream, Stream dataStream, long actualLength)
        {
            dataStream.CopyTo(archiveStream); // The data gets copied from the current position

            int paddingAfterData = TarHelpers.CalculatePadding(actualLength);

            if (paddingAfterData != 0)
            {
                Debug.Assert(paddingAfterData <= TarHelpers.RecordSize);

                Span <byte> padding = stackalloc byte[TarHelpers.RecordSize];
                padding = padding.Slice(0, paddingAfterData);
                padding.Clear();

                archiveStream.Write(padding);
            }
        }
Beispiel #10
0
        // Reads the attributes shared by the POSIX and GNU formats.
        // Throws if converting the bytes to their expected data type fails.
        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));

            // DevMajor and DevMinor only have values with character devices and block devices.
            // For all other typeflags, the values in these fields are irrelevant.
            if (_typeFlag is TarEntryType.CharacterDevice or TarEntryType.BlockDevice)
            {
                // Major number for a character device or block device entry.
                _devMajor = (int)TarHelpers.ParseOctal <uint>(buffer.Slice(FieldLocations.DevMajor, FieldLengths.DevMajor));

                // Minor number for a character device or block device entry.
                _devMinor = (int)TarHelpers.ParseOctal <uint>(buffer.Slice(FieldLocations.DevMinor, FieldLengths.DevMinor));
            }
        }
Beispiel #11
0
        // Constructs a GNU metadata header with default values for the specified entry type.
        private static TarHeader GetDefaultGnuLongMetadataHeader(int longTextLength, TarEntryType entryType)
        {
            Debug.Assert((entryType is TarEntryType.LongPath && longTextLength > FieldLengths.Name) ||
                         (entryType is TarEntryType.LongLink && longTextLength > FieldLengths.LinkName));

            TarHeader longMetadataHeader = new(TarEntryFormat.Gnu);

            longMetadataHeader._name       = GnuLongMetadataName; // Same name for both longpath or longlink
            longMetadataHeader._mode       = TarHelpers.GetDefaultMode(entryType);
            longMetadataHeader._uid        = 0;
            longMetadataHeader._gid        = 0;
            longMetadataHeader._mTime      = DateTimeOffset.MinValue; // 0
            longMetadataHeader._typeFlag   = entryType;
            longMetadataHeader._dataStream = new MemoryStream();

            return(longMetadataHeader);
        }
Beispiel #12
0
        // /// <summary>
        // /// Asynchronously writes the specified file into the archive stream as a tar entry.
        // /// </summary>
        // /// <param name="fileName">The path to the file to write to the archive.</param>
        // /// <param name="entryName">The name of the file as it should be represented in the archive. It should include the optional relative path and the filename.</param>
        // /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param>
        // public Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default)
        // {
        //     throw new NotImplementedException();
        // }

        /// <summary>
        /// Writes the specified entry into the archive stream.
        /// </summary>
        /// <param name="entry">The tar entry to write.</param>
        /// <remarks><para>Before writing an entry to the archive, if you wrote data into the entry's <see cref="TarEntry.DataStream"/>, make sure to rewind it to the desired start position.</para>
        /// <para>These are the entry types supported for writing on each format:</para>
        /// <list type="bullet">
        /// <item>
        /// <para><see cref="TarFormat.V7"/></para>
        /// <list type="bullet">
        /// <item><see cref="TarEntryType.Directory"/></item>
        /// <item><see cref="TarEntryType.HardLink"/></item>
        /// <item><see cref="TarEntryType.SymbolicLink"/></item>
        /// <item><see cref="TarEntryType.V7RegularFile"/></item>
        /// </list>
        /// </item>
        /// <item>
        /// <para><see cref="TarFormat.Ustar"/>, <see cref="TarFormat.Pax"/> and <see cref="TarFormat.Gnu"/></para>
        /// <list type="bullet">
        /// <item><see cref="TarEntryType.BlockDevice"/></item>
        /// <item><see cref="TarEntryType.CharacterDevice"/></item>
        /// <item><see cref="TarEntryType.Directory"/></item>
        /// <item><see cref="TarEntryType.Fifo"/></item>
        /// <item><see cref="TarEntryType.HardLink"/></item>
        /// <item><see cref="TarEntryType.RegularFile"/></item>
        /// <item><see cref="TarEntryType.SymbolicLink"/></item>
        /// </list>
        /// </item>
        /// </list>
        /// </remarks>
        /// <exception cref="ObjectDisposedException">The archive stream is disposed.</exception>
        /// <exception cref="InvalidOperationException">The entry type of the <paramref name="entry"/> is not supported for writing.</exception>
        /// <exception cref="IOException">An I/O problem occurred.</exception>
        public void WriteEntry(TarEntry entry)
        {
            ThrowIfDisposed();

            TarHelpers.VerifyEntryTypeIsSupported(entry.EntryType, Format, forWriting: true);

            WriteGlobalExtendedAttributesEntryIfNeeded();

            byte[] rented = ArrayPool <byte> .Shared.Rent(minimumLength : TarHelpers.RecordSize);

            Span <byte> buffer = rented.AsSpan(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger

            buffer.Clear();                                               // Rented arrays aren't clean
            try
            {
                switch (Format)
                {
                case TarFormat.V7:
                    entry._header.WriteAsV7(_archiveStream, buffer);
                    break;

                case TarFormat.Ustar:
                    entry._header.WriteAsUstar(_archiveStream, buffer);
                    break;

                case TarFormat.Pax:
                    entry._header.WriteAsPax(_archiveStream, buffer);
                    break;

                case TarFormat.Gnu:
                    entry._header.WriteAsGnu(_archiveStream, buffer);
                    break;

                case TarFormat.Unknown:
                default:
                    throw new FormatException(string.Format(SR.TarInvalidFormat, Format));
                }
            }
            finally
            {
                ArrayPool <byte> .Shared.Return(rented);
            }

            _wroteEntries = true;
        }
Beispiel #13
0
        // Returns a stream that represents the data section of the current header.
        // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned.
        // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream.
        // Otherwise, it returns an unseekable wrapper stream.
        private Stream?GetDataStream(Stream archiveStream, bool copyData)
        {
            if (_size == 0)
            {
                return(null);
            }

            if (copyData)
            {
                MemoryStream copiedData = new MemoryStream();
                TarHelpers.CopyBytes(archiveStream, copiedData, _size);
                return(copiedData);
            }

            return(archiveStream.CanSeek
                ? new SeekableSubReadStream(archiveStream, archiveStream.Position, _size)
                : new SubReadStream(archiveStream, 0, _size));
        }
Beispiel #14
0
        // Asynchronously writes the current header's data stream into the archive stream.
        private static async Task WriteDataAsync(Stream archiveStream, Stream dataStream, long actualLength, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            await dataStream.CopyToAsync(archiveStream, cancellationToken).ConfigureAwait(false); // The data gets copied from the current position

            int paddingAfterData = TarHelpers.CalculatePadding(actualLength);

            if (paddingAfterData != 0)
            {
                byte[] buffer = ArrayPool <byte> .Shared.Rent(paddingAfterData);

                Array.Clear(buffer, 0, paddingAfterData);

                await archiveStream.WriteAsync(buffer.AsMemory(0, paddingAfterData), cancellationToken).ConfigureAwait(false);

                ArrayPool <byte> .Shared.Return(buffer);
            }
        }
Beispiel #15
0
        // Asynchronously moves the underlying archive stream position pointer to the beginning of the next header.
        internal async ValueTask AdvanceDataStreamIfNeededAsync(CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (_previouslyReadEntry == null)
            {
                return;
            }

            if (_archiveStream.CanSeek)
            {
                Debug.Assert(_previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment > 0);
                _archiveStream.Position = _previouslyReadEntry._header._endOfHeaderAndDataAndBlockAlignment;
            }
            else if (_previouslyReadEntry._header._size > 0)
            {
                // When working with seekable streams, every time we return an entry, we avoid advancing the pointer beyond the data section
                // This is so the user can read the data if desired. But if the data was not read by the user, we need to advance the pointer
                // here until it's located at the beginning of the next entry header.
                // This should only be done if the previous entry came from a TarReader and it still had its original SubReadStream or SeekableSubReadStream.

                if (_previouslyReadEntry._header._dataStream is not SubReadStream dataStream)
                {
                    return;
                }

                if (!dataStream.HasReachedEnd)
                {
                    // If the user did not advance the position, we need to make sure the position
                    // pointer is located at the beginning of the next header.
                    if (dataStream.Position < (_previouslyReadEntry._header._size - 1))
                    {
                        long bytesToSkip = _previouslyReadEntry._header._size - dataStream.Position;
                        await TarHelpers.AdvanceStreamAsync(_archiveStream, bytesToSkip, cancellationToken).ConfigureAwait(false);

                        await TarHelpers.SkipBlockAlignmentPaddingAsync(_archiveStream, _previouslyReadEntry._header._size, cancellationToken).ConfigureAwait(false);

                        dataStream.HasReachedEnd = true; // Now the pointer is beyond the limit, so any read attempts should throw
                    }
                }
            }
        }
Beispiel #16
0
        // Returns a stream that represents the data section of the current header.
        // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned.
        // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream.
        // Otherwise, it returns an unseekable wrapper stream.
        private Stream?GetDataStream(Stream archiveStream, bool copyData)
        {
            if (_size == 0)
            {
                return(null);
            }

            if (copyData)
            {
                MemoryStream copiedData = new MemoryStream();
                TarHelpers.CopyBytes(archiveStream, copiedData, _size);
                // Reset position pointer so the user can do the first DataStream read from the beginning
                copiedData.Position = 0;
                return(copiedData);
            }

            return(archiveStream.CanSeek
                ? new SeekableSubReadStream(archiveStream, archiveStream.Position, _size)
                : new SubReadStream(archiveStream, 0, _size));
        }
Beispiel #17
0
        // Checks if the extended attributes dictionary contains 'atime' and 'ctime'.
        // If any of them is not found, it is added with the value of either the current entry's 'mtime',
        // or 'DateTimeOffset.UtcNow', depending on the value of 'useMTime'.
        private void AddNewAccessAndChangeTimestampsIfNotExist(bool useMTime)
        {
            Debug.Assert(!useMTime || (useMTime && _header._mTime != default));
            bool containsATime = _header.ExtendedAttributes.ContainsKey(TarHeader.PaxEaATime);
            bool containsCTime = _header.ExtendedAttributes.ContainsKey(TarHeader.PaxEaCTime);

            if (!containsATime || !containsCTime)
            {
                string secondsFromEpochString = TarHelpers.GetTimestampStringFromDateTimeOffset(useMTime ? _header._mTime : DateTimeOffset.UtcNow);

                if (!containsATime)
                {
                    _header.ExtendedAttributes[TarHeader.PaxEaATime] = secondsFromEpochString;
                }

                if (!containsCTime)
                {
                    _header.ExtendedAttributes[TarHeader.PaxEaCTime] = secondsFromEpochString;
                }
            }
        }
Beispiel #18
0
        // Some fields that have a reserved spot in the header, may not fit in such field anymore, but they can fit in the
        // extended attributes. They get collected and saved in that dictionary, with no restrictions.
        private void CollectExtendedAttributesFromStandardFieldsIfNeeded()
        {
            _extendedAttributes ??= new Dictionary <string, string>();
            _extendedAttributes.Add(PaxEaName, _name);

            if (!_extendedAttributes.ContainsKey(PaxEaMTime))
            {
                _extendedAttributes.Add(PaxEaMTime, TarHelpers.GetTimestampStringFromDateTimeOffset(_mTime));
            }
            TryAddStringField(_extendedAttributes, PaxEaGName, _gName, FieldLengths.GName);
            TryAddStringField(_extendedAttributes, PaxEaUName, _uName, FieldLengths.UName);

            if (!string.IsNullOrEmpty(_linkName))
            {
                _extendedAttributes.Add(PaxEaLinkName, _linkName);
            }

            if (_size > 99_999_999)
            {
                _extendedAttributes.Add(PaxEaSize, _size.ToString());
            }
Beispiel #19
0
        // Constructor called when the user creates a TarEntry instance from scratch.
        internal TarEntry(TarEntryType entryType, string entryName, TarEntryFormat format, bool isGea)
        {
            ArgumentException.ThrowIfNullOrEmpty(entryName);

            Debug.Assert(!isGea || entryType is TarEntryType.GlobalExtendedAttributes);

            if (!isGea)
            {
                TarHelpers.ThrowIfEntryTypeNotSupported(entryType, format);
            }

            _header         = default;
            _header._format = format;

            // Default values for fields shared by all supported formats
            _header._name     = entryName;
            _header._mode     = (int)TarHelpers.DefaultMode;
            _header._mTime    = DateTimeOffset.UtcNow;
            _header._typeFlag = entryType;
            _header._linkName = string.Empty;
        }
Beispiel #20
0
        // Asynchronously returns a stream that represents the data section of the current header.
        // If copyData is true, then a total number of _size bytes will be copied to a new MemoryStream, which is then returned.
        // Otherwise, if the archive stream is seekable, returns a seekable wrapper stream.
        // Otherwise, it returns an unseekable wrapper stream.
        private static async ValueTask <Stream?> GetDataStreamAsync(Stream archiveStream, bool copyData, long size, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (size == 0)
            {
                return(null);
            }

            if (copyData)
            {
                MemoryStream copiedData = new MemoryStream();
                await TarHelpers.CopyBytesAsync(archiveStream, copiedData, size, cancellationToken).ConfigureAwait(false);

                return(copiedData);
            }

            return(archiveStream.CanSeek
                ? new SeekableSubReadStream(archiveStream, archiveStream.Position, size)
                : new SubReadStream(archiveStream, 0, size));
        }
Beispiel #21
0
        // Collects the extended attributes found in the data section of a PAX entry of type 'x' or 'g'.
        // Throws if end of stream is reached or if an attribute is malformed.
        private void ReadExtendedAttributesBlock(Stream archiveStream)
        {
            Debug.Assert(_typeFlag is TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes);

            // Regardless of the size, this entry should always have a valid dictionary object
            _extendedAttributes ??= new Dictionary <string, string>();

            if (_size == 0)
            {
                return;
            }

            // It is not expected that the extended attributes data section will be longer than int.MaxValue, considering
            // 4096 is a common max path length, and also the size field is 12 bytes long, which is under int.MaxValue.
            if (_size > int.MaxValue)
            {
                throw new InvalidOperationException(string.Format(SR.TarSizeFieldTooLargeForExtendedAttribute, _typeFlag.ToString()));
            }

            byte[] buffer = new byte[(int)_size];
            if (archiveStream.Read(buffer.AsSpan()) != _size)
            {
                throw new EndOfStreamException();
            }

            string dataAsString = TarHelpers.GetTrimmedUtf8String(buffer);

            using StringReader reader = new(dataAsString);

            while (TryGetNextExtendedAttribute(reader, out string?key, out string?value))
            {
                _extendedAttributes ??= new Dictionary <string, string>();

                if (_extendedAttributes.ContainsKey(key))
                {
                    throw new FormatException(string.Format(SR.TarDuplicateExtendedAttribute, _name));
                }
                _extendedAttributes.Add(key, value);
            }
        }
Beispiel #22
0
        /// <summary>
        /// Initializes a new <see cref="PaxTarEntry"/> instance by converting the specified <paramref name="other"/> entry into the PAX format.
        /// </summary>
        public PaxTarEntry(TarEntry other)
            : base(other, TarEntryFormat.Pax)
        {
            if (other._header._format is TarEntryFormat.Ustar or TarEntryFormat.Pax)
            {
                _header._prefix = other._header._prefix;
            }

            if (other is PaxTarEntry paxOther)
            {
                _header.InitializeExtendedAttributesWithExisting(paxOther.ExtendedAttributes);
            }
            else
            {
                if (other is GnuTarEntry gnuOther)
                {
                    _header.ExtendedAttributes[TarHeader.PaxEaATime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.AccessTime);
                    _header.ExtendedAttributes[TarHeader.PaxEaCTime] = TarHelpers.GetTimestampStringFromDateTimeOffset(gnuOther.ChangeTime);
                }
            }

            AddNewAccessAndChangeTimestampsIfNotExist(useMTime: false);
        }
Beispiel #23
0
        // Reads the long path found in the data section of a GNU entry of type 'K' or 'L'
        // and replaces Name or LinkName, respectively, with the found string.
        // Throws if end of stream is reached.
        private void ReadGnuLongPathDataBlock(Stream archiveStream)
        {
            Debug.Assert(_typeFlag is TarEntryType.LongLink or TarEntryType.LongPath);

            if (_size == 0)
            {
                return;
            }

            byte[] buffer = new byte[(int)_size];
            archiveStream.ReadExactly(buffer);

            string longPath = TarHelpers.GetTrimmedUtf8String(buffer);

            if (_typeFlag == TarEntryType.LongLink)
            {
                _linkName = longPath;
            }
            else if (_typeFlag == TarEntryType.LongPath)
            {
                _name = longPath;
            }
        }
Beispiel #24
0
        // Reads fields only found in ustar format or above and converts them to their expected data type.
        // Throws if any conversion fails.
        private void ReadMagicAttribute(Span <byte> buffer)
        {
            Span <byte> magic = buffer.Slice(FieldLocations.Magic, FieldLengths.Magic);

            // If at this point the magic value is all nulls, we definitely have a V7
            if (TarHelpers.IsAllNullBytes(magic))
            {
                _format = TarFormat.V7;
                return;
            }

            // When the magic field is set, the archive is newer than v7.
            _magic = Encoding.ASCII.GetString(magic);

            if (_magic == GnuMagic)
            {
                _format = TarFormat.Gnu;
            }
            else if (_format == TarFormat.V7 && _magic == UstarMagic)
            {
                // Important: Only change to ustar if we had not changed the format to pax already
                _format = TarFormat.Ustar;
            }
        }
Beispiel #25
0
        // Unix specific implementation of the method that reads an entry from disk and writes it into the archive stream.
        partial void ReadFileFromDiskAndWriteToArchiveStreamAsEntry(string fullPath, string entryName)
        {
            Interop.Sys.FileStatus status = default;
            status.Mode = default;
            status.Dev  = default;
            Interop.CheckIo(Interop.Sys.LStat(fullPath, out status));

            TarEntryType entryType = (status.Mode & (uint)Interop.Sys.FileTypes.S_IFMT) switch
            {
                // Hard links are treated as regular files.
                // Unix socket files do not get added to tar files.
                Interop.Sys.FileTypes.S_IFBLK => TarEntryType.BlockDevice,
                Interop.Sys.FileTypes.S_IFCHR => TarEntryType.CharacterDevice,
                Interop.Sys.FileTypes.S_IFIFO => TarEntryType.Fifo,
                Interop.Sys.FileTypes.S_IFLNK => TarEntryType.SymbolicLink,
                Interop.Sys.FileTypes.S_IFREG => Format is TarFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile,
                Interop.Sys.FileTypes.S_IFDIR => TarEntryType.Directory,
                _ => throw new IOException(string.Format(SR.TarUnsupportedFile, fullPath)),
            };

            FileSystemInfo info = entryType is TarEntryType.Directory ? new DirectoryInfo(fullPath) : new FileInfo(fullPath);

            TarEntry entry = Format switch
            {
                TarFormat.V7 => new V7TarEntry(entryType, entryName),
                TarFormat.Ustar => new UstarTarEntry(entryType, entryName),
                TarFormat.Pax => new PaxTarEntry(entryType, entryName),
                TarFormat.Gnu => new GnuTarEntry(entryType, entryName),
                _ => throw new FormatException(string.Format(SR.TarInvalidFormat, Format)),
            };

            if ((entryType is TarEntryType.BlockDevice or TarEntryType.CharacterDevice) && status.Dev > 0)
            {
                uint major;
                uint minor;
                unsafe
                {
                    Interop.CheckIo(Interop.Sys.GetDeviceIdentifiers((ulong)status.Dev, &major, &minor));
                }

                entry._header._devMajor = (int)major;
                entry._header._devMinor = (int)minor;
            }

            entry._header._mTime = TarHelpers.GetDateTimeFromSecondsSinceEpoch(status.MTime);
            entry._header._aTime = TarHelpers.GetDateTimeFromSecondsSinceEpoch(status.ATime);
            entry._header._cTime = TarHelpers.GetDateTimeFromSecondsSinceEpoch(status.CTime);

            entry._header._mode = (status.Mode & 4095); // First 12 bits

            entry.Uid = (int)status.Uid;
            entry.Gid = (int)status.Gid;

            // TODO: Add these p/invokes https://github.com/dotnet/runtime/issues/68230
            entry._header._uName = ""; // Interop.Sys.GetUName();
            entry._header._gName = ""; // Interop.Sys.GetGName();

            if (entry.EntryType == TarEntryType.SymbolicLink)
            {
                entry.LinkName = info.LinkTarget ?? string.Empty;
            }

            if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile)
            {
                FileStreamOptions options = new()
                {
                    Mode    = FileMode.Open,
                    Access  = FileAccess.Read,
                    Share   = FileShare.Read,
                    Options = FileOptions.None
                };

                Debug.Assert(entry._header._dataStream == null);
                entry._header._dataStream = File.Open(fullPath, options);
            }

            WriteEntry(entry);
            if (entry._header._dataStream != null)
            {
                entry._header._dataStream.Dispose();
            }
        }
    }
Beispiel #26
0
        // Attempts to read the fields shared by all formats and stores them in their expected data type.
        // Throws if any data type conversion fails.
        // Returns true on success, false if checksum is zero.
        private bool TryReadCommonAttributes(Span <byte> buffer)
        {
            // Start by collecting fields that need special checks that return early when data is wrong

            // Empty checksum means this is an invalid (all blank) entry, finish early
            Span <byte> spanChecksum = buffer.Slice(FieldLocations.Checksum, FieldLengths.Checksum);

            if (TarHelpers.IsAllNullBytes(spanChecksum))
            {
                return(false);
            }
            _checksum = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(spanChecksum);
            // Zero checksum means the whole header is empty
            if (_checksum == 0)
            {
                return(false);
            }

            _size = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.Size, FieldLengths.Size));
            if (_size < 0)
            {
                throw new FormatException(string.Format(SR.TarSizeFieldNegative, _name));
            }

            // Continue with the rest of the fields that require no special checks

            _name = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.Name, FieldLengths.Name));
            _mode = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.Mode, FieldLengths.Mode));
            _uid  = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.Uid, FieldLengths.Uid));
            _gid  = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.Gid, FieldLengths.Gid));
            int mTime = TarHelpers.GetTenBaseNumberFromOctalAsciiChars(buffer.Slice(FieldLocations.MTime, FieldLengths.MTime));

            _mTime    = TarHelpers.GetDateTimeFromSecondsSinceEpoch(mTime);
            _typeFlag = (TarEntryType)buffer[FieldLocations.TypeFlag];
            _linkName = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName));

            if (_format == TarFormat.Unknown)
            {
                _format = _typeFlag switch
                {
                    TarEntryType.ExtendedAttributes or
                    TarEntryType.GlobalExtendedAttributes => TarFormat.Pax,

                                 TarEntryType.DirectoryList or
                                 TarEntryType.LongLink or
                                 TarEntryType.LongPath or
                                 TarEntryType.MultiVolume or
                                 TarEntryType.RenamedOrSymlinked or
                                 TarEntryType.SparseFile or
                                 TarEntryType.TapeVolume => TarFormat.Gnu,

                    // V7 is the only one that uses 'V7RegularFile'.
                                 TarEntryType.V7RegularFile => TarFormat.V7,

                    // We can quickly determine the *minimum* possible format if the entry type
                    // is the POSIX 'RegularFile', although later we could upgrade it to PAX or GNU
                                 _ => (_typeFlag == TarEntryType.RegularFile) ? TarFormat.Ustar : TarFormat.V7
                };
            }

            return(true);
        }
Beispiel #27
0
        // Creates an entry for writing using the specified path and entryName. If this is being called from an async method, FileOptions should contain Asynchronous.
        private TarEntry ConstructEntryForWriting(string fullPath, string entryName, FileOptions fileOptions)
        {
            Debug.Assert(!string.IsNullOrEmpty(fullPath));

            Interop.Sys.FileStatus status = default;
            status.Mode = default;
            status.Dev  = default;
            Interop.CheckIo(Interop.Sys.LStat(fullPath, out status));

            TarEntryType entryType = (status.Mode & (uint)Interop.Sys.FileTypes.S_IFMT) switch
            {
                // Hard links are treated as regular files.
                // Unix socket files do not get added to tar files.
                Interop.Sys.FileTypes.S_IFBLK => TarEntryType.BlockDevice,
                Interop.Sys.FileTypes.S_IFCHR => TarEntryType.CharacterDevice,
                Interop.Sys.FileTypes.S_IFIFO => TarEntryType.Fifo,
                Interop.Sys.FileTypes.S_IFLNK => TarEntryType.SymbolicLink,
                Interop.Sys.FileTypes.S_IFREG => Format is TarEntryFormat.V7 ? TarEntryType.V7RegularFile : TarEntryType.RegularFile,
                Interop.Sys.FileTypes.S_IFDIR => TarEntryType.Directory,
                _ => throw new IOException(string.Format(SR.TarUnsupportedFile, fullPath)),
            };

            FileSystemInfo info = entryType is TarEntryType.Directory ? new DirectoryInfo(fullPath) : new FileInfo(fullPath);

            TarEntry entry = Format switch
            {
                TarEntryFormat.V7 => new V7TarEntry(entryType, entryName),
                TarEntryFormat.Ustar => new UstarTarEntry(entryType, entryName),
                TarEntryFormat.Pax => new PaxTarEntry(entryType, entryName),
                TarEntryFormat.Gnu => new GnuTarEntry(entryType, entryName),
                _ => throw new FormatException(string.Format(SR.TarInvalidFormat, Format)),
            };

            if (entryType is TarEntryType.BlockDevice or TarEntryType.CharacterDevice)
            {
                uint major;
                uint minor;
                unsafe
                {
                    Interop.Sys.GetDeviceIdentifiers((ulong)status.RDev, &major, &minor);
                }

                entry._header._devMajor = (int)major;
                entry._header._devMinor = (int)minor;
            }

            entry._header._mTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.MTime);
            entry._header._aTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.ATime);
            entry._header._cTime = TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch(status.CTime);

            entry._header._mode = status.Mode & 4095; // First 12 bits

            // Uid and UName
            entry._header._uid = (int)status.Uid;
            if (!_userIdentifiers.TryGetValue(status.Uid, out string?uName))
            {
                uName = Interop.Sys.GetUserNameFromPasswd(status.Uid);
                _userIdentifiers.Add(status.Uid, uName);
            }
            entry._header._uName = uName;

            // Gid and GName
            entry._header._gid = (int)status.Gid;
            if (!_groupIdentifiers.TryGetValue(status.Gid, out string?gName))
            {
                gName = Interop.Sys.GetGroupName(status.Gid);
                _groupIdentifiers.Add(status.Gid, gName);
            }
            entry._header._gName = gName;

            if (entry.EntryType == TarEntryType.SymbolicLink)
            {
                entry.LinkName = info.LinkTarget ?? string.Empty;
            }

            if (entry.EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile)
            {
                Debug.Assert(entry._header._dataStream == null);
                entry._header._dataStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, fileOptions);
            }

            return(entry);
        }
Beispiel #28
0
        // Reads the elements from the passed dictionary, which comes from the previous extended attributes entry,
        // and inserts or replaces those elements into the current header's dictionary.
        // If any of the dictionary entries use the name of a standard attribute, that attribute's value gets replaced with the one from the dictionary.
        // Unlike the historic header, numeric values in extended attributes are stored using decimal, not octal.
        // Throws if any conversion from string to the expected data type fails.
        internal void ReplaceNormalAttributesWithExtended(Dictionary <string, string>?dictionaryFromExtendedAttributesHeader)
        {
            if (dictionaryFromExtendedAttributesHeader == null || dictionaryFromExtendedAttributesHeader.Count == 0)
            {
                return;
            }

            InitializeExtendedAttributesWithExisting(dictionaryFromExtendedAttributesHeader);

            // Find all the extended attributes with known names and save them in the expected standard attribute.

            // The 'name' header field only fits 100 bytes, so we always store the full name text to the dictionary.
            if (ExtendedAttributes.TryGetValue(PaxEaName, out string?paxEaName))
            {
                _name = paxEaName;
            }

            // The 'linkName' header field only fits 100 bytes, so we always store the full linkName text to the dictionary.
            if (ExtendedAttributes.TryGetValue(PaxEaLinkName, out string?paxEaLinkName))
            {
                _linkName = paxEaLinkName;
            }

            // The 'mtime' header field only fits 12 bytes, so a more precise timestamp goes in the extended attributes
            if (TarHelpers.TryGetDateTimeOffsetFromTimestampString(ExtendedAttributes, PaxEaMTime, out DateTimeOffset mTime))
            {
                _mTime = mTime;
            }

            // The user could've stored an override in the extended attributes
            if (TarHelpers.TryGetStringAsBaseTenInteger(ExtendedAttributes, PaxEaMode, out int mode))
            {
                _mode = mode;
            }

            // The 'size' header field only fits 12 bytes, so the data section length that surpases that limit needs to be retrieved
            if (TarHelpers.TryGetStringAsBaseTenLong(ExtendedAttributes, PaxEaSize, out long size))
            {
                _size = size;
            }

            // The 'uid' header field only fits 8 bytes, or the user could've stored an override in the extended attributes
            if (TarHelpers.TryGetStringAsBaseTenInteger(ExtendedAttributes, PaxEaUid, out int uid))
            {
                _uid = uid;
            }

            // The 'gid' header field only fits 8 bytes, or the user could've stored an override in the extended attributes
            if (TarHelpers.TryGetStringAsBaseTenInteger(ExtendedAttributes, PaxEaGid, out int gid))
            {
                _gid = gid;
            }

            // The 'uname' header field only fits 32 bytes
            if (ExtendedAttributes.TryGetValue(PaxEaUName, out string?paxEaUName))
            {
                _uName = paxEaUName;
            }

            // The 'gname' header field only fits 32 bytes
            if (ExtendedAttributes.TryGetValue(PaxEaGName, out string?paxEaGName))
            {
                _gName = paxEaGName;
            }

            // The 'devmajor' header field only fits 8 bytes, or the user could've stored an override in the extended attributes
            if (TarHelpers.TryGetStringAsBaseTenInteger(ExtendedAttributes, PaxEaDevMajor, out int devMajor))
            {
                _devMajor = devMajor;
            }

            // The 'devminor' header field only fits 8 bytes, or the user could've stored an override in the extended attributes
            if (TarHelpers.TryGetStringAsBaseTenInteger(ExtendedAttributes, PaxEaDevMinor, out int devMinor))
            {
                _devMinor = devMinor;
            }
        }
Beispiel #29
0
        // Attempts to read the fields shared by all formats and stores them in their expected data type.
        // Throws if any data type conversion fails.
        // Returns true on success, false if checksum is zero.
        private static TarHeader?TryReadCommonAttributes(Span <byte> buffer, TarEntryFormat initialFormat)
        {
            // Start by collecting fields that need special checks that return early when data is wrong

            // Empty checksum means this is an invalid (all blank) entry, finish early
            Span <byte> spanChecksum = buffer.Slice(FieldLocations.Checksum, FieldLengths.Checksum);

            if (TarHelpers.IsAllNullBytes(spanChecksum))
            {
                return(null);
            }
            int checksum = (int)TarHelpers.ParseOctal <uint>(spanChecksum);

            // Zero checksum means the whole header is empty
            if (checksum == 0)
            {
                return(null);
            }

            long size = (int)TarHelpers.ParseOctal <uint>(buffer.Slice(FieldLocations.Size, FieldLengths.Size));

            if (size < 0)
            {
                throw new FormatException(string.Format(SR.TarSizeFieldNegative));
            }

            // Continue with the rest of the fields that require no special checks
            TarHeader header = new(initialFormat,
                                   name : TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.Name, FieldLengths.Name)),
                                   mode : (int)TarHelpers.ParseOctal <uint>(buffer.Slice(FieldLocations.Mode, FieldLengths.Mode)),
                                   mTime : TarHelpers.GetDateTimeOffsetFromSecondsSinceEpoch((long)TarHelpers.ParseOctal <ulong>(buffer.Slice(FieldLocations.MTime, FieldLengths.MTime))),
                                   typeFlag : (TarEntryType)buffer[FieldLocations.TypeFlag])
            {
                _checksum = checksum,
                _size     = size,
                _uid      = (int)TarHelpers.ParseOctal <uint>(buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)),
                _gid      = (int)TarHelpers.ParseOctal <uint>(buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)),
                _linkName = TarHelpers.GetTrimmedUtf8String(buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName))
            };

            if (header._format == TarEntryFormat.Unknown)
            {
                header._format = header._typeFlag switch
                {
                    TarEntryType.ExtendedAttributes or
                    TarEntryType.GlobalExtendedAttributes => TarEntryFormat.Pax,

                                 TarEntryType.DirectoryList or
                                 TarEntryType.LongLink or
                                 TarEntryType.LongPath or
                                 TarEntryType.MultiVolume or
                                 TarEntryType.RenamedOrSymlinked or
                                 TarEntryType.TapeVolume => TarEntryFormat.Gnu,

                    // V7 is the only one that uses 'V7RegularFile'.
                                 TarEntryType.V7RegularFile => TarEntryFormat.V7,

                                 TarEntryType.SparseFile => throw new NotSupportedException(string.Format(SR.TarEntryTypeNotSupported, header._typeFlag)),

                          // We can quickly determine the *minimum* possible format if the entry type
                          // is the POSIX 'RegularFile', although later we could upgrade it to PAX or GNU
                          _ => (header._typeFlag == TarEntryType.RegularFile) ? TarEntryFormat.Ustar : TarEntryFormat.V7
                };
            }

            return(header);
        }
Beispiel #30
0
        private async Task ProcessDataBlockAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken)
        {
            bool skipBlockAlignmentPadding = true;

            switch (_typeFlag)
            {
            case TarEntryType.ExtendedAttributes or TarEntryType.GlobalExtendedAttributes:
                await ReadExtendedAttributesBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false);

                break;

            case TarEntryType.LongLink or TarEntryType.LongPath:
                await ReadGnuLongPathDataBlockAsync(archiveStream, cancellationToken).ConfigureAwait(false);

                break;

            case TarEntryType.BlockDevice:
            case TarEntryType.CharacterDevice:
            case TarEntryType.Directory:
            case TarEntryType.Fifo:
            case TarEntryType.HardLink:
            case TarEntryType.SymbolicLink:
                // No data section
                if (_size > 0)
                {
                    throw new FormatException(string.Format(SR.TarSizeFieldTooLargeForEntryType, _typeFlag));
                }
                break;

            case TarEntryType.RegularFile:
            case TarEntryType.V7RegularFile:      // Treated as regular file
            case TarEntryType.ContiguousFile:     // Treated as regular file
            case TarEntryType.DirectoryList:      // Contains the list of filesystem entries in the data section
            case TarEntryType.MultiVolume:        // Contains portion of a file
            case TarEntryType.RenamedOrSymlinked: // Might contain data
            case TarEntryType.SparseFile:         // Contains portion of a file
            case TarEntryType.TapeVolume:         // Might contain data
            default:                              // Unrecognized entry types could potentially have a data section
                _dataStream = await GetDataStreamAsync(archiveStream, copyData, _size, cancellationToken).ConfigureAwait(false);

                if (_dataStream is SeekableSubReadStream)
                {
                    await TarHelpers.AdvanceStreamAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false);
                }
                else if (_dataStream is SubReadStream)
                {
                    // This stream gives the user the chance to optionally read the data section
                    // when the underlying archive stream is unseekable
                    skipBlockAlignmentPadding = false;
                }

                break;
            }

            if (skipBlockAlignmentPadding)
            {
                if (_size > 0)
                {
                    await TarHelpers.SkipBlockAlignmentPaddingAsync(archiveStream, _size, cancellationToken).ConfigureAwait(false);
                }

                if (archiveStream.CanSeek)
                {
                    _endOfHeaderAndDataAndBlockAlignment = archiveStream.Position;
                }
            }
        }