File: System\Formats\Tar\TarFile.cs
Web Access
Project: src\src\libraries\System.Formats.Tar\src\System.Formats.Tar.csproj (System.Formats.Tar)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Enumeration;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Formats.Tar
{
    /// <summary>
    /// Provides static methods for creating and extracting tar archives.
    /// </summary>
    public static class TarFile
    {
        /// <inheritdoc cref="CreateFromDirectory(string, Stream, bool, TarEntryFormat)" />
        public static void CreateFromDirectory(string sourceDirectoryName, Stream destination, bool includeBaseDirectory)
            => CreateFromDirectory(sourceDirectoryName, destination, includeBaseDirectory, TarEntryFormat.Pax);
 
        /// <summary>
        /// Creates a tar stream that contains all the filesystem entries from the specified directory.
        /// </summary>
        /// <param name="sourceDirectoryName">The path of the directory to archive.</param>
        /// <param name="destination">The destination stream the archive.</param>
        /// <param name="includeBaseDirectory"><see langword="true"/> to include the base directory name as the first segment in all the names of the archive entries. <see langword="false"/> to exclude the base directory name from the archive entry names.</param>
        /// <param name="format">One of the enumeration values that specifies the tar entry format to use for the archive.</param>
        /// <exception cref="ArgumentNullException"><paramref name="sourceDirectoryName"/> or <paramref name="destination"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><para><paramref name="sourceDirectoryName"/> is empty.</para>
        /// <para>-or-</para>
        /// <para><paramref name="destination"/> does not support writing.</para></exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="sourceDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="format"/> is either <see cref="TarEntryFormat.Unknown"/>, or not one of the other enum values.</exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static void CreateFromDirectory(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, TarEntryFormat format)
        {
            ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName);
            ArgumentNullException.ThrowIfNull(destination);
            ValidateFormat(format);
 
            if (!destination.CanWrite)
            {
                throw new ArgumentException(SR.IO_NotSupported_UnwritableStream, nameof(destination));
            }
 
            if (!Directory.Exists(sourceDirectoryName))
            {
                throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceDirectoryName));
            }
 
            // Rely on Path.GetFullPath for validation of paths
            sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
 
            CreateFromDirectoryInternal(sourceDirectoryName, destination, includeBaseDirectory, leaveOpen: true, format);
        }
 
        /// <inheritdoc cref="CreateFromDirectoryAsync(string, Stream, bool, TarEntryFormat, CancellationToken)" />
        public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, CancellationToken cancellationToken = default)
            => CreateFromDirectoryAsync(sourceDirectoryName, destination, includeBaseDirectory, TarEntryFormat.Pax, cancellationToken);
 
        /// <summary>
        /// Asynchronously creates a tar stream that contains all the filesystem entries from the specified directory.
        /// </summary>
        /// <param name="sourceDirectoryName">The path of the directory to archive.</param>
        /// <param name="destination">The destination stream of the archive.</param>
        /// <param name="includeBaseDirectory"><see langword="true"/> to include the base directory name as the first path segment in all the names of the archive entries. <see langword="false"/> to exclude the base directory name from the entry name paths.</param>
        /// <param name="format">One of the enumeration values that specifies the tar entry format to use for the archive.</param>
        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param>
        /// <returns>A task that represents the asynchronous creation operation.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="sourceDirectoryName"/> or <paramref name="destination"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><para><paramref name="sourceDirectoryName"/> is empty.</para>
        /// <para>-or-</para>
        /// <para><paramref name="destination"/> does not support writing.</para></exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="sourceDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="format"/> is either <see cref="TarEntryFormat.Unknown"/>, or not one of the other enum values.</exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static Task CreateFromDirectoryAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, TarEntryFormat format, CancellationToken cancellationToken = default)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return Task.FromCanceled(cancellationToken);
            }
            ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName);
            ArgumentNullException.ThrowIfNull(destination);
            ValidateFormat(format);
 
            if (!destination.CanWrite)
            {
                return Task.FromException(new ArgumentException(SR.IO_NotSupported_UnwritableStream, nameof(destination)));
            }
 
            if (!Directory.Exists(sourceDirectoryName))
            {
                return Task.FromException(new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceDirectoryName)));
            }
 
            // Rely on Path.GetFullPath for validation of paths
            sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
 
            return CreateFromDirectoryInternalAsync(sourceDirectoryName, destination, includeBaseDirectory, leaveOpen: true, format, cancellationToken);
        }
 
        /// <inheritdoc cref="CreateFromDirectory(string, string, bool, TarEntryFormat)" />
        public static void CreateFromDirectory(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory)
            => CreateFromDirectory(sourceDirectoryName, destinationFileName, includeBaseDirectory, TarEntryFormat.Pax);
 
        /// <summary>
        /// Creates a tar file that contains all the filesystem entries from the specified directory.
        /// </summary>
        /// <param name="sourceDirectoryName">The path of the directory to archive.</param>
        /// <param name="destinationFileName">The path of the destination archive file.</param>
        /// <param name="includeBaseDirectory"><see langword="true"/> to include the base directory name as the first path segment in all the names of the archive entries. <see langword="false"/> to exclude the base directory name from the entry name paths.</param>
        /// <param name="format">One of the enumeration values that specifies the tar entry format to use for the archive.</param>
        /// <exception cref="ArgumentNullException"><paramref name="sourceDirectoryName"/> or <paramref name="destinationFileName"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="sourceDirectoryName"/> or <paramref name="destinationFileName"/> is empty.</exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="sourceDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="format"/> is either <see cref="TarEntryFormat.Unknown"/>, or not one of the other enum values.</exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static void CreateFromDirectory(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, TarEntryFormat format)
        {
            ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName);
            ArgumentException.ThrowIfNullOrEmpty(destinationFileName);
            ValidateFormat(format);
 
            // Rely on Path.GetFullPath for validation of paths
            sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
            destinationFileName = Path.GetFullPath(destinationFileName);
 
            if (!Directory.Exists(sourceDirectoryName))
            {
                throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceDirectoryName));
            }
 
            // Throws if the destination file exists
            using FileStream fs = new(destinationFileName, FileMode.CreateNew, FileAccess.Write);
 
            CreateFromDirectoryInternal(sourceDirectoryName, fs, includeBaseDirectory, leaveOpen: false, format);
        }
 
        /// <inheritdoc cref="CreateFromDirectoryAsync(string, string, bool, TarEntryFormat, CancellationToken)" />
        public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, CancellationToken cancellationToken = default)
            => CreateFromDirectoryAsync(sourceDirectoryName, destinationFileName, includeBaseDirectory, TarEntryFormat.Pax, cancellationToken);
 
        /// <summary>
        /// Asynchronously creates a tar archive from the contents of the specified directory, and outputs them into the specified path. Can optionally include the base directory as the prefix for the entry names.
        /// </summary>
        /// <param name="sourceDirectoryName">The path of the directory to archive.</param>
        /// <param name="destinationFileName">The path of the destination archive file.</param>
        /// <param name="includeBaseDirectory"><see langword="true"/> to include the base directory name as the first path segment in all the names of the archive entries. <see langword="false"/> to exclude the base directory name from the entry name paths.</param>
        /// <param name="format">One of the enumeration values that specifies the tar entry format to use for the archive.</param>
        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param>
        /// <returns>A task that represents the asynchronous creation operation.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="sourceDirectoryName"/> or <paramref name="destinationFileName"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="sourceDirectoryName"/> or <paramref name="destinationFileName"/> is empty.</exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="sourceDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="format"/> is either <see cref="TarEntryFormat.Unknown"/>, or not one of the other enum values.</exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static Task CreateFromDirectoryAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, TarEntryFormat format, CancellationToken cancellationToken = default)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return Task.FromCanceled(cancellationToken);
            }
            ArgumentException.ThrowIfNullOrEmpty(sourceDirectoryName);
            ArgumentException.ThrowIfNullOrEmpty(destinationFileName);
            ValidateFormat(format);
 
            // Rely on Path.GetFullPath for validation of paths
            sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
            destinationFileName = Path.GetFullPath(destinationFileName);
 
            if (!Directory.Exists(sourceDirectoryName))
            {
                return Task.FromException(new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceDirectoryName)));
            }
 
            return CreateFromDirectoryInternalAsync(sourceDirectoryName, destinationFileName, includeBaseDirectory, format, cancellationToken);
        }
 
        /// <summary>
        /// Extracts the contents of a stream that represents a tar archive into the specified directory.
        /// </summary>
        /// <param name="source">The stream containing the tar archive.</param>
        /// <param name="destinationDirectoryName">The path of the destination directory where the filesystem entries should be extracted.</param>
        /// <param name="overwriteFiles"><see langword="true"/> to overwrite files and directories in <paramref name="destinationDirectoryName"/>; <see langword="false"/> to avoid overwriting, and throw if any files or directories are found with existing names.</param>
        /// <remarks><para>Files of type <see cref="TarEntryType.BlockDevice"/>, <see cref="TarEntryType.CharacterDevice"/> or <see cref="TarEntryType.Fifo"/> can only be extracted in Unix platforms.</para>
        /// <para>Elevation is required to extract a <see cref="TarEntryType.BlockDevice"/> or <see cref="TarEntryType.CharacterDevice"/> to disk.</para></remarks>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="destinationDirectoryName"/> is <see langword="null"/>.</exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="destinationDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="UnauthorizedAccessException">Operation not permitted due to insufficient permissions.</exception>
        /// <exception cref="ArgumentException"><para>Extracting tar entry would have resulted in a file outside the specified destination directory.</para>
        /// <para>-or-</para>
        /// <para><paramref name="destinationDirectoryName"/> is empty.</para>
        /// <para>-or-</para>
        /// <para><paramref name="source"/> does not support reading.</para></exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static void ExtractToDirectory(Stream source, string destinationDirectoryName, bool overwriteFiles)
        {
            ArgumentNullException.ThrowIfNull(source);
            ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName);
 
            if (!source.CanRead)
            {
                throw new ArgumentException(SR.IO_NotSupported_UnreadableStream, nameof(source));
            }
 
            if (!Directory.Exists(destinationDirectoryName))
            {
                throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, destinationDirectoryName));
            }
 
            // Rely on Path.GetFullPath for validation of paths
            destinationDirectoryName = Path.GetFullPath(destinationDirectoryName);
            destinationDirectoryName = PathInternal.EnsureTrailingSeparator(destinationDirectoryName);
 
            ExtractToDirectoryInternal(source, destinationDirectoryName, overwriteFiles, leaveOpen: true);
        }
 
        /// <summary>
        /// Asynchronously extracts the contents of a stream that represents a tar archive into the specified directory.
        /// </summary>
        /// <param name="source">The stream containing the tar archive.</param>
        /// <param name="destinationDirectoryName">The path of the destination directory where the filesystem entries should be extracted.</param>
        /// <param name="overwriteFiles"><see langword="true"/> to overwrite files and directories in <paramref name="destinationDirectoryName"/>; <see langword="false"/> to avoid overwriting, and throw if any files or directories are found with existing names.</param>
        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param>
        /// <returns>A task that represents the asynchronous extraction operation.</returns>
        /// <remarks><para>Files of type <see cref="TarEntryType.BlockDevice"/>, <see cref="TarEntryType.CharacterDevice"/> or <see cref="TarEntryType.Fifo"/> can only be extracted in Unix platforms.</para>
        /// <para>Elevation is required to extract a <see cref="TarEntryType.BlockDevice"/> or <see cref="TarEntryType.CharacterDevice"/> to disk.</para></remarks>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="destinationDirectoryName"/> is <see langword="null"/>.</exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="destinationDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="UnauthorizedAccessException">Operation not permitted due to insufficient permissions.</exception>
        /// <exception cref="ArgumentException"><para>Extracting tar entry would have resulted in a file outside the specified destination directory.</para>
        /// <para>-or-</para>
        /// <para><paramref name="destinationDirectoryName"/> is empty.</para>
        /// <para>-or-</para>
        /// <para><paramref name="source"/> does not support reading.</para></exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return Task.FromCanceled(cancellationToken);
            }
            ArgumentNullException.ThrowIfNull(source);
            ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName);
 
            if (!source.CanRead)
            {
                return Task.FromException(new ArgumentException(SR.IO_NotSupported_UnreadableStream, nameof(source)));
            }
 
            if (!Directory.Exists(destinationDirectoryName))
            {
                return Task.FromException(new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, destinationDirectoryName)));
            }
 
            // Rely on Path.GetFullPath for validation of paths
            destinationDirectoryName = Path.GetFullPath(destinationDirectoryName);
            destinationDirectoryName = PathInternal.EnsureTrailingSeparator(destinationDirectoryName);
 
            return ExtractToDirectoryInternalAsync(source, destinationDirectoryName, overwriteFiles, leaveOpen: true, cancellationToken);
        }
 
        /// <summary>
        /// Extracts the contents of a tar file into the specified directory.
        /// </summary>
        /// <param name="sourceFileName">The path of the tar file to extract.</param>
        /// <param name="destinationDirectoryName">The path of the destination directory where the filesystem entries should be extracted.</param>
        /// <param name="overwriteFiles"><see langword="true"/> to overwrite files and directories in <paramref name="destinationDirectoryName"/>; <see langword="false"/> to avoid overwriting, and throw if any files or directories are found with existing names.</param>
        /// <remarks><para>Files of type <see cref="TarEntryType.BlockDevice"/>, <see cref="TarEntryType.CharacterDevice"/> or <see cref="TarEntryType.Fifo"/> can only be extracted in Unix platforms.</para>
        /// <para>Elevation is required to extract a <see cref="TarEntryType.BlockDevice"/> or <see cref="TarEntryType.CharacterDevice"/> to disk.</para></remarks>
        /// <exception cref="ArgumentNullException"><paramref name="sourceFileName"/> or <paramref name="destinationDirectoryName"/> is <see langword="null"/>.</exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="destinationDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="FileNotFoundException"> The <paramref name="sourceFileName"/> file path was not found.</exception>
        /// <exception cref="UnauthorizedAccessException">Operation not permitted due to insufficient permissions.</exception>
        /// <exception cref="ArgumentException"><para>Extracting tar entry would have resulted in a file outside the specified destination directory.</para>
        /// <para>-or-</para>
        /// <para><paramref name="sourceFileName"/> or <paramref name="destinationDirectoryName"/> is empty.</para></exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static void ExtractToDirectory(string sourceFileName, string destinationDirectoryName, bool overwriteFiles)
        {
            ArgumentException.ThrowIfNullOrEmpty(sourceFileName);
            ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName);
 
            // Rely on Path.GetFullPath for validation of paths
            sourceFileName = Path.GetFullPath(sourceFileName);
            destinationDirectoryName = Path.GetFullPath(destinationDirectoryName);
            destinationDirectoryName = PathInternal.EnsureTrailingSeparator(destinationDirectoryName);
 
            if (!File.Exists(sourceFileName))
            {
                throw new FileNotFoundException(SR.Format(SR.IO_FileNotFound_FileName, sourceFileName));
            }
 
            if (!Directory.Exists(destinationDirectoryName))
            {
                throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, destinationDirectoryName));
            }
 
            using FileStream archive = File.OpenRead(sourceFileName);
 
            ExtractToDirectoryInternal(archive, destinationDirectoryName, overwriteFiles, leaveOpen: false);
        }
 
        /// <summary>
        /// Asynchronously extracts the contents of a tar file into the specified directory.
        /// </summary>
        /// <param name="sourceFileName">The path of the tar file to extract.</param>
        /// <param name="destinationDirectoryName">The path of the destination directory where the filesystem entries should be extracted.</param>
        /// <param name="overwriteFiles"><see langword="true"/> to overwrite files and directories in <paramref name="destinationDirectoryName"/>; <see langword="false"/> to avoid overwriting, and throw if any files or directories are found with existing names.</param>
        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param>
        /// <returns>A task that represents the asynchronous extraction operation.</returns>
        /// <remarks><para>Files of type <see cref="TarEntryType.BlockDevice"/>, <see cref="TarEntryType.CharacterDevice"/> or <see cref="TarEntryType.Fifo"/> can only be extracted in Unix platforms.</para>
        /// <para>Elevation is required to extract a <see cref="TarEntryType.BlockDevice"/> or <see cref="TarEntryType.CharacterDevice"/> to disk.</para></remarks>
        /// <exception cref="ArgumentNullException"><paramref name="sourceFileName"/> or <paramref name="destinationDirectoryName"/> is <see langword="null"/>.</exception>
        /// <exception cref="DirectoryNotFoundException">The <paramref name="destinationDirectoryName"/> directory path was not found.</exception>
        /// <exception cref="FileNotFoundException"> The <paramref name="sourceFileName"/> file path was not found.</exception>
        /// <exception cref="UnauthorizedAccessException">Operation not permitted due to insufficient permissions.</exception>
        /// <exception cref="ArgumentException"><para>Extracting tar entry would have resulted in a file outside the specified destination directory.</para>
        /// <para>-or-</para>
        /// <para><paramref name="sourceFileName"/> or <paramref name="destinationDirectoryName"/> is empty.</para></exception>
        /// <exception cref="IOException">An I/O exception occurred.</exception>
        public static Task ExtractToDirectoryAsync(string sourceFileName, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return Task.FromCanceled(cancellationToken);
            }
            ArgumentException.ThrowIfNullOrEmpty(sourceFileName);
            ArgumentException.ThrowIfNullOrEmpty(destinationDirectoryName);
 
            // Rely on Path.GetFullPath for validation of paths
            sourceFileName = Path.GetFullPath(sourceFileName);
            destinationDirectoryName = Path.GetFullPath(destinationDirectoryName);
            destinationDirectoryName = PathInternal.EnsureTrailingSeparator(destinationDirectoryName);
 
            if (!File.Exists(sourceFileName))
            {
                return Task.FromException(new FileNotFoundException(SR.Format(SR.IO_FileNotFound_FileName, sourceFileName)));
            }
 
            if (!Directory.Exists(destinationDirectoryName))
            {
                return Task.FromException(new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, destinationDirectoryName)));
            }
 
            return ExtractToDirectoryInternalAsync(sourceFileName, destinationDirectoryName, overwriteFiles, cancellationToken);
        }
 
        // Creates an archive from the contents of a directory.
        // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not.
        private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen, TarEntryFormat format)
        {
            VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination);
 
            using (TarWriter writer = new TarWriter(destination, format, leaveOpen))
            {
                DirectoryInfo di = new(sourceDirectoryName);
 
                bool skipBaseDirRecursion = false;
                if (includeBaseDirectory)
                {
                    writer.WriteEntry(di.FullName, GetEntryNameForBaseDirectory(di.Name));
                    skipBaseDirRecursion = (di.Attributes & FileAttributes.ReparsePoint) != 0;
                }
 
                if (skipBaseDirRecursion)
                {
                    // The base directory is a symlink, do not recurse into it
                    return;
                }
 
                string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory);
                foreach ((string fullpath, string entryname) in GetFilesForCreation(sourceDirectoryName, basePath.Length))
                {
                    writer.WriteEntry(fullpath, entryname);
                }
            }
        }
 
        // Asynchronously creates a tar archive from the contents of the specified directory, and outputs them into the specified path.
        private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, string destinationFileName, bool includeBaseDirectory, TarEntryFormat format, CancellationToken cancellationToken)
        {
            Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName));
            Debug.Assert(!string.IsNullOrEmpty(destinationFileName));
 
            cancellationToken.ThrowIfCancellationRequested();
 
            FileStreamOptions options = new()
            {
                Access = FileAccess.Write,
                Mode = FileMode.CreateNew,
                Options = FileOptions.Asynchronous,
            };
            // Throws if the destination file exists
            FileStream archive = new(destinationFileName, options);
            await using (archive.ConfigureAwait(false))
            {
                await CreateFromDirectoryInternalAsync(sourceDirectoryName, archive, includeBaseDirectory, leaveOpen: false, format, cancellationToken).ConfigureAwait(false);
            }
        }
 
        // Asynchronously creates an archive from the contents of a directory.
        // It assumes the sourceDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not.
        private static async Task CreateFromDirectoryInternalAsync(string sourceDirectoryName, Stream destination, bool includeBaseDirectory, bool leaveOpen, TarEntryFormat format, CancellationToken cancellationToken)
        {
            VerifyCreateFromDirectoryArguments(sourceDirectoryName, destination);
            cancellationToken.ThrowIfCancellationRequested();
 
            TarWriter writer = new TarWriter(destination, format, leaveOpen);
            await using (writer.ConfigureAwait(false))
            {
                DirectoryInfo di = new(sourceDirectoryName);
 
                bool skipBaseDirRecursion = false;
                if (includeBaseDirectory)
                {
                    await writer.WriteEntryAsync(di.FullName, GetEntryNameForBaseDirectory(di.Name), cancellationToken).ConfigureAwait(false);
                    skipBaseDirRecursion = (di.Attributes & FileAttributes.ReparsePoint) != 0;
                }
 
                if (skipBaseDirRecursion)
                {
                    // The base directory is a symlink, do not recurse into it
                    return;
                }
 
                string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory);
                foreach ((string fullpath, string entryname) in GetFilesForCreation(sourceDirectoryName, basePath.Length))
                {
                    await writer.WriteEntryAsync(fullpath, entryname, cancellationToken).ConfigureAwait(false);
                }
            }
        }
 
        // Generates a recursive enumeration of the filesystem entries inside the specified source directory, while
        // making sure that directory symlinks do not get recursed.
        private static IEnumerable<(string fullpath, string entryname)> GetFilesForCreation(string sourceDirectoryName, int basePathLength)
        {
            // The default order to write a tar archive is to recurse into subdirectories first.
            // This order is expected by 'tar' to restore directory timestamps properly without the user explicitly specifying `--delay-directory-restore`.
            // FileSystemEnumerable RecurseSubdirectories will first write further entries before recursing, so we don't use it here.
 
            var fse = new FileSystemEnumerable<(string fullpath, string entryname, bool recurse)>(
                directory: sourceDirectoryName,
                transform: (ref FileSystemEntry entry) =>
                {
                    string fullPath = entry.ToFullPath();
                    bool isRealDirectory = entry.IsDirectory && (entry.Attributes & FileAttributes.ReparsePoint) == 0; // not a symlink.
                    string entryName = ArchivingUtils.EntryFromPath(fullPath.AsSpan(basePathLength), appendPathSeparator: isRealDirectory);
                    return (fullPath, entryName, isRealDirectory);
                });
 
            foreach ((string fullpath, string entryname, bool recurse) in fse)
            {
                yield return (fullpath, entryname);
 
                // Return entries for the subdirectory.
                if (recurse)
                {
                    foreach (var inner in GetFilesForCreation(fullpath, basePathLength))
                    {
                        yield return inner;
                    }
                }
            }
        }
 
        // Determines what should be the base path for all the entries when creating an archive.
        private static string GetBasePathForCreateFromDirectory(DirectoryInfo di, bool includeBaseDirectory) =>
            includeBaseDirectory && di.Parent != null ? di.Parent.FullName : di.FullName;
 
        private static string GetEntryNameForBaseDirectory(string name)
        {
            return ArchivingUtils.EntryFromPath(name, appendPathSeparator: true);
        }
 
        // Extracts an archive into the specified directory.
        // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not.
        private static void ExtractToDirectoryInternal(Stream source, string destinationDirectoryFullPath, bool overwriteFiles, bool leaveOpen)
        {
            VerifyExtractToDirectoryArguments(source, destinationDirectoryFullPath);
 
            using TarReader reader = new TarReader(source, leaveOpen);
 
            SortedDictionary<string, UnixFileMode>? pendingModes = TarHelpers.CreatePendingModesDictionary();
            var directoryModificationTimes = new Stack<(string, DateTimeOffset)>();
            TarEntry? entry;
            while ((entry = reader.GetNextEntry()) != null)
            {
                if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes)
                {
                    entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, pendingModes, directoryModificationTimes);
                }
            }
            TarHelpers.SetPendingModes(pendingModes);
            TarHelpers.SetPendingModificationTimes(directoryModificationTimes);
        }
 
        // Asynchronously extracts the contents of a tar file into the specified directory.
        private static async Task ExtractToDirectoryInternalAsync(string sourceFileName, string destinationDirectoryFullPath, bool overwriteFiles, CancellationToken cancellationToken)
        {
            Debug.Assert(!string.IsNullOrEmpty(sourceFileName));
            Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryFullPath));
 
            cancellationToken.ThrowIfCancellationRequested();
 
            FileStreamOptions options = new()
            {
                Access = FileAccess.Read,
                Mode = FileMode.Open,
                Options = FileOptions.Asynchronous,
            };
            FileStream archive = new(sourceFileName, options);
            await using (archive.ConfigureAwait(false))
            {
                await ExtractToDirectoryInternalAsync(archive, destinationDirectoryFullPath, overwriteFiles, leaveOpen: false, cancellationToken).ConfigureAwait(false);
            }
        }
 
        // Asynchronously extracts an archive into the specified directory.
        // It assumes the destinationDirectoryName is a fully qualified path, and allows choosing if the archive stream should be left open or not.
        private static async Task ExtractToDirectoryInternalAsync(Stream source, string destinationDirectoryFullPath, bool overwriteFiles, bool leaveOpen, CancellationToken cancellationToken)
        {
            VerifyExtractToDirectoryArguments(source, destinationDirectoryFullPath);
            cancellationToken.ThrowIfCancellationRequested();
 
            SortedDictionary<string, UnixFileMode>? pendingModes = TarHelpers.CreatePendingModesDictionary();
            var directoryModificationTimes = new Stack<(string, DateTimeOffset)>();
            TarReader reader = new TarReader(source, leaveOpen);
            await using (reader.ConfigureAwait(false))
            {
                TarEntry? entry;
                while ((entry = await reader.GetNextEntryAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) != null)
                {
                    if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes)
                    {
                        await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, pendingModes, directoryModificationTimes, cancellationToken).ConfigureAwait(false);
                    }
                }
            }
            TarHelpers.SetPendingModes(pendingModes);
            TarHelpers.SetPendingModificationTimes(directoryModificationTimes);
        }
 
        [Conditional("DEBUG")]
        private static void VerifyCreateFromDirectoryArguments(string sourceDirectoryName, Stream destination)
        {
            Debug.Assert(!string.IsNullOrEmpty(sourceDirectoryName));
            Debug.Assert(destination != null);
            Debug.Assert(Path.IsPathFullyQualified(sourceDirectoryName));
            Debug.Assert(destination.CanWrite);
        }
 
        [Conditional("DEBUG")]
        private static void VerifyExtractToDirectoryArguments(Stream source, string destinationDirectoryPath)
        {
            Debug.Assert(source != null);
            Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath));
            Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath));
            Debug.Assert(source.CanRead);
        }
 
        private static void ValidateFormat(TarEntryFormat format)
        {
            if (format is < TarEntryFormat.V7 or > TarEntryFormat.Gnu)
            {
                throw new ArgumentOutOfRangeException(nameof(format));
            }
        }
    }
}