Add 7z writer with LZMA compression support

This commit is contained in:
Daniil Bystrukhin
2026-02-21 23:12:38 -06:00
parent d15b728390
commit 5e8ea67e21
12 changed files with 1778 additions and 1 deletions

View File

@@ -0,0 +1,49 @@
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);
}
}

View File

@@ -0,0 +1,219 @@
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);
}
}

View File

@@ -0,0 +1,305 @@
using System;
using System.IO;
using SharpCompress.Compressors.LZMA.Utilities;
namespace SharpCompress.Common.SevenZip;
/// <summary>
/// Writes Digests (CRC32 arrays with optional-defined-vector) for 7z headers.
/// </summary>
internal sealed class SevenZipDigestsWriter(uint?[] crcs)
{
public uint?[] CRCs { get; } = crcs;
public void Write(Stream stream)
{
var defined = new bool[CRCs.Length];
for (var i = 0; i < CRCs.Length; i++)
{
defined[i] = CRCs[i] != null;
}
stream.WriteOptionalBoolVector(defined);
var buf = new byte[4];
for (var i = 0; i < CRCs.Length; i++)
{
if (CRCs[i] is { } crcValue)
{
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(buf, crcValue);
stream.Write(buf, 0, 4);
}
}
}
public bool HasAnyDefined()
{
for (var i = 0; i < CRCs.Length; i++)
{
if (CRCs[i] != null)
{
return true;
}
}
return false;
}
}
/// <summary>
/// Writes PackInfo section: packed stream positions, sizes, and CRCs.
/// </summary>
internal sealed class SevenZipPackInfoWriter
{
public ulong PackPos { get; init; }
public ulong[] Sizes { get; init; } = [];
public uint?[] CRCs { get; init; } = [];
public void Write(Stream stream)
{
stream.WriteEncodedUInt64(PackPos);
stream.WriteEncodedUInt64((ulong)Sizes.Length);
// Sizes
stream.WriteByte((byte)BlockType.Size);
for (var i = 0; i < Sizes.Length; i++)
{
stream.WriteEncodedUInt64(Sizes[i]);
}
// CRCs (optional)
var digests = new SevenZipDigestsWriter(CRCs);
if (digests.HasAnyDefined())
{
stream.WriteByte((byte)BlockType.Crc);
digests.Write(stream);
}
stream.WriteByte((byte)BlockType.End);
}
}
/// <summary>
/// Writes UnPackInfo section: folder definitions (coders, bind pairs, unpack sizes, CRCs).
/// </summary>
internal sealed class SevenZipUnPackInfoWriter
{
public CFolder[] Folders { get; init; } = [];
public void Write(Stream stream)
{
stream.WriteByte((byte)BlockType.Folder);
// Number of folders
stream.WriteEncodedUInt64((ulong)Folders.Length);
// External = 0 (inline)
stream.WriteByte(0);
// Write each folder's coder definitions
for (var i = 0; i < Folders.Length; i++)
{
WriteFolder(stream, Folders[i]);
}
// CodersUnPackSize
stream.WriteByte((byte)BlockType.CodersUnpackSize);
for (var i = 0; i < Folders.Length; i++)
{
for (var j = 0; j < Folders[i]._unpackSizes.Count; j++)
{
stream.WriteEncodedUInt64((ulong)Folders[i]._unpackSizes[j]);
}
}
// UnPackDigests (CRCs per folder)
var hasCrc = false;
for (var i = 0; i < Folders.Length; i++)
{
if (Folders[i]._unpackCrc != null)
{
hasCrc = true;
break;
}
}
if (hasCrc)
{
stream.WriteByte((byte)BlockType.Crc);
var crcs = new uint?[Folders.Length];
for (var i = 0; i < Folders.Length; i++)
{
crcs[i] = Folders[i]._unpackCrc;
}
new SevenZipDigestsWriter(crcs).Write(stream);
}
stream.WriteByte((byte)BlockType.End);
}
private static void WriteFolder(Stream stream, CFolder folder)
{
// NumCoders
stream.WriteEncodedUInt64((ulong)folder._coders.Count);
for (var i = 0; i < folder._coders.Count; i++)
{
WriteCoder(stream, folder._coders[i]);
}
// BindPairs
for (var i = 0; i < folder._bindPairs.Count; i++)
{
stream.WriteEncodedUInt64((ulong)folder._bindPairs[i]._inIndex);
stream.WriteEncodedUInt64((ulong)folder._bindPairs[i]._outIndex);
}
// PackedIndices (only if > 1 packed stream)
var numPackStreams = folder._packStreams.Count;
if (numPackStreams > 1)
{
for (var i = 0; i < numPackStreams; i++)
{
stream.WriteEncodedUInt64((ulong)folder._packStreams[i]);
}
}
}
private static void WriteCoder(Stream stream, CCoderInfo coder)
{
var codecIdLength = coder._methodId.GetLength();
byte attributes = (byte)(codecIdLength & 0x0F);
var isComplex = coder._numInStreams != 1 || coder._numOutStreams != 1;
if (isComplex)
{
attributes |= 0x10;
}
var hasProperties = coder._props != null && coder._props.Length > 0;
if (hasProperties)
{
attributes |= 0x20;
}
stream.WriteByte(attributes);
// Codec ID bytes (big-endian, most significant byte first)
var codecId = new byte[codecIdLength];
var id = coder._methodId._id;
for (var i = codecIdLength - 1; i >= 0; i--)
{
codecId[i] = (byte)(id & 0xFF);
id >>= 8;
}
stream.Write(codecId, 0, codecIdLength);
if (isComplex)
{
stream.WriteEncodedUInt64((ulong)coder._numInStreams);
stream.WriteEncodedUInt64((ulong)coder._numOutStreams);
}
if (hasProperties)
{
stream.WriteEncodedUInt64((ulong)coder._props!.Length);
stream.Write(coder._props);
}
}
}
/// <summary>
/// Writes SubStreamsInfo section: per-file unpack sizes and CRCs within folders.
/// </summary>
internal sealed class SevenZipSubStreamsInfoWriter
{
public CFolder[] Folders { get; init; } = [];
public ulong[] NumUnPackStreamsInFolders { get; init; } = [];
public ulong[] UnPackSizes { get; init; } = [];
public uint?[] CRCs { get; init; } = [];
public void Write(Stream stream)
{
var numFolders = (ulong)Folders.Length;
// NumUnPackStream per folder (skip if all folders have exactly 1 stream)
var totalStreams = 0UL;
var allSingle = true;
for (var i = 0; i < NumUnPackStreamsInFolders.Length; i++)
{
totalStreams += NumUnPackStreamsInFolders[i];
if (NumUnPackStreamsInFolders[i] != 1)
{
allSingle = false;
}
}
if (!allSingle)
{
stream.WriteByte((byte)BlockType.NumUnpackStream);
for (var i = 0; i < NumUnPackStreamsInFolders.Length; i++)
{
stream.WriteEncodedUInt64(NumUnPackStreamsInFolders[i]);
}
}
// UnPackSizes - write all except the last per folder (it's implicit from folder unpack size)
if (UnPackSizes.Length > 0)
{
stream.WriteByte((byte)BlockType.Size);
var sizeIndex = 0;
for (var i = 0; i < NumUnPackStreamsInFolders.Length; i++)
{
var numStreams = NumUnPackStreamsInFolders[i];
for (var j = 1UL; j < numStreams; j++)
{
stream.WriteEncodedUInt64(UnPackSizes[sizeIndex++]);
}
sizeIndex++; // skip the last (implicit)
}
}
// Digests for streams with unknown CRCs
var digests = new SevenZipDigestsWriter(CRCs);
if (digests.HasAnyDefined())
{
stream.WriteByte((byte)BlockType.Crc);
digests.Write(stream);
}
stream.WriteByte((byte)BlockType.End);
}
}
/// <summary>
/// Writes the complete StreamsInfo section (PackInfo + UnPackInfo + SubStreamsInfo).
/// </summary>
internal sealed class SevenZipStreamsInfoWriter
{
public SevenZipPackInfoWriter? PackInfo { get; init; }
public SevenZipUnPackInfoWriter? UnPackInfo { get; init; }
public SevenZipSubStreamsInfoWriter? SubStreamsInfo { get; init; }
public void Write(Stream stream)
{
if (PackInfo != null)
{
stream.WriteByte((byte)BlockType.PackInfo);
PackInfo.Write(stream);
}
if (UnPackInfo != null)
{
stream.WriteByte((byte)BlockType.UnpackInfo);
UnPackInfo.Write(stream);
}
if (SubStreamsInfo != null)
{
stream.WriteByte((byte)BlockType.SubStreamsInfo);
SubStreamsInfo.Write(stream);
}
stream.WriteByte((byte)BlockType.End);
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Buffers.Binary;
using System.IO;
using SharpCompress.Crypto;
namespace SharpCompress.Common.SevenZip;
/// <summary>
/// Handles writing the 7z signature header (32 bytes at position 0 of the archive).
/// Layout: [6 bytes magic] [2 bytes version] [4 bytes StartHeaderCRC] [20 bytes StartHeader]
/// </summary>
internal static class SevenZipSignatureHeaderWriter
{
/// <summary>
/// 7z file magic signature bytes.
/// </summary>
private static readonly byte[] Signature = [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C];
/// <summary>
/// Total size of the signature header in bytes (6+2+4+8+8+4 = 32).
/// </summary>
public const int HeaderSize = 32;
/// <summary>
/// Writes a placeholder signature header (all zeros for CRC/offset fields).
/// Call this at the start of archive creation to reserve space.
/// </summary>
public static void WritePlaceholder(Stream stream)
{
var header = new byte[HeaderSize];
// magic signature
Array.Copy(Signature, 0, header, 0, Signature.Length);
// version: major=0, minor=2 (standard 7z format)
header[6] = 0;
header[7] = 2;
// remaining 24 bytes are zero (placeholder for CRC and StartHeader)
stream.Write(header, 0, header.Length);
}
/// <summary>
/// Writes the final signature header with correct offsets and CRCs.
/// The stream must be seekable; this method seeks to position 0.
/// </summary>
/// <param name="stream">The archive output stream (seekable).</param>
/// <param name="nextHeaderOffset">Offset from end of signature header to start of metadata header.</param>
/// <param name="nextHeaderSize">Size of the metadata header in bytes.</param>
/// <param name="nextHeaderCrc">CRC32 of the metadata header bytes.</param>
public static void WriteFinal(
Stream stream,
ulong nextHeaderOffset,
ulong nextHeaderSize,
uint nextHeaderCrc
)
{
// Build StartHeader (20 bytes): NextHeaderOffset(8) + NextHeaderSize(8) + NextHeaderCRC(4)
var startHeader = new byte[20];
BinaryPrimitives.WriteUInt64LittleEndian(startHeader.AsSpan(0, 8), nextHeaderOffset);
BinaryPrimitives.WriteUInt64LittleEndian(startHeader.AsSpan(8, 8), nextHeaderSize);
BinaryPrimitives.WriteUInt32LittleEndian(startHeader.AsSpan(16, 4), nextHeaderCrc);
// CRC32 of StartHeader
var startHeaderCrc = Crc32Stream.Compute(Crc32Stream.DEFAULT_POLYNOMIAL, Crc32Stream.DEFAULT_SEED, startHeader);
// Assemble full 32-byte header
var header = new byte[HeaderSize];
// magic signature
Array.Copy(Signature, 0, header, 0, Signature.Length);
// version
header[6] = 0;
header[7] = 2;
// StartHeaderCRC
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(8, 4), startHeaderCrc);
// StartHeader
Array.Copy(startHeader, 0, header, 12, startHeader.Length);
// Write at position 0
stream.Position = 0;
stream.Write(header, 0, header.Length);
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Crypto;
namespace SharpCompress.Common.SevenZip;
/// <summary>
/// Result of compressing a stream - contains folder metadata, compressed sizes, and CRCs.
/// </summary>
internal sealed class PackedStream
{
public CFolder Folder { get; init; } = new();
public ulong[] Sizes { get; init; } = [];
public uint?[] CRCs { get; init; } = [];
}
/// <summary>
/// Compresses a single input stream using LZMA or LZMA2, writing compressed output
/// to the archive stream. Builds the CFolder metadata describing the compression.
/// Uses SharpCompress's existing LzmaStream encoder.
/// </summary>
internal sealed class SevenZipStreamsCompressor(Stream outputStream)
{
/// <summary>
/// Compresses the input stream to the output stream using the specified method.
/// Returns a PackedStream containing folder metadata, compressed size, and CRCs.
/// </summary>
/// <param name="inputStream">Uncompressed data to compress.</param>
/// <param name="isLzma2">True for LZMA2, false for LZMA.</param>
/// <param name="encoderProperties">LZMA encoder properties (null for defaults).</param>
public PackedStream Compress(
Stream inputStream,
bool isLzma2,
LzmaEncoderProperties? encoderProperties = null
)
{
encoderProperties ??= new LzmaEncoderProperties(eos: true);
var outStartOffset = outputStream.Position;
var inStartOffset = inputStream.CanSeek ? inputStream.Position : 0L;
// Wrap the output stream in CRC calculator
using var outCrcStream = new Crc32Stream(outputStream);
// Create LZMA encoder writing to CRC-wrapped output
using var lzmaStream = LzmaStream.Create(encoderProperties, isLzma2, outCrcStream);
var properties = lzmaStream.Properties;
// Copy input through the LZMA encoder while computing input CRC
uint inputCrc;
long inputSize;
CopyWithCrc(inputStream, lzmaStream, out inputCrc, out inputSize);
// Flush/finalize the LZMA encoder (writes remaining compressed data)
lzmaStream.Dispose();
var compressedSize = (ulong)(outputStream.Position - outStartOffset);
var uncompressedSize = (ulong)inputSize;
var outputCrc = outCrcStream.Crc;
// Build method ID
var methodId = isLzma2 ? CMethodId.K_LZMA2 : CMethodId.K_LZMA;
// Build folder metadata
var folder = new CFolder();
folder._coders.Add(
new CCoderInfo
{
_methodId = methodId,
_numInStreams = 1,
_numOutStreams = 1,
_props = properties,
}
);
folder._packStreams.Add(0);
folder._unpackSizes.Add((long)uncompressedSize);
folder._unpackCrc = inputCrc;
return new PackedStream
{
Folder = folder,
Sizes = [compressedSize],
CRCs = [outputCrc],
};
}
/// <summary>
/// Copies data from source to destination while computing CRC32 of the source data.
/// </summary>
private static void CopyWithCrc(Stream source, Stream destination, out uint crc, out long bytesRead)
{
var crcValue = Crc32Stream.DEFAULT_SEED;
var table = InitCrcTable();
var buffer = new byte[81920];
long totalRead = 0;
int read;
while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
{
// Update CRC
for (var i = 0; i < read; i++)
{
crcValue = (crcValue >> 8) ^ table[(crcValue ^ buffer[i]) & 0xFF];
}
destination.Write(buffer, 0, read);
totalRead += read;
}
crc = ~crcValue;
bytesRead = totalRead;
}
private static uint[] InitCrcTable()
{
var table = new uint[256];
for (var i = 0; i < 256; i++)
{
var entry = (uint)i;
for (var j = 0; j < 8; j++)
{
entry = (entry & 1) == 1 ? (entry >> 1) ^ Crc32Stream.DEFAULT_POLYNOMIAL : entry >> 1;
}
table[i] = entry;
}
return table;
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.IO;
namespace SharpCompress.Common.SevenZip;
/// <summary>
/// Stream extension methods for writing 7z binary format primitives.
/// Mirrors the read-side encoding in DataReader.ReadNumber() and the reference
/// StreamExtensions (ReadDecodedUInt64/WriteEncodedUInt64/WriteBoolVector).
/// </summary>
internal static class SevenZipWriteExtensions
{
/// <summary>
/// Writes a variable-length encoded 64-bit unsigned integer to the stream.
/// Uses the 7z VLQ format: the first byte has leading 1-bits indicating how many
/// extra bytes follow, with remaining bits holding the high part of the value.
/// </summary>
public static int WriteEncodedUInt64(this Stream stream, ulong value)
{
var data = new byte[9];
data[0] = 0xFF;
byte mask = 0x80;
var length = 1;
for (var i = 0; i < 8; i++)
{
if (value < mask)
{
var headerMask = (byte)((0xFF ^ mask) ^ (mask - 1u));
data[0] = (byte)(value | headerMask);
break;
}
data[length++] = (byte)(value & 0xFF);
value >>= 8;
mask >>= 1;
}
stream.Write(data, 0, length);
return length;
}
/// <summary>
/// Writes a boolean vector as a packed bitmask.
/// Each bool becomes one bit, MSB first, padded to byte boundary.
/// </summary>
public static ulong WriteBoolVector(this Stream stream, bool[] vector)
{
byte mask = 0x80;
byte b = 0;
ulong bytesWritten = 0;
for (var i = 0L; i < vector.LongLength; i++)
{
if (vector[i])
{
b |= mask;
}
mask >>= 1;
if (mask == 0)
{
stream.WriteByte(b);
bytesWritten++;
mask = 0x80;
b = 0;
}
}
if (mask != 0x80)
{
stream.WriteByte(b);
bytesWritten++;
}
return bytesWritten;
}
/// <summary>
/// Writes an optional bool vector. If all elements are true, writes a single 0x01 byte
/// (AllAreDefined marker). Otherwise writes 0x00 followed by the packed bitmask.
/// </summary>
public static void WriteOptionalBoolVector(this Stream stream, bool[] vector)
{
for (var i = 0L; i < vector.LongLength; i++)
{
if (!vector[i])
{
stream.WriteByte(0);
stream.WriteBoolVector(vector);
return;
}
}
stream.WriteByte(1);
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
@@ -5,15 +6,18 @@ using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Writers;
using SharpCompress.Writers.SevenZip;
namespace SharpCompress.Factories;
/// <summary>
/// Represents the foundation factory of 7Zip archive.
/// </summary>
public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory
public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IWriterFactory
{
#region IFactory
@@ -117,4 +121,30 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory
}
#endregion
#region IWriterFactory
/// <inheritdoc/>
public IWriter OpenWriter(Stream stream, IWriterOptions writerOptions)
{
SevenZipWriterOptions sevenZipOptions = writerOptions switch
{
SevenZipWriterOptions szo => szo,
WriterOptions wo => new SevenZipWriterOptions(wo),
_ => throw new ArgumentException(
$"Expected WriterOptions or SevenZipWriterOptions, got {writerOptions.GetType().Name}",
nameof(writerOptions)
),
};
return new SevenZipWriter(stream, sevenZipOptions);
}
/// <inheritdoc/>
public IAsyncWriter OpenAsyncWriter(
Stream stream,
IWriterOptions writerOptions,
CancellationToken cancellationToken = default
) => (IAsyncWriter)OpenWriter(stream, writerOptions);
#endregion
}

View File

@@ -0,0 +1,39 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Writers.SevenZip;
public partial class SevenZipWriter
{
/// <summary>
/// Asynchronously writes a file entry to the 7z archive.
/// Note: LZMA compression itself is synchronous; async is used for stream copying.
/// </summary>
public override ValueTask WriteAsync(
string filename,
Stream source,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
Write(filename, source, modificationTime);
return new ValueTask();
}
/// <summary>
/// Asynchronously writes a directory entry to the 7z archive.
/// </summary>
public override ValueTask WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
WriteDirectory(directoryName, modificationTime);
return new ValueTask();
}
}

View File

@@ -0,0 +1,62 @@
#if NET8_0_OR_GREATER
using System.IO;
namespace SharpCompress.Writers.SevenZip;
public partial class SevenZipWriter : IWriterOpenable<SevenZipWriterOptions>
{
/// <summary>
/// Opens a new SevenZipWriter for the specified file path.
/// </summary>
public static IWriter OpenWriter(string filePath, SevenZipWriterOptions writerOptions)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenWriter(new FileInfo(filePath), writerOptions);
}
/// <summary>
/// Opens a new SevenZipWriter for the specified file.
/// </summary>
public static IWriter OpenWriter(FileInfo fileInfo, SevenZipWriterOptions writerOptions)
{
fileInfo.NotNull(nameof(fileInfo));
return new SevenZipWriter(fileInfo.OpenWrite(), writerOptions);
}
/// <summary>
/// Opens a new SevenZipWriter for the specified stream.
/// </summary>
public static IWriter OpenWriter(Stream stream, SevenZipWriterOptions writerOptions)
{
stream.NotNull(nameof(stream));
return new SevenZipWriter(stream, writerOptions);
}
/// <summary>
/// Opens a new async SevenZipWriter for the specified file path.
/// </summary>
public static IAsyncWriter OpenAsyncWriter(string filePath, SevenZipWriterOptions writerOptions)
{
return (IAsyncWriter)OpenWriter(filePath, writerOptions);
}
/// <summary>
/// Opens a new async SevenZipWriter for the specified stream.
/// </summary>
public static IAsyncWriter OpenAsyncWriter(Stream stream, SevenZipWriterOptions writerOptions)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
}
/// <summary>
/// Opens a new async SevenZipWriter for the specified file.
/// </summary>
public static IAsyncWriter OpenAsyncWriter(
FileInfo fileInfo,
SevenZipWriterOptions writerOptions
)
{
return (IAsyncWriter)OpenWriter(fileInfo, writerOptions);
}
}
#endif

View File

@@ -0,0 +1,339 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Common.SevenZip;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Crypto;
using SharpCompress.IO;
namespace SharpCompress.Writers.SevenZip;
/// <summary>
/// Writes 7z archives in non-solid mode (each file compressed independently).
/// Requires a seekable output stream for back-patching the signature header.
/// TODO: solid mode support in a future iteration.
/// TODO: IWritableArchive support in a future iteration.
/// </summary>
public partial class SevenZipWriter : AbstractWriter
{
private readonly SevenZipWriterOptions sevenZipOptions;
private readonly List<SevenZipWriteEntry> entries = [];
private readonly List<PackedStream> packedStreams = [];
private bool finalized;
/// <summary>
/// Creates a new SevenZipWriter writing to the specified stream.
/// </summary>
/// <param name="destination">Seekable output stream.</param>
/// <param name="options">Writer options.</param>
public SevenZipWriter(Stream destination, SevenZipWriterOptions options)
: base(ArchiveType.SevenZip, options)
{
if (!destination.CanSeek)
{
throw new ArchiveOperationException("7z writing requires a seekable stream for header back-patching.");
}
sevenZipOptions = options;
if (options.LeaveStreamOpen)
{
destination = SharpCompressStream.CreateNonDisposing(destination);
}
InitializeStream(destination);
// Write placeholder signature header (32 bytes) - will be back-patched on finalize
SevenZipSignatureHeaderWriter.WritePlaceholder(OutputStream.NotNull());
}
/// <summary>
/// Writes a file entry to the archive.
/// </summary>
public override void Write(string filename, Stream source, DateTime? modificationTime)
{
filename = NormalizeFilename(filename);
var progressStream = WrapWithProgress(source, filename);
var isEmpty = source.CanSeek && source.Length == 0;
if (isEmpty)
{
// Empty file - no compression, just record metadata
entries.Add(
new SevenZipWriteEntry
{
Name = filename,
ModificationTime = modificationTime,
IsDirectory = false,
IsEmpty = true,
}
);
return;
}
// Compress file data to output stream
// TODO: LZMA2 encoding is not yet implemented in SharpCompress's LzmaStream
if (sevenZipOptions.IsLzma2)
{
throw new ArchiveOperationException(
"LZMA2 encoding is not yet implemented. Use LZMA (IsLzma2 = false) instead."
);
}
var compressor = new SevenZipStreamsCompressor(OutputStream.NotNull());
var packed = compressor.Compress(
progressStream,
isLzma2: false,
sevenZipOptions.LzmaProperties
);
packedStreams.Add(packed);
entries.Add(
new SevenZipWriteEntry
{
Name = filename,
ModificationTime = modificationTime,
IsDirectory = false,
IsEmpty = false,
}
);
}
/// <summary>
/// Writes a directory entry to the archive.
/// </summary>
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
{
directoryName = NormalizeFilename(directoryName);
if (!directoryName.EndsWith('/'))
{
directoryName += '/';
}
entries.Add(
new SevenZipWriteEntry
{
Name = directoryName,
ModificationTime = modificationTime,
IsDirectory = true,
IsEmpty = true,
Attributes = 0x10, // FILE_ATTRIBUTE_DIRECTORY
}
);
}
/// <summary>
/// Finalizes the archive - writes metadata headers and back-patches the signature header.
/// </summary>
protected override void Dispose(bool isDisposing)
{
if (isDisposing && !finalized)
{
finalized = true;
FinalizeArchive();
}
base.Dispose(isDisposing);
}
private void FinalizeArchive()
{
var output = OutputStream.NotNull();
// Current position = end of packed data streams
var endOfPackedData = output.Position;
// Build the header structures
var mainStreamsInfo = BuildStreamsInfo();
var filesInfo = new SevenZipFilesInfoWriter { Entries = entries.ToArray() };
// Write header to a temporary stream first
using var headerStream = new MemoryStream();
ArchiveHeaderWriter.WriteRawHeader(headerStream, mainStreamsInfo, filesInfo);
// Optionally compress the header
if (sevenZipOptions.CompressHeader && headerStream.Length > 0)
{
WriteCompressedHeader(headerStream, endOfPackedData);
}
else
{
WriteRawHeaderToOutput(headerStream, endOfPackedData);
}
}
private void WriteCompressedHeader(MemoryStream rawHeaderStream, long endOfPackedData)
{
var output = OutputStream.NotNull();
// Compress header using LZMA (always LZMA, not LZMA2, matching 7-Zip standard behavior)
rawHeaderStream.Position = 0;
var headerCompressor = new SevenZipStreamsCompressor(output);
var headerPacked = headerCompressor.Compress(
rawHeaderStream,
isLzma2: false,
sevenZipOptions.LzmaProperties
);
// Build EncodedHeader StreamsInfo (describes how to decompress the header)
var headerPackPos = (ulong)(endOfPackedData - SevenZipSignatureHeaderWriter.HeaderSize);
var headerStreamsInfo = new SevenZipStreamsInfoWriter
{
PackInfo = new SevenZipPackInfoWriter
{
PackPos = headerPackPos,
Sizes = headerPacked.Sizes,
CRCs = headerPacked.CRCs,
},
UnPackInfo = new SevenZipUnPackInfoWriter { Folders = [headerPacked.Folder] },
};
// Write encoded header to a second temporary stream
using var encodedHeaderStream = new MemoryStream();
ArchiveHeaderWriter.WriteEncodedHeader(encodedHeaderStream, headerStreamsInfo);
// Write the encoded header to the output
var headerStartPos = output.Position;
encodedHeaderStream.Position = 0;
encodedHeaderStream.CopyTo(output);
// Compute CRC of the encoded header
var headerCrc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
encodedHeaderStream.GetBuffer().AsSpan(0, (int)encodedHeaderStream.Length)
);
// Back-patch signature header
var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize);
var nextHeaderSize = (ulong)encodedHeaderStream.Length;
SevenZipSignatureHeaderWriter.WriteFinal(
output,
nextHeaderOffset,
nextHeaderSize,
headerCrc
);
// Seek to end
output.Seek(0, SeekOrigin.End);
}
private void WriteRawHeaderToOutput(MemoryStream rawHeaderStream, long endOfPackedData)
{
var output = OutputStream.NotNull();
// Write raw header directly
var headerStartPos = output.Position;
rawHeaderStream.Position = 0;
rawHeaderStream.CopyTo(output);
// Compute CRC of the raw header
var headerCrc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
rawHeaderStream.GetBuffer().AsSpan(0, (int)rawHeaderStream.Length)
);
// Back-patch signature header
var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize);
var nextHeaderSize = (ulong)rawHeaderStream.Length;
SevenZipSignatureHeaderWriter.WriteFinal(
output,
nextHeaderOffset,
nextHeaderSize,
headerCrc
);
// Seek to end
output.Seek(0, SeekOrigin.End);
}
private SevenZipStreamsInfoWriter? BuildStreamsInfo()
{
if (packedStreams.Count == 0)
{
return null;
}
// Collect all packed sizes and CRCs across all folders
var totalPackStreams = 0;
for (var i = 0; i < packedStreams.Count; i++)
{
totalPackStreams += packedStreams[i].Sizes.Length;
}
var allSizes = new ulong[totalPackStreams];
var allCRCs = new uint?[totalPackStreams];
var folders = new CFolder[packedStreams.Count];
var sizeIndex = 0;
for (var i = 0; i < packedStreams.Count; i++)
{
var ps = packedStreams[i];
for (var j = 0; j < ps.Sizes.Length; j++)
{
allSizes[sizeIndex] = ps.Sizes[j];
allCRCs[sizeIndex] = ps.CRCs[j];
sizeIndex++;
}
folders[i] = ps.Folder;
}
// Build per-file unpack sizes and CRCs for SubStreamsInfo
// In non-solid mode, each folder has exactly 1 file
var numUnPackStreamsPerFolder = new ulong[packedStreams.Count];
var unpackSizes = new ulong[packedStreams.Count];
var fileCRCs = new uint?[packedStreams.Count];
for (var i = 0; i < packedStreams.Count; i++)
{
numUnPackStreamsPerFolder[i] = 1;
unpackSizes[i] = (ulong)packedStreams[i].Folder.GetUnpackSize();
fileCRCs[i] = packedStreams[i].Folder._unpackCrc;
// Clear folder-level CRC (it's moved to SubStreamsInfo)
packedStreams[i].Folder._unpackCrc = null;
}
return new SevenZipStreamsInfoWriter
{
PackInfo = new SevenZipPackInfoWriter
{
PackPos = 0,
Sizes = allSizes,
CRCs = allCRCs,
},
UnPackInfo = new SevenZipUnPackInfoWriter { Folders = folders },
SubStreamsInfo = new SevenZipSubStreamsInfoWriter
{
Folders = folders,
NumUnPackStreamsInFolders = numUnPackStreamsPerFolder,
UnPackSizes = unpackSizes,
CRCs = fileCRCs,
},
};
}
/// <summary>
/// Normalizes a filename for 7z archive storage.
/// Converts backslashes to forward slashes and removes leading slashes.
/// </summary>
private static string NormalizeFilename(string filename)
{
filename = filename.Replace('\\', '/');
// Remove drive letter prefix (e.g., "C:/")
if (filename.Length >= 3 && filename[1] == ':' && filename[2] == '/')
{
filename = filename.Substring(3);
}
// Remove leading slashes
filename = filename.TrimStart('/');
return filename;
}
}

