diff --git a/src/SharpCompress/Common/SevenZip/ArchiveWriter.cs b/src/SharpCompress/Common/SevenZip/ArchiveWriter.cs
new file mode 100644
index 00000000..b3d93daf
--- /dev/null
+++ b/src/SharpCompress/Common/SevenZip/ArchiveWriter.cs
@@ -0,0 +1,49 @@
+using System.IO;
+using SharpCompress.Compressors.LZMA.Utilities;
+
+namespace SharpCompress.Common.SevenZip;
+
+///
+/// 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).
+///
+internal static class ArchiveHeaderWriter
+{
+ ///
+ /// Writes a raw (uncompressed) header containing MainStreamsInfo and FilesInfo.
+ ///
+ 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);
+ }
+
+ ///
+ /// Writes an encoded header - a StreamsInfo block that describes
+ /// how to decompress the actual header data.
+ ///
+ public static void WriteEncodedHeader(Stream stream, SevenZipStreamsInfoWriter headerStreamsInfo)
+ {
+ stream.WriteByte((byte)BlockType.EncodedHeader);
+ headerStreamsInfo.Write(stream);
+ }
+}
diff --git a/src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs b/src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs
new file mode 100644
index 00000000..c5310842
--- /dev/null
+++ b/src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs
@@ -0,0 +1,219 @@
+using System;
+using System.IO;
+using System.Text;
+using SharpCompress.Compressors.LZMA.Utilities;
+
+namespace SharpCompress.Common.SevenZip;
+
+///
+/// Entry metadata collected during writing, used to build FilesInfo header.
+///
+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; }
+}
+
+///
+/// Writes the FilesInfo section of a 7z header, including all file properties
+/// (names, timestamps, attributes, empty stream/file markers).
+///
+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);
+ }
+ }
+ }
+ );
+ }
+
+ ///
+ /// Writes a file property block: PropertyID + size + data.
+ /// Size is computed by writing to a temporary buffer first.
+ ///
+ private static void WriteFileProperty(Stream stream, BlockType propertyId, Action writeData)
+ {
+ using var dataStream = new MemoryStream();
+ writeData(dataStream);
+
+ stream.WriteByte((byte)propertyId);
+ stream.WriteEncodedUInt64((ulong)dataStream.Length);
+ dataStream.Position = 0;
+ dataStream.CopyTo(stream);
+ }
+}
diff --git a/src/SharpCompress/Common/SevenZip/SevenZipHeaderStructures.cs b/src/SharpCompress/Common/SevenZip/SevenZipHeaderStructures.cs
new file mode 100644
index 00000000..ed476d78
--- /dev/null
+++ b/src/SharpCompress/Common/SevenZip/SevenZipHeaderStructures.cs
@@ -0,0 +1,305 @@
+using System;
+using System.IO;
+using SharpCompress.Compressors.LZMA.Utilities;
+
+namespace SharpCompress.Common.SevenZip;
+
+///
+/// Writes Digests (CRC32 arrays with optional-defined-vector) for 7z headers.
+///
+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;
+ }
+}
+
+///
+/// Writes PackInfo section: packed stream positions, sizes, and CRCs.
+///
+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);
+ }
+}
+
+///
+/// Writes UnPackInfo section: folder definitions (coders, bind pairs, unpack sizes, CRCs).
+///
+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);
+ }
+ }
+}
+
+///
+/// Writes SubStreamsInfo section: per-file unpack sizes and CRCs within folders.
+///
+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);
+ }
+}
+
+///
+/// Writes the complete StreamsInfo section (PackInfo + UnPackInfo + SubStreamsInfo).
+///
+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);
+ }
+}
diff --git a/src/SharpCompress/Common/SevenZip/SevenZipSignatureHeader.cs b/src/SharpCompress/Common/SevenZip/SevenZipSignatureHeader.cs
new file mode 100644
index 00000000..0a9b98a0
--- /dev/null
+++ b/src/SharpCompress/Common/SevenZip/SevenZipSignatureHeader.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using SharpCompress.Crypto;
+
+namespace SharpCompress.Common.SevenZip;
+
+///
+/// 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]
+///
+internal static class SevenZipSignatureHeaderWriter
+{
+ ///
+ /// 7z file magic signature bytes.
+ ///
+ private static readonly byte[] Signature = [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C];
+
+ ///
+ /// Total size of the signature header in bytes (6+2+4+8+8+4 = 32).
+ ///
+ public const int HeaderSize = 32;
+
+ ///
+ /// Writes a placeholder signature header (all zeros for CRC/offset fields).
+ /// Call this at the start of archive creation to reserve space.
+ ///
+ 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);
+ }
+
+ ///
+ /// Writes the final signature header with correct offsets and CRCs.
+ /// The stream must be seekable; this method seeks to position 0.
+ ///
+ /// The archive output stream (seekable).
+ /// Offset from end of signature header to start of metadata header.
+ /// Size of the metadata header in bytes.
+ /// CRC32 of the metadata header bytes.
+ 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);
+ }
+}
diff --git a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs
new file mode 100644
index 00000000..44241c24
--- /dev/null
+++ b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs
@@ -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;
+
+///
+/// Result of compressing a stream - contains folder metadata, compressed sizes, and CRCs.
+///
+internal sealed class PackedStream
+{
+ public CFolder Folder { get; init; } = new();
+ public ulong[] Sizes { get; init; } = [];
+ public uint?[] CRCs { get; init; } = [];
+}
+
+///
+/// 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.
+///
+internal sealed class SevenZipStreamsCompressor(Stream outputStream)
+{
+ ///
+ /// Compresses the input stream to the output stream using the specified method.
+ /// Returns a PackedStream containing folder metadata, compressed size, and CRCs.
+ ///
+ /// Uncompressed data to compress.
+ /// True for LZMA2, false for LZMA.
+ /// LZMA encoder properties (null for defaults).
+ 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],
+ };
+ }
+
+ ///
+ /// Copies data from source to destination while computing CRC32 of the source data.
+ ///
+ 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;
+ }
+}
diff --git a/src/SharpCompress/Common/SevenZip/SevenZipWriteExtensions.cs b/src/SharpCompress/Common/SevenZip/SevenZipWriteExtensions.cs
new file mode 100644
index 00000000..26b8692c
--- /dev/null
+++ b/src/SharpCompress/Common/SevenZip/SevenZipWriteExtensions.cs
@@ -0,0 +1,97 @@
+using System;
+using System.IO;
+
+namespace SharpCompress.Common.SevenZip;
+
+///
+/// Stream extension methods for writing 7z binary format primitives.
+/// Mirrors the read-side encoding in DataReader.ReadNumber() and the reference
+/// StreamExtensions (ReadDecodedUInt64/WriteEncodedUInt64/WriteBoolVector).
+///
+internal static class SevenZipWriteExtensions
+{
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ ///
+ /// Writes a boolean vector as a packed bitmask.
+ /// Each bool becomes one bit, MSB first, padded to byte boundary.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+}
diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs
index 93826468..01a6eb15 100644
--- a/src/SharpCompress/Factories/SevenZipFactory.cs
+++ b/src/SharpCompress/Factories/SevenZipFactory.cs
@@ -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;
///
/// Represents the foundation factory of 7Zip archive.
///
-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
+
+ ///
+ 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);
+ }
+
+ ///
+ public IAsyncWriter OpenAsyncWriter(
+ Stream stream,
+ IWriterOptions writerOptions,
+ CancellationToken cancellationToken = default
+ ) => (IAsyncWriter)OpenWriter(stream, writerOptions);
+
+ #endregion
}
diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Async.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Async.cs
new file mode 100644
index 00000000..6fd59d87
--- /dev/null
+++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Async.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SharpCompress.Writers.SevenZip;
+
+public partial class SevenZipWriter
+{
+ ///
+ /// Asynchronously writes a file entry to the 7z archive.
+ /// Note: LZMA compression itself is synchronous; async is used for stream copying.
+ ///
+ public override ValueTask WriteAsync(
+ string filename,
+ Stream source,
+ DateTime? modificationTime,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ Write(filename, source, modificationTime);
+ return new ValueTask();
+ }
+
+ ///
+ /// Asynchronously writes a directory entry to the 7z archive.
+ ///
+ public override ValueTask WriteDirectoryAsync(
+ string directoryName,
+ DateTime? modificationTime,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ WriteDirectory(directoryName, modificationTime);
+ return new ValueTask();
+ }
+}
diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs
new file mode 100644
index 00000000..89e80414
--- /dev/null
+++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs
@@ -0,0 +1,62 @@
+#if NET8_0_OR_GREATER
+using System.IO;
+
+namespace SharpCompress.Writers.SevenZip;
+
+public partial class SevenZipWriter : IWriterOpenable
+{
+ ///
+ /// Opens a new SevenZipWriter for the specified file path.
+ ///
+ public static IWriter OpenWriter(string filePath, SevenZipWriterOptions writerOptions)
+ {
+ filePath.NotNullOrEmpty(nameof(filePath));
+ return OpenWriter(new FileInfo(filePath), writerOptions);
+ }
+
+ ///
+ /// Opens a new SevenZipWriter for the specified file.
+ ///
+ public static IWriter OpenWriter(FileInfo fileInfo, SevenZipWriterOptions writerOptions)
+ {
+ fileInfo.NotNull(nameof(fileInfo));
+ return new SevenZipWriter(fileInfo.OpenWrite(), writerOptions);
+ }
+
+ ///
+ /// Opens a new SevenZipWriter for the specified stream.
+ ///
+ public static IWriter OpenWriter(Stream stream, SevenZipWriterOptions writerOptions)
+ {
+ stream.NotNull(nameof(stream));
+ return new SevenZipWriter(stream, writerOptions);
+ }
+
+ ///
+ /// Opens a new async SevenZipWriter for the specified file path.
+ ///
+ public static IAsyncWriter OpenAsyncWriter(string filePath, SevenZipWriterOptions writerOptions)
+ {
+ return (IAsyncWriter)OpenWriter(filePath, writerOptions);
+ }
+
+ ///
+ /// Opens a new async SevenZipWriter for the specified stream.
+ ///
+ public static IAsyncWriter OpenAsyncWriter(Stream stream, SevenZipWriterOptions writerOptions)
+ {
+ return (IAsyncWriter)OpenWriter(stream, writerOptions);
+ }
+
+ ///
+ /// Opens a new async SevenZipWriter for the specified file.
+ ///
+ public static IAsyncWriter OpenAsyncWriter(
+ FileInfo fileInfo,
+ SevenZipWriterOptions writerOptions
+ )
+ {
+ return (IAsyncWriter)OpenWriter(fileInfo, writerOptions);
+ }
+}
+#endif
diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs
new file mode 100644
index 00000000..ff92d853
--- /dev/null
+++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs
@@ -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;
+
+///
+/// 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.
+///
+public partial class SevenZipWriter : AbstractWriter
+{
+ private readonly SevenZipWriterOptions sevenZipOptions;
+ private readonly List entries = [];
+ private readonly List packedStreams = [];
+ private bool finalized;
+
+ ///
+ /// Creates a new SevenZipWriter writing to the specified stream.
+ ///
+ /// Seekable output stream.
+ /// Writer options.
+ 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());
+ }
+
+ ///
+ /// Writes a file entry to the archive.
+ ///
+ 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,
+ }
+ );
+ }
+
+ ///
+ /// Writes a directory entry to the archive.
+ ///
+ 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
+ }
+ );
+ }
+
+ ///
+ /// Finalizes the archive - writes metadata headers and back-patches the signature header.
+ ///
+ 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,
+ },
+ };
+ }
+
+ ///
+ /// Normalizes a filename for 7z archive storage.
+ /// Converts backslashes to forward slashes and removes leading slashes.
+ ///
+ 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;
+ }
+}
diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs
new file mode 100644
index 00000000..65202d20
--- /dev/null
+++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs
@@ -0,0 +1,123 @@
+using System;
+using SharpCompress.Common;
+using SharpCompress.Common.Options;
+using SharpCompress.Compressors.LZMA;
+using SharpCompress.Providers;
+
+namespace SharpCompress.Writers.SevenZip;
+
+///
+/// Options for configuring 7z writer behavior.
+///
+public sealed record SevenZipWriterOptions : IWriterOptions
+{
+ private CompressionType _compressionType;
+ private int _compressionLevel;
+
+ ///
+ /// The compression type to use. Supported: LZMA (default), LZMA2 (via CompressionType.LZMA with IsLzma2=true).
+ ///
+ public CompressionType CompressionType
+ {
+ get => _compressionType;
+ init => _compressionType = value;
+ }
+
+ ///
+ /// Compression level (not used for LZMA in this implementation; reserved for future use).
+ ///
+ public int CompressionLevel
+ {
+ get => _compressionLevel;
+ init => _compressionLevel = value;
+ }
+
+ ///
+ /// SharpCompress will keep the supplied streams open. Default is true.
+ ///
+ public bool LeaveStreamOpen { get; init; } = true;
+
+ ///
+ /// Encoding to use for archive entry names.
+ ///
+ public IArchiveEncoding ArchiveEncoding { get; init; } = new ArchiveEncoding();
+
+ ///
+ /// An optional progress reporter for tracking compression operations.
+ ///
+ public IProgress? Progress { get; init; }
+
+ ///
+ /// Registry of compression providers.
+ /// Defaults to but can be replaced with custom implementations.
+ ///
+ public CompressionProviderRegistry Providers { get; init; } =
+ CompressionProviderRegistry.Default;
+
+ ///
+ /// Whether to use LZMA2 instead of LZMA. Default is false (LZMA).
+ ///
+ public bool IsLzma2 { get; init; }
+
+ ///
+ /// Whether to compress the archive header itself using LZMA.
+ /// Default is true, matching standard 7-Zip behavior.
+ ///
+ public bool CompressHeader { get; init; } = true;
+
+ ///
+ /// Custom LZMA encoder properties. Null uses defaults (1MB dictionary, 32 fast bytes).
+ ///
+ public LzmaEncoderProperties? LzmaProperties { get; init; }
+
+ ///
+ /// Creates a new SevenZipWriterOptions instance with LZMA compression.
+ ///
+ public SevenZipWriterOptions()
+ {
+ CompressionType = CompressionType.LZMA;
+ }
+
+ ///
+ /// Creates a new SevenZipWriterOptions instance with the specified compression type.
+ ///
+ /// The compression type for the archive.
+ public SevenZipWriterOptions(CompressionType compressionType)
+ {
+ CompressionType = compressionType;
+ }
+
+ ///
+ /// Creates a new SevenZipWriterOptions instance from an existing WriterOptions instance.
+ ///
+ /// The WriterOptions to copy values from.
+ public SevenZipWriterOptions(WriterOptions options)
+ {
+ CompressionType = options.CompressionType;
+ CompressionLevel = options.CompressionLevel;
+ LeaveStreamOpen = options.LeaveStreamOpen;
+ ArchiveEncoding = options.ArchiveEncoding;
+ Progress = options.Progress;
+ Providers = options.Providers;
+ }
+
+ ///
+ /// Creates a new SevenZipWriterOptions from an existing IWriterOptions instance.
+ ///
+ /// The IWriterOptions to copy values from.
+ public SevenZipWriterOptions(IWriterOptions options)
+ {
+ CompressionType = options.CompressionType;
+ CompressionLevel = options.CompressionLevel;
+ LeaveStreamOpen = options.LeaveStreamOpen;
+ ArchiveEncoding = options.ArchiveEncoding;
+ Progress = options.Progress;
+ Providers = options.Providers;
+ }
+
+ ///
+ /// Implicit conversion from CompressionType to SevenZipWriterOptions.
+ ///
+ public static implicit operator SevenZipWriterOptions(CompressionType compressionType) =>
+ new(compressionType);
+}
diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipWriterTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipWriterTests.cs
new file mode 100644
index 00000000..5e74bb61
--- /dev/null
+++ b/tests/SharpCompress.Test/SevenZip/SevenZipWriterTests.cs
@@ -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(() => 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(
+ () => new SevenZipWriter(nonSeekable, new SevenZipWriterOptions())
+ );
+ }
+
+ private class NonSeekableStream : MemoryStream
+ {
+ public override bool CanSeek => false;
+ }
+}