Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SharpCompress/Common/CompressionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum CompressionType
Deflate,
Rar,
LZMA,
LZMA2,
BCJ,
BCJ2,
LZip,
Expand Down
52 changes: 52 additions & 0 deletions src/SharpCompress/Common/SevenZip/ArchiveWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.IO;
using SharpCompress.Compressors.LZMA.Utilities;

namespace SharpCompress.Common.SevenZip;

/// <summary>
/// Top-level orchestrator for writing 7z archive headers.
/// Assembles the complete header from StreamsInfo and FilesInfo,
/// and supports writing either a raw header (kHeader) or an
/// encoded/compressed header (kEncodedHeader).
/// </summary>
internal static class ArchiveHeaderWriter
{
/// <summary>
/// Writes a raw (uncompressed) header containing MainStreamsInfo and FilesInfo.
/// </summary>
public static void WriteRawHeader(
Stream stream,
SevenZipStreamsInfoWriter? mainStreamsInfo,
SevenZipFilesInfoWriter? filesInfo
)
{
stream.WriteByte((byte)BlockType.Header);

if (mainStreamsInfo != null)
{
stream.WriteByte((byte)BlockType.MainStreamsInfo);
mainStreamsInfo.Write(stream);
}

if (filesInfo != null)
{
stream.WriteByte((byte)BlockType.FilesInfo);
filesInfo.Write(stream);
}

stream.WriteByte((byte)BlockType.End);
}

/// <summary>
/// Writes an encoded header - a StreamsInfo block that describes
/// how to decompress the actual header data.
/// </summary>
public static void WriteEncodedHeader(
Stream stream,
SevenZipStreamsInfoWriter headerStreamsInfo
)
{
stream.WriteByte((byte)BlockType.EncodedHeader);
headerStreamsInfo.Write(stream);
}
}
226 changes: 226 additions & 0 deletions src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
using System;
using System.IO;
using System.Text;
using SharpCompress.Compressors.LZMA.Utilities;

namespace SharpCompress.Common.SevenZip;

/// <summary>
/// Entry metadata collected during writing, used to build FilesInfo header.
/// </summary>
internal sealed class SevenZipWriteEntry
{
public string Name { get; init; } = string.Empty;
public DateTime? ModificationTime { get; init; }
public uint? Attributes { get; init; }
public bool IsDirectory { get; init; }
public bool IsEmpty { get; init; }
}

/// <summary>
/// Writes the FilesInfo section of a 7z header, including all file properties
/// (names, timestamps, attributes, empty stream/file markers).
/// </summary>
internal sealed class SevenZipFilesInfoWriter
{
public SevenZipWriteEntry[] Entries { get; init; } = [];

public void Write(Stream stream)
{
var numFiles = (ulong)Entries.Length;
stream.WriteEncodedUInt64(numFiles);

// Count empty streams (directories + zero-length files)
var emptyStreamCount = 0;
for (var i = 0; i < Entries.Length; i++)
{
if (Entries[i].IsEmpty || Entries[i].IsDirectory)
{
emptyStreamCount++;
}
}

// EmptyStream property
if (emptyStreamCount > 0)
{
WriteEmptyStreamProperty(stream, emptyStreamCount);
}

// Names property
WriteNameProperty(stream);

// MTime property
WriteMTimeProperty(stream);

// Attributes property
WriteAttributesProperty(stream);

stream.WriteByte((byte)BlockType.End);
}

private void WriteEmptyStreamProperty(Stream stream, int emptyStreamCount)
{
var emptyStreams = new bool[Entries.Length];
var emptyFiles = new bool[emptyStreamCount];
var hasEmptyFile = false;
var emptyIndex = 0;

for (var i = 0; i < Entries.Length; i++)
{
if (Entries[i].IsEmpty || Entries[i].IsDirectory)
{
emptyStreams[i] = true;
var isEmptyFile = !Entries[i].IsDirectory;
emptyFiles[emptyIndex++] = isEmptyFile;
if (isEmptyFile)
{
hasEmptyFile = true;
}
}
}

// kEmptyStream
WriteFileProperty(stream, BlockType.EmptyStream, s => s.WriteBoolVector(emptyStreams));

// kEmptyFile (only if there are actual empty files, not just directories)
if (hasEmptyFile)
{
WriteFileProperty(stream, BlockType.EmptyFile, s => s.WriteBoolVector(emptyFiles));
}
}

private void WriteNameProperty(Stream stream)
{
WriteFileProperty(
stream,
BlockType.Name,
s =>
{
// External = 0 (inline)
s.WriteByte(0);

for (var i = 0; i < Entries.Length; i++)
{
var nameBytes = Encoding.Unicode.GetBytes(Entries[i].Name);
s.Write(nameBytes);
// null terminator (2 bytes for UTF-16)
s.WriteByte(0);
s.WriteByte(0);
}
}
);
}

private void WriteMTimeProperty(Stream stream)
{
var hasTimes = false;
for (var i = 0; i < Entries.Length; i++)
{
if (Entries[i].ModificationTime != null)
{
hasTimes = true;
break;
}
}

if (!hasTimes)
{
return;
}

WriteFileProperty(
stream,
BlockType.MTime,
s =>
{
var defined = new bool[Entries.Length];
for (var i = 0; i < Entries.Length; i++)
{
defined[i] = Entries[i].ModificationTime != null;
}
s.WriteOptionalBoolVector(defined);

// External = 0 (inline)
s.WriteByte(0);

var buf = new byte[8];
for (var i = 0; i < Entries.Length; i++)
{
if (Entries[i].ModificationTime is { } mtime)
{
var fileTime = (ulong)mtime.ToUniversalTime().ToFileTimeUtc();
System.Buffers.Binary.BinaryPrimitives.WriteUInt64LittleEndian(
buf,
fileTime
);
s.Write(buf, 0, 8);
}
}
}
);
}

private void WriteAttributesProperty(Stream stream)
{
var hasAttrs = false;
for (var i = 0; i < Entries.Length; i++)
{
if (Entries[i].Attributes != null)
{
hasAttrs = true;
break;
}
}

if (!hasAttrs)
{
return;
}

WriteFileProperty(
stream,
BlockType.WinAttributes,
s =>
{
var defined = new bool[Entries.Length];
for (var i = 0; i < Entries.Length; i++)
{
defined[i] = Entries[i].Attributes != null;
}
s.WriteOptionalBoolVector(defined);

// External = 0 (inline)
s.WriteByte(0);

var buf = new byte[4];
for (var i = 0; i < Entries.Length; i++)
{
if (Entries[i].Attributes is { } attrs)
{
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(buf, attrs);
s.Write(buf, 0, 4);
}
}
}
);
}

/// <summary>
/// Writes a file property block: PropertyID + size + data.
/// Size is computed by writing to a temporary buffer first.
/// </summary>
private static void WriteFileProperty(
Stream stream,
BlockType propertyId,
Action<Stream> writeData
)
{
using var dataStream = new MemoryStream();
writeData(dataStream);

stream.WriteByte((byte)propertyId);
stream.WriteEncodedUInt64((ulong)dataStream.Length);
dataStream.Position = 0;
dataStream.CopyTo(stream);
}
}
Loading