View File

@@ -0,0 +1,123 @@
using System;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Providers;
namespace SharpCompress.Writers.SevenZip;
/// <summary>
/// Options for configuring 7z writer behavior.
/// </summary>
public sealed record SevenZipWriterOptions : IWriterOptions
{
private CompressionType _compressionType;
private int _compressionLevel;
/// <summary>
/// The compression type to use. Supported: LZMA (default), LZMA2 (via CompressionType.LZMA with IsLzma2=true).
/// </summary>
public CompressionType CompressionType
{
get => _compressionType;
init => _compressionType = value;
}
/// <summary>
/// Compression level (not used for LZMA in this implementation; reserved for future use).
/// </summary>
public int CompressionLevel
{
get => _compressionLevel;
init => _compressionLevel = value;
}
/// <summary>
/// SharpCompress will keep the supplied streams open. Default is true.
/// </summary>
public bool LeaveStreamOpen { get; init; } = true;
/// <summary>
/// Encoding to use for archive entry names.
/// </summary>
public IArchiveEncoding ArchiveEncoding { get; init; } = new ArchiveEncoding();
/// <summary>
/// An optional progress reporter for tracking compression operations.
/// </summary>
public IProgress<ProgressReport>? Progress { get; init; }
/// <summary>
/// Registry of compression providers.
/// Defaults to <see cref="CompressionProviderRegistry.Default" /> but can be replaced with custom implementations.
/// </summary>
public CompressionProviderRegistry Providers { get; init; } =
CompressionProviderRegistry.Default;
/// <summary>
/// Whether to use LZMA2 instead of LZMA. Default is false (LZMA).
/// </summary>
public bool IsLzma2 { get; init; }
/// <summary>
/// Whether to compress the archive header itself using LZMA.
/// Default is true, matching standard 7-Zip behavior.
/// </summary>
public bool CompressHeader { get; init; } = true;
/// <summary>
/// Custom LZMA encoder properties. Null uses defaults (1MB dictionary, 32 fast bytes).
/// </summary>
public LzmaEncoderProperties? LzmaProperties { get; init; }
/// <summary>
/// Creates a new SevenZipWriterOptions instance with LZMA compression.
/// </summary>
public SevenZipWriterOptions()
{
CompressionType = CompressionType.LZMA;
}
/// <summary>
/// Creates a new SevenZipWriterOptions instance with the specified compression type.
/// </summary>
/// <param name="compressionType">The compression type for the archive.</param>
public SevenZipWriterOptions(CompressionType compressionType)
{
CompressionType = compressionType;
}
/// <summary>
/// Creates a new SevenZipWriterOptions instance from an existing WriterOptions instance.
/// </summary>
/// <param name="options">The WriterOptions to copy values from.</param>
public SevenZipWriterOptions(WriterOptions options)
{
CompressionType = options.CompressionType;
CompressionLevel = options.CompressionLevel;
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
Providers = options.Providers;
}
/// <summary>
/// Creates a new SevenZipWriterOptions from an existing IWriterOptions instance.
/// </summary>
/// <param name="options">The IWriterOptions to copy values from.</param>
public SevenZipWriterOptions(IWriterOptions options)
{
CompressionType = options.CompressionType;
CompressionLevel = options.CompressionLevel;
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
Providers = options.Providers;
}
/// <summary>
/// Implicit conversion from CompressionType to SevenZipWriterOptions.
/// </summary>
public static implicit operator SevenZipWriterOptions(CompressionType compressionType) =>
new(compressionType);
}

