mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-22 15:04:43 +00:00
Add 7z writer with LZMA compression support
This commit is contained in:
49
src/SharpCompress/Common/SevenZip/ArchiveWriter.cs
Normal file
49
src/SharpCompress/Common/SevenZip/ArchiveWriter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
219
src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs
Normal file
219
src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
305
src/SharpCompress/Common/SevenZip/SevenZipHeaderStructures.cs
Normal file
305
src/SharpCompress/Common/SevenZip/SevenZipHeaderStructures.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
87
src/SharpCompress/Common/SevenZip/SevenZipSignatureHeader.cs
Normal file
87
src/SharpCompress/Common/SevenZip/SevenZipSignatureHeader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
131
src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs
Normal file
131
src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
97
src/SharpCompress/Common/SevenZip/SevenZipWriteExtensions.cs
Normal file
97
src/SharpCompress/Common/SevenZip/SevenZipWriteExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
39
src/SharpCompress/Writers/SevenZip/SevenZipWriter.Async.cs
Normal file
39
src/SharpCompress/Writers/SevenZip/SevenZipWriter.Async.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
62
src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs
Normal file
62
src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs
Normal 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
|
||||
339
src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs
Normal file
339
src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
123
src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs
Normal file
123
src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs
Normal 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);
|
||||
}
|
||||
296
tests/SharpCompress.Test/SevenZip/SevenZipWriterTests.cs
Normal file
296
tests/SharpCompress.Test/SevenZip/SevenZipWriterTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user