-
Notifications
You must be signed in to change notification settings - Fork 509
Add alternative option for writing TAR archives with USTAR header format #1063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||
| using System.Buffers.Binary; | ||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||
| using System.IO; | ||||||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -9,8 +10,16 @@ internal sealed class TarHeader | |||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| public TarHeader(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding; | ||||||||||||||||||||||||||
| public TarHeader( | ||||||||||||||||||||||||||
| ArchiveEncoding archiveEncoding, | ||||||||||||||||||||||||||
| TarHeaderWriteFormat writeFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| ArchiveEncoding = archiveEncoding; | ||||||||||||||||||||||||||
| WriteFormat = writeFormat; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| internal TarHeaderWriteFormat WriteFormat { get; set; } | ||||||||||||||||||||||||||
| internal string? Name { get; set; } | ||||||||||||||||||||||||||
| internal string? LinkName { get; set; } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -30,6 +39,114 @@ internal sealed class TarHeader | |||||||||||||||||||||||||
| private const int MAX_LONG_NAME_SIZE = 32768; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| internal void Write(Stream output) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| switch (WriteFormat) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| case TarHeaderWriteFormat.GNU_TAR_LONG_LINK: | ||||||||||||||||||||||||||
| WriteGnuTarLongLink(output); | ||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||
| case TarHeaderWriteFormat.USTAR: | ||||||||||||||||||||||||||
| WriteUstar(output); | ||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||
| throw new Exception("This should be impossible..."); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| internal void WriteUstar(Stream output) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| var buffer = new byte[BLOCK_SIZE]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| WriteOctalBytes(511, buffer, 100, 8); // file mode | ||||||||||||||||||||||||||
| WriteOctalBytes(0, buffer, 108, 8); // owner ID | ||||||||||||||||||||||||||
| WriteOctalBytes(0, buffer, 116, 8); // group ID | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| //ArchiveEncoding.UTF8.GetBytes("magic").CopyTo(buffer, 257); | ||||||||||||||||||||||||||
| var nameByteCount = ArchiveEncoding | ||||||||||||||||||||||||||
| .GetEncoding() | ||||||||||||||||||||||||||
| .GetByteCount(Name.NotNull("Name is null")); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (nameByteCount > 100) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| // if name is longer, try to split it into name and namePrefix | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| string fullName = Name.NotNull("Name is null"); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // find all directory separators | ||||||||||||||||||||||||||
| List<int> dirSeps = new List<int>(); | ||||||||||||||||||||||||||
| for (int i = 0; i < fullName.Length; i++) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| if (fullName[i] == Path.DirectorySeparatorChar) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| dirSeps.Add(i); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // find the right place to split the name | ||||||||||||||||||||||||||
| int splitIndex = -1; | ||||||||||||||||||||||||||
| for (int i = 0; i < dirSeps.Count; i++) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| int count = ArchiveEncoding | ||||||||||||||||||||||||||
| .GetEncoding() | ||||||||||||||||||||||||||
| .GetByteCount(fullName.Substring(0, dirSeps[i])); | ||||||||||||||||||||||||||
| if (count < 155) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| splitIndex = dirSeps[i]; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (splitIndex == -1) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| throw new Exception( | ||||||||||||||||||||||||||
| $"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Directory separator not found! Try using GNU Tar format instead!" | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| string namePrefix = fullName.Substring(0, splitIndex); | ||||||||||||||||||||||||||
| string name = fullName.Substring(splitIndex + 1); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (this.ArchiveEncoding.GetEncoding().GetByteCount(namePrefix) >= 155) | ||||||||||||||||||||||||||
| throw new Exception( | ||||||||||||||||||||||||||
| $"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Try using GNU Tar format instead!" | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (this.ArchiveEncoding.GetEncoding().GetByteCount(name) >= 100) | ||||||||||||||||||||||||||
| throw new Exception( | ||||||||||||||||||||||||||
| $"Tar header USTAR format can not fit file name \"{fullName}\" of length {nameByteCount}! Try using GNU Tar format instead!" | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // write name prefix | ||||||||||||||||||||||||||
| WriteStringBytes(ArchiveEncoding.Encode(namePrefix), buffer, 345, 100); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| WriteStringBytes(ArchiveEncoding.Encode(namePrefix), buffer, 345, 100); | |
| WriteStringBytes(ArchiveEncoding.Encode(namePrefix), buffer, 345, 155); |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This call to WriteStringBytes is missing the offset parameter. It should be WriteStringBytes(ArchiveEncoding.Encode(name), buffer, 0, 100); to write the name starting at offset 0. Without the offset parameter, this will use the wrong overload and write to an incorrect location.
| WriteStringBytes(ArchiveEncoding.Encode(name), buffer, 100); | |
| } | |
| else | |
| { | |
| WriteStringBytes(ArchiveEncoding.Encode(Name.NotNull("Name is null")), buffer, 100); | |
| WriteStringBytes(ArchiveEncoding.Encode(name), buffer, 0, 100); | |
| } | |
| else | |
| { | |
| WriteStringBytes(ArchiveEncoding.Encode(Name.NotNull("Name is null")), buffer, 0, 100); |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This call to WriteStringBytes is missing the offset parameter. It should be WriteStringBytes(ArchiveEncoding.Encode(Name.NotNull("Name is null")), buffer, 0, 100); to write the name starting at offset 0. Without the offset parameter, this will use the wrong overload and write to an incorrect location.
| WriteStringBytes(ArchiveEncoding.Encode(Name.NotNull("Name is null")), buffer, 100); | |
| WriteStringBytes(ArchiveEncoding.Encode(Name.NotNull("Name is null")), buffer, 100, 100); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| namespace SharpCompress.Common.Tar.Headers; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| public enum TarHeaderWriteFormat | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| GNU_TAR_LONG_LINK, | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2
to
+5
|
||||||||||||||||||||||||||||||||||||||||||||
| public enum TarHeaderWriteFormat | |
| { | |
| GNU_TAR_LONG_LINK, | |
| /// <summary> | |
| /// Specifies the TAR header format to use when writing archives. | |
| /// </summary> | |
| public enum TarHeaderWriteFormat | |
| { | |
| /// <summary> | |
| /// GNU TAR format with long link support. Supports filenames longer than 100 characters. | |
| /// This is the default format and recommended for modern use. | |
| /// </summary> | |
| GNU_TAR_LONG_LINK, | |
| /// <summary> | |
| /// USTAR (Uniform Standard Tape Archive) format. Better compatibility with older tools | |
| /// but limits filenames to 100 characters (or 255 with prefix splitting). | |
| /// Use this format for compatibility with legacy software. | |
| /// </summary> |
adamhathcock marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -15,11 +15,13 @@ namespace SharpCompress.Writers.Tar; | |||||
| public class TarWriter : AbstractWriter | ||||||
| { | ||||||
| private readonly bool finalizeArchiveOnClose; | ||||||
| private TarHeaderWriteFormat headerFormat; | ||||||
|
||||||
| private TarHeaderWriteFormat headerFormat; | |
| private readonly TarHeaderWriteFormat headerFormat; |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||
| using SharpCompress.Common; | ||||||||||||||
| using SharpCompress.Common.Tar.Headers; | ||||||||||||||
|
|
||||||||||||||
| namespace SharpCompress.Writers.Tar; | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -9,8 +10,18 @@ public class TarWriterOptions : WriterOptions | |||||||||||||
| /// </summary> | ||||||||||||||
| public bool FinalizeArchiveOnClose { get; } | ||||||||||||||
|
|
||||||||||||||
|
||||||||||||||
| /// <summary> | |
| /// Gets the TAR header format to use when writing the archive. | |
| /// Defaults to GNU_TAR_LONG_LINK for maximum filename length support. | |
| /// </summary> |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The internal constructor doesn't pass the headerFormat parameter, which means it will always use the default GNU_TAR_LONG_LINK format. This could lead to unexpected behavior when creating a TarWriter from generic WriterOptions. Consider adding a parameter or explicitly setting the header format in this constructor.
| internal TarWriterOptions(WriterOptions options) | |
| : this(options.CompressionType, true) => ArchiveEncoding = options.ArchiveEncoding; | |
| internal TarWriterOptions(WriterOptions options, TarHeaderWriteFormat headerFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK) | |
| : this(options.CompressionType, true, headerFormat) => ArchiveEncoding = options.ArchiveEncoding; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commented-out code should be removed. If it was for testing purposes, it should not be included in the final PR.