View File

@@ -0,0 +1,296 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.SevenZip;
using Xunit;
namespace SharpCompress.Test.SevenZip;
public class SevenZipWriterTests : TestBase
{
[Fact]
public void SevenZipWriter_SingleFile_RoundTrip()
{
var content = "Hello, 7z world! This is a test of the SevenZipWriter."u8.ToArray();
using var archiveStream = new MemoryStream();
// Write archive
using (var writer = new SevenZipWriter(archiveStream, new SevenZipWriterOptions()))
{
using var source = new MemoryStream(content);
writer.Write("test.txt", source, DateTime.UtcNow);
}
// Read back and verify
archiveStream.Position = 0;
using (var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(archiveStream))
{
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
Assert.Single(entries);
Assert.Equal("test.txt", entries[0].Key);
Assert.Equal(content.Length, (int)entries[0].Size);
using var output = new MemoryStream();
using (var entryStream = entries[0].OpenEntryStream())
{
entryStream.CopyTo(output);
}
Assert.Equal(content, output.ToArray());
}
}
[Fact]
public void SevenZipWriter_MultipleFiles_RoundTrip()
{
var files = new[]
{
("file1.txt", "Content of file 1"),
("subdir/file2.txt", "Content of file 2 in subdirectory"),
("file3.bin", "Some binary-ish content with special bytes"),
};
using var archiveStream = new MemoryStream();
// Write archive
using (var writer = new SevenZipWriter(archiveStream, new SevenZipWriterOptions()))
{
foreach (var (name, text) in files)
{
using var source = new MemoryStream(Encoding.UTF8.GetBytes(text));
writer.Write(name, source, DateTime.UtcNow);
}
}
// Read back and verify
archiveStream.Position = 0;
using (var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(archiveStream))
{
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
Assert.Equal(files.Length, entries.Count);
for (var i = 0; i < files.Length; i++)
{
var entry = entries.First(e => e.Key == files[i].Item1);
using var output = new MemoryStream();
using (var entryStream = entry.OpenEntryStream())
{
entryStream.CopyTo(output);
}
var extractedText = Encoding.UTF8.GetString(output.ToArray());
Assert.Equal(files[i].Item2, extractedText);
}
}
}
[Fact]
public void SevenZipWriter_WithDirectory_RoundTrip()
{
using var archiveStream = new MemoryStream();
// Write archive with directory and file
using (var writer = new SevenZipWriter(archiveStream, new SevenZipWriterOptions()))
{
writer.WriteDirectory("mydir", DateTime.UtcNow);
using var source = new MemoryStream("file inside dir"u8.ToArray());
writer.Write("mydir/data.txt", source, DateTime.UtcNow);
}
// Read back and verify
archiveStream.Position = 0;
using (var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(archiveStream))
{
var allEntries = archive.Entries.ToList();
Assert.Equal(2, allEntries.Count);
var dirEntry = allEntries.FirstOrDefault(e => e.IsDirectory);
Assert.NotNull(dirEntry);
var fileEntry = allEntries.FirstOrDefault(e => !e.IsDirectory);
Assert.NotNull(fileEntry);
Assert.Equal("mydir/data.txt", fileEntry!.Key);
using var output = new MemoryStream();
using (var entryStream = fileEntry.OpenEntryStream())
{
entryStream.CopyTo(output);
}
Assert.Equal("file inside dir", Encoding.UTF8.GetString(output.ToArray()));
}
}
[Fact]
public void SevenZipWriter_EmptyFile_RoundTrip()
{
using var archiveStream = new MemoryStream();
// Write archive with an empty file
using (var writer = new SevenZipWriter(archiveStream, new SevenZipWriterOptions()))
{
using var source = new MemoryStream();
writer.Write("empty.txt", source, DateTime.UtcNow);
using var source2 = new MemoryStream("not empty"u8.ToArray());
writer.Write("notempty.txt", source2, DateTime.UtcNow);
}
// Read back and verify
archiveStream.Position = 0;
using (var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(archiveStream))
{
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
Assert.Equal(2, entries.Count);
var emptyEntry = entries.First(e => e.Key == "empty.txt");
Assert.Equal(0, (int)emptyEntry.Size);
var nonEmptyEntry = entries.First(e => e.Key == "notempty.txt");
using var output = new MemoryStream();
using (var entryStream = nonEmptyEntry.OpenEntryStream())
{
entryStream.CopyTo(output);
}
Assert.Equal("not empty", Encoding.UTF8.GetString(output.ToArray()));
}
}
[Fact]
public void SevenZipWriter_LZMA2_ThrowsNotSupported()
{
// LZMA2 encoding is not yet implemented in SharpCompress's LzmaStream
using var archiveStream = new MemoryStream();
using var writer = new SevenZipWriter(
archiveStream,
new SevenZipWriterOptions { IsLzma2 = true }
);
using var source = new MemoryStream("test"u8.ToArray());
Assert.Throws<ArchiveOperationException>(() => writer.Write("test.txt", source, DateTime.UtcNow));
}
[Fact]
public void SevenZipWriter_UncompressedHeader_RoundTrip()
{
var content = "Testing with uncompressed header"u8.ToArray();
using var archiveStream = new MemoryStream();
// Write archive with uncompressed header
using (var writer = new SevenZipWriter(
archiveStream,
new SevenZipWriterOptions { CompressHeader = false }
))
{
using var source = new MemoryStream(content);
writer.Write("rawheader.txt", source, DateTime.UtcNow);
}
// Read back and verify
archiveStream.Position = 0;
using (var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(archiveStream))
{
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
Assert.Single(entries);
using var output = new MemoryStream();
using (var entryStream = entries[0].OpenEntryStream())
{
entryStream.CopyTo(output);
}
Assert.Equal(content, output.ToArray());
}
}
[Fact]
public void SevenZipWriter_ViaWriterFactory()
{
var content = "Factory-created archive"u8.ToArray();
using var archiveStream = new MemoryStream();
// Write via WriterFactory
using (var writer = WriterFactory.OpenWriter(
archiveStream,
ArchiveType.SevenZip,
new SevenZipWriterOptions()
))
{
using var source = new MemoryStream(content);
writer.Write("factory.txt", source, DateTime.UtcNow);
}
// Read back and verify
archiveStream.Position = 0;
using (var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(archiveStream))
{
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
Assert.Single(entries);
using var output = new MemoryStream();
using (var entryStream = entries[0].OpenEntryStream())
{
entryStream.CopyTo(output);
}
Assert.Equal(content, output.ToArray());
}
}
[Fact]
public void SevenZipWriter_LargerFile_RoundTrip()
{
// Create 100KB of repeating pattern data (compresses well)
var content = new byte[100 * 1024];
var pattern = Encoding.UTF8.GetBytes("This is a repeating pattern for compression testing. ");
for (var i = 0; i < content.Length; i++)
{
content[i] = pattern[i % pattern.Length];
}
using var archiveStream = new MemoryStream();
// Write archive
using (var writer = new SevenZipWriter(archiveStream, new SevenZipWriterOptions()))
{
using var source = new MemoryStream(content);
writer.Write("large.bin", source, DateTime.UtcNow);
}
// Verify compressed size is smaller than original
Assert.True(archiveStream.Length < content.Length, "Archive should be smaller than uncompressed data");
// Read back and verify
archiveStream.Position = 0;
using (var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(archiveStream))
{
var entries = archive.Entries.Where(e => !e.IsDirectory).ToList();
Assert.Single(entries);
Assert.Equal(content.Length, (int)entries[0].Size);
using var output = new MemoryStream();
using (var entryStream = entries[0].OpenEntryStream())
{
entryStream.CopyTo(output);
}
Assert.Equal(content, output.ToArray());
}
}
[Fact]
public void SevenZipWriter_RequiresSeekableStream()
{
var nonSeekable = new NonSeekableStream();
Assert.Throws<ArchiveOperationException>(
() => new SevenZipWriter(nonSeekable, new SevenZipWriterOptions())
);
}
private class NonSeekableStream : MemoryStream
{
public override bool CanSeek => false;
}
}