diff --git a/src/libraries/Common/src/System/IO/Archiving.Utils.Unix.cs b/src/libraries/Common/src/System/IO/Archiving.Utils.Unix.cs index ca9b37759a31cf..18ab525f919b39 100644 --- a/src/libraries/Common/src/System/IO/Archiving.Utils.Unix.cs +++ b/src/libraries/Common/src/System/IO/Archiving.Utils.Unix.cs @@ -6,5 +6,25 @@ namespace System.IO internal static partial class ArchivingUtils { internal static string SanitizeEntryFilePath(string entryPath) => entryPath.Replace('\0', '_'); + + public static unsafe string EntryFromPath(ReadOnlySpan path, bool appendPathSeparator = false) + { + // Remove leading separators. + int nonSlash = path.IndexOfAnyExcept('/'); + if (nonSlash == -1) + { + nonSlash = path.Length; + } + path = path.Slice(nonSlash); + + // Append a separator if necessary. + return (path.IsEmpty, appendPathSeparator) switch + { + (false, false) => path.ToString(), + (false, true) => string.Concat(path, "/"), + (true, false) => string.Empty, + (true, true) => "/", + }; + } } } diff --git a/src/libraries/Common/src/System/IO/Archiving.Utils.Windows.cs b/src/libraries/Common/src/System/IO/Archiving.Utils.Windows.cs index 88f7ffb14f4d29..412563966fc722 100644 --- a/src/libraries/Common/src/System/IO/Archiving.Utils.Windows.cs +++ b/src/libraries/Common/src/System/IO/Archiving.Utils.Windows.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.InteropServices; using System.Text; namespace System.IO @@ -42,5 +43,47 @@ internal static string SanitizeEntryFilePath(string entryPath) // There weren't any characters to sanitize. Just return the original string. return entryPath; } + + public static unsafe string EntryFromPath(ReadOnlySpan path, bool appendPathSeparator = false) + { + // Remove leading separators. + int nonSlash = path.IndexOfAnyExcept('/', '\\'); + if (nonSlash == -1) + { + nonSlash = path.Length; + } + path = path.Slice(nonSlash); + + // Replace \ with /, and append a separator if necessary. + + if (path.IsEmpty) + { + return appendPathSeparator ? + "/" : + string.Empty; + } + + fixed (char* pathPtr = &MemoryMarshal.GetReference(path)) + { + return string.Create(appendPathSeparator ? path.Length + 1 : path.Length, (appendPathSeparator, (IntPtr)pathPtr, path.Length), static (dest, state) => + { + ReadOnlySpan path = new ReadOnlySpan((char*)state.Item2, state.Length); + path.CopyTo(dest); + if (state.appendPathSeparator) + { + dest[^1] = '/'; + } + + // To ensure tar files remain compatible with Unix, and per the ZIP File Format Specification 4.4.17.1, + // all slashes should be forward slashes. + int pos; + while ((pos = dest.IndexOf('\\')) >= 0) + { + dest[pos] = '/'; + dest = dest.Slice(pos + 1); + } + }); + } + } } } diff --git a/src/libraries/Common/src/System/IO/Archiving.Utils.cs b/src/libraries/Common/src/System/IO/Archiving.Utils.cs index 80d633228cc0c1..23cc77a30c75f1 100644 --- a/src/libraries/Common/src/System/IO/Archiving.Utils.cs +++ b/src/libraries/Common/src/System/IO/Archiving.Utils.cs @@ -9,60 +9,6 @@ namespace System.IO { internal static partial class ArchivingUtils { - // To ensure tar files remain compatible with Unix, - // and per the ZIP File Format Specification 4.4.17.1, - // all slashes should be forward slashes. - private const char PathSeparatorChar = '/'; - private const string PathSeparatorString = "/"; - - public static string EntryFromPath(string entry, int offset, int length, bool appendPathSeparator = false) - { - Debug.Assert(length <= entry.Length - offset); - - // Remove any leading slashes from the entry name: - while (length > 0) - { - if (entry[offset] != Path.DirectorySeparatorChar && - entry[offset] != Path.AltDirectorySeparatorChar) - break; - - offset++; - length--; - } - - if (length == 0) - { - return appendPathSeparator ? PathSeparatorString : string.Empty; - } - - if (appendPathSeparator) - { - length++; - } - - return string.Create(length, (appendPathSeparator, offset, entry), static (dest, state) => - { - state.entry.AsSpan(state.offset).CopyTo(dest); - - // '/' is a more broadly recognized directory separator on all platforms (eg: mac, linux) - // We don't use Path.DirectorySeparatorChar or AltDirectorySeparatorChar because this is - // explicitly trying to standardize to '/' - for (int i = 0; i < dest.Length; i++) - { - char ch = dest[i]; - if (ch == Path.DirectorySeparatorChar || ch == Path.AltDirectorySeparatorChar) - { - dest[i] = PathSeparatorChar; - } - } - - if (state.appendPathSeparator) - { - dest[^1] = PathSeparatorChar; - } - }); - } - public static void EnsureCapacity(ref char[] buffer, int min) { Debug.Assert(buffer != null); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 33d971bfc56053..127b00f13fb557 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -344,16 +344,13 @@ private static string GetBasePathForCreateFromDirectory(DirectoryInfo di, bool i // Constructs the entry name used for a filesystem entry when creating an archive. private static string GetEntryNameForFileSystemInfo(FileSystemInfo file, int basePathLength) { - int entryNameLength = file.FullName.Length - basePathLength; - Debug.Assert(entryNameLength > 0); - bool isDirectory = (file.Attributes & FileAttributes.Directory) != 0; - return ArchivingUtils.EntryFromPath(file.FullName, basePathLength, entryNameLength, appendPathSeparator: isDirectory); + return ArchivingUtils.EntryFromPath(file.FullName.AsSpan(basePathLength), appendPathSeparator: isDirectory); } private static string GetEntryNameForBaseDirectory(string name) { - return ArchivingUtils.EntryFromPath(name, 0, name.Length, appendPathSeparator: true); + return ArchivingUtils.EntryFromPath(name, appendPathSeparator: true); } // Extracts an archive into the specified directory. diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index 724274d1689d92..2afcfb8a9f23df 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs @@ -559,8 +559,16 @@ private static async Task WriteDataAsync(Stream archiveStream, Stream dataStream // The format is: // "XX attribute=value\n" // where "XX" is the number of characters in the entry, including those required for the count itself. + // If prepending the length digits increases the number of digits, we need to expand. int length = 3 + Encoding.UTF8.GetByteCount(attribute) + Encoding.UTF8.GetByteCount(value); - length += CountDigits(length); + int originalDigitCount = CountDigits(length), newDigitCount; + length += originalDigitCount; + while ((newDigitCount = CountDigits(length)) != originalDigitCount) + { + length += newDigitCount - originalDigitCount; + originalDigitCount = newDigitCount; + } + Debug.Assert(length == CountDigits(length) + 3 + Encoding.UTF8.GetByteCount(attribute) + Encoding.UTF8.GetByteCount(value)); // Get a large enough buffer if we don't already have one. if (span.Length < length) @@ -569,8 +577,7 @@ private static async Task WriteDataAsync(Stream archiveStream, Stream dataStream { ArrayPool.Shared.Return(buffer); } - buffer = ArrayPool.Shared.Rent(length); - span = buffer; + span = buffer = ArrayPool.Shared.Rent(length); } // Format the contents. diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Create.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Create.cs index 33923d91b9b8f1..70894d2b74f7ef 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Create.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Create.cs @@ -379,13 +379,10 @@ private static void DoCreateFromDirectory(string sourceDirectoryName, string des { directoryIsEmpty = false; - int entryNameLength = file.FullName.Length - basePath.Length; - Debug.Assert(entryNameLength > 0); - if (file is FileInfo) { // Create entry for file: - string entryName = ArchivingUtils.EntryFromPath(file.FullName, basePath.Length, entryNameLength); + string entryName = ArchivingUtils.EntryFromPath(file.FullName.AsSpan(basePath.Length)); ZipFileExtensions.DoCreateEntryFromFile(archive, file.FullName, entryName, compressionLevel); } else @@ -395,7 +392,7 @@ private static void DoCreateFromDirectory(string sourceDirectoryName, string des { // FullName never returns a directory separator character on the end, // but Zip archives require it to specify an explicit directory: - string entryName = ArchivingUtils.EntryFromPath(file.FullName, basePath.Length, entryNameLength, appendPathSeparator: true); + string entryName = ArchivingUtils.EntryFromPath(file.FullName.AsSpan(basePath.Length), appendPathSeparator: true); archive.CreateEntry(entryName); } } @@ -403,7 +400,7 @@ private static void DoCreateFromDirectory(string sourceDirectoryName, string des // If no entries create an empty root directory entry: if (includeBaseDirectory && directoryIsEmpty) - archive.CreateEntry(ArchivingUtils.EntryFromPath(di.Name, 0, di.Name.Length, appendPathSeparator: true)); + archive.CreateEntry(ArchivingUtils.EntryFromPath(di.Name, appendPathSeparator: true)); } } }