API Reference · version 1.0.0 · a fully managed .NET archive library — ZIP, 7z, tar and 60 more formats
← Bastion Archive Sdk product page
Bastion Archive Sdk is a fully managed .NET archive library for Windows. Every archive-level operation returns an OperationResult rather than throwing, so a caller checks ErrorCode instead of catching.
It reads 63 archive, compression and disk-image formats and creates ZIP, 7z, WIM, tar and its compressed forms, gzip, bzip2, XZ, LZMA, Zstandard and self-extracting archives — see Format support. Extraction is safe by default: an archive cannot write outside its target, exhaust the machine or hand back bytes that fail their checksum. See Security.
The exception to the no-throw rule is the stream-shaped codecs in Bastion.Archive.Codecs, which honour the Stream contract and throw ArchiveException.
| Namespace | Types | What is in it |
|---|---|---|
Bastion.Archive | 23 | The archive API: Archive to open and extract, ArchiveWriter to create, ArchiveUpdate to change an existing archive, and the options, results and enumerations they take. |
Bastion.Archive.Codecs | 35 | Compression codecs and branch filters as Stream classes, for use without an archive around them. |
Bastion.Archive.Diagnostics | 18 | OperationMonitor and the event arguments for progress, entries, prompts, logging and errors. |
Bastion.Archive.FileSystem | 14 | A file-system abstraction for copying between disk, memory and archives with one set of calls. |
Bastion.Archive.Formats | 6 | Format detection and the handler registry. |
Bastion.Archive.Formats.GZip | 2 | gzip member streams. |
Bastion.Archive.Formats.Lzma | 2 | Raw .lzma (LZMA-alone) streams. |
Bastion.Archive.Formats.Tar | 8 | tar reading and writing, including forward-only streams. |
Bastion.Archive.Formats.Xz | 3 | XZ container streams. |
Bastion.Archive.Formats.Zip | 2 | Forward-only ZIP reading from streams that cannot seek. |
Bastion.Archive.Hashing | 8 | Checksums and hashes used by the formats, available directly. |
Bastion.Archive.Properties | 2 | Named item properties exposed by format handlers. |
Bastion.Archive.Security | 4 | ExtractionPolicy and the settings that keep extraction inside its target. |
Bastion.Archive.Sfx | 2 | Self-extracting archive options and information. |
Every recipe in the Cookbook is shown in VB.NET and C#, from the sources the shipped samples are compiled from.
Every format below is detected from its own signature and can be listed, extracted and tested. The other columns show what else the library does with it; — means the format has no such capability or the library does not offer it.
| Format | Name | Read | Create | Update | Encryption | Volumes | Streaming |
|---|---|---|---|---|---|---|---|
| ZIP (PKWARE APPNOTE), including zipx method extensions and WinZip AES | Zip | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 7-Zip 7z container | SevenZip | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| POSIX tar (ustar, GNU, pax) | Tar | ✓ | ✓ | — | — | — | ✓ |
| gzip (RFC 1952) member stream | GZip | ✓ | ✓ | — | — | — | ✓ |
| bzip2 stream | BZip2 | ✓ | ✓ | — | — | — | ✓ |
| XZ container | Xz | ✓ | ✓ | — | — | — | ✓ |
| Raw .lzma (LZMA-alone) stream | Lzma | ✓ | ✓ | — | — | — | ✓ |
| Windows Imaging Format | Wim | ✓ | ✓ | — | — | ✓ | — |
| ISO 9660 / Joliet / Rock Ridge image | Iso | ✓ | — | — | — | — | — |
| cpio (odc, newc, crc, bin) | Cpio | ✓ | — | — | — | — | — |
| Unix ar archive, including Debian packages | Ar | ✓ | — | — | — | — | — |
| Apple xar archive | Xar | ✓ | — | — | — | — | — |
| Unix compress (.Z, LZW) | Z | ✓ | — | — | — | — | ✓ |
| Zstandard frame (.zst) | Zstd | ✓ | ✓ | — | — | — | ✓ |
| LZ4 frame | Lz4 | ✓ | — | — | — | — | ✓ |
| Brotli stream | Brotli | ✓ | — | — | — | — | ✓ |
| Virtual Hard Disk (VHD) | Vhd | ✓ | — | — | — | — | — |
| VMware Virtual Machine Disk (VMDK) | Vmdk | ✓ | — | — | — | ✓ | — |
| QEMU Copy-On-Write v2/v3 (QCOW2) | Qcow2 | ✓ | — | — | — | — | — |
| VirtualBox Disk Image (VDI) | Vdi | ✓ | — | — | — | — | — |
| Android sparse image (simg) | Sparse | ✓ | — | — | — | — | — |
| Microsoft SZDD compression (.??_) | MsLz | ✓ | — | — | — | — | ✓ |
| Apple Partition Map | Apm | ✓ | — | — | — | — | — |
| Base64 text (RFC 4648) wrapper | Base64 | ✓ | — | — | — | — | ✓ |
| Flash Video, demuxed per stream | Flv | ✓ | — | — | — | — | — |
| Mach-O universal (fat) binary | Mub | ✓ | — | — | — | — | — |
| COFF object file (.obj) | Coff | ✓ | — | — | — | — | — |
| Flash movie, plain (tags) and compressed (unwrapped) | Swf | ✓ | — | — | — | — | — |
| lzip container (.lz), LZMA with fixed properties; multi-member | Lzip | ✓ | — | — | — | — | ✓ |
| Snappy framing format (.sz), chunked with a CRC-32C per chunk | Snappy | ✓ | — | — | — | — | ✓ |
| lzop file (.lzo), blocked LZO1X; filters and lzo-rle refused by name | Lzo | ✓ | — | — | — | — | ✓ |
| MacBinary I, II and III (.bin, .macbin); forks and Finder codes | MacBinary | ✓ | — | — | — | — | — |
| BinHex 4.0 (.hqx), with its run-length form | BinHex | ✓ | — | — | — | — | — |
| AppleDouble header file (._name); resource fork and Finder metadata | AppleDouble | ✓ | — | — | — | — | — |
| U-Boot legacy uImage, single and multi-file; six compressions | UImage | ✓ | — | — | — | — | — |
| CUE sheet (.cue) resolving to the MODE1/2048 image beside it | Cue | ✓ | — | — | — | — | — |
| LUKS1 and LUKS2 volumes, aes-xts-plain64; Argon2id/Argon2i/PBKDF2 key slots | Luks | ✓ | — | — | ✓ | — | — |
| FAT12/16/32 file system image | Fat | ✓ | — | — | — | — | — |
| ext2/3/4 file system image | Ext | ✓ | — | — | — | — | — |
| HFS+ and HFSX file system image (HFS refused by name) | Hfs | ✓ | — | — | — | — | — |
| Apple File System container and volumes | Apfs | ✓ | — | — | — | — | — |
| GUID Partition Table container | Gpt | ✓ | — | — | — | — | — |
| Master Boot Record partition container | Mbr | ✓ | — | — | — | — | — |
| SquashFS image | SquashFs | ✓ | — | — | — | — | — |
| CramFS image | CramFs | ✓ | — | — | — | — | — |
| Intel HEX | Ihex | ✓ | — | — | — | — | — |
| Motorola S-record (.s19, .s28, .s37, .srec, .mot) | Srecord | ✓ | — | — | — | — | — |
| LHA/LZH archive | Lzh | ✓ | — | — | — | — | — |
| ARJ archive | Arj | ✓ | — | — | — | — | — |
| Microsoft Compiled HTML Help (ITSS) | Chm | ✓ | — | — | — | — | — |
| Nullsoft Scriptable Install System installer | Nsis | ✓ | — | — | — | — | — |
| Apple Disk Image (UDIF) | Dmg | ✓ | — | — | — | — | — |
| Compound File Binary (OLE2), including MSI | Compound | ✓ | — | — | — | — | — |
| Portable Executable sections and resources | Pe | partial | — | — | — | — | — |
| ELF sections | Elf | ✓ | — | — | — | — | — |
| Mach-O sections, and universal binary slices | MachO | ✓ | — | — | — | — | — |
| Numeric split set (.001, .002 …) reassembled as one stream | Split | ✓ | — | — | — | ✓ | — |
| RAR 5.0 (extract only; RAR 1.5-4.x refused by name) | Rar | ✓ | — | — | — | — | — |
| tar.gz composite (gzip over tar) | TarGz | ✓ | ✓ | — | — | — | ✓ |
| tar.bz2 composite | TarBz2 | ✓ | ✓ | — | — | — | ✓ |
| tar.xz composite | TarXz | ✓ | ✓ | — | — | — | ✓ |
| tar.zst composite | TarZst | ✓ | ✓ | — | — | — | ✓ |
| self-extracting archive | Sfx | ✓ | ✓ | — | ✓ | ✓ | — |
A format the library cannot read correctly is refused by name with an ErrorCode, never read approximately. See Security.
Bastion Archive Sdk reads files that somebody else made. This section says what the library defends against, what it does by default, what it leaves to the caller, and how to report a fault in it.
Report a security fault to support@bastionsoftwaresolutions.com. Say which format and which version, and attach the archive if you can: almost every fault of this kind needs the bytes to reproduce. Please do not open a public issue for a fault that lets an archive reach outside its target directory or run code.
The archive is hostile. Everything in it — every name, every length, every offset, every checksum — was chosen by whoever made it, and none of it may be believed without being checked. The caller is trusted: a program that asks this library to extract to C:\Windows\System32 gets what it asked for.
Four things an archive must never be able to do, whatever it contains:
.., not through an absolute path, not through a drive letter or a UNC path, not through a symbolic link that points out and a later entry that writes through it, and not through a name that Windows resolves differently from how it reads (a trailing dot or space, an alternate data stream, a reserved device name).OperationResult carrying an ErrorCode, never an unhandled exception, and never a corrupt state that a later call trips over.ExtractionPolicy holds the defaults. They are deliberately strict; loosening one is a decision the caller makes in code, which is a decision somebody can find in review.
| Setting | Default | What it stops |
|---|---|---|
AllowAbsolutePaths | False | C:\Windows\..., /etc/... and UNC paths in entry names |
AllowParentTraversal | False | .. climbing out of the target directory |
RejectReservedNames | True | CON, PRN, AUX, NUL, COM1-COM9, LPT1-LPT9 on Windows |
RejectAlternateStreams | True | file.txt:hidden writing an NTFS stream nobody asked for |
SymbolicLinks / HardLinks | skip, with a warning | a link written first and followed by a later entry |
Overwrite | fail if it exists | an archive replacing files that are already there |
VerifyChecksums | True | bytes that do not match what the archive declared |
RefuseOverlappingEntries | True | two entries claiming the same bytes, which is how a file can read as two things (ZIP) |
RefuseDuplicateEntries | True | the same name twice, where what lands depends on the order (ZIP) |
MaxTotalBytes | 1 TiB | an archive that unpacks to more than a disk holds |
MaxEntryCount | 10 000 000 | an archive whose listing alone is the attack |
MaxCompressionRatio | 100 000 | zip bombs; the Fifield class exceeds 28 000 000 |
RatioCheckThresholdBytes | 16 MiB | the ratio is judged only once enough has been produced to judge it |
MaxNestingDepth | 256 | an archive in an archive in an archive, without end |
MaxPathLength | the platform's | a name longer than the file system can hold |
PropagateMarkOfTheWeb | False | off by default; when on, what came from the internet stays marked |
The last two are marked ZIP because that is where they belong. Overlap is about local headers, which only ZIP has. Duplicate names are refused in ZIP, where two entries of the same name are how a bomb quotes itself, but not in tar, where appending a newer version of a file under the same name is what the format is for and refusing it would break honest archives. In every other format the protection is Overwrite: a second entry writing over the first fails unless the caller asked for overwriting. Two names differing only in case, or only in Unicode normalisation, count as the same name, because Windows and macOS think so even where Linux does not.
Every limit takes zero to mean "no limit", and every one of them is enforced while the operation runs rather than checked once at the start.
LinkPolicy.CreateValidated), or for the first one to fail the operation outright (LinkPolicy.Reject). Creating them is right for a backup tool restoring a tree it made and wrong for a download folder.Three layers, each run on every build:
A format this library cannot read correctly is refused by name, with an ErrorCode saying so. It is never read approximately and never read part-way: NTFS compression, NSIS's own variant of bzip2, ARJ's method 4, the EFI standard compression of a UEFI volume, and an encrypted QCOW2 or VDI parent are all refused rather than guessed. Format support lists what is read.
support@bastionsoftwaresolutions.com, from Bastion Software Solutions Ltd (company no. 17239067). A fault that lets an archive escape its directory, run code, or exhaust a machine is treated as urgent; anything that makes a malformed archive crash rather than fail is treated as a defect in the same class.
An API mapping, nothing more. These are other people's libraries; what is described here is how to say in Bastion Archive Sdk what you already say somewhere else, and no claim is made about how they work inside.
Nothing at the archive level throws. Every operation returns an OperationResult carrying an ErrorCode, an ErrorDescription, a Detail and any Warnings. Code written against a library that throws will compile after a mechanical translation and then ignore every failure, which is worse than not migrating at all, so the first thing to do in any port is to decide where the check goes.
' What a throwing library's code becomes
Dim opened = Archive.Open(path)
If Not opened.Succeeded Then Return opened ' or throw, or log, as the caller wants
Using archive = opened.Archive
Dim result = archive.ExtractAll(target)
If Not result.Succeeded Then Return result
End Using
The exception to the rule is the stream-shaped codecs — Bastion.Archive.Codecs.*Stream — which honour the BCL Stream contract and throw ArchiveException, because a Stream that returned a result object would not be a Stream.
| You had | You want |
|---|---|
ZipFile.CreateFromDirectory(dir, path) | ArchiveWriter.Create(path), then AddDirectory(dir, Nothing, Nothing), then Complete() |
ZipFile.ExtractToDirectory(path, dir) | Archive.Open(path), then ExtractAll(dir) |
ZipFile.Open(path, ZipArchiveMode.Read) | Archive.Open(path) |
ZipArchive.Entries | Archive.Entries |
ZipArchiveEntry.Open() | Archive.ExtractToStream(entry, target, options) |
ZipArchiveEntry.FullName | ArchiveEntry.Name |
ZipArchiveEntry.Length / CompressedLength | ArchiveEntry.Size / CompressedSize |
CompressionLevel.Optimal | CompressionSettings.Level = 9 |
DeflateStream, GZipStream, BrotliStream, ZLibStream | DeflateDecoderStream, GZipDecoderStream, BrotliDecoderStream, ZlibDecoderStream and the matching encoders |
| Nothing — it has no such thing | Passwords, 7z, tar, volumes, self-extracting archives, and every format in Format support |
The framework's ZipArchiveEntry.Open() hands back a stream you read; this library hands you a method you give a stream to. That is deliberate: it keeps the copy inside the library, where the limits, the checksum check and the progress reporting are.
| You had | You want |
|---|---|
FastZip.CreateZip(path, dir, recurse, filter) | ArchiveWriter.Create(path) + AddDirectory(dir, Nothing, New AdditionOptions() With {.Recursive = recurse, .Filter = filter}) |
FastZip.ExtractZip(path, dir, filter) | Archive.Open(path) + ExtractAll(dir, options) |
ZipFile, ZipEntry | Archive, ArchiveEntry |
ZipInputStream / ZipOutputStream | Archive.ExtractToStream / ArchiveWriter.AddStream |
ZipFile.Password | ArchiveOpenOptions.Password, ExtractionOptions.Password, CompressionSettings.Password |
TarArchive.CreateOutputTarArchive | TarArchive.CreateFromDirectory |
GZipInputStream, BZip2InputStream | GZipDecoderStream, BZip2DecoderStream |
ZipEntry.IsCrypted | ArchiveEntry.IsEncrypted |
| You had | You want |
|---|---|
Using zip As New ZipFile() … zip.Save(path) | ArchiveWriter.Create(path) … Complete() |
zip.AddFile(file, directoryInArchive) | writer.AddFile(file, entryName, options) |
zip.AddDirectory(dir) | writer.AddDirectory(dir, prefix, options) |
zip.Password / zip.Encryption | CompressionSettings.Password / .Encryption |
zip.UseZip64WhenSaving | Nothing: Zip64 appears when the size needs it |
zip.MaxOutputSegmentSize | ArchiveWriter.CreateSplit(path, volumeSize, settings, monitor) |
zip.SaveProgress | OperationMonitor.Progress |
zip.ExtractProgress | OperationMonitor.Progress on the extraction |
zip.ExtractExistingFile | ExtractionPolicy.Overwrite |
| You had | You want |
|---|---|
ArchiveFactory.Open(path) | Archive.Open(path) — the format is detected the same way, from the bytes |
IArchive.Entries | Archive.Entries |
IArchiveEntry.WriteToDirectory(dir, options) | Archive.Extract(entries, dir, options) |
ReaderFactory.Open(stream) | Archive.Open(stream, options) |
IReader.MoveToNextEntry() | Archive.Entries is a list; forward-only reading is the tar and single-stream path |
WriterFactory.Open(stream, type, options) | ArchiveWriter.Create(stream, settings, monitor) |
The FileSystem abstraction is the reason this mapping is short: Bastion.Archive.FileSystem exists because that model is a good one for copying between an archive and a disk, and the shapes are deliberately familiar.
| You had | You want |
|---|---|
ZipArchive over an AbstractFile | ArchiveFolder over a DiskFile |
AbstractFolder.CopyFilesTo | AbstractFolder.CopyFilesTo(destination, options) — the destination being an ArchiveFolder is what makes it an archive copy |
ZippedFile / ZippedFolder | ArchivedFile / ArchivedFolder |
DiskFile / DiskFolder | DiskFile / DiskFolder |
MemoryFile / MemoryFolder | MemoryFile / MemoryFolder |
ByteProgression events | OperationMonitor.Progress |
ItemException handling | OperationResult.Warnings, one per item that could not be done |
SelfExtractorSettings | SelfExtractorOptions |
.., links skipped, checksums verified. Code that relied on a looser library extracting a hostile name will now get a refusal, which is the point. Security lists every default and what it stops.One section per recipe, in Visual Basic and C#, from the sources the samples are compiled from. Every one of them runs on every build of the cookbook, so what is shown here is code that was executed rather than code that was typed.
| Recipe | What it shows |
|---|---|
| Async await | Await the library: write an archive to a stream that refuses synchronous writes, open one from a stream that cannot seek, and cancel an extraction — which gives a result, not an exception. |
| Bzip2 stream usage | Compress and decompress with bzip2, and see what the level actually buys. |
| Copy archive to archive | Convert a ZIP to a 7z by copying one archive folder into another, with nothing written to disk in between. |
| Copy disk folder to archive | Zip and unzip by copying folders: a disk folder into an archive, and the archive back out. |
| Create sfx custom dialog | Dress a self-extractor: its own title, message, licence, suggested folder, icon, and a file to open afterwards. |
| Create sfx | Make a self-extracting archive: one program that unpacks itself, which every ZIP tool also opens as a ZIP. |
| Create sfx silent | A self-extractor that never shows a window: it unpacks to the folder it was given and reports by exit code. |
| Create zip | Create an archive from a handful of files. |
| Events and prompts | Answer the library's questions as they come up: which items to take, what to do about a file that is already there, and which password to try. |
| Extract selective | Extract only the entries you choose. |
| Extract zip | Extract every entry and prove the bytes survived. |
| Forward only tar | Write and read a tar through a stream that refuses to seek — a pipe, a socket, a network response. |
| Forward only zip | Read a ZIP off a stream that cannot seek — a socket, a pipe, or a download in progress. |
| Gzip stream usage | Compress and decompress one stream, with the header fields gzip actually carries. |
| List entries | List an archive the way <c>7z l -slt</c> does. |
| Many entries | Pass 65 535 entries, which needs the Zip64 end record. |
| Mark of the web | Keep a download's Mark of the Web on what comes out of it, so Windows still treats those files as from the internet. |
| Memory folder round trip | Build content in memory, zip it, and unzip it into memory again, touching disk only for the archive. |
| Password aes256 | Write and read WinZip AES-256, and be refused without the password. |
| Password zip crypto read | Read a traditionally encrypted archive — the old ZIP encryption, which you will meet and should not write. |
| Progress and cancel | Watch an operation's progress, and stop it part way through without leaving anything broken behind. |
| Seven zip create | Write a <c>.7z</c> and read it back, which is the same two calls as a ZIP. |
| Seven zip encryption | Encrypt a 7z with AES-256, and then encrypt its header so the names go too. |
| Seven zip methods | Write the same files with each of 7z's six coders, and see what each one costs. |
| Seven zip solid | Solid or not: the trade 7z makes that ZIP cannot, and what it costs either way. |
| Seven zip update | Remove, rename and add inside a <c>.7z</c> — the same <see cref="ArchiveUpdate"/> calls as for a ZIP. |
| Spanned disks | Span an archive across removable disks, asking for each one as it is needed, and read it back the same way. |
| Split volumes | Write a split archive and read it back from its first volume. |
| Streaming writer | Write and read an archive that never touches the disk. |
| Tar gz | Make and read a <c>.tar.gz</c> in one call each. |
| Tar xz | Make a <c>.tar.xz</c>, choose the tar dialect, and make the same bytes twice. |
| Tar zst | Make and read a <c>.tar.zst</c>, the newest of the compressed tarballs. |
| Test archive | Verify every checksum, and see a damaged one fail. |
| Timestamps and attributes | Carry timestamps and attributes across, and write the same bytes twice. |
| Unicode names | Keep non-ASCII entry names exactly as they were given. |
| Update archive | Add, rename and remove by rebuilding into a new file. |
| Wim create | Write a Windows image (<c>.wim</c>) and read it back, then read one Microsoft made. |
| Wim security | Read the two things a WIM records that no other format here does: each file's Windows security descriptor, and its NTFS alternate data streams. |
| Xz stream usage | Write an <c>.xz</c> file in blocks, then jump straight to the middle of it. |
| Zip64 large file | Cross the four-gibibyte line that needs Zip64. |
| Zip bomb guard | Stop a decompression bomb with a total-byte budget. |
| Zip folder recursive | Add a whole directory tree under a prefix. |
| Zipx methods | Write the same files with bzip2, LZMA and XZ, and see what each one costs. |
| Zstd decode | Compress and decompress Zstandard, including the frames a reader is expected to walk past. |
Await the library: write an archive to a stream that refuses synchronous writes, open one from a stream that cannot seek, and cancel an extraction — which gives a result, not an exception.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Threading
Imports System.Threading.Tasks
Imports Bastion.Archive
Imports Bastion.Archive.Diagnostics
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Await the library: write an archive to a stream that refuses synchronous writes, open one from a stream that
''' cannot seek, and cancel an extraction — which gives a result, not an exception.
''' </summary>
''' <remarks>
''' ASP.NET Core's request and response bodies refuse synchronous I/O by default. <c>CreateAsync</c> writes to such
''' a stream through <c>WriteAsync</c> alone, and <c>OpenAsync</c> copies a stream that cannot seek to a temporary
''' file first, since ZIP keeps its directory at the end. Every <c>Async</c> method completes with the same result
''' its synchronous twin returns; a cancelled token ends it with <see cref="ErrorCode.Cancelled"/> rather than
''' throwing. The recipe runner is synchronous, so it waits on the task at the top; an application awaits.
''' </remarks>
Friend Module AsyncAwaitRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
' A console program has no synchronisation context, so waiting here cannot deadlock.
Return RunAsync(context).GetAwaiter().GetResult()
End Function
Private Async Function RunAsync(context As RecipeContext) As Task(Of RecipeOutcome)
Dim source = context.CreateSampleTree()
' 1. Write to a "response body" that throws on any synchronous write.
Dim body As New MemoryStream()
Dim started = Await ArchiveWriter.CreateAsync(New AsyncOnlyStream(body), Nothing, Nothing)
If Not started.Succeeded Then Return RecipeOutcome.Failed
Using writer = started.Writer
If Not (Await writer.AddDirectoryAsync(source, Nothing)).Succeeded Then Return RecipeOutcome.Failed
Dim completed = Await writer.CompleteAsync()
If Not completed.Succeeded Then
context.Say(completed.ToString())
Return RecipeOutcome.Failed
End If
End Using
context.Say("wrote {0} bytes through WriteAsync alone", body.Length)
' 2. Open it again from an "upload" that cannot seek and refuses synchronous reads.
body.Position = 0
Dim opened = Await Archive.OpenAsync(New AsyncOnlyStream(body))
If Not opened.Succeeded Then Return RecipeOutcome.Failed
Using archive = opened.Archive
context.Say("opened from a stream that cannot seek: {0} entries", archive.Entries.Count)
' 3. Cancel before extracting: the task completes, and the result says so.
Using cancellation As New CancellationTokenSource()
cancellation.Cancel()
Dim cancelled = Await archive.ExtractAllAsync(context.PathTo("cancelled"), Nothing, cancellation.Token)
context.Say("a cancelled extraction returned {0}", cancelled.ErrorCode)
If cancelled.ErrorCode <> ErrorCode.Cancelled Then Return RecipeOutcome.Failed
End Using
Dim extracted = Await archive.ExtractAllAsync(context.PathTo("unpacked"))
context.Say("then extracted: {0}", If(extracted.Succeeded, "succeeded", extracted.ToString()))
If Not extracted.Succeeded Then Return RecipeOutcome.Failed
End Using
Return If(File.Exists(Path.Combine(context.PathTo("unpacked"), "readme.txt")), RecipeOutcome.Passed, RecipeOutcome.Failed)
End Function
''' <summary>A stream as ASP.NET Core's bodies are by default: no seeking, and no synchronous reads or writes.</summary>
Private NotInheritable Class AsyncOnlyStream
Inherits Stream
Private ReadOnly _inner As Stream
Public Sub New(inner As Stream)
_inner = inner
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return True
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return True
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Throw New NotSupportedException()
End Get
End Property
Public Overrides Property Position As Long
Get
Throw New NotSupportedException()
End Get
Set(value As Long)
Throw New NotSupportedException()
End Set
End Property
Public Overrides Sub Flush()
Throw New InvalidOperationException("Synchronous operations are disallowed.")
End Sub
Public Overrides Function FlushAsync(cancellationToken As CancellationToken) As Task
Return Task.CompletedTask
End Function
Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
Throw New InvalidOperationException("Synchronous operations are disallowed.")
End Function
Public Overrides Function ReadAsync(buffer() As Byte, offset As Integer, count As Integer, cancellationToken As CancellationToken) As Task(Of Integer)
Return _inner.ReadAsync(buffer, offset, count, cancellationToken)
End Function
Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
Throw New InvalidOperationException("Synchronous operations are disallowed.")
End Sub
Public Overrides Function WriteAsync(buffer() As Byte, offset As Integer, count As Integer, cancellationToken As CancellationToken) As Task
Return _inner.WriteAsync(buffer, offset, count, cancellationToken)
End Function
#If NETCOREAPP2_1_OR_GREATER Then
Public Overloads Overrides Function ReadAsync(buffer As Memory(Of Byte), Optional cancellationToken As CancellationToken = Nothing) As ValueTask(Of Integer)
Return _inner.ReadAsync(buffer, cancellationToken)
End Function
Public Overloads Overrides Function WriteAsync(buffer As ReadOnlyMemory(Of Byte), Optional cancellationToken As CancellationToken = Nothing) As ValueTask
Return _inner.WriteAsync(buffer, cancellationToken)
End Function
#End If
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Throw New NotSupportedException()
End Function
Public Overrides Sub SetLength(value As Long)
Throw New NotSupportedException()
End Sub
End Class
End Module
End NamespaceC#
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Await the library: write an archive to a stream that refuses synchronous writes, open one from a stream that
/// cannot seek, and cancel an extraction — which gives a result, not an exception.
/// </summary>
/// <remarks>
/// ASP.NET Core's request and response bodies refuse synchronous I/O by default. <c>CreateAsync</c> writes to such
/// a stream through <c>WriteAsync</c> alone, and <c>OpenAsync</c> copies a stream that cannot seek to a temporary
/// file first, since ZIP keeps its directory at the end. Every <c>Async</c> method completes with the same result
/// its synchronous twin returns; a cancelled token ends it with <see cref="ErrorCode.Cancelled"/> rather than
/// throwing. The recipe runner is synchronous, so it waits on the task at the top; an application awaits.
/// </remarks>
internal static class AsyncAwaitRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
// A console program has no synchronisation context, so waiting here cannot deadlock.
return RunAsync(context).GetAwaiter().GetResult();
}
private static async Task<RecipeOutcome> RunAsync(RecipeContext context)
{
var source = context.CreateSampleTree();
// 1. Write to a "response body" that throws on any synchronous write.
var body = new MemoryStream();
var started = await ArchiveWriter.CreateAsync(new AsyncOnlyStream(body), null, null);
if (!started.Succeeded) return RecipeOutcome.Failed;
using (var writer = started.Writer)
{
if (!(await writer.AddDirectoryAsync(source, null)).Succeeded) return RecipeOutcome.Failed;
var completed = await writer.CompleteAsync();
if (!completed.Succeeded)
{
context.Say(completed.ToString());
return RecipeOutcome.Failed;
}
}
context.Say("wrote {0} bytes through WriteAsync alone", body.Length);
// 2. Open it again from an "upload" that cannot seek and refuses synchronous reads.
body.Position = 0;
var opened = await Archive.OpenAsync(new AsyncOnlyStream(body));
if (!opened.Succeeded) return RecipeOutcome.Failed;
using (var archive = opened.Archive)
{
context.Say("opened from a stream that cannot seek: {0} entries", archive.Entries.Count);
// 3. Cancel before extracting: the task completes, and the result says so.
using (var cancellation = new CancellationTokenSource())
{
cancellation.Cancel();
var cancelled = await archive.ExtractAllAsync(context.PathTo("cancelled"), null, cancellation.Token);
context.Say("a cancelled extraction returned {0}", cancelled.ErrorCode);
if (cancelled.ErrorCode != ErrorCode.Cancelled) return RecipeOutcome.Failed;
}
var extracted = await archive.ExtractAllAsync(context.PathTo("unpacked"));
context.Say("then extracted: {0}", extracted.Succeeded ? "succeeded" : extracted.ToString());
if (!extracted.Succeeded) return RecipeOutcome.Failed;
}
return File.Exists(Path.Combine(context.PathTo("unpacked"), "readme.txt")) ? RecipeOutcome.Passed : RecipeOutcome.Failed;
}
/// <summary>A stream as ASP.NET Core's bodies are by default: no seeking, and no synchronous reads or writes.</summary>
private sealed class AsyncOnlyStream : Stream
{
private readonly Stream _inner;
public AsyncOnlyStream(Stream inner) => _inner = inner;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush() => throw new InvalidOperationException("Synchronous operations are disallowed.");
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public override int Read(byte[] buffer, int offset, int count) => throw new InvalidOperationException("Synchronous operations are disallowed.");
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.ReadAsync(buffer, offset, count, cancellationToken);
public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException("Synchronous operations are disallowed.");
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.WriteAsync(buffer, offset, count, cancellationToken);
#if NETCOREAPP2_1_OR_GREATER
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => _inner.ReadAsync(buffer, cancellationToken);
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => _inner.WriteAsync(buffer, cancellationToken);
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
}
}
}Compress and decompress with bzip2, and see what the level actually buys.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.Codecs
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Compress and decompress with bzip2, and see what the level actually buys.
''' </summary>
''' <remarks>
''' bzip2's level is not an effort dial in the way gzip's is: it is the **block size**, a hundred kilobytes
''' per step, and it is the size of the window the Burrows–Wheeler transform sorts. That is why the level
''' decides how much memory the codec holds as well as how well it compresses, and why raising it does
''' nothing at all for a file smaller than one block — there is no second block for the extra room to hold.
''' <para>
''' Like gzip, a <c>.bz2</c> file may be several streams concatenated, and this reads them as one.
''' </para>
''' </remarks>
Friend Module BZip2StreamUsageRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim content = Encoding.UTF8.GetBytes(
GZipStreamUsageRecipe.Repeat("the quick brown fox jumps over the lazy dog. ", 4000))
For Each level In New Integer() {1, 5, 9}
Dim compressedPath = context.PathTo("notes-" & level.ToString(Globalization.CultureInfo.InvariantCulture) & ".bz2")
Dim held As Long
Using file As New FileStream(compressedPath, FileMode.Create, FileAccess.Write)
Using writer As New BZip2EncoderStream(file, level, leaveOpen:=True)
held = writer.WorkingMemoryBytes
writer.Write(content, 0, content.Length)
End Using
End Using
context.Say("level {0}: {1} -> {2}, holding {3} while it works",
level, RecipeContext.Readable(content.Length),
RecipeContext.Readable(New FileInfo(compressedPath).Length),
RecipeContext.Readable(held))
Using file As New FileStream(compressedPath, FileMode.Open, FileAccess.Read)
Using reader As New BZip2DecoderStream(file, leaveOpen:=True)
Using produced As New MemoryStream()
GZipStreamUsageRecipe.CopyAll(reader, produced)
If Not RecipeContext.SameBytes(content, produced.ToArray()) Then
context.Say("level {0} did not come back the same", level)
Return RecipeOutcome.Failed
End If
End Using
End Using
End Using
Next
' Three streams in one file, which is what happens when .bz2 files are concatenated.
Dim joinedPath = context.PathTo("joined.bz2")
Dim parts = New String() {"first", "second", "third"}
Using file As New FileStream(joinedPath, FileMode.Create, FileAccess.Write)
For Each part In parts
Dim bytes = Encoding.UTF8.GetBytes(part & Environment.NewLine)
Using writer As New BZip2EncoderStream(file, 1, leaveOpen:=True)
writer.Write(bytes, 0, bytes.Length)
End Using
Next
End Using
Using file As New FileStream(joinedPath, FileMode.Open, FileAccess.Read)
Using reader As New BZip2DecoderStream(file, leaveOpen:=True)
Using produced As New MemoryStream()
GZipStreamUsageRecipe.CopyAll(reader, produced)
Dim text = Encoding.UTF8.GetString(produced.ToArray())
Dim expected = String.Join(Environment.NewLine, parts) & Environment.NewLine
If Not String.Equals(text, expected, StringComparison.Ordinal) Then
context.Say("three joined streams read back as: " & text.Replace(Environment.NewLine, "|"))
Return RecipeOutcome.Failed
End If
context.Say("three joined streams read as one")
End Using
End Using
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.Globalization;
using System.IO;
using System.Text;
using Bastion.Archive.Codecs;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Compress and decompress with bzip2, and see what the level actually buys.
/// </summary>
/// <remarks>
/// bzip2's level is not an effort dial in the way gzip's is: it is the <b>block size</b>, a hundred
/// kilobytes per step, and it is the size of the window the Burrows–Wheeler transform sorts. That is why
/// the level decides how much memory the codec holds as well as how well it compresses, and why raising it
/// does nothing at all for a file smaller than one block — there is no second block for the extra room to
/// hold.
/// <para>
/// Like gzip, a <c>.bz2</c> file may be several streams concatenated, and this reads them as one.
/// </para>
/// </remarks>
internal static class BZip2StreamUsageRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var content = Encoding.UTF8.GetBytes(
GZipStreamUsageRecipe.Repeat("the quick brown fox jumps over the lazy dog. ", 4000));
foreach (var level in new[] { 1, 5, 9 })
{
var compressedPath = context.PathTo("notes-" + level.ToString(CultureInfo.InvariantCulture) + ".bz2");
long held;
using (var file = new FileStream(compressedPath, FileMode.Create, FileAccess.Write))
using (var writer = new BZip2EncoderStream(file, level, leaveOpen: true))
{
held = writer.WorkingMemoryBytes;
writer.Write(content, 0, content.Length);
}
context.Say("level {0}: {1} -> {2}, holding {3} while it works",
level, RecipeContext.Readable(content.Length),
RecipeContext.Readable(new FileInfo(compressedPath).Length),
RecipeContext.Readable(held));
using (var file = new FileStream(compressedPath, FileMode.Open, FileAccess.Read))
using (var reader = new BZip2DecoderStream(file, leaveOpen: true))
using (var produced = new MemoryStream())
{
GZipStreamUsageRecipe.CopyAll(reader, produced);
if (!RecipeContext.SameBytes(content, produced.ToArray()))
{
context.Say("level {0} did not come back the same", level);
return RecipeOutcome.Failed;
}
}
}
// Three streams in one file, which is what happens when .bz2 files are concatenated.
var joinedPath = context.PathTo("joined.bz2");
var parts = new[] { "first", "second", "third" };
using (var file = new FileStream(joinedPath, FileMode.Create, FileAccess.Write))
{
foreach (var part in parts)
{
var bytes = Encoding.UTF8.GetBytes(part + Environment.NewLine);
using (var writer = new BZip2EncoderStream(file, 1, leaveOpen: true))
{
writer.Write(bytes, 0, bytes.Length);
}
}
}
using (var file = new FileStream(joinedPath, FileMode.Open, FileAccess.Read))
using (var reader = new BZip2DecoderStream(file, leaveOpen: true))
using (var produced = new MemoryStream())
{
GZipStreamUsageRecipe.CopyAll(reader, produced);
var text = Encoding.UTF8.GetString(produced.ToArray());
var expected = string.Join(Environment.NewLine, parts) + Environment.NewLine;
if (!string.Equals(text, expected, StringComparison.Ordinal))
{
context.Say("three joined streams read back as: " + text.Replace(Environment.NewLine, "|"));
return RecipeOutcome.Failed;
}
context.Say("three joined streams read as one");
}
return RecipeOutcome.Passed;
}
}
}Convert a ZIP to a 7z by copying one archive folder into another, with nothing written to disk in between.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.FileSystem
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Convert a ZIP to a 7z by copying one archive folder into another, with nothing written to disk in between.
''' </summary>
''' <remarks>
''' Each entry is streamed out of the ZIP, its checksum verified, and into the 7z, which is written once. The
''' same call copies between any two archives, and a nested archive — a file inside another archive — can be
''' opened as a folder too, up to <c>ExtractionPolicy.MaxNestingDepth</c> levels deep.
''' </remarks>
Friend Module CopyArchiveToArchiveRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source As New DiskFolder(context.CreateSampleTree())
Dim zip As New ArchiveFolder(New DiskFile(context.PathTo("books.zip")))
If Not source.CopyFilesTo(zip, Nothing).Succeeded Then Return RecipeOutcome.Failed
Dim sevenZip As New ArchiveFolder(New DiskFile(context.PathTo("books.7z")))
Dim converted = zip.CopyFilesTo(sevenZip, Nothing)
If Not converted.Succeeded Then
context.Say(converted.ToString())
Return RecipeOutcome.Failed
End If
context.Say("books.zip {0} became books.7z {1}",
RecipeContext.Readable(New FileInfo(zip.FullName).Length),
RecipeContext.Readable(New FileInfo(sevenZip.FullName).Length))
' Put the ZIP inside the 7z and read a file out of it through two archives at once.
If Not zip.ArchiveFile.CopyTo(sevenZip, Nothing).Succeeded Then Return RecipeOutcome.Failed
Dim nested As New ArchiveFolder(sevenZip.GetFile("books.zip"))
Dim opened = nested.GetFile("readme.txt").OpenRead()
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using reader As New StreamReader(opened.Stream)
context.Say("read through two archives: ""{0}""", reader.ReadToEnd())
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.IO;
using Bastion.Archive.FileSystem;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Convert a ZIP to a 7z by copying one archive folder into another, with nothing written to disk in between.
/// </summary>
/// <remarks>
/// Each entry is streamed out of the ZIP, its checksum verified, and into the 7z, which is written once. The
/// same call copies between any two archives, and a nested archive — a file inside another archive — can be
/// opened as a folder too, up to <c>ExtractionPolicy.MaxNestingDepth</c> levels deep.
/// </remarks>
internal static class CopyArchiveToArchiveRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = new DiskFolder(context.CreateSampleTree());
var zip = new ArchiveFolder(new DiskFile(context.PathTo("books.zip")));
if (!source.CopyFilesTo(zip, null).Succeeded) return RecipeOutcome.Failed;
var sevenZip = new ArchiveFolder(new DiskFile(context.PathTo("books.7z")));
var converted = zip.CopyFilesTo(sevenZip, null);
if (!converted.Succeeded)
{
context.Say(converted.ToString());
return RecipeOutcome.Failed;
}
context.Say("books.zip {0} became books.7z {1}",
RecipeContext.Readable(new FileInfo(zip.FullName).Length),
RecipeContext.Readable(new FileInfo(sevenZip.FullName).Length));
// Put the ZIP inside the 7z and read a file out of it through two archives at once.
if (!zip.ArchiveFile.CopyTo(sevenZip, null).Succeeded) return RecipeOutcome.Failed;
var nested = new ArchiveFolder(sevenZip.GetFile("books.zip"));
var opened = nested.GetFile("readme.txt").OpenRead();
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var reader = new StreamReader(opened.Stream))
{
context.Say("read through two archives: \"{0}\"", reader.ReadToEnd());
}
return RecipeOutcome.Passed;
}
}
}Zip and unzip by copying folders: a disk folder into an archive, and the archive back out.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.FileSystem
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Zip and unzip by copying folders: a disk folder into an archive, and the archive back out.
''' </summary>
''' <remarks>
''' In the FileSystem model an archive is a folder (<see cref="ArchiveFolder"/>), so zipping is copying into it
''' and unzipping is copying out of it — the same call either way, and the same as copying between two disk
''' folders. The archive's name decides its format, so ".7z" here would give a 7z with no other change. Copying
''' out canonicalises every name the way extraction does, so an archive cannot steer a file outside the folder
''' it is copied to.
''' </remarks>
Friend Module CopyDiskFolderToArchiveRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source As New DiskFolder(context.CreateSampleTree())
Dim archive As New ArchiveFolder(New DiskFile(context.PathTo("books.zip")))
Dim zipped = source.CopyFilesTo(archive, Nothing)
If Not zipped.Succeeded Then
context.Say(zipped.ToString())
Return RecipeOutcome.Failed
End If
context.Say("zipped into {0}: {1}", archive.Name, RecipeContext.Readable(New FileInfo(archive.FullName).Length))
Dim listed = archive.GetItems(True)
If Not listed.Succeeded Then Return RecipeOutcome.Failed
For Each item In listed.Items
Dim file = TryCast(item, ArchivedFile)
context.Say(" {0}", If(file IsNot Nothing, file.PathInArchive, DirectCast(item, ArchivedFolder).PathInArchive & "/"))
Next
Dim target As New DiskFolder(context.PathTo("unzipped"))
Dim unzipped = archive.CopyFilesTo(target, Nothing)
If Not unzipped.Succeeded Then
context.Say(unzipped.ToString())
Return RecipeOutcome.Failed
End If
If Not SevenZipCreateRecipe.SameTree(source.FullName, target.FullName, context) Then Return RecipeOutcome.Failed
context.Say("copied back out, every file the same")
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.IO;
using Bastion.Archive.FileSystem;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Zip and unzip by copying folders: a disk folder into an archive, and the archive back out.
/// </summary>
/// <remarks>
/// In the FileSystem model an archive is a folder (<see cref="ArchiveFolder"/>), so zipping is copying into it
/// and unzipping is copying out of it — the same call either way, and the same as copying between two disk
/// folders. The archive's name decides its format, so ".7z" here would give a 7z with no other change. Copying
/// out canonicalises every name the way extraction does, so an archive cannot steer a file outside the folder
/// it is copied to.
/// </remarks>
internal static class CopyDiskFolderToArchiveRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = new DiskFolder(context.CreateSampleTree());
var archive = new ArchiveFolder(new DiskFile(context.PathTo("books.zip")));
var zipped = source.CopyFilesTo(archive, null);
if (!zipped.Succeeded)
{
context.Say(zipped.ToString());
return RecipeOutcome.Failed;
}
context.Say("zipped into {0}: {1}", archive.Name, RecipeContext.Readable(new FileInfo(archive.FullName).Length));
var listed = archive.GetItems(true);
if (!listed.Succeeded) return RecipeOutcome.Failed;
foreach (var item in listed.Items)
{
var file = item as ArchivedFile;
context.Say(" {0}", file != null ? file.PathInArchive : ((ArchivedFolder)item).PathInArchive + "/");
}
var target = new DiskFolder(context.PathTo("unzipped"));
var unzipped = archive.CopyFilesTo(target, null);
if (!unzipped.Succeeded)
{
context.Say(unzipped.ToString());
return RecipeOutcome.Failed;
}
if (!SevenZipCreateRecipe.SameTree(source.FullName, target.FullName, context)) return RecipeOutcome.Failed;
context.Say("copied back out, every file the same");
return RecipeOutcome.Passed;
}
}
}Dress a self-extractor: its own title, message, licence, suggested folder, icon, and a file to open afterwards.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Security
Imports Bastion.Archive.Sfx
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Dress a self-extractor: its own title, message, licence, suggested folder, icon, and a file to open afterwards.
''' </summary>
''' <remarks>
''' Everything here ends up in the program's window when it is double-clicked. The icon is written into the program
''' itself, so Explorer shows it too; it is done in managed code, so a build server on Linux makes the same program.
''' The recipe does not run the program — its window would wait for someone to click — but reads its settings back,
''' which is also how a tool can tell what an unknown self-extractor will do.
''' </remarks>
Friend Module CreateSfxCustomDialogRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim iconPath = context.PathTo("brand.ico")
File.WriteAllBytes(iconPath, SolidIcon(&H1F, &H3A, &H5F))
Dim options As New SelfExtractorOptions() With {
.Title = "Contoso report pack",
.Prompt = "The quarterly reports will be copied to the folder below.",
.LicenceText = "These reports are confidential." & Environment.NewLine & "Do not forward them outside the company.",
.DefaultDirectory = "%USERPROFILE%\Documents\Contoso reports",
.RunAfterExtraction = "readme.txt",
.Overwrite = OverwriteMode.Ask,
.IconPath = iconPath
}
Dim executable = context.PathTo("reports.exe")
Dim started = ArchiveWriter.CreateSelfExtracting(executable, options, Nothing, Nothing)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then Return RecipeOutcome.Failed
End Using
Using opened = Archive.Open(executable).Archive
Dim carried = opened.SelfExtractor.Options
context.Say("title: {0}", carried.Title)
context.Say("prompt: {0}", carried.Prompt)
context.Say("licence: {0} lines", carried.LicenceText.Split(New String() {Environment.NewLine}, StringSplitOptions.None).Length)
context.Say("folder: {0}", carried.DefaultDirectory)
context.Say("afterwards: {0}", carried.RunAfterExtraction)
context.Say("overwrite: {0}", carried.Overwrite)
Return If(carried.Title = options.Title, RecipeOutcome.Passed, RecipeOutcome.Failed)
End Using
End Function
''' <summary>A one-image 16-pixel icon in one colour; a real application would ship its own .ico.</summary>
Private Function SolidIcon(red As Byte, green As Byte, blue As Byte) As Byte()
Dim image(40 + 16 * 16 * 4 + 16 * 4 - 1) As Byte
BitConverter.GetBytes(40).CopyTo(image, 0)
BitConverter.GetBytes(16).CopyTo(image, 4)
BitConverter.GetBytes(32).CopyTo(image, 8)
BitConverter.GetBytes(CShort(1)).CopyTo(image, 12)
BitConverter.GetBytes(CShort(32)).CopyTo(image, 14)
For index = 0 To 16 * 16 - 1
image(40 + index * 4) = blue
image(40 + index * 4 + 1) = green
image(40 + index * 4 + 2) = red
image(40 + index * 4 + 3) = 255
Next
Dim icon(22 + image.Length - 1) As Byte
BitConverter.GetBytes(CShort(1)).CopyTo(icon, 2)
BitConverter.GetBytes(CShort(1)).CopyTo(icon, 4)
icon(6) = 16
icon(7) = 16
BitConverter.GetBytes(CShort(1)).CopyTo(icon, 10)
BitConverter.GetBytes(CShort(32)).CopyTo(icon, 12)
BitConverter.GetBytes(image.Length).CopyTo(icon, 14)
BitConverter.GetBytes(22).CopyTo(icon, 18)
image.CopyTo(icon, 22)
Return icon
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using Bastion.Archive.Security;
using Bastion.Archive.Sfx;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Dress a self-extractor: its own title, message, licence, suggested folder, icon, and a file to open afterwards.
/// </summary>
/// <remarks>
/// Everything here ends up in the program's window when it is double-clicked. The icon is written into the program
/// itself, so Explorer shows it too; it is done in managed code, so a build server on Linux makes the same program.
/// The recipe does not run the program — its window would wait for someone to click — but reads its settings back,
/// which is also how a tool can tell what an unknown self-extractor will do.
/// </remarks>
internal static class CreateSfxCustomDialogRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var iconPath = context.PathTo("brand.ico");
File.WriteAllBytes(iconPath, SolidIcon(0x1F, 0x3A, 0x5F));
var options = new SelfExtractorOptions
{
Title = "Contoso report pack",
Prompt = "The quarterly reports will be copied to the folder below.",
LicenceText = "These reports are confidential." + Environment.NewLine + "Do not forward them outside the company.",
DefaultDirectory = @"%USERPROFILE%\Documents\Contoso reports",
RunAfterExtraction = "readme.txt",
Overwrite = OverwriteMode.Ask,
IconPath = iconPath,
};
var executable = context.PathTo("reports.exe");
var started = ArchiveWriter.CreateSelfExtracting(executable, options, null, null);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded) return RecipeOutcome.Failed;
}
using (var archive = Archive.Open(executable).Archive)
{
var carried = archive.SelfExtractor.Options;
context.Say("title: {0}", carried.Title);
context.Say("prompt: {0}", carried.Prompt);
context.Say("licence: {0} lines", carried.LicenceText.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Length);
context.Say("folder: {0}", carried.DefaultDirectory);
context.Say("afterwards: {0}", carried.RunAfterExtraction);
context.Say("overwrite: {0}", carried.Overwrite);
return carried.Title == options.Title ? RecipeOutcome.Passed : RecipeOutcome.Failed;
}
}
/// <summary>A one-image 16-pixel icon in one colour; a real application would ship its own .ico.</summary>
private static byte[] SolidIcon(byte red, byte green, byte blue)
{
var image = new byte[40 + 16 * 16 * 4 + 16 * 4];
BitConverter.GetBytes(40).CopyTo(image, 0);
BitConverter.GetBytes(16).CopyTo(image, 4);
BitConverter.GetBytes(32).CopyTo(image, 8);
BitConverter.GetBytes((short)1).CopyTo(image, 12);
BitConverter.GetBytes((short)32).CopyTo(image, 14);
for (var index = 0; index < 16 * 16; index++)
{
image[40 + index * 4] = blue;
image[40 + index * 4 + 1] = green;
image[40 + index * 4 + 2] = red;
image[40 + index * 4 + 3] = 255;
}
var icon = new byte[22 + image.Length];
BitConverter.GetBytes((short)1).CopyTo(icon, 2);
BitConverter.GetBytes((short)1).CopyTo(icon, 4);
icon[6] = 16;
icon[7] = 16;
BitConverter.GetBytes((short)1).CopyTo(icon, 10);
BitConverter.GetBytes((short)32).CopyTo(icon, 12);
BitConverter.GetBytes(image.Length).CopyTo(icon, 14);
BitConverter.GetBytes(22).CopyTo(icon, 18);
image.CopyTo(icon, 22);
return icon;
}
}
}Make a self-extracting archive: one program that unpacks itself, which every ZIP tool also opens as a ZIP.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Diagnostics
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Sfx
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Make a self-extracting archive: one program that unpacks itself, which every ZIP tool also opens as a ZIP.
''' </summary>
''' <remarks>
''' <c>CreateSelfExtracting</c> takes the same entries as any archive. The program needs nothing installed beyond
''' what Windows 10 and 11 carry, runs natively on x64 and ARM64, and is run here from its command line — <c>-s</c>
''' for silent, <c>-d</c> for the folder — to show it working; double-clicked, it opens its window instead.
''' </remarks>
Friend Module CreateSfxRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim executable = context.PathTo("package.exe")
Dim started = ArchiveWriter.CreateSelfExtracting(executable, New SelfExtractorOptions() With {.Title = "Sample package"}, Nothing, Nothing)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then Return RecipeOutcome.Failed
End Using
context.Say("wrote package.exe, {0} KiB", New FileInfo(executable).Length \ 1024)
' It is also a ZIP archive, readable as one.
Using opened = Archive.Open(executable).Archive
context.Say("opened as an archive: {0} entries after a {1} KiB program", opened.Entries.Count, opened.SelfExtractor.PrefixLength \ 1024)
End Using
' Run it the way a script would.
Dim target = context.PathTo("unpacked")
Using process = System.Diagnostics.Process.Start(New ProcessStartInfo(executable, $"-s -d ""{target}""") With {.UseShellExecute = False, .CreateNoWindow = True})
process.WaitForExit()
context.Say("package.exe -s exited with {0}", process.ExitCode)
If process.ExitCode <> 0 Then Return RecipeOutcome.Failed
End Using
Return If(File.Exists(Path.Combine(target, "readme.txt")), RecipeOutcome.Passed, RecipeOutcome.Failed)
End Function
End Module
End NamespaceC#
using System.Diagnostics;
using System.IO;
using Bastion.Archive.Sfx;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Make a self-extracting archive: one program that unpacks itself, which every ZIP tool also opens as a ZIP.
/// </summary>
/// <remarks>
/// <c>CreateSelfExtracting</c> takes the same entries as any archive. The program needs nothing installed beyond
/// what Windows 10 and 11 carry, runs natively on x64 and ARM64, and is run here from its command line — <c>-s</c>
/// for silent, <c>-d</c> for the folder — to show it working; double-clicked, it opens its window instead.
/// </remarks>
internal static class CreateSfxRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var executable = context.PathTo("package.exe");
var started = ArchiveWriter.CreateSelfExtracting(executable, new SelfExtractorOptions { Title = "Sample package" }, null, null);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded) return RecipeOutcome.Failed;
}
context.Say("wrote package.exe, {0} KiB", new FileInfo(executable).Length / 1024);
// It is also a ZIP archive, readable as one.
using (var archive = Archive.Open(executable).Archive)
{
context.Say("opened as an archive: {0} entries after a {1} KiB program", archive.Entries.Count, archive.SelfExtractor.PrefixLength / 1024);
}
// Run it the way a script would.
var target = context.PathTo("unpacked");
using (var process = Process.Start(new ProcessStartInfo(executable, $"-s -d \"{target}\"") { UseShellExecute = false, CreateNoWindow = true }))
{
process.WaitForExit();
context.Say("package.exe -s exited with {0}", process.ExitCode);
if (process.ExitCode != 0) return RecipeOutcome.Failed;
}
return File.Exists(Path.Combine(target, "readme.txt")) ? RecipeOutcome.Passed : RecipeOutcome.Failed;
}
}
}A self-extractor that never shows a window: it unpacks to the folder it was given and reports by exit code.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Diagnostics
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Security
Imports Bastion.Archive.Sfx
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' A self-extractor that never shows a window: it unpacks to the folder it was given and reports by exit code.
''' </summary>
''' <remarks>
''' For deployment scripts and installers. <c>DefaultDirectory</c> may use environment variables such as
''' <c>%ProgramData%</c>, expanded on the machine that runs it. Nothing already there is replaced unless
''' <c>Overwrite</c> says so, and the exit code is 0 done, 1 failed, 2 cancelled — here the second run fails,
''' because the files from the first are still there.
''' </remarks>
Friend Module CreateSfxSilentRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim target = context.PathTo("deployed")
Dim executable = context.PathTo("deploy.exe")
Dim options As New SelfExtractorOptions() With {.Silent = True, .DefaultDirectory = target, .Overwrite = OverwriteMode.FailIfExists}
Dim started = ArchiveWriter.CreateSelfExtracting(executable, options, Nothing, Nothing)
If Not started.Succeeded Then Return RecipeOutcome.Failed
Using writer = started.Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then Return RecipeOutcome.Failed
End Using
Dim first = RunOnce(executable)
context.Say("first run exited with {0}; readme.txt is {1}", first, If(File.Exists(Path.Combine(target, "readme.txt")), "there", "missing"))
Dim second = RunOnce(executable)
context.Say("second run exited with {0}: the files were already there", second)
Return If(first = 0 AndAlso second = 1, RecipeOutcome.Passed, RecipeOutcome.Failed)
End Function
Private Function RunOnce(executable As String) As Integer
Using process = System.Diagnostics.Process.Start(New ProcessStartInfo(executable) With {.UseShellExecute = False, .CreateNoWindow = True})
process.WaitForExit()
Return process.ExitCode
End Using
End Function
End Module
End NamespaceC#
using System.Diagnostics;
using System.IO;
using Bastion.Archive.Security;
using Bastion.Archive.Sfx;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// A self-extractor that never shows a window: it unpacks to the folder it was given and reports by exit code.
/// </summary>
/// <remarks>
/// For deployment scripts and installers. <c>DefaultDirectory</c> may use environment variables such as
/// <c>%ProgramData%</c>, expanded on the machine that runs it. Nothing already there is replaced unless
/// <c>Overwrite</c> says so, and the exit code is 0 done, 1 failed, 2 cancelled — here the second run fails,
/// because the files from the first are still there.
/// </remarks>
internal static class CreateSfxSilentRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var target = context.PathTo("deployed");
var executable = context.PathTo("deploy.exe");
var options = new SelfExtractorOptions { Silent = true, DefaultDirectory = target, Overwrite = OverwriteMode.FailIfExists };
var started = ArchiveWriter.CreateSelfExtracting(executable, options, null, null);
if (!started.Succeeded) return RecipeOutcome.Failed;
using (var writer = started.Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded) return RecipeOutcome.Failed;
}
var first = RunOnce(executable);
context.Say("first run exited with {0}; readme.txt is {1}", first, File.Exists(Path.Combine(target, "readme.txt")) ? "there" : "missing");
var second = RunOnce(executable);
context.Say("second run exited with {0}: the files were already there", second);
return first == 0 && second == 1 ? RecipeOutcome.Passed : RecipeOutcome.Failed;
}
private static int RunOnce(string executable)
{
using (var process = Process.Start(new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true }))
{
process.WaitForExit();
return process.ExitCode;
}
}
}
}Create an archive from a handful of files.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Create an archive from a handful of files.
''' </summary>
''' <remarks>
''' Three things in this recipe are the library's whole contract in miniature. Nothing throws — every call
''' hands back an <see cref="OperationResult"/> to look at. The archive is not an archive until
''' <c>Complete</c> returns, because until then it is a temporary neighbour of the file you asked for, so a
''' crash leaves no half-written archive that looks whole. And <c>Create</c> refuses to write
''' over a file that already exists, which you have to ask for rather than get by accident.
''' </remarks>
Friend Module CreateZipRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("created.zip")
' Level 6 is the default; naming it makes the recipe explicit about where the setting lives.
Dim settings As New CompressionSettings() With {.Level = 6}
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say("could not start the archive: " & started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
For Each name In New String() {"readme.txt", "data.bin"}
Dim added = writer.AddFile(Path.Combine(source, name), name)
If Not added.Succeeded Then
context.Say("could not add " & name & ": " & added.ToString())
Return RecipeOutcome.Failed
End If
Next
' A stream is a first-class source: nothing has to exist on disk to go into an archive.
Using generated As New MemoryStream(RecipeContext.PseudoRandom(4096, 99))
Dim added = writer.AddStream(generated, "generated/noise.bin", Nothing)
If Not added.Succeeded Then
context.Say("could not add the generated entry: " & added.ToString())
Return RecipeOutcome.Failed
End If
End Using
writer.Comment = "Written by the Bastion Archive SDK cookbook."
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say("could not finish the archive: " & finished.ToString())
Return RecipeOutcome.Failed
End If
context.Say("wrote {0} entries in {1:N0} ms", writer.EntryCount, finished.Elapsed.TotalMilliseconds)
End Using
' Writing over an existing archive is refused, not silently done.
Dim again = ArchiveWriter.Create(archivePath)
If again.Succeeded Then
again.Writer.Dispose()
context.Say("the second Create should have been refused")
Return RecipeOutcome.Failed
End If
context.Say("a second Create on the same path returned " & again.ErrorCode.ToString())
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say("could not reopen the archive: " & opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim packed = 0L
Dim unpacked = 0L
For Each entry In archive.Entries
packed += entry.CompressedSize
unpacked += entry.Size
Next
context.Say("{0} entries, {1} in, {2} on disk, comment {3}",
archive.Entries.Count, RecipeContext.Readable(unpacked),
RecipeContext.Readable(packed),
If(archive.Comment.Length > 0, "present", "absent"))
If archive.Entries.Count <> 3 Then Return RecipeOutcome.Failed
If archive.Comment.Length = 0 Then Return RecipeOutcome.Failed
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Create an archive from a handful of files.
/// </summary>
/// <remarks>
/// Three things in this recipe are the library's whole contract in miniature. Nothing throws — every call
/// hands back an <see cref="OperationResult"/> to look at. The archive is not an archive until
/// <c>Complete</c> returns, because until then it is a temporary neighbour of the file you asked for, so a
/// crash leaves no half-written archive that looks whole. And <c>Create</c> refuses to write
/// over a file that already exists, which you have to ask for rather than get by accident.
/// </remarks>
internal static class CreateZipRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("created.zip");
// Level 6 is the default; naming it makes the recipe explicit about where the setting lives.
var settings = new CompressionSettings { Level = 6 };
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say("could not start the archive: " + started);
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
foreach (var name in new[] { "readme.txt", "data.bin" })
{
var added = writer.AddFile(Path.Combine(source, name), name);
if (!added.Succeeded)
{
context.Say("could not add " + name + ": " + added);
return RecipeOutcome.Failed;
}
}
// A stream is a first-class source: nothing has to exist on disk to go into an archive.
using (var generated = new MemoryStream(RecipeContext.PseudoRandom(4096, 99)))
{
var added = writer.AddStream(generated, "generated/noise.bin", null);
if (!added.Succeeded)
{
context.Say("could not add the generated entry: " + added);
return RecipeOutcome.Failed;
}
}
writer.Comment = "Written by the Bastion Archive SDK cookbook.";
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say("could not finish the archive: " + finished);
return RecipeOutcome.Failed;
}
context.Say("wrote {0} entries in {1:N0} ms", writer.EntryCount, finished.Elapsed.TotalMilliseconds);
}
// Writing over an existing archive is refused, not silently done.
var again = ArchiveWriter.Create(archivePath);
if (again.Succeeded)
{
again.Writer.Dispose();
context.Say("the second Create should have been refused");
return RecipeOutcome.Failed;
}
context.Say("a second Create on the same path returned " + again.ErrorCode);
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say("could not reopen the archive: " + opened);
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var packed = 0L;
var unpacked = 0L;
foreach (var entry in archive.Entries)
{
packed += entry.CompressedSize;
unpacked += entry.Size;
}
context.Say("{0} entries, {1} in, {2} on disk, comment {3}",
archive.Entries.Count, RecipeContext.Readable(unpacked), RecipeContext.Readable(packed),
archive.Comment.Length > 0 ? "present" : "absent");
if (archive.Entries.Count != 3) return RecipeOutcome.Failed;
if (archive.Comment.Length == 0) return RecipeOutcome.Failed;
}
return RecipeOutcome.Passed;
}
}
}Answer the library's questions as they come up: which items to take, what to do about a file that is already there, and which password to try.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Diagnostics
Imports Bastion.Archive.Security
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Answer the library's questions as they come up: which items to take, what to do about a file that is
''' already there, and which password to try.
''' </summary>
''' <remarks>
''' Every question is an event on <see cref="OperationMonitor"/>, and every one has a safe answer when nobody
''' listens — take the item, refuse to overwrite, give up on the password — so wiring one up changes nothing
''' else. Here a first extraction fills a folder, then a second one runs into what it left: one file is written
''' under a new name, one is skipped, and one is left out before it is even read. The archive is encrypted and
''' the password is supplied only when asked for.
''' </remarks>
Friend Module EventsAndPromptsRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Const password As String = "asked for, not given"
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("prompts.zip")
Dim started = ArchiveWriter.Create(archivePath, New CompressionSettings() With {.Password = password})
Using writer = started.Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then Return RecipeOutcome.Failed
End Using
Dim monitor As New OperationMonitor()
AddHandler monitor.InvalidPassword,
Sub(sender, e)
context.Say("password {0} for {1}: supplying it", If(e.PasswordWasGiven, "refused", "needed"), e.EntryPath)
e.NewPassword = password
End Sub
AddHandler monitor.PreviewItem,
Sub(sender, e)
If e.ItemPath = "data.bin" Then
context.Say("leaving out {0}", e.ItemPath)
e.Include = False
End If
End Sub
AddHandler monitor.ItemExists,
Sub(sender, e)
If e.ItemPath = "readme.txt" Then
context.Say("{0} exists: writing it as readme (2).txt", e.ItemPath)
e.Decision = ExistsDecision.Rename
e.NewName = "readme (2).txt"
Else
context.Say("{0} exists: skipping it", e.ItemPath)
e.Decision = ExistsDecision.Skip
End If
End Sub
Dim target = context.PathTo("unpacked")
For Each pass In New Integer() {1, 2}
context.Say("extraction {0}", pass)
Dim opened = Archive.Open(archivePath, New ArchiveOpenOptions() With {.Monitor = monitor})
If Not opened.Succeeded Then Return RecipeOutcome.Failed
Using archive = opened.Archive
Dim options As New ExtractionOptions()
options.Policy.Overwrite = OverwriteMode.Ask
Dim result = archive.ExtractAll(target, options)
If Not result.Succeeded Then
context.Say(result.ToString())
Return RecipeOutcome.Failed
End If
End Using
Next
If Not File.Exists(Path.Combine(target, "readme (2).txt")) OrElse File.Exists(Path.Combine(target, "data.bin")) Then Return RecipeOutcome.Failed
context.Say("both extractions finished; the second answered every question it met")
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.IO;
using Bastion.Archive.Diagnostics;
using Bastion.Archive.Security;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Answer the library's questions as they come up: which items to take, what to do about a file that is
/// already there, and which password to try.
/// </summary>
/// <remarks>
/// Every question is an event on <see cref="OperationMonitor"/>, and every one has a safe answer when nobody
/// listens — take the item, refuse to overwrite, give up on the password — so wiring one up changes nothing
/// else. Here a first extraction fills a folder, then a second one runs into what it left: one file is written
/// under a new name, one is skipped, and one is left out before it is even read. The archive is encrypted and
/// the password is supplied only when asked for.
/// </remarks>
internal static class EventsAndPromptsRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
const string password = "asked for, not given";
var source = context.CreateSampleTree();
var archivePath = context.PathTo("prompts.zip");
var started = ArchiveWriter.Create(archivePath, new CompressionSettings { Password = password });
using (var writer = started.Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded) return RecipeOutcome.Failed;
}
var monitor = new OperationMonitor();
monitor.InvalidPassword += (sender, e) =>
{
context.Say("password {0} for {1}: supplying it", e.PasswordWasGiven ? "refused" : "needed", e.EntryPath);
e.NewPassword = password;
};
monitor.PreviewItem += (sender, e) =>
{
if (e.ItemPath == "data.bin")
{
context.Say("leaving out {0}", e.ItemPath);
e.Include = false;
}
};
monitor.ItemExists += (sender, e) =>
{
if (e.ItemPath == "readme.txt")
{
context.Say("{0} exists: writing it as readme (2).txt", e.ItemPath);
e.Decision = ExistsDecision.Rename;
e.NewName = "readme (2).txt";
}
else
{
context.Say("{0} exists: skipping it", e.ItemPath);
e.Decision = ExistsDecision.Skip;
}
};
var target = context.PathTo("unpacked");
foreach (var pass in new[] { 1, 2 })
{
context.Say("extraction {0}", pass);
var opened = Archive.Open(archivePath, new ArchiveOpenOptions { Monitor = monitor });
if (!opened.Succeeded) return RecipeOutcome.Failed;
using (var archive = opened.Archive)
{
var options = new ExtractionOptions();
options.Policy.Overwrite = OverwriteMode.Ask;
var result = archive.ExtractAll(target, options);
if (!result.Succeeded)
{
context.Say(result.ToString());
return RecipeOutcome.Failed;
}
}
}
if (!File.Exists(Path.Combine(target, "readme (2).txt")) || File.Exists(Path.Combine(target, "data.bin"))) return RecipeOutcome.Failed;
context.Say("both extractions finished; the second answered every question it met");
return RecipeOutcome.Passed;
}
}
}Extract only the entries you choose.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Extract only the entries you choose.
''' </summary>
''' <remarks>
''' There is no pattern language to learn: <c>Extract</c> takes entries from <c>Entries</c>, so the selection
''' is ordinary code over ordinary objects — a name test, a size test, a date test, a dialogue's checked
''' items. An entry from a different archive is refused with <c>EntryNotFound</c> rather than quietly
''' extracting whatever happens to sit at that index.
''' <c>ExtractToStream</c> is the other half: one entry, straight into memory or into a network stream, with
''' the checksum still verified on the way past.
''' </remarks>
Friend Module ExtractSelectiveRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("selective.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim wanted As New List(Of ArchiveEntry)()
For Each entry In archive.Entries
If Not entry.IsDirectory AndAlso entry.Name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) Then
wanted.Add(entry)
End If
Next
context.Say("{0} of {1} entries are text files", wanted.Count, archive.Entries.Count)
Dim target = context.PathTo("out")
Dim extracted = archive.Extract(wanted, target, Nothing)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
Dim produced = Directory.GetFiles(target, "*", SearchOption.AllDirectories)
context.Say("the output holds {0} files", produced.Length)
If produced.Length <> wanted.Count Then
context.Say("the output should hold exactly what was asked for")
Return RecipeOutcome.Failed
End If
If File.Exists(Path.Combine(target, "data.bin")) Then
context.Say("data.bin was not asked for and should not be there")
Return RecipeOutcome.Failed
End If
' One entry into memory, without a file anywhere. The checksum is still checked as it streams.
Dim readme As ArchiveEntry = Nothing
For Each entry In archive.Entries
If String.Equals(entry.Name, "readme.txt", StringComparison.Ordinal) Then readme = entry
Next
If readme Is Nothing Then Return RecipeOutcome.Failed
Using buffer As New MemoryStream()
Dim streamed = archive.ExtractToStream(readme, buffer, Nothing)
If Not streamed.Succeeded Then
context.Say(streamed.ToString())
Return RecipeOutcome.Failed
End If
context.Say("readme.txt into memory: {0} bytes", buffer.Length)
If buffer.Length <> readme.Size Then Return RecipeOutcome.Failed
End Using
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.Collections.Generic;
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Extract only the entries you choose.
/// </summary>
/// <remarks>
/// There is no pattern language to learn: <c>Extract</c> takes entries from <c>Entries</c>, so the selection
/// is ordinary code over ordinary objects — a name test, a size test, a date test, a dialogue's checked
/// items. An entry from a different archive is refused with <c>EntryNotFound</c> rather than quietly
/// extracting whatever happens to sit at that index.
/// <c>ExtractToStream</c> is the other half: one entry, straight into memory or into a network stream, with
/// the checksum still verified on the way past.
/// </remarks>
internal static class ExtractSelectiveRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("selective.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var wanted = new List<ArchiveEntry>();
foreach (var entry in archive.Entries)
{
if (!entry.IsDirectory && entry.Name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
{
wanted.Add(entry);
}
}
context.Say("{0} of {1} entries are text files", wanted.Count, archive.Entries.Count);
var target = context.PathTo("out");
var extracted = archive.Extract(wanted, target, null);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
var produced = Directory.GetFiles(target, "*", SearchOption.AllDirectories);
context.Say("the output holds {0} files", produced.Length);
if (produced.Length != wanted.Count)
{
context.Say("the output should hold exactly what was asked for");
return RecipeOutcome.Failed;
}
if (File.Exists(Path.Combine(target, "data.bin")))
{
context.Say("data.bin was not asked for and should not be there");
return RecipeOutcome.Failed;
}
// One entry into memory, without a file anywhere. The checksum is still checked as it streams.
ArchiveEntry readme = null;
foreach (var entry in archive.Entries)
{
if (string.Equals(entry.Name, "readme.txt", StringComparison.Ordinal)) readme = entry;
}
if (readme == null) return RecipeOutcome.Failed;
using (var buffer = new MemoryStream())
{
var streamed = archive.ExtractToStream(readme, buffer, null);
if (!streamed.Succeeded)
{
context.Say(streamed.ToString());
return RecipeOutcome.Failed;
}
context.Say("readme.txt into memory: {0} bytes", buffer.Length);
if (buffer.Length != readme.Size) return RecipeOutcome.Failed;
}
}
return RecipeOutcome.Passed;
}
}
}Extract every entry and prove the bytes survived.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Security
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Extract every entry and prove the bytes survived.
''' </summary>
''' <remarks>
''' Extraction refuses to overwrite by default: a recipe that
''' extracts the same archive twice into the same directory gets an error the second time, and has to say
''' <see cref="OverwriteMode.Overwrite"/> to mean it. Each file is written to a temporary neighbour and moved
''' into place, so an interrupted extraction never leaves a truncated file wearing the right name.
''' </remarks>
Friend Module ExtractZipRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("to-extract.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then Return Report(context, started)
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then Return Report(context, added)
Dim finished = writer.Complete()
If Not finished.Succeeded Then Return Report(context, finished)
End Using
Dim target = context.PathTo("out")
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then Return Report(context, opened)
Using archive = opened.Archive
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then Return Report(context, extracted)
context.Say("extracted {0} entries in {1:N0} ms", archive.Entries.Count, extracted.Elapsed.TotalMilliseconds)
' The second attempt into the same directory is refused, because overwriting is a decision.
Dim refused = archive.ExtractAll(target)
If refused.Succeeded Then
context.Say("the second extraction should have been refused")
Return RecipeOutcome.Failed
End If
context.Say("extracting again returned " & refused.ErrorCode.ToString())
Dim overwriting As New ExtractionOptions()
overwriting.Policy.Overwrite = OverwriteMode.Overwrite
Dim second = archive.ExtractAll(target, overwriting)
If Not second.Succeeded Then Return Report(context, second)
context.Say("asking for Overwrite explicitly: " & second.ErrorCode.ToString())
End Using
' Every file that went in came out byte for byte, and the empty directory survived as a directory.
For Each original In Directory.GetFiles(source, "*", SearchOption.AllDirectories)
Dim relative = original.Substring(source.Length).TrimStart(Path.DirectorySeparatorChar)
Dim produced = Path.Combine(target, relative)
If Not File.Exists(produced) Then
context.Say("missing from the output: " & relative)
Return RecipeOutcome.Failed
End If
If Not RecipeContext.SameBytes(File.ReadAllBytes(original), File.ReadAllBytes(produced)) Then
context.Say("differs from the input: " & relative)
Return RecipeOutcome.Failed
End If
Next
If Not Directory.Exists(Path.Combine(target, "empty-folder")) Then
context.Say("the empty directory did not survive")
Return RecipeOutcome.Failed
End If
context.Say("every file matches its input, and the empty directory is still a directory")
Return RecipeOutcome.Passed
End Function
Private Function Report(context As RecipeContext, result As OperationResult) As RecipeOutcome
context.Say(result.ToString())
Return RecipeOutcome.Failed
End Function
End Module
End NamespaceC#
using System.IO;
using Bastion.Archive.Security;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Extract every entry and prove the bytes survived.
/// </summary>
/// <remarks>
/// Extraction refuses to overwrite by default: a recipe that
/// extracts the same archive twice into the same directory gets an error the second time, and has to say
/// <see cref="OverwriteMode.Overwrite"/> to mean it. Each file is written to a temporary neighbour and moved
/// into place, so an interrupted extraction never leaves a truncated file wearing the right name.
/// </remarks>
internal static class ExtractZipRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("to-extract.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded) return Report(context, started);
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded) return Report(context, added);
var finished = writer.Complete();
if (!finished.Succeeded) return Report(context, finished);
}
var target = context.PathTo("out");
var opened = Archive.Open(archivePath);
if (!opened.Succeeded) return Report(context, opened);
using (var archive = opened.Archive)
{
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded) return Report(context, extracted);
context.Say("extracted {0} entries in {1:N0} ms", archive.Entries.Count, extracted.Elapsed.TotalMilliseconds);
// The second attempt into the same directory is refused, because overwriting is a decision.
var refused = archive.ExtractAll(target);
if (refused.Succeeded)
{
context.Say("the second extraction should have been refused");
return RecipeOutcome.Failed;
}
context.Say("extracting again returned " + refused.ErrorCode);
var overwriting = new ExtractionOptions();
overwriting.Policy.Overwrite = OverwriteMode.Overwrite;
var second = archive.ExtractAll(target, overwriting);
if (!second.Succeeded) return Report(context, second);
context.Say("asking for Overwrite explicitly: " + second.ErrorCode);
}
// Every file that went in came out byte for byte, and the empty directory survived as a directory.
foreach (var original in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
var relative = original.Substring(source.Length).TrimStart(Path.DirectorySeparatorChar);
var produced = Path.Combine(target, relative);
if (!File.Exists(produced))
{
context.Say("missing from the output: " + relative);
return RecipeOutcome.Failed;
}
if (!RecipeContext.SameBytes(File.ReadAllBytes(original), File.ReadAllBytes(produced)))
{
context.Say("differs from the input: " + relative);
return RecipeOutcome.Failed;
}
}
if (!Directory.Exists(Path.Combine(target, "empty-folder")))
{
context.Say("the empty directory did not survive");
return RecipeOutcome.Failed;
}
context.Say("every file matches its input, and the empty directory is still a directory");
return RecipeOutcome.Passed;
}
private static RecipeOutcome Report(RecipeContext context, OperationResult result)
{
context.Say(result.ToString());
return RecipeOutcome.Failed;
}
}
}Write and read a tar through a stream that refuses to seek — a pipe, a socket, a network response.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.Formats.Tar
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write and read a tar through a stream that refuses to seek — a pipe, a socket, a network response.
''' </summary>
''' <remarks>
''' This is the property tar has that ZIP does not. A ZIP is read from its central directory at the end, so
''' a reader must be able to get to the end and come back; tar is a sequence of headers each followed by
''' its content, so it can be produced and consumed a block at a time by something that can only go
''' forwards. That is why tar is what comes down a pipe, and why <c>tar | ssh</c> works at all.
''' <para>
''' The stream below refuses to seek and refuses to report a length, rather than merely not being asked to.
''' Writing the recipe against a <c>MemoryStream</c> would prove nothing: a writer that quietly went back to
''' fix a length it had already written would pass, and then fail the first time somebody used it on the
''' thing it exists for.
''' </para>
''' </remarks>
Friend Module ForwardOnlyTarRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim entries = New String() {"readme.txt", "data/first.bin", "data/second.bin"}
Dim archivePath = context.PathTo("stream.tar")
' Written through something that cannot seek and does not know how long it will be.
Using file As New FileStream(archivePath, FileMode.Create, FileAccess.Write)
Using guarded As New ForwardOnlyStream(file)
Using writer As New TarWriter(guarded, TarFormat.Pax, leaveOpen:=True)
writer.AddDirectory("data", 1577836800L, -1)
For Each name In entries
Dim content = Encoding.UTF8.GetBytes("the content of " & name & Environment.NewLine)
Using source As New MemoryStream(content)
writer.AddFile(name, source, content.Length, 1577836800L, -1)
End Using
Next
writer.Finish()
End Using
End Using
End Using
context.Say("wrote {0} entries to a stream that cannot seek", entries.Length)
' And read back the same way: one pass, no going back.
Dim seen = 0
Using file As New FileStream(archivePath, FileMode.Open, FileAccess.Read)
Using guarded As New ForwardOnlyStream(file)
Using reader As New TarReader(guarded, leaveOpen:=True)
Do While reader.MoveNext()
Dim entry = reader.Current
If entry.EntryType = TarEntryType.Directory Then
context.Say(" {0}/", entry.Name.TrimEnd("/"c))
Continue Do
End If
' The content is read from the same forward-only stream, in place, before the
' reader is asked for the next entry. There is no other order available.
Using produced As New MemoryStream()
GZipStreamUsageRecipe.CopyAll(New BoundedReader(reader, entry.Length), produced)
Dim text = Encoding.UTF8.GetString(produced.ToArray())
Dim expected = "the content of " & entry.Name & Environment.NewLine
If Not String.Equals(text, expected, StringComparison.Ordinal) Then
context.Say("'{0}' read back as '{1}'", entry.Name, text.Trim())
Return RecipeOutcome.Failed
End If
context.Say(" {0} ({1} bytes, {2:yyyy-MM-dd})", entry.Name, entry.Length, entry.ModifiedUtc)
End Using
seen += 1
Loop
End Using
End Using
End Using
If seen <> entries.Length Then
context.Say("expected {0} files and saw {1}", entries.Length, seen)
Return RecipeOutcome.Failed
End If
context.Say("read {0} entries in one pass, never seeking", seen)
Return RecipeOutcome.Passed
End Function
''' <summary>
''' A stream that can only go forwards, wrapped around one that could do more.
''' </summary>
''' <remarks>
''' Every member that would let a caller go back raises, so a reader or writer that tried would be
''' caught here rather than in production. This is what a pipe, a socket and an HTTP response body all
''' look like from the inside.
''' </remarks>
Private NotInheritable Class ForwardOnlyStream
Inherits Stream
Private ReadOnly _inner As Stream
Public Sub New(inner As Stream)
If inner Is Nothing Then Throw New ArgumentNullException(NameOf(inner))
_inner = inner
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return _inner.CanRead
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return _inner.CanWrite
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Throw New NotSupportedException("A forward-only stream does not know its length.")
End Get
End Property
Public Overrides Property Position As Long
Get
Throw New NotSupportedException("A forward-only stream does not know where it is.")
End Get
Set(value As Long)
Throw New NotSupportedException("A forward-only stream cannot seek.")
End Set
End Property
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Throw New NotSupportedException("A forward-only stream cannot seek.")
End Function
Public Overrides Sub SetLength(value As Long)
Throw New NotSupportedException("A forward-only stream cannot be resized.")
End Sub
Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
Return _inner.Read(buffer, offset, count)
End Function
Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
_inner.Write(buffer, offset, count)
End Sub
Public Overrides Sub Flush()
_inner.Flush()
End Sub
End Class
''' <summary>
''' The current entry's content as a stream that stops at the entry's length.
''' </summary>
''' <remarks>
''' <see cref="TarReader.Read"/> serves the current entry and stops at its end on its own; this wrapper
''' exists only so the content can be handed to something that takes a <see cref="Stream"/>.
''' </remarks>
Private NotInheritable Class BoundedReader
Inherits Stream
Private ReadOnly _reader As TarReader
Private _left As Long
Public Sub New(reader As TarReader, length As Long)
_reader = reader
_left = length
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return True
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Throw New NotSupportedException("An entry being streamed does not answer for its length here.")
End Get
End Property
Public Overrides Property Position As Long
Get
Throw New NotSupportedException("An entry being streamed does not know where it is.")
End Get
Set(value As Long)
Throw New NotSupportedException("An entry being streamed cannot seek.")
End Set
End Property
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Throw New NotSupportedException("An entry being streamed cannot seek.")
End Function
Public Overrides Sub SetLength(value As Long)
Throw New NotSupportedException("An entry being streamed cannot be resized.")
End Sub
Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
Throw New NotSupportedException("An entry being read cannot be written to.")
End Sub
Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
If _left <= 0L Then Return 0
Dim wanted = CInt(Math.Min(CLng(count), _left))
Dim taken = _reader.Read(buffer, offset, wanted)
If taken > 0 Then _left -= CLng(taken)
Return taken
End Function
Public Overrides Sub Flush()
End Sub
End Class
End Module
End NamespaceC#
using System;
using System.IO;
using System.Text;
using Bastion.Archive.Formats.Tar;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write and read a tar through a stream that refuses to seek — a pipe, a socket, a network response.
/// </summary>
/// <remarks>
/// This is the property tar has that ZIP does not. A ZIP is read from its central directory at the end, so
/// a reader must be able to get to the end and come back; tar is a sequence of headers each followed by
/// its content, so it can be produced and consumed a block at a time by something that can only go
/// forwards. That is why tar is what comes down a pipe, and why <c>tar | ssh</c> works at all.
/// <para>
/// The stream below refuses to seek and refuses to report a length, rather than merely not being asked to.
/// Writing the recipe against a <c>MemoryStream</c> would prove nothing: a writer that quietly went back
/// to fix a length it had already written would pass, and then fail the first time somebody used it on the
/// thing it exists for.
/// </para>
/// </remarks>
internal static class ForwardOnlyTarRecipe
{
/// <summary>Midnight on the first of January 2020, in Unix seconds.</summary>
private const long FixedTime = 1577836800L;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var entries = new[] { "readme.txt", "data/first.bin", "data/second.bin" };
var archivePath = context.PathTo("stream.tar");
// Written through something that cannot seek and does not know how long it will be.
using (var file = new FileStream(archivePath, FileMode.Create, FileAccess.Write))
using (var guarded = new ForwardOnlyStream(file))
using (var writer = new TarWriter(guarded, TarFormat.Pax, leaveOpen: true))
{
writer.AddDirectory("data", FixedTime, -1);
foreach (var name in entries)
{
var content = Encoding.UTF8.GetBytes("the content of " + name + Environment.NewLine);
using (var source = new MemoryStream(content))
{
writer.AddFile(name, source, content.Length, FixedTime, -1);
}
}
writer.Finish();
}
context.Say("wrote {0} entries to a stream that cannot seek", entries.Length);
// And read back the same way: one pass, no going back.
var seen = 0;
using (var file = new FileStream(archivePath, FileMode.Open, FileAccess.Read))
using (var guarded = new ForwardOnlyStream(file))
using (var reader = new TarReader(guarded, leaveOpen: true))
{
while (reader.MoveNext())
{
var entry = reader.Current;
if (entry.EntryType == TarEntryType.Directory)
{
context.Say(" {0}/", entry.Name.TrimEnd('/'));
continue;
}
// The content is read from the same forward-only stream, in place, before the reader is
// asked for the next entry. There is no other order available.
using (var produced = new MemoryStream())
{
GZipStreamUsageRecipe.CopyAll(new BoundedReader(reader, entry.Length), produced);
var text = Encoding.UTF8.GetString(produced.ToArray());
var expected = "the content of " + entry.Name + Environment.NewLine;
if (!string.Equals(text, expected, StringComparison.Ordinal))
{
context.Say("'{0}' read back as '{1}'", entry.Name, text.Trim());
return RecipeOutcome.Failed;
}
context.Say(" {0} ({1} bytes, {2:yyyy-MM-dd})", entry.Name, entry.Length, entry.ModifiedUtc);
}
seen++;
}
}
if (seen != entries.Length)
{
context.Say("expected {0} files and saw {1}", entries.Length, seen);
return RecipeOutcome.Failed;
}
context.Say("read {0} entries in one pass, never seeking", seen);
return RecipeOutcome.Passed;
}
/// <summary>
/// A stream that can only go forwards, wrapped around one that could do more.
/// </summary>
/// <remarks>
/// Every member that would let a caller go back raises, so a reader or writer that tried would be
/// caught here rather than in production. This is what a pipe, a socket and an HTTP response body all
/// look like from the inside.
/// </remarks>
private sealed class ForwardOnlyStream : Stream
{
private readonly Stream _inner;
public ForwardOnlyStream(Stream inner)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
public override bool CanRead => _inner.CanRead;
public override bool CanWrite => _inner.CanWrite;
public override bool CanSeek => false;
public override long Length =>
throw new NotSupportedException("A forward-only stream does not know its length.");
public override long Position
{
get => throw new NotSupportedException("A forward-only stream does not know where it is.");
set => throw new NotSupportedException("A forward-only stream cannot seek.");
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException("A forward-only stream cannot seek.");
}
public override void SetLength(long value)
{
throw new NotSupportedException("A forward-only stream cannot be resized.");
}
public override int Read(byte[] buffer, int offset, int count)
{
return _inner.Read(buffer, offset, count);
}
public override void Write(byte[] buffer, int offset, int count)
{
_inner.Write(buffer, offset, count);
}
public override void Flush()
{
_inner.Flush();
}
}
/// <summary>
/// The current entry's content as a stream that stops at the entry's length.
/// </summary>
/// <remarks>
/// <see cref="TarReader.Read"/> serves the current entry and stops at its end on its own; this wrapper
/// exists only so the content can be handed to something that takes a <see cref="Stream"/>.
/// </remarks>
private sealed class BoundedReader : Stream
{
private readonly TarReader _reader;
private long _left;
public BoundedReader(TarReader reader, long length)
{
_reader = reader;
_left = length;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length =>
throw new NotSupportedException("An entry being streamed does not answer for its length here.");
public override long Position
{
get => throw new NotSupportedException("An entry being streamed does not know where it is.");
set => throw new NotSupportedException("An entry being streamed cannot seek.");
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException("An entry being streamed cannot seek.");
}
public override void SetLength(long value)
{
throw new NotSupportedException("An entry being streamed cannot be resized.");
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException("An entry being read cannot be written to.");
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_left <= 0L) return 0;
var wanted = (int)Math.Min(count, _left);
var taken = _reader.Read(buffer, offset, wanted);
if (taken > 0) _left -= taken;
return taken;
}
public override void Flush()
{
}
}
}
}Read a ZIP off a stream that cannot seek — a socket, a pipe, or a download in progress.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.Formats.Zip
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Read a ZIP off a stream that cannot seek — a socket, a pipe, or a download in progress.
''' </summary>
''' <remarks>
''' <c>Archive.Open</c> starts at the central directory, which lives at the <em>end</em> of a ZIP, so it
''' needs a stream it can seek. <c>ZipStreamReader</c> walks the local file headers instead, in the order
''' they were written, and never looks back — which is what lets an archive be read as it arrives.
''' <para>
''' The trade is worth understanding. The central directory is where a ZIP records the truth about each
''' entry; a local header can disagree with it, and a forward-only reader has only the local header to go
''' on. Where the whole archive is in hand, <c>Archive.Open</c> remains the better reader.
''' </para>
''' </remarks>
Friend Module ForwardOnlyZipRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("streamed.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, "tree", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
' Read it back through something that refuses to seek, which is the whole point of the exercise.
Dim entries = 0
Dim bytes = 0L
Using file As New FileStream(archivePath, FileMode.Open, FileAccess.Read)
Using arriving As New ArrivingStream(file)
Using reader As New ZipStreamReader(arriving, leaveOpen:=True)
While reader.MoveNext()
Dim entry = reader.Current
If entry.IsDirectory Then Continue While
' The content is read once, forwards. The reader owns this stream and finishes it
' on the next MoveNext, so it is deliberately not disposed here.
Dim content = reader.OpenEntry()
Dim buffer(65535) As Byte
Dim taken = content.Read(buffer, 0, buffer.Length)
While taken > 0
bytes += taken
taken = content.Read(buffer, 0, buffer.Length)
End While
entries += 1
End While
End Using
End Using
End Using
context.Say("{0} entries and {1} read forwards, off a stream that cannot seek",
entries, RecipeContext.Readable(bytes))
If entries = 0 OrElse bytes = 0 Then Return RecipeOutcome.Failed
Return RecipeOutcome.Passed
End Function
''' <summary>A stream that reads but will not seek, standing in for a socket.</summary>
Private NotInheritable Class ArrivingStream
Inherits Stream
Private ReadOnly _inner As Stream
Public Sub New(inner As Stream)
_inner = inner
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return True
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Throw New NotSupportedException()
End Get
End Property
Public Overrides Property Position As Long
Get
Throw New NotSupportedException()
End Get
Set(value As Long)
Throw New NotSupportedException()
End Set
End Property
''' <summary>Hands over a little at a time, as a network would.</summary>
Public Overrides Function Read(buffer As Byte(), offset As Integer, count As Integer) As Integer
Return _inner.Read(buffer, offset, Math.Min(count, 4096))
End Function
Public Overrides Sub Flush()
End Sub
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Throw New NotSupportedException()
End Function
Public Overrides Sub SetLength(value As Long)
Throw New NotSupportedException()
End Sub
Public Overrides Sub Write(buffer As Byte(), offset As Integer, count As Integer)
Throw New NotSupportedException()
End Sub
End Class
End Module
End NamespaceC#
using System;
using System.IO;
using Bastion.Archive.Formats.Zip;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Read a ZIP off a stream that cannot seek — a socket, a pipe, or a download in progress.
/// </summary>
/// <remarks>
/// <c>Archive.Open</c> starts at the central directory, which lives at the <em>end</em> of a ZIP, so it
/// needs a stream it can seek. <c>ZipStreamReader</c> walks the local file headers instead, in the order
/// they were written, and never looks back — which is what lets an archive be read as it arrives.
/// <para>
/// The trade is worth understanding. The central directory is where a ZIP records the truth about each
/// entry; a local header can disagree with it, and a forward-only reader has only the local header to go
/// on. Where the whole archive is in hand, <c>Archive.Open</c> remains the better reader.
/// </para>
/// </remarks>
internal static class ForwardOnlyZipRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("streamed.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, "tree", null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
// Read it back through something that refuses to seek, which is the whole point of the exercise.
var entries = 0;
var bytes = 0L;
using (var file = new FileStream(archivePath, FileMode.Open, FileAccess.Read))
using (var arriving = new ArrivingStream(file))
using (var reader = new ZipStreamReader(arriving, leaveOpen: true))
{
while (reader.MoveNext())
{
var entry = reader.Current;
if (entry.IsDirectory) continue;
// The content is read once, forwards. The reader owns this stream and finishes it on the
// next MoveNext, so it is deliberately not disposed here.
var content = reader.OpenEntry();
var buffer = new byte[65536];
int taken;
while ((taken = content.Read(buffer, 0, buffer.Length)) > 0) bytes += taken;
entries++;
}
}
context.Say("{0} entries and {1} read forwards, off a stream that cannot seek",
entries, RecipeContext.Readable(bytes));
return entries == 0 || bytes == 0 ? RecipeOutcome.Failed : RecipeOutcome.Passed;
}
/// <summary>A stream that reads but will not seek, standing in for a socket.</summary>
private sealed class ArrivingStream : Stream
{
private readonly Stream _inner;
public ArrivingStream(Stream inner) => _inner = inner;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
/// <summary>Hands over a little at a time, as a network would.</summary>
public override int Read(byte[] buffer, int offset, int count) =>
_inner.Read(buffer, offset, Math.Min(count, 4096));
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
}
}Compress and decompress one stream, with the header fields gzip actually carries.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.Formats.GZip
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Compress and decompress one stream, with the header fields gzip actually carries.
''' </summary>
''' <remarks>
''' A <c>.gz</c> file is not an archive: it holds one stream of bytes and a little metadata about it, which
''' is why the name of the original file is a field in the header rather than something the format works
''' out. That field is the reason <c>gzip -d</c> can restore a name a download mangled.
''' <para>
''' The part worth knowing is the last one. A gzip file is **members concatenated**, which is what
''' <c>cat a.gz b.gz</c> produces and what every log rotation produces. A reader that stops at the first
''' trailer is right about almost every file it will ever meet and silently truncates the rest.
''' </para>
''' </remarks>
Friend Module GZipStreamUsageRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim content = Encoding.UTF8.GetBytes(Repeat("the quick brown fox jumps over the lazy dog. ", 400))
Dim compressedPath = context.PathTo("notes.txt.gz")
Using file As New FileStream(compressedPath, FileMode.Create, FileAccess.Write)
' Level, the original name, and the modification time in Unix seconds.
Using writer As New GZipEncoderStream(file, 9, "notes.txt", 1577836800L, leaveOpen:=True)
writer.Write(content, 0, content.Length)
End Using
End Using
Dim compressed = New FileInfo(compressedPath).Length
context.Say("{0} in, {1} out ({2:P1} of the original)",
RecipeContext.Readable(content.Length), RecipeContext.Readable(compressed),
compressed / CDbl(content.Length))
Using file As New FileStream(compressedPath, FileMode.Open, FileAccess.Read)
Using reader As New GZipDecoderStream(file, leaveOpen:=True)
Using produced As New MemoryStream()
CopyAll(reader, produced)
If Not RecipeContext.SameBytes(content, produced.ToArray()) Then
context.Say("the bytes that came back are not the bytes that went in")
Return RecipeOutcome.Failed
End If
context.Say("header says the original was called '{0}', modified {1:yyyy-MM-dd}",
reader.OriginalName,
New DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(reader.ModifiedUnixTime))
End Using
End Using
End Using
' Two members, joined the way a rotated log is joined.
Dim joinedPath = context.PathTo("joined.gz")
Dim first = Encoding.UTF8.GetBytes("the first member" & Environment.NewLine)
Dim second = Encoding.UTF8.GetBytes("the second member" & Environment.NewLine)
Using file As New FileStream(joinedPath, FileMode.Create, FileAccess.Write)
For Each part In New Byte()() {first, second}
Using writer As New GZipEncoderStream(file, 6, Nothing, 0L, leaveOpen:=True)
writer.Write(part, 0, part.Length)
End Using
Next
End Using
Using file As New FileStream(joinedPath, FileMode.Open, FileAccess.Read)
Using reader As New GZipDecoderStream(file, leaveOpen:=True)
Using produced As New MemoryStream()
CopyAll(reader, produced)
Dim expected(first.Length + second.Length - 1) As Byte
System.Buffer.BlockCopy(first, 0, expected, 0, first.Length)
System.Buffer.BlockCopy(second, 0, expected, first.Length, second.Length)
If Not RecipeContext.SameBytes(expected, produced.ToArray()) Then
context.Say("a joined file read back short, so only the first member was read")
Return RecipeOutcome.Failed
End If
context.Say("two joined members read as one stream of {0} bytes; MemberCount is {1}",
produced.Length, reader.MemberCount)
If reader.MemberCount <> 2 Then
context.Say("expected two members to be reported")
Return RecipeOutcome.Failed
End If
End Using
End Using
End Using
Return RecipeOutcome.Passed
End Function
''' <summary>
''' Copies a stream to the end. Written out rather than using <c>CopyTo</c>, which net46 does have but
''' which hides the one thing a decoder recipe should show: a read returning fewer bytes than asked for
''' is normal, and only zero means the end.
''' </summary>
Friend Sub CopyAll(source As Stream, destination As Stream)
Dim buffer(81919) As Byte
Do
Dim taken = source.Read(buffer, 0, buffer.Length)
If taken <= 0 Then Exit Do
destination.Write(buffer, 0, taken)
Loop
End Sub
''' <summary>A string repeated, without depending on which frameworks have string.Repeat.</summary>
Friend Function Repeat(value As String, times As Integer) As String
Dim builder As New StringBuilder(value.Length * times)
For index = 1 To times
builder.Append(value)
Next
Return builder.ToString()
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using System.Text;
using Bastion.Archive.Formats.GZip;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Compress and decompress one stream, with the header fields gzip actually carries.
/// </summary>
/// <remarks>
/// A <c>.gz</c> file is not an archive: it holds one stream of bytes and a little metadata about it, which
/// is why the name of the original file is a field in the header rather than something the format works
/// out. That field is the reason <c>gzip -d</c> can restore a name a download mangled.
/// <para>
/// The part worth knowing is the last one. A gzip file is <b>members concatenated</b>, which is what
/// <c>cat a.gz b.gz</c> produces and what every log rotation produces. A reader that stops at the first
/// trailer is right about almost every file it will ever meet and silently truncates the rest.
/// </para>
/// </remarks>
internal static class GZipStreamUsageRecipe
{
/// <summary>Midnight on the first of January 2020, in Unix seconds.</summary>
private const long FixedTime = 1577836800L;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var content = Encoding.UTF8.GetBytes(Repeat("the quick brown fox jumps over the lazy dog. ", 400));
var compressedPath = context.PathTo("notes.txt.gz");
using (var file = new FileStream(compressedPath, FileMode.Create, FileAccess.Write))
{
// Level, the original name, and the modification time in Unix seconds.
using (var writer = new GZipEncoderStream(file, 9, "notes.txt", FixedTime, leaveOpen: true))
{
writer.Write(content, 0, content.Length);
}
}
var compressed = new FileInfo(compressedPath).Length;
context.Say("{0} in, {1} out ({2:P1} of the original)",
RecipeContext.Readable(content.Length), RecipeContext.Readable(compressed),
compressed / (double)content.Length);
using (var file = new FileStream(compressedPath, FileMode.Open, FileAccess.Read))
using (var reader = new GZipDecoderStream(file, leaveOpen: true))
using (var produced = new MemoryStream())
{
CopyAll(reader, produced);
if (!RecipeContext.SameBytes(content, produced.ToArray()))
{
context.Say("the bytes that came back are not the bytes that went in");
return RecipeOutcome.Failed;
}
context.Say("header says the original was called '{0}', modified {1:yyyy-MM-dd}",
reader.OriginalName,
new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(reader.ModifiedUnixTime));
}
// Two members, joined the way a rotated log is joined.
var joinedPath = context.PathTo("joined.gz");
var first = Encoding.UTF8.GetBytes("the first member" + Environment.NewLine);
var second = Encoding.UTF8.GetBytes("the second member" + Environment.NewLine);
using (var file = new FileStream(joinedPath, FileMode.Create, FileAccess.Write))
{
foreach (var part in new[] { first, second })
{
using (var writer = new GZipEncoderStream(file, 6, null, 0L, leaveOpen: true))
{
writer.Write(part, 0, part.Length);
}
}
}
using (var file = new FileStream(joinedPath, FileMode.Open, FileAccess.Read))
using (var reader = new GZipDecoderStream(file, leaveOpen: true))
using (var produced = new MemoryStream())
{
CopyAll(reader, produced);
var expected = new byte[first.Length + second.Length];
Buffer.BlockCopy(first, 0, expected, 0, first.Length);
Buffer.BlockCopy(second, 0, expected, first.Length, second.Length);
if (!RecipeContext.SameBytes(expected, produced.ToArray()))
{
context.Say("a joined file read back short, so only the first member was read");
return RecipeOutcome.Failed;
}
context.Say("two joined members read as one stream of {0} bytes; MemberCount is {1}",
produced.Length, reader.MemberCount);
if (reader.MemberCount != 2)
{
context.Say("expected two members to be reported");
return RecipeOutcome.Failed;
}
}
return RecipeOutcome.Passed;
}
/// <summary>
/// Copies a stream to the end. Written out rather than using <c>CopyTo</c>, which net46 does have but
/// which hides the one thing a decoder recipe should show: a read returning fewer bytes than asked for
/// is normal, and only zero means the end.
/// </summary>
internal static void CopyAll(Stream source, Stream destination)
{
var buffer = new byte[81920];
while (true)
{
var taken = source.Read(buffer, 0, buffer.Length);
if (taken <= 0) break;
destination.Write(buffer, 0, taken);
}
}
/// <summary>A string repeated, without depending on which frameworks have string.Repeat.</summary>
internal static string Repeat(string value, int times)
{
var builder = new StringBuilder(value.Length * times);
for (var index = 0; index < times; index++)
{
builder.Append(value);
}
return builder.ToString();
}
}
}List an archive the way <c>7z l -slt</c> does.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Globalization
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' List an archive the way <c>7z l -slt</c> does.
''' </summary>
''' <remarks>
''' Listing reads the central directory only, so it is fast and it never touches entry data — which is also
''' why the listing of an encrypted archive is complete while its contents stay unreadable.
''' Three properties on the archive itself are worth a look and are missing from most libraries:
''' <c>EmbeddedStubSize</c> is non-zero for a self-extracting archive, <c>TrailingDataSize</c> counts bytes
''' after the end record that were ignored, and the warnings on the open result say what was odd about the
''' file rather than leaving you to guess.
''' </remarks>
Friend Module ListEntriesRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("listing.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
writer.Comment = "A listing sample."
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
context.Say("format {0}, {1} entries, {2} volume(s), stub {3} bytes, trailing {4} bytes",
archive.Format, archive.Entries.Count, archive.VolumeCount,
archive.EmbeddedStubSize, archive.TrailingDataSize)
If archive.Comment.Length > 0 Then context.Say("comment: " & archive.Comment)
For Each warning In opened.Warnings
context.Say("warning {0}: {1}", warning.Code, warning.Message)
Next
context.Say(" {0} {1} {2} {3} {4} {5}",
"size".PadLeft(9), "packed".PadLeft(9), "ratio".PadLeft(6),
"method".PadRight(8), "modified (UTC)".PadRight(19), "name")
For Each entry In archive.Entries
Dim ratio = If(entry.Size > 0, 100.0 * entry.CompressedSize / entry.Size, 0.0)
context.Say(" {0} {1} {2} {3} {4} {5}{6}",
entry.Size.ToString("N0", CultureInfo.CurrentCulture).PadLeft(9),
entry.CompressedSize.ToString("N0", CultureInfo.CurrentCulture).PadLeft(9),
ratio.ToString("N1", CultureInfo.CurrentCulture).PadLeft(5) & "%",
entry.Method.ToString().PadRight(8),
entry.LastWriteUtc.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
entry.Name,
Detail(entry))
Next
' A listing is only worth printing if the facts in it are facts.
For Each entry In archive.Entries
If entry.IsDirectory Then Continue For
If entry.Size < 0 OrElse entry.CompressedSize < 0 Then Return RecipeOutcome.Failed
If entry.Crc32 < 0 Then Return RecipeOutcome.Failed
If entry.LastWriteUtc = DateTime.MinValue Then Return RecipeOutcome.Failed
Next
End Using
Return RecipeOutcome.Passed
End Function
''' <summary>The parts of an entry worth saying only when they are true of it.</summary>
Private Function Detail(entry As ArchiveEntry) As String
Dim parts = ""
If entry.IsDirectory Then parts &= " (directory)"
If entry.IsEncrypted Then parts &= " (" & entry.Encryption.ToString() & ")"
If entry.IsSymbolicLink Then parts &= " (symbolic link)"
If Not entry.IsDirectory Then
parts &= String.Format(CultureInfo.InvariantCulture, " crc={0:X8}", entry.Crc32)
End If
Return parts
End Function
End Module
End NamespaceC#
using System;
using System.Globalization;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// List an archive the way <c>7z l -slt</c> does.
/// </summary>
/// <remarks>
/// Listing reads the central directory only, so it is fast and it never touches entry data — which is also
/// why the listing of an encrypted archive is complete while its contents stay unreadable.
/// Three properties on the archive itself are worth a look and are missing from most libraries:
/// <c>EmbeddedStubSize</c> is non-zero for a self-extracting archive, <c>TrailingDataSize</c> counts bytes
/// after the end record that were ignored, and the warnings on the open result say what was odd about the
/// file rather than leaving you to guess.
/// </remarks>
internal static class ListEntriesRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("listing.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
writer.Comment = "A listing sample.";
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
context.Say("format {0}, {1} entries, {2} volume(s), stub {3} bytes, trailing {4} bytes",
archive.Format, archive.Entries.Count, archive.VolumeCount,
archive.EmbeddedStubSize, archive.TrailingDataSize);
if (archive.Comment.Length > 0) context.Say("comment: " + archive.Comment);
foreach (var warning in opened.Warnings)
{
context.Say("warning {0}: {1}", warning.Code, warning.Message);
}
context.Say(" {0} {1} {2} {3} {4} {5}",
"size".PadLeft(9), "packed".PadLeft(9), "ratio".PadLeft(6),
"method".PadRight(8), "modified (UTC)".PadRight(19), "name");
foreach (var entry in archive.Entries)
{
var ratio = entry.Size > 0 ? 100.0 * entry.CompressedSize / entry.Size : 0.0;
context.Say(" {0} {1} {2} {3} {4} {5}{6}",
entry.Size.ToString("N0", CultureInfo.CurrentCulture).PadLeft(9),
entry.CompressedSize.ToString("N0", CultureInfo.CurrentCulture).PadLeft(9),
ratio.ToString("N1", CultureInfo.CurrentCulture).PadLeft(5) + "%",
entry.Method.ToString().PadRight(8),
entry.LastWriteUtc.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
entry.Name,
Detail(entry));
}
// A listing is only worth printing if the facts in it are facts.
foreach (var entry in archive.Entries)
{
if (entry.IsDirectory) continue;
if (entry.Size < 0 || entry.CompressedSize < 0) return RecipeOutcome.Failed;
if (entry.Crc32 < 0) return RecipeOutcome.Failed;
if (entry.LastWriteUtc == DateTime.MinValue) return RecipeOutcome.Failed;
}
}
return RecipeOutcome.Passed;
}
/// <summary>The parts of an entry worth saying only when they are true of it.</summary>
private static string Detail(ArchiveEntry entry)
{
var parts = "";
if (entry.IsDirectory) parts += " (directory)";
if (entry.IsEncrypted) parts += " (" + entry.Encryption + ")";
if (entry.IsSymbolicLink) parts += " (symbolic link)";
if (!entry.IsDirectory)
{
parts += string.Format(CultureInfo.InvariantCulture, " crc={0:X8}", entry.Crc32);
}
return parts;
}
}
}Pass 65 535 entries, which needs the Zip64 end record.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Globalization
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Pass 65 535 entries, which needs the Zip64 end record.
''' </summary>
''' <remarks>
''' The end-of-central-directory record holds the entry count in 16 bits, so 65 535 is as far as the
''' original layout reaches. Past that the archive needs the Zip64 end record and its locator, and — the
''' same trap as the sizes — <c>0xFFFF</c> is a reserved redirection rather than a count, so exactly 65 535
''' entries needs Zip64 as well.
''' This recipe writes seventy thousand entries to put the count genuinely over the line. It is also the
''' recipe that shows memory is decoupled from entry count: writing and then listing seventy
''' thousand entries streams, and the process does not grow with the archive.
''' </remarks>
Friend Module ManyEntriesRecipe
''' <summary>Comfortably past the 16-bit limit, and past the 0xFFFF sentinel.</summary>
Private Const EntryCount As Integer = 70000
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim archivePath = context.PathTo("many.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
context.Say("writing {0:N0} entries", EntryCount)
Dim payload = Encoding.ASCII.GetBytes("one small entry among seventy thousand")
Using writer = started.Writer
For index = 0 To EntryCount - 1
' Foldered names, so the archive also exercises a directory tree a browser has to build.
Dim name = String.Format(CultureInfo.InvariantCulture, "bucket-{0:D3}/entry-{1:D5}.txt",
index Mod 100, index)
Using source As New MemoryStream(payload, False)
Dim added = writer.AddStream(source, name, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
End Using
Next
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
context.Say("wrote {0:N0} entries in {1:N1} s, archive {2}",
writer.EntryCount, finished.Elapsed.TotalSeconds,
RecipeContext.Readable(New FileInfo(archivePath).Length))
End Using
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
context.Say("read back {0:N0} entries in {1:N1} s",
archive.Entries.Count, opened.Elapsed.TotalSeconds)
If archive.Entries.Count <> EntryCount Then
context.Say("the entry count did not survive the round trip")
Return RecipeOutcome.Failed
End If
' The first and the last, because an off-by-one in the directory shows up at the ends.
context.Say("first: {0}", archive.Entries(0).Name)
context.Say("last: {0}", archive.Entries(archive.Entries.Count - 1).Name)
If Not String.Equals(archive.Entries(0).Name, "bucket-000/entry-00000.txt", StringComparison.Ordinal) Then
Return RecipeOutcome.Failed
End If
If Not String.Equals(archive.Entries(archive.Entries.Count - 1).Name, "bucket-099/entry-69999.txt",
StringComparison.Ordinal) Then
Return RecipeOutcome.Failed
End If
' Extracting seventy thousand files would take longer than it proves; one is enough to show the
' directory offsets are right, and Test below reads every one of them.
Dim chosen = archive.Entries(EntryCount \ 2)
Using buffer As New MemoryStream()
Dim streamed = archive.ExtractToStream(chosen, buffer, Nothing)
If Not streamed.Succeeded Then
context.Say(streamed.ToString())
Return RecipeOutcome.Failed
End If
If buffer.Length <> payload.Length Then Return RecipeOutcome.Failed
context.Say("entry {0:N0} of the set read back {1} bytes", EntryCount \ 2, buffer.Length)
End Using
Dim tested = archive.Test(Nothing)
If Not tested.Succeeded Then
context.Say(tested.ToString())
Return RecipeOutcome.Failed
End If
context.Say("every entry verified in {0:N1} s", tested.Elapsed.TotalSeconds)
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Pass 65 535 entries, which needs the Zip64 end record.
/// </summary>
/// <remarks>
/// The end-of-central-directory record holds the entry count in 16 bits, so 65 535 is as far as the
/// original layout reaches. Past that the archive needs the Zip64 end record and its locator, and — the
/// same trap as the sizes — <c>0xFFFF</c> is a reserved redirection rather than a count, so exactly 65 535
/// entries needs Zip64 as well.
/// This recipe writes seventy thousand entries to put the count genuinely over the line. It is also the
/// recipe that shows memory is decoupled from entry count: writing and then listing seventy
/// thousand entries streams, and the process does not grow with the archive.
/// </remarks>
internal static class ManyEntriesRecipe
{
/// <summary>Comfortably past the 16-bit limit, and past the 0xFFFF sentinel.</summary>
private const int EntryCount = 70000;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var archivePath = context.PathTo("many.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
context.Say("writing {0:N0} entries", EntryCount);
var payload = Encoding.ASCII.GetBytes("one small entry among seventy thousand");
using (var writer = started.Writer)
{
for (var index = 0; index < EntryCount; index++)
{
// Foldered names, so the archive also exercises a directory tree a browser has to build.
var name = string.Format(CultureInfo.InvariantCulture, "bucket-{0:D3}/entry-{1:D5}.txt",
index % 100, index);
using (var source = new MemoryStream(payload, false))
{
var added = writer.AddStream(source, name, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
context.Say("wrote {0:N0} entries in {1:N1} s, archive {2}",
writer.EntryCount, finished.Elapsed.TotalSeconds,
RecipeContext.Readable(new FileInfo(archivePath).Length));
}
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
context.Say("read back {0:N0} entries in {1:N1} s", archive.Entries.Count, opened.Elapsed.TotalSeconds);
if (archive.Entries.Count != EntryCount)
{
context.Say("the entry count did not survive the round trip");
return RecipeOutcome.Failed;
}
// The first and the last, because an off-by-one in the directory shows up at the ends.
context.Say("first: {0}", archive.Entries[0].Name);
context.Say("last: {0}", archive.Entries[archive.Entries.Count - 1].Name);
if (!string.Equals(archive.Entries[0].Name, "bucket-000/entry-00000.txt", StringComparison.Ordinal))
{
return RecipeOutcome.Failed;
}
if (!string.Equals(archive.Entries[archive.Entries.Count - 1].Name, "bucket-099/entry-69999.txt",
StringComparison.Ordinal))
{
return RecipeOutcome.Failed;
}
// Extracting seventy thousand files would take longer than it proves; one is enough to show the
// directory offsets are right, and Test below reads every one of them.
var chosen = archive.Entries[EntryCount / 2];
using (var buffer = new MemoryStream())
{
var streamed = archive.ExtractToStream(chosen, buffer, null);
if (!streamed.Succeeded)
{
context.Say(streamed.ToString());
return RecipeOutcome.Failed;
}
if (buffer.Length != payload.Length) return RecipeOutcome.Failed;
context.Say("entry {0:N0} of the set read back {1} bytes", EntryCount / 2, buffer.Length);
}
var tested = archive.Test(null);
if (!tested.Succeeded)
{
context.Say(tested.ToString());
return RecipeOutcome.Failed;
}
context.Say("every entry verified in {0:N1} s", tested.Elapsed.TotalSeconds);
}
return RecipeOutcome.Passed;
}
}
}Keep a download's Mark of the Web on what comes out of it, so Windows still treats those files as from the internet.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Security
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Keep a download's Mark of the Web on what comes out of it, so Windows still treats those files as from the internet.
''' </summary>
''' <remarks>
''' A browser marks a download with a <c>Zone.Identifier</c> stream; SmartScreen and Office's Protected View read it.
''' An archive tool that drops the mark on extraction launders every file inside, which is how malware has slipped past
''' both. <see cref="ExtractionPolicy.PropagateMarkOfTheWeb"/> copies the archive's mark onto every file extracted. It
''' needs Windows and NTFS; elsewhere the recipe says so and skips.
''' </remarks>
Friend Module MarkOfTheWebRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
If Environment.OSVersion.Platform <> PlatformID.Win32NT Then
context.Say("the Mark of the Web is an NTFS stream, so this needs Windows")
Return RecipeOutcome.Skipped
End If
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("downloaded.zip")
Using writer = ArchiveWriter.Create(archivePath, Nothing).Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then Return RecipeOutcome.Failed
End Using
' What a browser writes on a download; the \\?\ form reaches the stream on .NET Framework too.
Dim mark = "[ZoneTransfer]" & Environment.NewLine & "ZoneId=3" & Environment.NewLine & "HostUrl=https://example.com/downloaded.zip" & Environment.NewLine
Try
File.WriteAllText(StreamPath(archivePath), mark)
Catch ex As IOException
context.Say("this volume cannot hold alternate streams ({0}), so there is no mark to keep", ex.Message)
Return RecipeOutcome.Skipped
End Try
context.Say("the archive is marked as downloaded (zone 3, the internet)")
For Each propagate In New Boolean() {False, True}
Dim target = context.PathTo(If(propagate, "kept", "laundered"))
Using opened = Archive.Open(archivePath).Archive
Dim options As New ExtractionOptions()
options.Policy.PropagateMarkOfTheWeb = propagate
If Not opened.ExtractAll(target, options).Succeeded Then Return RecipeOutcome.Failed
End Using
Dim extracted = StreamPath(Path.Combine(target, "readme.txt"))
Dim carried = File.Exists(extracted) AndAlso File.ReadAllText(extracted).Contains("ZoneId=3")
context.Say("PropagateMarkOfTheWeb = {0}: readme.txt {1}", propagate, If(carried, "carries the mark", "has no mark"))
If carried <> propagate Then Return RecipeOutcome.Failed
Next
Return RecipeOutcome.Passed
End Function
Private Function StreamPath(filePath As String) As String
Return "\\?\" & Path.GetFullPath(filePath) & ":Zone.Identifier"
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using Bastion.Archive.Security;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Keep a download's Mark of the Web on what comes out of it, so Windows still treats those files as from the internet.
/// </summary>
/// <remarks>
/// A browser marks a download with a <c>Zone.Identifier</c> stream; SmartScreen and Office's Protected View read it.
/// An archive tool that drops the mark on extraction launders every file inside, which is how malware has slipped past
/// both. <see cref="ExtractionPolicy.PropagateMarkOfTheWeb"/> copies the archive's mark onto every file extracted. It
/// needs Windows and NTFS; elsewhere the recipe says so and skips.
/// </remarks>
internal static class MarkOfTheWebRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
{
context.Say("the Mark of the Web is an NTFS stream, so this needs Windows");
return RecipeOutcome.Skipped;
}
var source = context.CreateSampleTree();
var archivePath = context.PathTo("downloaded.zip");
using (var writer = ArchiveWriter.Create(archivePath, null).Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded) return RecipeOutcome.Failed;
}
// What a browser writes on a download; the \\?\ form reaches the stream on .NET Framework too.
var mark = "[ZoneTransfer]" + Environment.NewLine + "ZoneId=3" + Environment.NewLine + "HostUrl=https://example.com/downloaded.zip" + Environment.NewLine;
try
{
File.WriteAllText(StreamPath(archivePath), mark);
}
catch (IOException ex)
{
context.Say("this volume cannot hold alternate streams ({0}), so there is no mark to keep", ex.Message);
return RecipeOutcome.Skipped;
}
context.Say("the archive is marked as downloaded (zone 3, the internet)");
foreach (var propagate in new[] { false, true })
{
var target = context.PathTo(propagate ? "kept" : "laundered");
using (var archive = Archive.Open(archivePath).Archive)
{
var options = new ExtractionOptions();
options.Policy.PropagateMarkOfTheWeb = propagate;
if (!archive.ExtractAll(target, options).Succeeded) return RecipeOutcome.Failed;
}
var extracted = StreamPath(Path.Combine(target, "readme.txt"));
var carried = File.Exists(extracted) && File.ReadAllText(extracted).Contains("ZoneId=3");
context.Say("PropagateMarkOfTheWeb = {0}: readme.txt {1}", propagate ? "True" : "False", carried ? "carries the mark" : "has no mark");
if (carried != propagate) return RecipeOutcome.Failed;
}
return RecipeOutcome.Passed;
}
private static string StreamPath(string filePath) => @"\\?\" + Path.GetFullPath(filePath) + ":Zone.Identifier";
}
}Build content in memory, zip it, and unzip it into memory again, touching disk only for the archive.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.FileSystem
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Build content in memory, zip it, and unzip it into memory again, touching disk only for the archive.
''' </summary>
''' <remarks>
''' A <see cref="MemoryFolder"/> is a folder like any other, so it can be copied into an archive and an archive
''' can be copied into it. Changes to an archive are rebuilds; <see cref="ArchiveFolder.BeginUpdate"/> and
''' <see cref="ArchiveFolder.EndUpdate"/> gather several into one.
''' </remarks>
Friend Module MemoryFolderRoundTripRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim memory As New MemoryFolder()
For Each name In New String() {"a.txt", "b.txt", "notes/c.txt"}
Dim opened = memory.GetFile(name).OpenWrite(False)
If Not opened.Succeeded Then Return RecipeOutcome.Failed
Dim bytes = Encoding.UTF8.GetBytes("content of " & name)
Using stream = opened.Stream
stream.Write(bytes, 0, bytes.Length)
End Using
Next
Dim archive As New ArchiveFolder(New DiskFile(context.PathTo("memory.zip")))
If Not memory.CopyFilesTo(archive, Nothing).Succeeded Then Return RecipeOutcome.Failed
' Two changes, one rebuild.
archive.BeginUpdate()
If Not archive.GetFile("a.txt").Delete().Succeeded Then Return RecipeOutcome.Failed
If Not archive.GetFolder("empty").Create().Succeeded Then Return RecipeOutcome.Failed
Dim ended = archive.EndUpdate()
If Not ended.Succeeded Then
context.Say(ended.ToString())
Return RecipeOutcome.Failed
End If
Dim back As New MemoryFolder()
If Not archive.CopyFilesTo(back, Nothing).Succeeded Then Return RecipeOutcome.Failed
Dim listed = back.GetItems(True)
If Not listed.Succeeded Then Return RecipeOutcome.Failed
For Each item In listed.Items
context.Say(" {0}{1}", item.FullName, If(TypeOf item Is AbstractFolder, "/", ""))
Next
If back.GetFile("a.txt").Exists OrElse Not back.GetFile("notes/c.txt").Exists Then Return RecipeOutcome.Failed
context.Say("round trip through memory.zip with one update applied")
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.Text;
using Bastion.Archive.FileSystem;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Build content in memory, zip it, and unzip it into memory again, touching disk only for the archive.
/// </summary>
/// <remarks>
/// A <see cref="MemoryFolder"/> is a folder like any other, so it can be copied into an archive and an archive
/// can be copied into it. Changes to an archive are rebuilds; <see cref="ArchiveFolder.BeginUpdate"/> and
/// <see cref="ArchiveFolder.EndUpdate()"/> gather several into one.
/// </remarks>
internal static class MemoryFolderRoundTripRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var memory = new MemoryFolder();
foreach (var name in new[] { "a.txt", "b.txt", "notes/c.txt" })
{
var opened = memory.GetFile(name).OpenWrite(false);
if (!opened.Succeeded) return RecipeOutcome.Failed;
var bytes = Encoding.UTF8.GetBytes("content of " + name);
using (var stream = opened.Stream)
{
stream.Write(bytes, 0, bytes.Length);
}
}
var archive = new ArchiveFolder(new DiskFile(context.PathTo("memory.zip")));
if (!memory.CopyFilesTo(archive, null).Succeeded) return RecipeOutcome.Failed;
// Two changes, one rebuild.
archive.BeginUpdate();
if (!archive.GetFile("a.txt").Delete().Succeeded) return RecipeOutcome.Failed;
if (!archive.GetFolder("empty").Create().Succeeded) return RecipeOutcome.Failed;
var ended = archive.EndUpdate();
if (!ended.Succeeded)
{
context.Say(ended.ToString());
return RecipeOutcome.Failed;
}
var back = new MemoryFolder();
if (!archive.CopyFilesTo(back, null).Succeeded) return RecipeOutcome.Failed;
var listed = back.GetItems(true);
if (!listed.Succeeded) return RecipeOutcome.Failed;
foreach (var item in listed.Items)
{
context.Say(" {0}{1}", item.FullName, item is AbstractFolder ? "/" : "");
}
if (back.GetFile("a.txt").Exists || !back.GetFile("notes/c.txt").Exists) return RecipeOutcome.Failed;
context.Say("round trip through memory.zip with one update applied");
return RecipeOutcome.Passed;
}
}
}Write and read WinZip AES-256, and be refused without the password.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write and read WinZip AES-256, and be refused without the password.
''' </summary>
''' <remarks>
''' WinZip AES is the only strong encryption the ZIP format has: PBKDF2-HMAC-SHA1 over a fresh random salt,
''' AES in counter mode, and an HMAC-SHA1-80 authentication code checked before any plaintext is handed over.
''' The two-byte password verifier means a wrong password is usually reported as <c>PasswordInvalid</c>
''' straight away rather than after decoding a gigabyte.
''' Two properties of the format are worth knowing before choosing it. Entry <em>names</em> are never
''' encrypted — an observer sees what is in the archive, only not its contents — and the iteration count is
''' fixed at 1000 by the specification, which is low by any modern standard and cannot be raised without
''' making the archive unreadable to everyone else. For new archives where interoperability is not required,
''' 7z encryption is the better choice; AES-256 here is for reading and writing what the world already uses.
''' The password itself is never logged, never put in a message and never written to a result (no credentials in any diagnostic).
''' </remarks>
Friend Module PasswordAes256Recipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Const password As String = "Correct Horse Battery Staple"
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("secret.zip")
Dim settings As New CompressionSettings() With {
.Encryption = EncryptionMethod.Aes256,
.Password = password
}
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
' Anything about the archive a third-party reader may not like is reported, not hidden.
For Each warning In writer.InteropWarnings
context.Say("interoperability: {0} — {1}", warning.Feature, warning.AffectedReaders)
Next
End Using
' Without the password the directory still reads: names and sizes are not encrypted by this format.
Dim blind = Archive.Open(archivePath)
If Not blind.Succeeded Then
context.Say(blind.ToString())
Return RecipeOutcome.Failed
End If
Using archive = blind.Archive
Dim encrypted = 0
For Each entry In archive.Entries
If entry.IsDirectory Then Continue For
If Not entry.IsEncrypted OrElse entry.Encryption <> EncryptionMethod.Aes256 Then
context.Say("entry '" & entry.Name & "' is not AES-256 encrypted")
Return RecipeOutcome.Failed
End If
encrypted += 1
Next
context.Say("{0} encrypted entries are listed without the password", encrypted)
Dim refused = archive.ExtractAll(context.PathTo("out-blind"))
If refused.Succeeded Then
context.Say("extraction without the password should have been refused")
Return RecipeOutcome.Failed
End If
context.Say("extracting without it returned " & refused.ErrorCode.ToString())
If refused.ErrorCode <> ErrorCode.PasswordRequired Then Return RecipeOutcome.Failed
End Using
' A wrong password is caught by the verifier, before any data is produced.
Dim wrong = Archive.Open(archivePath, New ArchiveOpenOptions() With {.Password = "not the password"})
If Not wrong.Succeeded Then
context.Say(wrong.ToString())
Return RecipeOutcome.Failed
End If
Using archive = wrong.Archive
Dim rejected = archive.Test(Nothing)
If rejected.Succeeded Then
context.Say("a wrong password should not have passed the test")
Return RecipeOutcome.Failed
End If
context.Say("a wrong password returned " & rejected.ErrorCode.ToString())
End Using
Dim opened = Archive.Open(archivePath, New ArchiveOpenOptions() With {.Password = password})
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim target = context.PathTo("out")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
Dim recovered = File.ReadAllBytes(Path.Combine(target, "readme.txt"))
Dim expected = File.ReadAllBytes(Path.Combine(source, "readme.txt"))
If Not RecipeContext.SameBytes(expected, recovered) Then
context.Say("the decrypted bytes do not match the input")
Return RecipeOutcome.Failed
End If
context.Say("decrypted and verified: readme.txt matches its input exactly")
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write and read WinZip AES-256, and be refused without the password.
/// </summary>
/// <remarks>
/// WinZip AES is the only strong encryption the ZIP format has: PBKDF2-HMAC-SHA1 over a fresh random salt,
/// AES in counter mode, and an HMAC-SHA1-80 authentication code checked before any plaintext is handed over.
/// The two-byte password verifier means a wrong password is usually reported as <c>PasswordInvalid</c>
/// straight away rather than after decoding a gigabyte.
/// Two properties of the format are worth knowing before choosing it. Entry <em>names</em> are never
/// encrypted — an observer sees what is in the archive, only not its contents — and the iteration count is
/// fixed at 1000 by the specification, which is low by any modern standard and cannot be raised without
/// making the archive unreadable to everyone else. For new archives where interoperability is not required,
/// 7z encryption is the better choice; AES-256 here is for reading and writing what the world already uses.
/// The password itself is never logged, never put in a message and never written to a result (no credentials in any diagnostic).
/// </remarks>
internal static class PasswordAes256Recipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
const string password = "Correct Horse Battery Staple";
var source = context.CreateSampleTree();
var archivePath = context.PathTo("secret.zip");
var settings = new CompressionSettings
{
Encryption = EncryptionMethod.Aes256,
Password = password
};
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
// Anything about the archive a third-party reader may not like is reported, not hidden.
foreach (var warning in writer.InteropWarnings)
{
context.Say("interoperability: {0} — {1}", warning.Feature, warning.AffectedReaders);
}
}
// Without the password the directory still reads: names and sizes are not encrypted by this format.
var blind = Archive.Open(archivePath);
if (!blind.Succeeded)
{
context.Say(blind.ToString());
return RecipeOutcome.Failed;
}
using (var archive = blind.Archive)
{
var encrypted = 0;
foreach (var entry in archive.Entries)
{
if (entry.IsDirectory) continue;
if (!entry.IsEncrypted || entry.Encryption != EncryptionMethod.Aes256)
{
context.Say("entry '" + entry.Name + "' is not AES-256 encrypted");
return RecipeOutcome.Failed;
}
encrypted++;
}
context.Say("{0} encrypted entries are listed without the password", encrypted);
var refused = archive.ExtractAll(context.PathTo("out-blind"));
if (refused.Succeeded)
{
context.Say("extraction without the password should have been refused");
return RecipeOutcome.Failed;
}
context.Say("extracting without it returned " + refused.ErrorCode);
if (refused.ErrorCode != ErrorCode.PasswordRequired) return RecipeOutcome.Failed;
}
// A wrong password is caught by the verifier, before any data is produced.
var wrong = Archive.Open(archivePath, new ArchiveOpenOptions { Password = "not the password" });
if (!wrong.Succeeded)
{
context.Say(wrong.ToString());
return RecipeOutcome.Failed;
}
using (var archive = wrong.Archive)
{
var rejected = archive.Test(null);
if (rejected.Succeeded)
{
context.Say("a wrong password should not have passed the test");
return RecipeOutcome.Failed;
}
context.Say("a wrong password returned " + rejected.ErrorCode);
}
var opened = Archive.Open(archivePath, new ArchiveOpenOptions { Password = password });
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var target = context.PathTo("out");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
var recovered = File.ReadAllBytes(Path.Combine(target, "readme.txt"));
var expected = File.ReadAllBytes(Path.Combine(source, "readme.txt"));
if (!RecipeContext.SameBytes(expected, recovered))
{
context.Say("the decrypted bytes do not match the input");
return RecipeOutcome.Failed;
}
context.Say("decrypted and verified: readme.txt matches its input exactly");
}
return RecipeOutcome.Passed;
}
}
}Read a traditionally encrypted archive — the old ZIP encryption, which you will meet and should not write.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Read a traditionally encrypted archive — the old ZIP encryption, which you will meet and should not
''' write.
''' </summary>
''' <remarks>
''' Traditional PKWARE encryption is what every ZIP tool has implemented since 1990, and it was broken in
''' 1994: a known-plaintext attack recovers the key stream from a few bytes a reader can often guess. So
''' this recipe exists for the archives that already exist. Writing it is a deliberate choice the library
''' makes you spell out — <c>EncryptionMethod.ZipCrypto</c> carries an <c>Obsolete</c> attribute, so using
''' it is a compile-time warning, and the writer reports it as an interoperability warning as well.
''' Two details of the format show up here. The twelve-byte encryption header ends in a **check byte**, and
''' a wrong password fails that check better than 255 times in 256 — so a wrong password is usually reported
''' at once, and occasionally only when the entry's CRC fails at the end. And because the check byte has to
''' be chosen before the data is read, an entry written this way always carries its sizes in a data
''' descriptor after the data rather than in its local header.
''' The password is never logged, never put in a message and never written to a result.
''' </remarks>
Friend Module PasswordZipCryptoReadRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Const password As String = "the old way"
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("legacy.zip")
' Deliberate, and deliberately noisy: the enum member is obsolete, so this is the one place in the
' cookbook that silences a compiler warning, and it says why.
#Disable Warning BC40000
Dim settings As New CompressionSettings() With {
.Encryption = EncryptionMethod.ZipCrypto,
.Password = password
}
#Enable Warning BC40000
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Dim warnedAboutStrength = False
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
For Each warning In writer.InteropWarnings
context.Say("interoperability: {0} — {1}", warning.Feature, warning.AffectedReaders)
If warning.Feature.IndexOf("Traditional", StringComparison.Ordinal) >= 0 Then warnedAboutStrength = True
Next
End Using
If Not warnedAboutStrength Then
context.Say("the writer should have warned that traditional encryption is broken")
Return RecipeOutcome.Failed
End If
' The entry names are in the clear, as they are with AES: only the contents are encrypted.
Dim listed = Archive.Open(archivePath)
If Not listed.Succeeded Then
context.Say(listed.ToString())
Return RecipeOutcome.Failed
End If
Using archive = listed.Archive
Dim encrypted = 0
For Each entry In archive.Entries
If entry.IsDirectory Then Continue For
If Not entry.IsEncrypted Then
context.Say("entry '" & entry.Name & "' is not encrypted")
Return RecipeOutcome.Failed
End If
encrypted += 1
Next
context.Say("{0} encrypted entries listed without the password", encrypted)
End Using
' A wrong password is caught by the check byte in the encryption header, not by decoding everything.
Dim wrong = Archive.Open(archivePath, New ArchiveOpenOptions() With {.Password = "the wrong way"})
If Not wrong.Succeeded Then
context.Say(wrong.ToString())
Return RecipeOutcome.Failed
End If
Using archive = wrong.Archive
Dim rejected = archive.Test(Nothing)
If rejected.Succeeded Then
context.Say("a wrong password should not have passed the test")
Return RecipeOutcome.Failed
End If
context.Say("a wrong password returned {0}", rejected.ErrorCode)
End Using
Dim opened = Archive.Open(archivePath, New ArchiveOpenOptions() With {.Password = password})
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim target = context.PathTo("out")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
Dim expected = File.ReadAllBytes(Path.Combine(source, "readme.txt"))
Dim recovered = File.ReadAllBytes(Path.Combine(target, "readme.txt"))
If Not RecipeContext.SameBytes(expected, recovered) Then
context.Say("the decrypted bytes do not match the input")
Return RecipeOutcome.Failed
End If
context.Say("decrypted and verified: readme.txt matches its input exactly")
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Read a traditionally encrypted archive — the old ZIP encryption, which you will meet and should not
/// write.
/// </summary>
/// <remarks>
/// Traditional PKWARE encryption is what every ZIP tool has implemented since 1990, and it was broken in
/// 1994: a known-plaintext attack recovers the key stream from a few bytes a reader can often guess. So
/// this recipe exists for the archives that already exist. Writing it is a deliberate choice the library
/// makes you spell out — <c>EncryptionMethod.ZipCrypto</c> carries an <c>Obsolete</c> attribute, so using
/// it is a compile-time warning, and the writer reports it as an interoperability warning as well.
/// Two details of the format show up here. The twelve-byte encryption header ends in a <em>check byte</em>,
/// and a wrong password fails that check better than 255 times in 256 — so a wrong password is usually
/// reported at once, and occasionally only when the entry's CRC fails at the end. And because the check
/// byte has to be chosen before the data is read, an entry written this way always carries its sizes in a
/// data descriptor after the data rather than in its local header.
/// The password is never logged, never put in a message and never written to a result.
/// </remarks>
internal static class PasswordZipCryptoReadRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
const string password = "the old way";
var source = context.CreateSampleTree();
var archivePath = context.PathTo("legacy.zip");
// Deliberate, and deliberately noisy: the enum member is obsolete, so this is the one place in the
// cookbook that silences a compiler warning, and it says why.
#pragma warning disable CS0618
var settings = new CompressionSettings
{
Encryption = EncryptionMethod.ZipCrypto,
Password = password
};
#pragma warning restore CS0618
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
var warnedAboutStrength = false;
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
foreach (var warning in writer.InteropWarnings)
{
context.Say("interoperability: {0} — {1}", warning.Feature, warning.AffectedReaders);
if (warning.Feature.IndexOf("Traditional", StringComparison.Ordinal) >= 0)
{
warnedAboutStrength = true;
}
}
}
if (!warnedAboutStrength)
{
context.Say("the writer should have warned that traditional encryption is broken");
return RecipeOutcome.Failed;
}
// The entry names are in the clear, as they are with AES: only the contents are encrypted.
var listed = Archive.Open(archivePath);
if (!listed.Succeeded)
{
context.Say(listed.ToString());
return RecipeOutcome.Failed;
}
using (var archive = listed.Archive)
{
var encrypted = 0;
foreach (var entry in archive.Entries)
{
if (entry.IsDirectory)
{
continue;
}
if (!entry.IsEncrypted)
{
context.Say("entry '" + entry.Name + "' is not encrypted");
return RecipeOutcome.Failed;
}
encrypted++;
}
context.Say("{0} encrypted entries listed without the password", encrypted);
}
// A wrong password is caught by the check byte in the encryption header, not by decoding everything.
var wrong = Archive.Open(archivePath, new ArchiveOpenOptions { Password = "the wrong way" });
if (!wrong.Succeeded)
{
context.Say(wrong.ToString());
return RecipeOutcome.Failed;
}
using (var archive = wrong.Archive)
{
var rejected = archive.Test(null);
if (rejected.Succeeded)
{
context.Say("a wrong password should not have passed the test");
return RecipeOutcome.Failed;
}
context.Say("a wrong password returned {0}", rejected.ErrorCode);
}
var opened = Archive.Open(archivePath, new ArchiveOpenOptions { Password = password });
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var target = context.PathTo("out");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
var expected = File.ReadAllBytes(Path.Combine(source, "readme.txt"));
var recovered = File.ReadAllBytes(Path.Combine(target, "readme.txt"));
if (!RecipeContext.SameBytes(expected, recovered))
{
context.Say("the decrypted bytes do not match the input");
return RecipeOutcome.Failed;
}
context.Say("decrypted and verified: readme.txt matches its input exactly");
}
return RecipeOutcome.Passed;
}
}
}Watch an operation's progress, and stop it part way through without leaving anything broken behind.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Threading
Imports Bastion.Archive
Imports Bastion.Archive.Diagnostics
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Watch an operation's progress, and stop it part way through without leaving anything broken behind.
''' </summary>
''' <remarks>
''' Progress reports totals as well as the current entry, so a bar can be drawn from the first event. Cancelling
''' is a token on the monitor: the operation stops at the next entry boundary, the result says
''' <see cref="ErrorCode.Cancelled"/>, and — because every file is written through a temporary one swapped
''' into place — the destination holds only whole files. In a UI, make the monitor on the UI thread and its
''' events arrive there too.
''' </remarks>
Friend Module ProgressAndCancelRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.PathTo("many")
Directory.CreateDirectory(source)
For index = 0 To 39
File.WriteAllBytes(Path.Combine(source, $"file{index:00}.bin"), RecipeContext.PseudoRandom(20000, index + 1))
Next
Dim archivePath = context.PathTo("many.zip")
Dim started = ArchiveWriter.Create(archivePath)
Using writer = started.Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then Return RecipeOutcome.Failed
End Using
Using cancellation As New CancellationTokenSource()
Dim monitor As New OperationMonitor(Nothing) With {.CancellationToken = cancellation.Token, .ProgressInterval = TimeSpan.Zero}
Dim lastShown = -1
AddHandler monitor.Progress,
Sub(sender, e)
Dim percent = CInt(Math.Floor(e.TotalPercent / 25.0)) * 25
If percent <> lastShown Then
lastShown = percent
context.Say(" {0}% — {1} of {2} items", percent, e.ProcessedItems, e.TotalItems)
End If
End Sub
AddHandler monitor.EntryCompleted,
Sub(sender, e)
' Stop after the tenth file, as a user pressing Cancel would.
If e.EntryIndex = 9 Then cancellation.Cancel()
End Sub
Dim opened = Archive.Open(archivePath, New ArchiveOpenOptions() With {.Monitor = monitor})
If Not opened.Succeeded Then Return RecipeOutcome.Failed
Dim target = context.PathTo("partial")
Using archive = opened.Archive
Dim result = archive.ExtractAll(target)
context.Say("the extraction ended with {0}", result.ErrorCode)
If result.ErrorCode <> ErrorCode.Cancelled Then Return RecipeOutcome.Failed
End Using
Dim written = Directory.GetFiles(target)
For Each file In written
If New FileInfo(file).Length <> 20000 Then
context.Say("found a partial file: {0}", file)
Return RecipeOutcome.Failed
End If
Next
context.Say("{0} whole files written before the stop, and nothing half-written", written.Length)
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using System.Threading;
using Bastion.Archive.Diagnostics;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Watch an operation's progress, and stop it part way through without leaving anything broken behind.
/// </summary>
/// <remarks>
/// Progress reports totals as well as the current entry, so a bar can be drawn from the first event. Cancelling
/// is a token on the monitor: the operation stops at the next entry boundary, the result says
/// <see cref="ErrorCode.Cancelled"/>, and — because every file is written through a temporary one swapped
/// into place — the destination holds only whole files. In a UI, make the monitor on the UI thread and its
/// events arrive there too.
/// </remarks>
internal static class ProgressAndCancelRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.PathTo("many");
Directory.CreateDirectory(source);
for (var index = 0; index <= 39; index++)
{
File.WriteAllBytes(Path.Combine(source, $"file{index:00}.bin"), RecipeContext.PseudoRandom(20000, index + 1));
}
var archivePath = context.PathTo("many.zip");
var started = ArchiveWriter.Create(archivePath);
using (var writer = started.Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded) return RecipeOutcome.Failed;
}
using (var cancellation = new CancellationTokenSource())
{
var monitor = new OperationMonitor(null) { CancellationToken = cancellation.Token, ProgressInterval = TimeSpan.Zero };
var lastShown = -1;
monitor.Progress += (sender, e) =>
{
var percent = (int)Math.Floor(e.TotalPercent / 25.0) * 25;
if (percent != lastShown)
{
lastShown = percent;
context.Say(" {0}% — {1} of {2} items", percent, e.ProcessedItems, e.TotalItems);
}
};
monitor.EntryCompleted += (sender, e) =>
{
// Stop after the tenth file, as a user pressing Cancel would.
if (e.EntryIndex == 9) cancellation.Cancel();
};
var opened = Archive.Open(archivePath, new ArchiveOpenOptions { Monitor = monitor });
if (!opened.Succeeded) return RecipeOutcome.Failed;
var target = context.PathTo("partial");
using (var archive = opened.Archive)
{
var result = archive.ExtractAll(target);
context.Say("the extraction ended with {0}", result.ErrorCode);
if (result.ErrorCode != ErrorCode.Cancelled) return RecipeOutcome.Failed;
}
var written = Directory.GetFiles(target);
foreach (var file in written)
{
if (new FileInfo(file).Length != 20000)
{
context.Say("found a partial file: {0}", file);
return RecipeOutcome.Failed;
}
}
context.Say("{0} whole files written before the stop, and nothing half-written", written.Length);
}
return RecipeOutcome.Passed;
}
}
}Write a <c>.7z</c> and read it back, which is the same two calls as a ZIP.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write a <c>.7z</c> and read it back, which is the same two calls as a ZIP.
''' </summary>
''' <remarks>
''' The point of this recipe is how little there is to it. The format is chosen by the name — a path
''' ending <c>.7z</c> writes 7z and anything else writes ZIP — so a caller who has learnt one format has
''' learnt both, and switching an application from one to the other is a change of file extension rather
''' than a change of API. Set <see cref="CompressionSettings.Format"/> when there is no name to
''' read, which is the case for the stream overloads.
''' <para>
''' What you get by default is what <c>7z a</c> gives: LZMA2, one solid block, and every entry's type
''' inspected so that executable code gets a branch filter and text does not.
''' </para>
''' </remarks>
Friend Module SevenZipCreateRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("books.7z")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, "books", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
context.Say("wrote {0} entries as {1}", writer.EntryCount,
RecipeContext.Readable(New FileInfo(archivePath).Length))
End Using
' Opening is the same call as for a ZIP: the format comes from the archive's own signature rather
' than from its name, so an archive misnamed .zip still opens as what it actually is.
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
context.Say("opened as {0} with {1} entries", archive.Format, archive.Entries.Count)
Dim target = context.PathTo("unpacked")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
If Not SameTree(source, Path.Combine(target, "books"), context) Then Return RecipeOutcome.Failed
End Using
context.Say("every file came back with the same bytes")
Return RecipeOutcome.Passed
End Function
''' <summary>Compares two trees file by file, which is the only check worth making here.</summary>
Friend Function SameTree(expected As String, actual As String, context As RecipeContext) As Boolean
For Each original In Directory.GetFiles(expected, "*", SearchOption.AllDirectories)
Dim relative = original.Substring(expected.Length).TrimStart(Path.DirectorySeparatorChar)
Dim produced = Path.Combine(actual, relative)
If Not File.Exists(produced) Then
context.Say("missing after extraction: {0}", relative)
Return False
End If
If Not RecipeContext.SameBytes(File.ReadAllBytes(original), File.ReadAllBytes(produced)) Then
context.Say("different after extraction: {0}", relative)
Return False
End If
Next
Return True
End Function
End Module
End NamespaceC#
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write a <c>.7z</c> and read it back, which is the same two calls as a ZIP.
/// </summary>
/// <remarks>
/// The point of this recipe is how little there is to it. The format is chosen by the name — a path
/// ending <c>.7z</c> writes 7z and anything else writes ZIP — so a caller who has learnt one format has
/// learnt both, and switching an application from one to the other is a change of file extension rather
/// than a change of API. Set <see cref="CompressionSettings.Format"/> when there is no name to
/// read, which is the case for the stream overloads.
/// <para>
/// What you get by default is what <c>7z a</c> gives: LZMA2, one solid block, and every entry's type
/// inspected so that executable code gets a branch filter and text does not.
/// </para>
/// </remarks>
internal static class SevenZipCreateRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("books.7z");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, "books", null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
context.Say("wrote {0} entries as {1}", writer.EntryCount,
RecipeContext.Readable(new FileInfo(archivePath).Length));
}
// Opening is the same call as for a ZIP: the format comes from the archive's own signature rather
// than from its name, so an archive misnamed .zip still opens as what it actually is.
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
context.Say("opened as {0} with {1} entries", archive.Format, archive.Entries.Count);
var target = context.PathTo("unpacked");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
if (!SameTree(source, Path.Combine(target, "books"), context))
{
return RecipeOutcome.Failed;
}
}
context.Say("every file came back with the same bytes");
return RecipeOutcome.Passed;
}
/// <summary>Compares two trees file by file, which is the only check worth making here.</summary>
internal static bool SameTree(string expected, string actual, RecipeContext context)
{
foreach (var original in Directory.GetFiles(expected, "*", SearchOption.AllDirectories))
{
var relative = original.Substring(expected.Length).TrimStart(Path.DirectorySeparatorChar);
var produced = Path.Combine(actual, relative);
if (!File.Exists(produced))
{
context.Say("missing after extraction: {0}", relative);
return false;
}
if (!RecipeContext.SameBytes(File.ReadAllBytes(original), File.ReadAllBytes(produced)))
{
context.Say("different after extraction: {0}", relative);
return false;
}
}
return true;
}
}
}Encrypt a 7z with AES-256, and then encrypt its header so the names go too.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Encrypt a 7z with AES-256, and then encrypt its header so the names go too.
''' </summary>
''' <remarks>
''' This is the one place 7z is plainly better than ZIP. A password-protected ZIP hides the *contents* of
''' its entries and publishes their **names, sizes and structure** to anyone who opens it — the central
''' directory has to stay readable for the archive to be an archive. 7z can encrypt the header too, and
''' then an archive without the password is a wall: no names, no sizes, no count.
''' <para>
''' Both use AES-256 over a key derived from the password, so the strength is the password's. The recipe
''' shows the refusal as well as the success, because an encrypted archive that opens without the
''' password is not encrypted, and that is worth proving rather than assuming.
''' </para>
''' </remarks>
Friend Module SevenZipEncryptionRecipe
Private Const Password As String = "correct horse battery staple"
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
If Not Write(context, source, "content-only.7z", encryptHeader:=False) Then Return RecipeOutcome.Failed
If Not Write(context, source, "names-too.7z", encryptHeader:=True) Then Return RecipeOutcome.Failed
' With the content encrypted but the header in the clear, the names are readable by anyone.
Dim listed = Archive.Open(context.PathTo("content-only.7z"))
If Not listed.Succeeded Then
context.Say(listed.ToString())
Return RecipeOutcome.Failed
End If
Using archive = listed.Archive
context.Say("without the password, content-only.7z still lists {0} entries, first '{1}'",
archive.Entries.Count, archive.Entries(0).Name)
End Using
' With the header encrypted there is nothing to list without the password.
Dim blind = Archive.Open(context.PathTo("names-too.7z"))
If blind.Succeeded Then
Using blind.Archive
End Using
context.Say("names-too.7z listed without a password, which defeats the point of -mhe")
Return RecipeOutcome.Failed
End If
context.Say("without the password, names-too.7z does not open at all: {0}", blind.ErrorCode)
' And with it, everything comes back.
Dim options As New ArchiveOpenOptions()
options.Password = Password
Dim opened = Archive.Open(context.PathTo("names-too.7z"), options)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim target = context.PathTo("unpacked")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
If Not SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "books"), context) Then
Return RecipeOutcome.Failed
End If
End Using
context.Say("with the password, every file came back with the same bytes")
Return RecipeOutcome.Passed
End Function
''' <summary>Writes one encrypted archive.</summary>
Private Function Write(context As RecipeContext, source As String, name As String,
encryptHeader As Boolean) As Boolean
Dim settings As New CompressionSettings()
settings.Password = Password
settings.EncryptHeaders = encryptHeader
settings.Level = 5
Dim started = ArchiveWriter.Create(context.PathTo(name), settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return False
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, "books", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return False
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return False
End If
End Using
context.Say("wrote {0} ({1})", name,
If(encryptHeader, "content and names encrypted", "content encrypted, names readable"))
Return True
End Function
End Module
End NamespaceC#
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Encrypt a 7z with AES-256, and then encrypt its header so the names go too.
/// </summary>
/// <remarks>
/// This is the one place 7z is plainly better than ZIP. A password-protected ZIP hides the <i>contents</i>
/// of its entries and publishes their <b>names, sizes and structure</b> to anyone who opens it — the
/// central directory has to stay readable for the archive to be an archive. 7z can encrypt the header
/// too, and then an archive without the password is a wall: no names, no sizes, no count.
/// <para>
/// Both use AES-256 over a key derived from the password, so the strength is the password's. The recipe
/// shows the refusal as well as the success, because an encrypted archive that opens without the password
/// is not encrypted, and that is worth proving rather than assuming.
/// </para>
/// </remarks>
internal static class SevenZipEncryptionRecipe
{
private const string Password = "correct horse battery staple";
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
if (!Write(context, source, "content-only.7z", encryptHeader: false))
{
return RecipeOutcome.Failed;
}
if (!Write(context, source, "names-too.7z", encryptHeader: true))
{
return RecipeOutcome.Failed;
}
// With the content encrypted but the header in the clear, the names are readable by anyone.
var listed = Archive.Open(context.PathTo("content-only.7z"));
if (!listed.Succeeded)
{
context.Say(listed.ToString());
return RecipeOutcome.Failed;
}
using (var archive = listed.Archive)
{
context.Say("without the password, content-only.7z still lists {0} entries, first '{1}'",
archive.Entries.Count, archive.Entries[0].Name);
}
// With the header encrypted there is nothing to list without the password.
var blind = Archive.Open(context.PathTo("names-too.7z"));
if (blind.Succeeded)
{
blind.Archive.Dispose();
context.Say("names-too.7z listed without a password, which defeats the point of -mhe");
return RecipeOutcome.Failed;
}
context.Say("without the password, names-too.7z does not open at all: {0}", blind.ErrorCode);
// And with it, everything comes back.
var options = new ArchiveOpenOptions { Password = Password };
var opened = Archive.Open(context.PathTo("names-too.7z"), options);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var target = context.PathTo("unpacked");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
if (!SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "books"), context))
{
return RecipeOutcome.Failed;
}
}
context.Say("with the password, every file came back with the same bytes");
return RecipeOutcome.Passed;
}
/// <summary>Writes one encrypted archive.</summary>
private static bool Write(RecipeContext context, string source, string name, bool encryptHeader)
{
var settings = new CompressionSettings
{
Password = Password,
EncryptHeaders = encryptHeader,
Level = 5
};
var started = ArchiveWriter.Create(context.PathTo(name), settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return false;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, "books", null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return false;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return false;
}
}
context.Say("wrote {0} ({1})", name,
encryptHeader ? "content and names encrypted" : "content encrypted, names readable");
return true;
}
}
}Write the same files with each of 7z's six coders, and see what each one costs.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write the same files with each of 7z's six coders, and see what each one costs.
''' </summary>
''' <remarks>
''' 7z carries six: Copy, Deflate, BZip2, LZMA, LZMA2 and PPMd. LZMA2 is the default and the right answer
''' almost always — it is LZMA with a framing that lets it be split across threads. The two worth knowing
''' about are the ends of the range: **Copy** stores, which is what you want for data that is already
''' compressed, and **PPMd** models text far better than anything else here, at a cost in memory and
''' speed that only text repays.
''' <para>
''' A method the format has no coder for — Deflate64, Xz, Zstandard, all of which this library writes in
''' other formats — is **refused by name** rather than quietly replaced. A caller who asked for Zstandard
''' and silently received LZMA2 would have an archive that works and settings that lie about it.
''' </para>
''' </remarks>
Friend Module SevenZipMethodsRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim sizes As New List(Of String)()
For Each method In New CompressionMethod() {CompressionMethod.Store, CompressionMethod.Deflate,
CompressionMethod.BZip2, CompressionMethod.Lzma,
CompressionMethod.Lzma2, CompressionMethod.Ppmd}
Dim archivePath = context.PathTo("by-" & method.ToString().ToLowerInvariant() & ".7z")
Dim settings As New CompressionSettings()
settings.Method = method
' Store is the one method that must be asked for at level 0; a compressing level with Copy is
' a contradiction the writer refuses rather than resolves.
settings.Level = If(method = CompressionMethod.Store, 0, 5)
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, "books", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim length = New FileInfo(archivePath).Length
sizes.Add(method.ToString() & " " & RecipeContext.Readable(length))
context.Say("{0,-8} {1}", method, RecipeContext.Readable(length))
If Not ReadsBack(archivePath, source, context) Then Return RecipeOutcome.Failed
Next
' The refusal is part of the contract, so it is shown rather than described.
Dim refusedSettings As New CompressionSettings()
refusedSettings.Method = CompressionMethod.Zstd
Dim refused = ArchiveWriter.Create(context.PathTo("impossible.7z"), refusedSettings)
If refused.Succeeded Then
Using refused.Writer
End Using
context.Say("expected Zstandard in a 7z to be refused, and it was not")
Return RecipeOutcome.Failed
End If
context.Say("asking for Zstandard in a 7z is refused: {0}", refused.ErrorDescription)
Return RecipeOutcome.Passed
End Function
''' <summary>Opens an archive and checks every file survived the round trip.</summary>
Private Function ReadsBack(archivePath As String, source As String, context As RecipeContext) As Boolean
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return False
End If
Using archive = opened.Archive
Dim target = archivePath & ".out"
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return False
End If
Return SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "books"), context)
End Using
End Function
End Module
End NamespaceC#
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write the same files with each of 7z's six coders, and see what each one costs.
/// </summary>
/// <remarks>
/// 7z carries six: Copy, Deflate, BZip2, LZMA, LZMA2 and PPMd. LZMA2 is the default and the right answer
/// almost always — it is LZMA with a framing that lets it be split across threads. The two worth knowing
/// about are the ends of the range: <b>Copy</b> stores, which is what you want for data that is already
/// compressed, and <b>PPMd</b> models text far better than anything else here, at a cost in memory and
/// speed that only text repays.
/// <para>
/// A method the format has no coder for — Deflate64, Xz, Zstandard, all of which this library writes in
/// other formats — is <b>refused by name</b> rather than quietly replaced. A caller who asked for
/// Zstandard and silently received LZMA2 would have an archive that works and settings that lie about it.
/// </para>
/// </remarks>
internal static class SevenZipMethodsRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var methods = new[]
{
CompressionMethod.Store, CompressionMethod.Deflate, CompressionMethod.BZip2,
CompressionMethod.Lzma, CompressionMethod.Lzma2, CompressionMethod.Ppmd
};
foreach (var method in methods)
{
var archivePath = context.PathTo("by-" + method.ToString().ToLowerInvariant() + ".7z");
var settings = new CompressionSettings
{
Method = method,
// Store is the one method that must be asked for at level 0; a compressing level with
// Copy is a contradiction the writer refuses rather than resolves.
Level = method == CompressionMethod.Store ? 0 : 5
};
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, "books", null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
context.Say("{0,-8} {1}", method, RecipeContext.Readable(new FileInfo(archivePath).Length));
if (!ReadsBack(archivePath, source, context))
{
return RecipeOutcome.Failed;
}
}
// The refusal is part of the contract, so it is shown rather than described.
var refusedSettings = new CompressionSettings { Method = CompressionMethod.Zstd };
var refused = ArchiveWriter.Create(context.PathTo("impossible.7z"), refusedSettings);
if (refused.Succeeded)
{
refused.Writer.Dispose();
context.Say("expected Zstandard in a 7z to be refused, and it was not");
return RecipeOutcome.Failed;
}
context.Say("asking for Zstandard in a 7z is refused: {0}", refused.ErrorDescription);
return RecipeOutcome.Passed;
}
/// <summary>Opens an archive and checks every file survived the round trip.</summary>
private static bool ReadsBack(string archivePath, string source, RecipeContext context)
{
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return false;
}
using (var archive = opened.Archive)
{
var target = archivePath + ".out";
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return false;
}
return SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "books"), context);
}
}
}
}Solid or not: the trade 7z makes that ZIP cannot, and what it costs either way.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Solid or not: the trade 7z makes that ZIP cannot, and what it costs either way.
''' </summary>
''' <remarks>
''' A ZIP compresses every entry on its own, so any entry can be read without touching the others and
''' nothing learns from what came before. 7z compresses entries together in a **folder** — a run of
''' entries treated as one stream — so a hundred similar files compress against each other and the
''' archive is far smaller.
''' <para>
''' The price is paid on reading. Reaching the fiftieth entry of a solid folder means decoding the
''' forty-nine before it, because there is no index into the middle of an LZMA stream. That is fine for
''' an archive read from front to back, and wrong for one a program dips into. <c>SolidMode.NonSolid</c>
''' puts each entry in a folder of its own and buys back the random access.
''' </para>
''' <para>
''' The numbers this prints are the argument. Nothing here is a rule of thumb: run it on your own data.
''' </para>
''' </remarks>
Friend Module SevenZipSolidRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim solidBytes = 0L
Dim looseBytes = 0L
For Each mode In New SolidMode() {SolidMode.Automatic, SolidMode.NonSolid}
Dim archivePath = context.PathTo(If(mode = SolidMode.NonSolid, "loose.7z", "solid.7z"))
Dim settings As New CompressionSettings()
settings.Solid = mode
settings.Level = 5
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, "books", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim length = New FileInfo(archivePath).Length
If mode = SolidMode.NonSolid Then looseBytes = length Else solidBytes = length
context.Say("{0,-10} {1}", mode, RecipeContext.Readable(length))
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim target = archivePath & ".out"
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
If Not SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "books"), context) Then
Return RecipeOutcome.Failed
End If
End Using
Next
If solidBytes > 0L AndAlso looseBytes > 0L Then
context.Say("solid is {0:0.0}% the size of one-folder-per-entry, and the cost is that reaching",
100.0R * solidBytes / looseBytes)
context.Say("any entry means decoding the ones before it in its folder")
End If
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Solid or not: the trade 7z makes that ZIP cannot, and what it costs either way.
/// </summary>
/// <remarks>
/// A ZIP compresses every entry on its own, so any entry can be read without touching the others and
/// nothing learns from what came before. 7z compresses entries together in a <b>folder</b> — a run of
/// entries treated as one stream — so a hundred similar files compress against each other and the
/// archive is far smaller.
/// <para>
/// The price is paid on reading. Reaching the fiftieth entry of a solid folder means decoding the
/// forty-nine before it, because there is no index into the middle of an LZMA stream. That is fine for an
/// archive read from front to back, and wrong for one a program dips into. <c>SolidMode.NonSolid</c> puts
/// each entry in a folder of its own and buys back the random access.
/// </para>
/// <para>
/// The numbers this prints are the argument. Nothing here is a rule of thumb: run it on your own data.
/// </para>
/// </remarks>
internal static class SevenZipSolidRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
long solidBytes = 0;
long looseBytes = 0;
foreach (var mode in new[] { SolidMode.Automatic, SolidMode.NonSolid })
{
var archivePath = context.PathTo(mode == SolidMode.NonSolid ? "loose.7z" : "solid.7z");
var settings = new CompressionSettings { Solid = mode, Level = 5 };
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, "books", null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var length = new FileInfo(archivePath).Length;
if (mode == SolidMode.NonSolid)
{
looseBytes = length;
}
else
{
solidBytes = length;
}
context.Say("{0,-10} {1}", mode, RecipeContext.Readable(length));
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var target = archivePath + ".out";
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
if (!SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "books"), context))
{
return RecipeOutcome.Failed;
}
}
}
if (solidBytes > 0 && looseBytes > 0)
{
context.Say("solid is {0:0.0}% the size of one-folder-per-entry, and the cost is that reaching",
100.0 * solidBytes / looseBytes);
context.Say("any entry means decoding the ones before it in its folder");
}
return RecipeOutcome.Passed;
}
}
}Remove, rename and add inside a <c>.7z</c> — the same <see cref="ArchiveUpdate"/> calls as for a ZIP.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Linq
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Remove, rename and add inside a <c>.7z</c> — the same <see cref="ArchiveUpdate"/> calls as for a ZIP.
''' </summary>
''' <remarks>
''' The difference is underneath, and worth knowing. A ZIP update carries untouched entries across still
''' compressed; a 7z one cannot, because a solid 7z compresses its entries together and taking one out
''' changes the bytes of everything after it. So a 7z update decodes what it keeps and writes it again,
''' with the settings you pass governing the whole result — and the settings' password is also what opens
''' the source if it is encrypted. Like every update, it writes a new file and never touches the original.
''' </remarks>
Friend Module SevenZipUpdateRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("books.7z")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then
context.Say("the starting archive could not be written")
Return RecipeOutcome.Failed
End If
End Using
Dim cover = context.PathTo("cover.txt")
File.WriteAllText(cover, "a cover added by the update")
Dim update As New ArchiveUpdate(archivePath)
update.Remove("data.bin")
update.Rename("readme.txt", "docs/readme.txt")
update.AddOrReplaceFile(cover, "cover.txt")
Dim updatedPath = context.PathTo("books-updated.7z")
Dim result = update.Apply(updatedPath, Nothing)
If Not result.Succeeded Then
context.Say(result.ToString())
Return RecipeOutcome.Failed
End If
Dim opened = Archive.Open(updatedPath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim names As New List(Of String)()
For Each entry In archive.Entries
If Not entry.IsDirectory Then names.Add(entry.Name)
Next
names.Sort(StringComparer.Ordinal)
context.Say("the updated archive holds {0}", String.Join(", ", names))
Dim wanted = New String() {"cover.txt", "docs/readme.txt", "documents/notes.txt"}
If Not names.SequenceEqual(wanted) Then
context.Say("expected {0}", String.Join(", ", wanted))
Return RecipeOutcome.Failed
End If
Dim target = context.PathTo("unpacked")
If Not archive.ExtractAll(target).Succeeded Then Return RecipeOutcome.Failed
If File.ReadAllText(Path.Combine(target, "docs", "readme.txt")) <> File.ReadAllText(Path.Combine(source, "readme.txt")) Then
context.Say("the renamed entry did not keep its content")
Return RecipeOutcome.Failed
End If
End Using
context.Say("removed, renamed and added; the original is unchanged at {0}",
RecipeContext.Readable(New FileInfo(archivePath).Length))
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Remove, rename and add inside a <c>.7z</c> — the same <see cref="ArchiveUpdate"/> calls as for a ZIP.
/// </summary>
/// <remarks>
/// The difference is underneath, and worth knowing. A ZIP update carries untouched entries across still
/// compressed; a 7z one cannot, because a solid 7z compresses its entries together and taking one out
/// changes the bytes of everything after it. So a 7z update decodes what it keeps and writes it again,
/// with the settings you pass governing the whole result — and the settings' password is also what opens
/// the source if it is encrypted. Like every update, it writes a new file and never touches the original.
/// </remarks>
internal static class SevenZipUpdateRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("books.7z");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded)
{
context.Say("the starting archive could not be written");
return RecipeOutcome.Failed;
}
}
var cover = context.PathTo("cover.txt");
File.WriteAllText(cover, "a cover added by the update");
var update = new ArchiveUpdate(archivePath);
update.Remove("data.bin");
update.Rename("readme.txt", "docs/readme.txt");
update.AddOrReplaceFile(cover, "cover.txt");
var updatedPath = context.PathTo("books-updated.7z");
var result = update.Apply(updatedPath, null);
if (!result.Succeeded)
{
context.Say(result.ToString());
return RecipeOutcome.Failed;
}
var opened = Archive.Open(updatedPath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var names = new List<string>();
foreach (var entry in archive.Entries)
{
if (!entry.IsDirectory) names.Add(entry.Name);
}
names.Sort(StringComparer.Ordinal);
context.Say("the updated archive holds {0}", string.Join(", ", names));
var wanted = new[] { "cover.txt", "docs/readme.txt", "documents/notes.txt" };
if (!names.SequenceEqual(wanted))
{
context.Say("expected {0}", string.Join(", ", wanted));
return RecipeOutcome.Failed;
}
var target = context.PathTo("unpacked");
if (!archive.ExtractAll(target).Succeeded) return RecipeOutcome.Failed;
if (File.ReadAllText(Path.Combine(target, "docs", "readme.txt")) != File.ReadAllText(Path.Combine(source, "readme.txt")))
{
context.Say("the renamed entry did not keep its content");
return RecipeOutcome.Failed;
}
}
context.Say("removed, renamed and added; the original is unchanged at {0}",
RecipeContext.Readable(new FileInfo(archivePath).Length));
return RecipeOutcome.Passed;
}
}
}Span an archive across removable disks, asking for each one as it is needed, and read it back the same way.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Diagnostics
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Span an archive across removable disks, asking for each one as it is needed, and read it back the same way.
''' </summary>
''' <remarks>
''' <see cref="OperationMonitor.VolumeNeeded"/> is the prompt a backup program shows: "insert disk 3". Here a
''' folder plays the drive and another the shelf of disks, and every disk carries the archive's own name, as
''' PKZIP's floppies did. Writing closes each disk before asking for the next; reading asks for each disk as
''' its bytes are wanted and refuses one that is not the disk it asked for. On a real drive the handler would
''' show a message box and leave <see cref="VolumeNeededEventArgs.Path"/> alone. To fill each disk rather than
''' cut it at a fixed size, pass <see cref="ArchiveWriter.FillEachMedium"/> as the volume size.
''' </remarks>
Friend Module SpannedDisksRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.PathTo("documents")
Directory.CreateDirectory(source)
For index = 1 To 5
File.WriteAllBytes(Path.Combine(source, $"scan{index}.bin"), RecipeContext.PseudoRandom(60000, index))
Next
Dim drive = context.PathTo("drive")
Dim shelf = context.PathTo("shelf")
Directory.CreateDirectory(drive)
Dim archivePath = Path.Combine(drive, "backup.zip")
' Writing: when a disk is full, it goes on the shelf and a blank one goes in.
Dim writing As New OperationMonitor()
AddHandler writing.VolumeNeeded,
Sub(sender, e)
Dim full = Path.Combine(shelf, $"disk{e.VolumeNumber - 1}")
Directory.CreateDirectory(full)
File.Move(Directory.GetFiles(drive)(0), Path.Combine(full, "backup.zip"))
End Sub
Dim started = ArchiveWriter.CreateSpanned(archivePath, 65536, Nothing, writing)
If Not started.Succeeded Then Return RecipeOutcome.Failed
Using writer = started.Writer
If Not writer.AddDirectory(source, Nothing, Nothing).Succeeded OrElse Not writer.Complete().Succeeded Then Return RecipeOutcome.Failed
End Using
Dim disks = Directory.GetDirectories(shelf).Length + 1
Directory.CreateDirectory(Path.Combine(shelf, $"disk{disks}"))
File.Copy(archivePath, Path.Combine(shelf, $"disk{disks}", "backup.zip"))
context.Say("{0} files spanned across {1} disks of 64 KiB", 5, disks)
' Reading: the last disk is in the drive; every other one is asked for when it is wanted.
Dim changes = 0
Dim reading As New OperationMonitor()
AddHandler reading.VolumeNeeded,
Sub(sender, e)
Dim wanted = If(e.VolumeNumber = 0, disks, e.VolumeNumber)
File.Copy(Path.Combine(shelf, $"disk{wanted}", "backup.zip"), archivePath, overwrite:=True)
e.Path = archivePath
changes += 1
End Sub
Dim opened = Archive.Open(archivePath, New ArchiveOpenOptions() With {.Monitor = reading})
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Dim restored = context.PathTo("restored")
Using archive = opened.Archive
Dim result = archive.ExtractAll(restored)
If Not result.Succeeded Then
context.Say(result.ToString())
Return RecipeOutcome.Failed
End If
End Using
For index = 1 To 5
Dim name = $"scan{index}.bin"
If Not File.ReadAllBytes(Path.Combine(source, name)).AsSpan().SequenceEqual(File.ReadAllBytes(Path.Combine(restored, name))) Then Return RecipeOutcome.Failed
Next
context.Say("read back byte for byte after {0} disk changes", changes)
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using Bastion.Archive.Diagnostics;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Span an archive across removable disks, asking for each one as it is needed, and read it back the same way.
/// </summary>
/// <remarks>
/// <see cref="OperationMonitor.VolumeNeeded"/> is the prompt a backup program shows: "insert disk 3". Here a
/// folder plays the drive and another the shelf of disks, and every disk carries the archive's own name, as
/// PKZIP's floppies did. Writing closes each disk before asking for the next; reading asks for each disk as
/// its bytes are wanted and refuses one that is not the disk it asked for. On a real drive the handler would
/// show a message box and leave <see cref="VolumeNeededEventArgs.Path"/> alone. To fill each disk rather than
/// cut it at a fixed size, pass <see cref="ArchiveWriter.FillEachMedium"/> as the volume size.
/// </remarks>
internal static class SpannedDisksRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.PathTo("documents");
Directory.CreateDirectory(source);
for (var index = 1; index <= 5; index++)
{
File.WriteAllBytes(Path.Combine(source, $"scan{index}.bin"), RecipeContext.PseudoRandom(60000, index));
}
var drive = context.PathTo("drive");
var shelf = context.PathTo("shelf");
Directory.CreateDirectory(drive);
var archivePath = Path.Combine(drive, "backup.zip");
// Writing: when a disk is full, it goes on the shelf and a blank one goes in.
var writing = new OperationMonitor();
writing.VolumeNeeded += (sender, e) =>
{
var full = Path.Combine(shelf, $"disk{e.VolumeNumber - 1}");
Directory.CreateDirectory(full);
File.Move(Directory.GetFiles(drive)[0], Path.Combine(full, "backup.zip"));
};
var started = ArchiveWriter.CreateSpanned(archivePath, 65536, null, writing);
if (!started.Succeeded) return RecipeOutcome.Failed;
using (var writer = started.Writer)
{
if (!writer.AddDirectory(source, null, null).Succeeded || !writer.Complete().Succeeded) return RecipeOutcome.Failed;
}
var disks = Directory.GetDirectories(shelf).Length + 1;
Directory.CreateDirectory(Path.Combine(shelf, $"disk{disks}"));
File.Copy(archivePath, Path.Combine(shelf, $"disk{disks}", "backup.zip"));
context.Say("{0} files spanned across {1} disks of 64 KiB", 5, disks);
// Reading: the last disk is in the drive; every other one is asked for when it is wanted.
var changes = 0;
var reading = new OperationMonitor();
reading.VolumeNeeded += (sender, e) =>
{
var wanted = e.VolumeNumber == 0 ? disks : e.VolumeNumber;
File.Copy(Path.Combine(shelf, $"disk{wanted}", "backup.zip"), archivePath, overwrite: true);
e.Path = archivePath;
changes++;
};
var opened = Archive.Open(archivePath, new ArchiveOpenOptions { Monitor = reading });
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
var restored = context.PathTo("restored");
using (var archive = opened.Archive)
{
var result = archive.ExtractAll(restored);
if (!result.Succeeded)
{
context.Say(result.ToString());
return RecipeOutcome.Failed;
}
}
for (var index = 1; index <= 5; index++)
{
var name = $"scan{index}.bin";
if (!File.ReadAllBytes(Path.Combine(source, name)).AsSpan().SequenceEqual(File.ReadAllBytes(Path.Combine(restored, name)))) return RecipeOutcome.Failed;
}
context.Say("read back byte for byte after {0} disk changes", changes);
return RecipeOutcome.Passed;
}
}
}Write a split archive and read it back from its first volume.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write a split archive and read it back from its first volume.
''' </summary>
''' <remarks>
''' There are two kinds of split ZIP and they are not the same thing. The modern kind, which 7-Zip
''' writes and which this library writes, is one ordinary archive cut into <c>name.zip.001</c>,
''' <c>name.zip.002</c> and so on; any reader that can concatenate them can read it. The older PKWARE kind
''' uses <c>name.z01</c>, <c>name.z02</c>, <c>name.zip</c>, records a disk number in every central header
''' and lets an entry's data cross a volume boundary. This library reads both, and the two are told apart by
''' the end record's own disk number.
''' Opening takes any volume of the set: the reader finds the others by name and keeps exactly one of them
''' open at a time, so a hundred-volume set costs one file handle.
''' </remarks>
Friend Module SplitVolumesRecipe
''' <summary>The smallest volume the writer accepts, which makes a small sample produce several.</summary>
Private Const VolumeSize As Long = 65536
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
' Enough incompressible data to need several volumes at 64 KiB each.
File.WriteAllBytes(Path.Combine(source, "payload.bin"), RecipeContext.PseudoRandom(300000, 7))
Dim archivePath = context.PathTo("split.zip")
Dim started = ArchiveWriter.CreateSplit(archivePath, VolumeSize, Nothing, Nothing)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim volumes = Directory.GetFiles(context.WorkingDirectory, "split.zip.*")
Array.Sort(volumes, StringComparer.Ordinal)
context.Say("{0} volumes at {1} each:", volumes.Length, RecipeContext.Readable(VolumeSize))
For Each volume In volumes
context.Say(" {0} {1}", Path.GetFileName(volume), RecipeContext.Readable(New FileInfo(volume).Length))
Next
If volumes.Length < 2 Then
context.Say("the sample did not need splitting, so this recipe proved nothing")
Return RecipeOutcome.Failed
End If
' No volume is bigger than asked for. That is the whole promise of a volume size.
For Each volume In volumes
If New FileInfo(volume).Length > VolumeSize Then
context.Say("a volume exceeded the size asked for: " & Path.GetFileName(volume))
Return RecipeOutcome.Failed
End If
Next
' Opening from the first volume; the reader finds the rest by name.
Dim opened = Archive.Open(volumes(0))
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
context.Say("opened from {0}: {1} entries across {2} volumes",
Path.GetFileName(volumes(0)), archive.Entries.Count, archive.VolumeCount)
If archive.VolumeCount <> volumes.Length Then
context.Say("the reader did not find every volume")
Return RecipeOutcome.Failed
End If
Dim target = context.PathTo("out")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
Dim expected = File.ReadAllBytes(Path.Combine(source, "payload.bin"))
Dim recovered = File.ReadAllBytes(Path.Combine(target, "payload.bin"))
If Not RecipeContext.SameBytes(expected, recovered) Then
context.Say("the payload did not survive the volume boundaries")
Return RecipeOutcome.Failed
End If
context.Say("payload.bin came back byte for byte across {0} volumes", archive.VolumeCount)
End Using
' A missing volume is reported as a missing volume, not as a corrupt archive.
Dim removed = volumes(volumes.Length - 1)
Dim kept = removed & ".kept"
File.Move(removed, kept)
Dim incomplete = Archive.Open(volumes(0))
If incomplete.Succeeded Then
Using archive = incomplete.Archive
Dim tested = archive.Test(Nothing)
context.Say("with the last volume missing, the test returned {0}", tested.ErrorCode)
If tested.Succeeded Then
context.Say("an incomplete set should not pass")
Return RecipeOutcome.Failed
End If
End Using
Else
context.Say("with the last volume missing, opening returned {0}", incomplete.ErrorCode)
End If
File.Move(kept, removed)
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write a split archive and read it back from its first volume.
/// </summary>
/// <remarks>
/// There are two kinds of split ZIP and they are not the same thing. The modern kind, which 7-Zip
/// writes and which this library writes, is one ordinary archive cut into <c>name.zip.001</c>,
/// <c>name.zip.002</c> and so on; any reader that can concatenate them can read it. The older PKWARE kind
/// uses <c>name.z01</c>, <c>name.z02</c>, <c>name.zip</c>, records a disk number in every central header
/// and lets an entry's data cross a volume boundary. This library reads both, and the two are told apart by
/// the end record's own disk number.
/// Opening takes any volume of the set: the reader finds the others by name and keeps exactly one of them
/// open at a time, so a hundred-volume set costs one file handle.
/// </remarks>
internal static class SplitVolumesRecipe
{
/// <summary>The smallest volume the writer accepts, which makes a small sample produce several.</summary>
private const long VolumeSize = 65536;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
// Enough incompressible data to need several volumes at 64 KiB each.
File.WriteAllBytes(Path.Combine(source, "payload.bin"), RecipeContext.PseudoRandom(300000, 7));
var archivePath = context.PathTo("split.zip");
var started = ArchiveWriter.CreateSplit(archivePath, VolumeSize, null, null);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var volumes = Directory.GetFiles(context.WorkingDirectory, "split.zip.*");
Array.Sort(volumes, StringComparer.Ordinal);
context.Say("{0} volumes at {1} each:", volumes.Length, RecipeContext.Readable(VolumeSize));
foreach (var volume in volumes)
{
context.Say(" {0} {1}", Path.GetFileName(volume), RecipeContext.Readable(new FileInfo(volume).Length));
}
if (volumes.Length < 2)
{
context.Say("the sample did not need splitting, so this recipe proved nothing");
return RecipeOutcome.Failed;
}
// No volume is bigger than asked for. That is the whole promise of a volume size.
foreach (var volume in volumes)
{
if (new FileInfo(volume).Length > VolumeSize)
{
context.Say("a volume exceeded the size asked for: " + Path.GetFileName(volume));
return RecipeOutcome.Failed;
}
}
// Opening from the first volume; the reader finds the rest by name.
var opened = Archive.Open(volumes[0]);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
context.Say("opened from {0}: {1} entries across {2} volumes",
Path.GetFileName(volumes[0]), archive.Entries.Count, archive.VolumeCount);
if (archive.VolumeCount != volumes.Length)
{
context.Say("the reader did not find every volume");
return RecipeOutcome.Failed;
}
var target = context.PathTo("out");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
var expected = File.ReadAllBytes(Path.Combine(source, "payload.bin"));
var recovered = File.ReadAllBytes(Path.Combine(target, "payload.bin"));
if (!RecipeContext.SameBytes(expected, recovered))
{
context.Say("the payload did not survive the volume boundaries");
return RecipeOutcome.Failed;
}
context.Say("payload.bin came back byte for byte across {0} volumes", archive.VolumeCount);
}
// A missing volume is reported as a missing volume, not as a corrupt archive.
var removed = volumes[volumes.Length - 1];
var kept = removed + ".kept";
File.Move(removed, kept);
var incomplete = Archive.Open(volumes[0]);
if (incomplete.Succeeded)
{
using (var archive = incomplete.Archive)
{
var tested = archive.Test(null);
context.Say("with the last volume missing, the test returned {0}", tested.ErrorCode);
if (tested.Succeeded)
{
context.Say("an incomplete set should not pass");
return RecipeOutcome.Failed;
}
}
}
else
{
context.Say("with the last volume missing, opening returned {0}", incomplete.ErrorCode);
}
File.Move(kept, removed);
return RecipeOutcome.Passed;
}
}
}Write and read an archive that never touches the disk.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Collections.Generic
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.Diagnostics
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write and read an archive that never touches the disk.
''' </summary>
''' <remarks>
''' Both ends of the library take a stream, so an archive can be built into memory, into a response body or
''' into a network socket, and read back from one. Two rules apply to a caller-supplied stream: the library
''' never disposes it, because it did not open it, and writing needs a stream it can seek back over to
''' rewrite each local header once the entry's true size is known.
''' This is also the recipe that shows the events. The monitor raises <c>EntryStarted</c>,
''' <c>EntryCompleted</c>, <c>Progress</c> and <c>LogMessage</c>, and marshals each one through the
''' synchronisation context it captured when it was built — which is why a user interface can update a
''' progress bar straight from a handler without an <c>Invoke</c> of its own.
''' </remarks>
Friend Module StreamingWriterRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim documents As New Dictionary(Of String, Byte())(StringComparer.Ordinal) From {
{"reports/january.txt", Encoding.UTF8.GetBytes(New String("J"c, 30000))},
{"reports/february.txt", Encoding.UTF8.GetBytes(New String("F"c, 25000))},
{"images/noise.bin", RecipeContext.PseudoRandom(12000, 3)}
}
Dim started As List(Of String) = New List(Of String)()
Dim completed As New List(Of String)()
Dim progressReports = 0
Dim monitor As New OperationMonitor()
AddHandler monitor.EntryStarted, Sub(sender, e) started.Add(e.EntryPath)
AddHandler monitor.EntryCompleted, Sub(sender, e) completed.Add(e.EntryPath)
AddHandler monitor.Progress, Sub(sender, e) progressReports += 1
' Progress is throttled so a handler cannot be swamped; for a sample that finishes in milliseconds,
' asking for every report is what makes the events visible at all.
monitor.ProgressInterval = TimeSpan.Zero
Dim archiveBytes As Byte()
Using buffer As New MemoryStream()
Dim starting = ArchiveWriter.Create(buffer, Nothing, monitor)
If Not starting.Succeeded Then
context.Say(starting.ToString())
Return RecipeOutcome.Failed
End If
Using writer = starting.Writer
For Each document In documents
Using source As New MemoryStream(document.Value, False)
Dim added = writer.AddStream(source, document.Key, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
End Using
Next
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
' The stream is still open and still ours: the writer never disposes what it was handed.
If Not buffer.CanRead Then
context.Say("the writer disposed a stream it did not own")
Return RecipeOutcome.Failed
End If
archiveBytes = buffer.ToArray()
End Using
context.Say("built a {0} archive entirely in memory", RecipeContext.Readable(archiveBytes.Length))
context.Say("events: {0} entries started, {1} completed, {2} progress reports",
started.Count, completed.Count, progressReports)
If started.Count <> documents.Count OrElse completed.Count <> documents.Count Then
context.Say("every entry should have raised both events")
Return RecipeOutcome.Failed
End If
If progressReports = 0 Then
context.Say("no progress was reported at all")
Return RecipeOutcome.Failed
End If
' Read it back from a stream too, without ever writing a file.
Using buffer As New MemoryStream(archiveBytes, False)
Dim options As New ArchiveOpenOptions() With {.LeaveStreamOpen = True}
Dim opened = Archive.Open(buffer, options)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
For Each entry In archive.Entries
Using recovered As New MemoryStream()
Dim streamed = archive.ExtractToStream(entry, recovered, Nothing)
If Not streamed.Succeeded Then
context.Say(streamed.ToString())
Return RecipeOutcome.Failed
End If
If Not RecipeContext.SameBytes(documents(entry.Name), recovered.ToArray()) Then
context.Say("'" & entry.Name & "' did not survive the round trip")
Return RecipeOutcome.Failed
End If
End Using
Next
context.Say("all {0} entries read back from memory and matched", archive.Entries.Count)
End Using
If Not buffer.CanRead Then
context.Say("LeaveStreamOpen was asked for and not honoured")
Return RecipeOutcome.Failed
End If
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Bastion.Archive.Diagnostics;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write and read an archive that never touches the disk.
/// </summary>
/// <remarks>
/// Both ends of the library take a stream, so an archive can be built into memory, into a response body or
/// into a network socket, and read back from one. Two rules apply to a caller-supplied stream: the library
/// never disposes it, because it did not open it, and writing needs a stream it can seek back over to
/// rewrite each local header once the entry's true size is known.
/// This is also the recipe that shows the events. The monitor raises <c>EntryStarted</c>,
/// <c>EntryCompleted</c>, <c>Progress</c> and <c>LogMessage</c>, and marshals each one through the
/// synchronisation context it captured when it was built — which is why a user interface can update a
/// progress bar straight from a handler without an <c>Invoke</c> of its own.
/// </remarks>
internal static class StreamingWriterRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var documents = new Dictionary<string, byte[]>(StringComparer.Ordinal)
{
{ "reports/january.txt", Encoding.UTF8.GetBytes(new string('J', 30000)) },
{ "reports/february.txt", Encoding.UTF8.GetBytes(new string('F', 25000)) },
{ "images/noise.bin", RecipeContext.PseudoRandom(12000, 3) }
};
var entriesStarted = new List<string>();
var entriesCompleted = new List<string>();
var progressReports = 0;
var monitor = new OperationMonitor();
monitor.EntryStarted += (sender, e) => entriesStarted.Add(e.EntryPath);
monitor.EntryCompleted += (sender, e) => entriesCompleted.Add(e.EntryPath);
monitor.Progress += (sender, e) => progressReports++;
// Progress is throttled so a handler cannot be swamped; for a sample that finishes in milliseconds,
// asking for every report is what makes the events visible at all.
monitor.ProgressInterval = TimeSpan.Zero;
byte[] archiveBytes;
using (var buffer = new MemoryStream())
{
var starting = ArchiveWriter.Create(buffer, null, monitor);
if (!starting.Succeeded)
{
context.Say(starting.ToString());
return RecipeOutcome.Failed;
}
using (var writer = starting.Writer)
{
foreach (var document in documents)
{
using (var source = new MemoryStream(document.Value, false))
{
var added = writer.AddStream(source, document.Key, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
// The stream is still open and still ours: the writer never disposes what it was handed.
if (!buffer.CanRead)
{
context.Say("the writer disposed a stream it did not own");
return RecipeOutcome.Failed;
}
archiveBytes = buffer.ToArray();
}
context.Say("built a {0} archive entirely in memory", RecipeContext.Readable(archiveBytes.Length));
context.Say("events: {0} entries started, {1} completed, {2} progress reports",
entriesStarted.Count, entriesCompleted.Count, progressReports);
if (entriesStarted.Count != documents.Count || entriesCompleted.Count != documents.Count)
{
context.Say("every entry should have raised both events");
return RecipeOutcome.Failed;
}
if (progressReports == 0)
{
context.Say("no progress was reported at all");
return RecipeOutcome.Failed;
}
// Read it back from a stream too, without ever writing a file.
using (var buffer = new MemoryStream(archiveBytes, false))
{
var options = new ArchiveOpenOptions { LeaveStreamOpen = true };
var opened = Archive.Open(buffer, options);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
foreach (var entry in archive.Entries)
{
using (var recovered = new MemoryStream())
{
var streamed = archive.ExtractToStream(entry, recovered, null);
if (!streamed.Succeeded)
{
context.Say(streamed.ToString());
return RecipeOutcome.Failed;
}
if (!RecipeContext.SameBytes(documents[entry.Name], recovered.ToArray()))
{
context.Say("'" + entry.Name + "' did not survive the round trip");
return RecipeOutcome.Failed;
}
}
}
context.Say("all {0} entries read back from memory and matched", archive.Entries.Count);
}
if (!buffer.CanRead)
{
context.Say("LeaveStreamOpen was asked for and not honoured");
return RecipeOutcome.Failed;
}
}
return RecipeOutcome.Passed;
}
}
}Make and read a <c>.tar.gz</c> in one call each.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Formats.Tar
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Make and read a <c>.tar.gz</c> in one call each.
''' </summary>
''' <remarks>
''' A compressed tarball is two formats stacked: tar decides what the entries are, gzip decides how the
''' bytes travel, and neither knows about the other. <c>TarArchive</c> takes the compressor from the
''' archive's name, so <c>.tar.gz</c> and <c>.tgz</c> mean the same thing and neither needs a setting.
''' <para>
''' Like every archive-level call in this library it returns an <see cref="OperationResult"/> and never
''' throws at the caller, and it canonicalises entry paths on the way out, so an archive
''' carrying <c>../../etc/passwd</c> is refused rather than obeyed.
''' </para>
''' </remarks>
Friend Module TarGzRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("bundle.tar.gz")
Dim created = TarArchive.CreateFromDirectory(source, archivePath)
If Not created.Succeeded Then
context.Say(created.ToString())
Return RecipeOutcome.Failed
End If
Dim plain = New FileInfo(archivePath).Length
context.Say("wrote {0} from {1} of files", RecipeContext.Readable(plain), RecipeContext.Readable(SizeOf(source)))
' The short spelling means exactly the same thing, which is worth showing because half the world
' writes one and half the other.
Dim shortName = context.PathTo("bundle.tgz")
Dim alsoCreated = TarArchive.CreateFromDirectory(source, shortName)
If Not alsoCreated.Succeeded Then
context.Say(alsoCreated.ToString())
Return RecipeOutcome.Failed
End If
context.Say(".tgz is the same archive: {0}", RecipeContext.Readable(New FileInfo(shortName).Length))
Dim target = context.PathTo("unpacked")
Dim extracted = TarArchive.ExtractToDirectory(archivePath, target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
' The proof is the bytes, not the absence of an error.
If Not SameTree(source, target, context) Then Return RecipeOutcome.Failed
context.Say("every file came back with the same bytes")
Return RecipeOutcome.Passed
End Function
''' <summary>How much is in a directory, so the archive's size means something.</summary>
Friend Function SizeOf(root As String) As Long
Dim total = 0L
For Each item In Directory.GetFiles(root, "*", SearchOption.AllDirectories)
total += New FileInfo(item).Length
Next
Return total
End Function
''' <summary>
''' Every file under one root is under the other with the same bytes, which is what a round trip means.
''' </summary>
Friend Function SameTree(source As String, target As String, context As RecipeContext) As Boolean
For Each item In Directory.GetFiles(source, "*", SearchOption.AllDirectories)
Dim relative = item.Substring(source.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
Dim copied = Path.Combine(target, relative)
If Not File.Exists(copied) Then
context.Say("'{0}' did not come back", relative)
Return False
End If
If Not RecipeContext.SameBytes(File.ReadAllBytes(item), File.ReadAllBytes(copied)) Then
context.Say("'{0}' came back with different bytes", relative)
Return False
End If
Next
Return True
End Function
End Module
End NamespaceC#
using System.IO;
using Bastion.Archive.Formats.Tar;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Make and read a <c>.tar.gz</c> in one call each.
/// </summary>
/// <remarks>
/// A compressed tarball is two formats stacked: tar decides what the entries are, gzip decides how the
/// bytes travel, and neither knows about the other. <c>TarArchive</c> takes the compressor from the
/// archive's name, so <c>.tar.gz</c> and <c>.tgz</c> mean the same thing and neither needs a setting.
/// <para>
/// Like every archive-level call in this library it returns an <see cref="OperationResult"/> and never
/// throws at the caller, and it canonicalises entry paths on the way out, so an archive
/// carrying <c>../../etc/passwd</c> is refused rather than obeyed.
/// </para>
/// </remarks>
internal static class TarGzRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("bundle.tar.gz");
var created = TarArchive.CreateFromDirectory(source, archivePath);
if (!created.Succeeded)
{
context.Say(created.ToString());
return RecipeOutcome.Failed;
}
var plain = new FileInfo(archivePath).Length;
context.Say("wrote {0} from {1} of files", RecipeContext.Readable(plain), RecipeContext.Readable(SizeOf(source)));
// The short spelling means exactly the same thing, which is worth showing because half the world
// writes one and half the other.
var shortName = context.PathTo("bundle.tgz");
var alsoCreated = TarArchive.CreateFromDirectory(source, shortName);
if (!alsoCreated.Succeeded)
{
context.Say(alsoCreated.ToString());
return RecipeOutcome.Failed;
}
context.Say(".tgz is the same archive: {0}", RecipeContext.Readable(new FileInfo(shortName).Length));
var target = context.PathTo("unpacked");
var extracted = TarArchive.ExtractToDirectory(archivePath, target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
// The proof is the bytes, not the absence of an error.
if (!SameTree(source, target, context)) return RecipeOutcome.Failed;
context.Say("every file came back with the same bytes");
return RecipeOutcome.Passed;
}
/// <summary>How much is in a directory, so the archive's size means something.</summary>
internal static long SizeOf(string root)
{
var total = 0L;
foreach (var item in Directory.GetFiles(root, "*", SearchOption.AllDirectories))
{
total += new FileInfo(item).Length;
}
return total;
}
/// <summary>
/// Every file under one root is under the other with the same bytes, which is what a round trip means.
/// </summary>
internal static bool SameTree(string source, string target, RecipeContext context)
{
foreach (var item in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
var relative = item.Substring(source.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var copied = Path.Combine(target, relative);
if (!File.Exists(copied))
{
context.Say("'{0}' did not come back", relative);
return false;
}
if (!RecipeContext.SameBytes(File.ReadAllBytes(item), File.ReadAllBytes(copied)))
{
context.Say("'{0}' came back with different bytes", relative);
return false;
}
}
return true;
}
}
}Make a <c>.tar.xz</c>, choose the tar dialect, and make the same bytes twice.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Formats.Tar
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Make a <c>.tar.xz</c>, choose the tar dialect, and make the same bytes twice.
''' </summary>
''' <remarks>
''' Everything <c>TarGz</c> shows applies here with a different compressor, so what this recipe is
''' actually about is <see cref="TarArchiveOptions"/>: which tar dialect the entries are written in, how
''' hard the compressor works, and — the one worth having — a fixed modification time.
''' <para>
''' A fixed time is what makes an archive reproducible. Two runs over the same files
''' normally differ, because the entries carry the files' timestamps and those change when the files are
''' rewritten; pinning the time makes the archive a function of its contents alone, which is what lets a
''' build system compare a hash and a release be verified by whoever receives it.
''' </para>
''' </remarks>
Friend Module TarXzRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim options As New TarArchiveOptions()
options.Format = TarFormat.Pax
options.CompressionLevel = 6
' Midnight on the first of January 2020, in Unix seconds. Any fixed value would do; what matters is
' that it does not come from the file system.
options.FixedModifiedUnixTime = 1577836800L
Dim firstPath = context.PathTo("first.tar.xz")
Dim created = TarArchive.CreateFromDirectory(source, firstPath, options)
If Not created.Succeeded Then
context.Say(created.ToString())
Return RecipeOutcome.Failed
End If
context.Say("pax entries, xz level 6: {0}", RecipeContext.Readable(New FileInfo(firstPath).Length))
' The same inputs and the same settings, written again after the files have been touched.
For Each item In Directory.GetFiles(source, "*", SearchOption.AllDirectories)
File.SetLastWriteTimeUtc(item, DateTime.UtcNow)
Next
Dim secondPath = context.PathTo("second.tar.xz")
Dim again = TarArchive.CreateFromDirectory(source, secondPath, options)
If Not again.Succeeded Then
context.Say(again.ToString())
Return RecipeOutcome.Failed
End If
If Not RecipeContext.SameBytes(File.ReadAllBytes(firstPath), File.ReadAllBytes(secondPath)) Then
context.Say("the two archives differ, so the timestamps were not actually pinned")
Return RecipeOutcome.Failed
End If
context.Say("byte-identical after the files were touched, because the time came from the settings")
' GNU rather than pax is a different archive of the same files, which is the point of the setting.
Dim gnuOptions As New TarArchiveOptions()
gnuOptions.Format = TarFormat.Gnu
gnuOptions.CompressionLevel = 6
gnuOptions.FixedModifiedUnixTime = 1577836800L
Dim gnuPath = context.PathTo("gnu.tar.xz")
Dim gnu = TarArchive.CreateFromDirectory(source, gnuPath, gnuOptions)
If Not gnu.Succeeded Then
context.Say(gnu.ToString())
Return RecipeOutcome.Failed
End If
context.Say("the same files as GNU tar: {0}", RecipeContext.Readable(New FileInfo(gnuPath).Length))
Dim target = context.PathTo("unpacked")
Dim extracted = TarArchive.ExtractToDirectory(firstPath, target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
If Not TarGzRecipe.SameTree(source, target, context) Then Return RecipeOutcome.Failed
context.Say("every file came back with the same bytes")
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using Bastion.Archive.Formats.Tar;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Make a <c>.tar.xz</c>, choose the tar dialect, and make the same bytes twice.
/// </summary>
/// <remarks>
/// Everything <c>TarGz</c> shows applies here with a different compressor, so what this recipe is
/// actually about is <see cref="TarArchiveOptions"/>: which tar dialect the entries are written in, how
/// hard the compressor works, and — the one worth having — a fixed modification time.
/// <para>
/// A fixed time is what makes an archive reproducible. Two runs over the same files
/// normally differ, because the entries carry the files' timestamps and those change when the files are
/// rewritten; pinning the time makes the archive a function of its contents alone, which is what lets a
/// build system compare a hash and a release be verified by whoever receives it.
/// </para>
/// </remarks>
internal static class TarXzRecipe
{
/// <summary>Midnight on the first of January 2020, in Unix seconds.</summary>
private const long FixedTime = 1577836800L;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var options = new TarArchiveOptions
{
Format = TarFormat.Pax,
CompressionLevel = 6,
// Any fixed value would do; what matters is that it does not come from the file system.
FixedModifiedUnixTime = FixedTime
};
var firstPath = context.PathTo("first.tar.xz");
var created = TarArchive.CreateFromDirectory(source, firstPath, options);
if (!created.Succeeded)
{
context.Say(created.ToString());
return RecipeOutcome.Failed;
}
context.Say("pax entries, xz level 6: {0}", RecipeContext.Readable(new FileInfo(firstPath).Length));
// The same inputs and the same settings, written again after the files have been touched.
foreach (var item in Directory.GetFiles(source, "*", SearchOption.AllDirectories))
{
File.SetLastWriteTimeUtc(item, DateTime.UtcNow);
}
var secondPath = context.PathTo("second.tar.xz");
var again = TarArchive.CreateFromDirectory(source, secondPath, options);
if (!again.Succeeded)
{
context.Say(again.ToString());
return RecipeOutcome.Failed;
}
if (!RecipeContext.SameBytes(File.ReadAllBytes(firstPath), File.ReadAllBytes(secondPath)))
{
context.Say("the two archives differ, so the timestamps were not actually pinned");
return RecipeOutcome.Failed;
}
context.Say("byte-identical after the files were touched, because the time came from the settings");
// GNU rather than pax is a different archive of the same files, which is the point of the setting.
var gnuOptions = new TarArchiveOptions
{
Format = TarFormat.Gnu,
CompressionLevel = 6,
FixedModifiedUnixTime = FixedTime
};
var gnuPath = context.PathTo("gnu.tar.xz");
var gnu = TarArchive.CreateFromDirectory(source, gnuPath, gnuOptions);
if (!gnu.Succeeded)
{
context.Say(gnu.ToString());
return RecipeOutcome.Failed;
}
context.Say("the same files as GNU tar: {0}", RecipeContext.Readable(new FileInfo(gnuPath).Length));
var target = context.PathTo("unpacked");
var extracted = TarArchive.ExtractToDirectory(firstPath, target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
if (!TarGzRecipe.SameTree(source, target, context)) return RecipeOutcome.Failed;
context.Say("every file came back with the same bytes");
return RecipeOutcome.Passed;
}
}
}Make and read a <c>.tar.zst</c>, the newest of the compressed tarballs.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Formats.Tar
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Make and read a <c>.tar.zst</c>, the newest of the compressed tarballs.
''' </summary>
''' <remarks>
''' This recipe used to demonstrate a refusal. Bastion.Archive decoded Zstandard without encoding it, so
''' asking for a <c>.tar.zst</c> was refused **by name** rather than quietly given a different compressor.
''' The Zstandard encoder was added later and the refusal went with it, so the recipe now does what the other three
''' tarball recipes do — which is the point: a caller writes the name they want and the library works out
''' the rest.
''' <para>
''' Zstandard is the one worth reaching for when the archive will be read more often than it is written.
''' It decompresses several times faster than gzip at a better ratio, which is why it has become the
''' default for container images and package managers.
''' </para>
''' </remarks>
Friend Module TarZstRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("bundle.tar.zst")
Dim created = TarArchive.CreateFromDirectory(source, archivePath)
If Not created.Succeeded Then
context.Say(created.ToString())
Return RecipeOutcome.Failed
End If
context.Say("wrote {0} from {1} of files",
RecipeContext.Readable(New FileInfo(archivePath).Length),
RecipeContext.Readable(TarGzRecipe.SizeOf(source)))
' The short spelling means the same thing here as it does for the others.
Dim shortName = context.PathTo("bundle.tzst")
Dim alsoCreated = TarArchive.CreateFromDirectory(source, shortName)
If Not alsoCreated.Succeeded Then
context.Say(alsoCreated.ToString())
Return RecipeOutcome.Failed
End If
context.Say(".tzst is the same archive: {0}", RecipeContext.Readable(New FileInfo(shortName).Length))
Dim target = context.PathTo("unpacked")
Dim extracted = TarArchive.ExtractToDirectory(archivePath, target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
If Not TarGzRecipe.SameTree(source, target, context) Then Return RecipeOutcome.Failed
context.Say("every file came back through the Zstandard decoder with the same bytes")
' Against the other three, on the same files, so the choice can be made on numbers.
For Each name In New String() {"bundle.tar.gz", "bundle.tar.bz2", "bundle.tar.xz"}
Dim other = context.PathTo(name)
Dim made = TarArchive.CreateFromDirectory(source, other)
If Not made.Succeeded Then
context.Say(made.ToString())
Return RecipeOutcome.Failed
End If
context.Say(" {0,-16} {1}", name, RecipeContext.Readable(New FileInfo(other).Length))
Next
context.Say(" {0,-16} {1}", "bundle.tar.zst", RecipeContext.Readable(New FileInfo(archivePath).Length))
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.IO;
using Bastion.Archive.Formats.Tar;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Make and read a <c>.tar.zst</c>, the newest of the compressed tarballs.
/// </summary>
/// <remarks>
/// This recipe used to demonstrate a refusal. Bastion.Archive decoded Zstandard without encoding it, so
/// asking for a <c>.tar.zst</c> was refused <b>by name</b> rather than quietly given a different
/// compressor. The Zstandard encoder was added later and the refusal went with it, so the recipe now does what the
/// other three tarball recipes do — which is the point: a caller writes the name they want and the library
/// works out the rest.
/// <para>
/// Zstandard is the one worth reaching for when the archive will be read more often than it is written.
/// It decompresses several times faster than gzip at a better ratio, which is why it has become the
/// default for container images and package managers.
/// </para>
/// </remarks>
internal static class TarZstRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("bundle.tar.zst");
var created = TarArchive.CreateFromDirectory(source, archivePath);
if (!created.Succeeded)
{
context.Say(created.ToString());
return RecipeOutcome.Failed;
}
context.Say("wrote {0} from {1} of files",
RecipeContext.Readable(new FileInfo(archivePath).Length),
RecipeContext.Readable(TarGzRecipe.SizeOf(source)));
// The short spelling means the same thing here as it does for the others.
var shortName = context.PathTo("bundle.tzst");
var alsoCreated = TarArchive.CreateFromDirectory(source, shortName);
if (!alsoCreated.Succeeded)
{
context.Say(alsoCreated.ToString());
return RecipeOutcome.Failed;
}
context.Say(".tzst is the same archive: {0}", RecipeContext.Readable(new FileInfo(shortName).Length));
var target = context.PathTo("unpacked");
var extracted = TarArchive.ExtractToDirectory(archivePath, target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
if (!TarGzRecipe.SameTree(source, target, context)) return RecipeOutcome.Failed;
context.Say("every file came back through the Zstandard decoder with the same bytes");
// Against the other three, on the same files, so the choice can be made on numbers.
foreach (var name in new[] { "bundle.tar.gz", "bundle.tar.bz2", "bundle.tar.xz" })
{
var other = context.PathTo(name);
var made = TarArchive.CreateFromDirectory(source, other);
if (!made.Succeeded)
{
context.Say(made.ToString());
return RecipeOutcome.Failed;
}
context.Say(" {0,-16} {1}", name, RecipeContext.Readable(new FileInfo(other).Length));
}
context.Say(" {0,-16} {1}", "bundle.tar.zst", RecipeContext.Readable(new FileInfo(archivePath).Length));
return RecipeOutcome.Passed;
}
}
}Verify every checksum, and see a damaged one fail.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Verify every checksum, and see a damaged one fail.
''' </summary>
''' <remarks>
''' <c>Test</c> reads every entry to the end and throws the data away, which is exactly what <c>7z t</c>
''' does: it verifies each CRC-32, and each authentication code on an encrypted entry, without writing
''' anything. This recipe then damages one byte of an entry's data and shows the test refusing the archive,
''' because a library that verifies checksums only when convenient is a library that hands you corrupt data.
''' </remarks>
Friend Module TestArchiveRecipe
Private Const Marker As String = "CHECKSUM-MARKER-FLIP-THIS-BYTE"
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim archivePath = context.PathTo("sound.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
' Stored rather than deflated, so the marker is findable in the file and one flipped byte
' damages the data without disturbing any structure around it.
Dim stored As New AdditionOptions() With {.Method = CompressionMethod.Store}
Using payload As New MemoryStream(Encoding.ASCII.GetBytes(Marker & " " & New String("."c, 400)))
Dim added = writer.AddStream(payload, "marked.txt", stored)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
End Using
Using other As New MemoryStream(RecipeContext.PseudoRandom(20000, 5))
Dim added = writer.AddStream(other, "noise.bin", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim sound = Archive.Open(archivePath)
If Not sound.Succeeded Then
context.Say(sound.ToString())
Return RecipeOutcome.Failed
End If
Using archive = sound.Archive
Dim tested = archive.Test(Nothing)
If Not tested.Succeeded Then
context.Say("the sound archive should have passed: " & tested.ToString())
Return RecipeOutcome.Failed
End If
context.Say("sound archive: {0} entries verified in {1:N0} ms",
archive.Entries.Count, tested.Elapsed.TotalMilliseconds)
End Using
' Damage a copy, never the original. One byte, inside the stored entry's own data.
Dim damagedPath = context.PathTo("damaged.zip")
Dim bytes = File.ReadAllBytes(archivePath)
Dim offset = IndexOf(bytes, Encoding.ASCII.GetBytes(Marker))
If offset < 0 Then
context.Say("the marker was not found in the archive, so nothing was damaged")
Return RecipeOutcome.Failed
End If
bytes(offset + 5) = CByte(bytes(offset + 5) Xor &H20)
File.WriteAllBytes(damagedPath, bytes)
context.Say("flipped one bit at offset {0}, inside the data of 'marked.txt'", offset + 5)
Dim damaged = Archive.Open(damagedPath)
If Not damaged.Succeeded Then
context.Say("open refused the damaged archive with " & damaged.ErrorCode.ToString())
Return RecipeOutcome.Passed
End If
Using archive = damaged.Archive
Dim tested = archive.Test(Nothing)
If tested.Succeeded Then
context.Say("the damaged archive passed the test, which it must not")
Return RecipeOutcome.Failed
End If
context.Say("damaged archive: {0} — {1}", tested.ErrorCode, tested.ErrorDescription)
If tested.ErrorCode <> ErrorCode.ChecksumMismatch Then
context.Say("expected ChecksumMismatch for a flipped data byte")
Return RecipeOutcome.Failed
End If
End Using
Return RecipeOutcome.Passed
End Function
''' <summary>Finds a byte sequence, which is all this recipe needs to know where to do its damage.</summary>
Private Function IndexOf(haystack As Byte(), needle As Byte()) As Integer
For start = 0 To haystack.Length - needle.Length
Dim matched = True
For index = 0 To needle.Length - 1
If haystack(start + index) <> needle(index) Then
matched = False
Exit For
End If
Next
If matched Then Return start
Next
Return -1
End Function
End Module
End NamespaceC#
using System.IO;
using System.Text;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Verify every checksum, and see a damaged one fail.
/// </summary>
/// <remarks>
/// <c>Test</c> reads every entry to the end and throws the data away, which is exactly what <c>7z t</c>
/// does: it verifies each CRC-32, and each authentication code on an encrypted entry, without writing
/// anything. This recipe then damages one byte of an entry's data and shows the test refusing the archive,
/// because a library that verifies checksums only when convenient is a library that hands you corrupt data.
/// </remarks>
internal static class TestArchiveRecipe
{
private const string Marker = "CHECKSUM-MARKER-FLIP-THIS-BYTE";
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var archivePath = context.PathTo("sound.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
// Stored rather than deflated, so the marker is findable in the file and one flipped byte
// damages the data without disturbing any structure around it.
var stored = new AdditionOptions { Method = CompressionMethod.Store };
using (var payload = new MemoryStream(Encoding.ASCII.GetBytes(Marker + " " + new string('.', 400))))
{
var added = writer.AddStream(payload, "marked.txt", stored);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
using (var other = new MemoryStream(RecipeContext.PseudoRandom(20000, 5)))
{
var added = writer.AddStream(other, "noise.bin", null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var sound = Archive.Open(archivePath);
if (!sound.Succeeded)
{
context.Say(sound.ToString());
return RecipeOutcome.Failed;
}
using (var archive = sound.Archive)
{
var tested = archive.Test(null);
if (!tested.Succeeded)
{
context.Say("the sound archive should have passed: " + tested);
return RecipeOutcome.Failed;
}
context.Say("sound archive: {0} entries verified in {1:N0} ms",
archive.Entries.Count, tested.Elapsed.TotalMilliseconds);
}
// Damage a copy, never the original. One byte, inside the stored entry's own data.
var damagedPath = context.PathTo("damaged.zip");
var bytes = File.ReadAllBytes(archivePath);
var offset = IndexOf(bytes, Encoding.ASCII.GetBytes(Marker));
if (offset < 0)
{
context.Say("the marker was not found in the archive, so nothing was damaged");
return RecipeOutcome.Failed;
}
bytes[offset + 5] = (byte)(bytes[offset + 5] ^ 0x20);
File.WriteAllBytes(damagedPath, bytes);
context.Say("flipped one bit at offset {0}, inside the data of 'marked.txt'", offset + 5);
var damaged = Archive.Open(damagedPath);
if (!damaged.Succeeded)
{
context.Say("open refused the damaged archive with " + damaged.ErrorCode);
return RecipeOutcome.Passed;
}
using (var archive = damaged.Archive)
{
var tested = archive.Test(null);
if (tested.Succeeded)
{
context.Say("the damaged archive passed the test, which it must not");
return RecipeOutcome.Failed;
}
context.Say("damaged archive: {0} — {1}", tested.ErrorCode, tested.ErrorDescription);
if (tested.ErrorCode != ErrorCode.ChecksumMismatch)
{
context.Say("expected ChecksumMismatch for a flipped data byte");
return RecipeOutcome.Failed;
}
}
return RecipeOutcome.Passed;
}
/// <summary>Finds a byte sequence, which is all this recipe needs to know where to do its damage.</summary>
private static int IndexOf(byte[] haystack, byte[] needle)
{
for (var start = 0; start <= haystack.Length - needle.Length; start++)
{
var matched = true;
for (var index = 0; index < needle.Length; index++)
{
if (haystack[start + index] != needle[index])
{
matched = false;
break;
}
}
if (matched) return start;
}
return -1;
}
}
}Carry timestamps and attributes across, and write the same bytes twice.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Globalization
Imports System.IO
Imports System.Security.Cryptography
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Carry timestamps and attributes across, and write the same bytes twice.
''' </summary>
''' <remarks>
''' A ZIP entry's own timestamp field is a DOS date and time, which has two-second resolution and no time
''' zone at all. The library writes the NTFS extra field alongside it, so a modified time survives to
''' 100-nanosecond resolution in UTC, and falls back to the DOS field for a reader that does not know the
''' extra field. A timestamp that has to be truncated is reported as a warning rather than silently rounded.
''' The second half of this recipe is determinism. With
''' <see cref="TimestampMode.Fixed"/> and a stable entry order, the same input gives a byte-identical
''' archive — the same on .NET Framework 4.6 as on .NET 10, on one thread or several. That is what makes an
''' archive's SHA-256 something a build can publish, and it is impossible for any library that puts
''' <c>DateTime.Now</c> in the output.
''' </remarks>
Friend Module TimestampsAndAttributesRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.PathTo("source")
Directory.CreateDirectory(source)
Dim readOnlyFile = Path.Combine(source, "fixed.txt")
Dim ordinaryFile = Path.Combine(source, "ordinary.txt")
File.WriteAllText(readOnlyFile, "This one is read-only and dated 1999.")
File.WriteAllText(ordinaryFile, "This one is ordinary.")
' A time with an odd second and sub-second precision, which the DOS field alone cannot hold.
Dim stamp = New DateTime(1999, 12, 31, 23, 59, 59, 123, DateTimeKind.Utc)
File.SetLastWriteTimeUtc(readOnlyFile, stamp)
File.SetAttributes(readOnlyFile, FileAttributes.ReadOnly)
Dim archivePath = context.PathTo("metadata.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
For Each filePath In New String() {readOnlyFile, ordinaryFile}
Dim added = writer.AddFile(filePath, Path.GetFileName(filePath))
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Next
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
For Each entry In archive.Entries
context.Say(" {0} modified {1} attributes 0x{2:X2}",
entry.Name.PadRight(14),
entry.LastWriteUtc.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
entry.DosAttributes)
Next
Dim target = context.PathTo("out")
Dim options As New ExtractionOptions() With {
.RestoreTimestamps = True,
.RestoreAttributes = True
}
Dim extracted = archive.ExtractAll(target, options)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
For Each warning In extracted.Warnings
context.Say("warning {0}: {1}", warning.Code, warning.Message)
Next
Dim producedPath = Path.Combine(target, "fixed.txt")
Dim produced = File.GetLastWriteTimeUtc(producedPath)
Dim drift = (produced - stamp).Duration()
context.Say("the extracted file is dated {0}, {1:N0} ms from the original",
produced.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
drift.TotalMilliseconds)
' The NTFS extra field carries it exactly; two seconds is what the DOS field alone would cost.
If drift > TimeSpan.FromSeconds(2) Then
context.Say("the timestamp did not survive")
Return RecipeOutcome.Failed
End If
Dim attributes = File.GetAttributes(producedPath)
Dim isReadOnly = (attributes And FileAttributes.ReadOnly) <> 0
context.Say("the read-only attribute {0}", If(isReadOnly, "came across", "did not come across"))
If Not isReadOnly Then Return RecipeOutcome.Failed
' Leave it writable, so the scratch directory can be cleaned up afterwards.
File.SetAttributes(producedPath, FileAttributes.Normal)
End Using
Return Determinism(context, source)
End Function
''' <summary>
''' Writes the same tree twice with a fixed timestamp and compares the two archives by SHA-256.
''' </summary>
Private Function Determinism(context As RecipeContext, source As String) As RecipeOutcome
Dim settings As New CompressionSettings() With {
.Timestamps = TimestampMode.Fixed,
.FixedTimestampUtc = New DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
.Ordering = EntryOrdering.OrdinalByPath,
.Level = 6
}
Dim digests(1) As String
For attempt = 0 To 1
Dim archivePath = context.PathTo("deterministic-" & attempt.ToString(CultureInfo.InvariantCulture) & ".zip")
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
digests(attempt) = Digest(archivePath)
Next
context.Say("first SHA-256 {0}", digests(0))
context.Say("second SHA-256 {0}", digests(1))
If Not String.Equals(digests(0), digests(1), StringComparison.Ordinal) Then
context.Say("the same input produced two different archives")
Return RecipeOutcome.Failed
End If
context.Say("the same input produced the same bytes, which is what makes a digest publishable")
Return RecipeOutcome.Passed
End Function
Private Function Digest(path As String) As String
Using algorithm = SHA256.Create()
Using stream As New FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)
Dim hash = algorithm.ComputeHash(stream)
Dim builder As New System.Text.StringBuilder(hash.Length * 2)
For Each value In hash
builder.Append(value.ToString("x2", CultureInfo.InvariantCulture))
Next
Return builder.ToString()
End Using
End Using
End Function
End Module
End NamespaceC#
using System;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Carry timestamps and attributes across, and write the same bytes twice.
/// </summary>
/// <remarks>
/// A ZIP entry's own timestamp field is a DOS date and time, which has two-second resolution and no time
/// zone at all. The library writes the NTFS extra field alongside it, so a modified time survives to
/// 100-nanosecond resolution in UTC, and falls back to the DOS field for a reader that does not know the
/// extra field. A timestamp that has to be truncated is reported as a warning rather than silently rounded.
/// The second half of this recipe is determinism. With
/// <see cref="TimestampMode.Fixed"/> and a stable entry order, the same input gives a byte-identical
/// archive — the same on .NET Framework 4.6 as on .NET 10, on one thread or several. That is what makes an
/// archive's SHA-256 something a build can publish, and it is impossible for any library that puts
/// <c>DateTime.Now</c> in the output.
/// </remarks>
internal static class TimestampsAndAttributesRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.PathTo("source");
Directory.CreateDirectory(source);
var readOnlyFile = Path.Combine(source, "fixed.txt");
var ordinaryFile = Path.Combine(source, "ordinary.txt");
File.WriteAllText(readOnlyFile, "This one is read-only and dated 1999.");
File.WriteAllText(ordinaryFile, "This one is ordinary.");
// A time with an odd second and sub-second precision, which the DOS field alone cannot hold.
var stamp = new DateTime(1999, 12, 31, 23, 59, 59, 123, DateTimeKind.Utc);
File.SetLastWriteTimeUtc(readOnlyFile, stamp);
File.SetAttributes(readOnlyFile, FileAttributes.ReadOnly);
var archivePath = context.PathTo("metadata.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
foreach (var filePath in new[] { readOnlyFile, ordinaryFile })
{
var added = writer.AddFile(filePath, Path.GetFileName(filePath));
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
foreach (var entry in archive.Entries)
{
context.Say(" {0} modified {1} attributes 0x{2:X2}",
entry.Name.PadRight(14),
entry.LastWriteUtc.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
entry.DosAttributes);
}
var target = context.PathTo("out");
var options = new ExtractionOptions { RestoreTimestamps = true, RestoreAttributes = true };
var extracted = archive.ExtractAll(target, options);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
foreach (var warning in extracted.Warnings)
{
context.Say("warning {0}: {1}", warning.Code, warning.Message);
}
var producedPath = Path.Combine(target, "fixed.txt");
var produced = File.GetLastWriteTimeUtc(producedPath);
var drift = (produced - stamp).Duration();
context.Say("the extracted file is dated {0}, {1:N0} ms from the original",
produced.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
drift.TotalMilliseconds);
// The NTFS extra field carries it exactly; two seconds is what the DOS field alone would cost.
if (drift > TimeSpan.FromSeconds(2))
{
context.Say("the timestamp did not survive");
return RecipeOutcome.Failed;
}
var attributes = File.GetAttributes(producedPath);
var isReadOnly = (attributes & FileAttributes.ReadOnly) != 0;
context.Say("the read-only attribute {0}", isReadOnly ? "came across" : "did not come across");
if (!isReadOnly) return RecipeOutcome.Failed;
// Leave it writable, so the scratch directory can be cleaned up afterwards.
File.SetAttributes(producedPath, FileAttributes.Normal);
}
return Determinism(context, source);
}
/// <summary>
/// Writes the same tree twice with a fixed timestamp and compares the two archives by SHA-256.
/// </summary>
private static RecipeOutcome Determinism(RecipeContext context, string source)
{
var settings = new CompressionSettings
{
Timestamps = TimestampMode.Fixed,
FixedTimestampUtc = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
Ordering = EntryOrdering.OrdinalByPath,
Level = 6
};
var digests = new string[2];
for (var attempt = 0; attempt < 2; attempt++)
{
var archivePath = context.PathTo("deterministic-" + attempt.ToString(CultureInfo.InvariantCulture) + ".zip");
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
digests[attempt] = Digest(archivePath);
}
context.Say("first SHA-256 {0}", digests[0]);
context.Say("second SHA-256 {0}", digests[1]);
if (!string.Equals(digests[0], digests[1], StringComparison.Ordinal))
{
context.Say("the same input produced two different archives");
return RecipeOutcome.Failed;
}
context.Say("the same input produced the same bytes, which is what makes a digest publishable");
return RecipeOutcome.Passed;
}
private static string Digest(string path)
{
using (var algorithm = SHA256.Create())
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
var hash = algorithm.ComputeHash(stream);
var builder = new StringBuilder(hash.Length * 2);
foreach (var value in hash)
{
builder.Append(value.ToString("x2", CultureInfo.InvariantCulture));
}
return builder.ToString();
}
}
}
}
}Keep non-ASCII entry names exactly as they were given.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Globalization
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Keep non-ASCII entry names exactly as they were given.
''' </summary>
''' <remarks>
''' The format's own name encoding is IBM code page 437, which has no room for most of the world. The way
''' out is general purpose bit 11, the "language encoding flag": set it and the name is UTF-8. This library
''' sets it for any name that needs it and leaves it clear for a name that does not, so an ASCII archive
''' stays byte-identical to what an old tool would have written.
''' Reading is the harder half, because an archive written by an old tool has no flag and no declaration.
''' <c>ArchiveOpenOptions.FallbackCodePage</c> is what says "treat unflagged names as code page 932", which
''' is the only honest answer: the information is not in the file, so it has to come from the caller.
''' Names are compared and printed here by code point rather than by eye, because whether a console can
''' render an emoji says nothing about whether the archive is right.
''' </remarks>
Friend Module UnicodeNamesRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
' Latin with diacritics, Greek, Cyrillic, Japanese, an emoji outside the basic plane, and one
' plain ASCII name to show that the flag is set per entry rather than per archive.
Dim names = New String() {
"documents/résumé-außergewöhnlich.txt",
"documents/Ελληνικά.txt",
"документы/отчёт.txt",
"書類/報告書.txt",
"emoji/" & Char.ConvertFromUtf32(&H1F4E6) & "-parcel.txt",
"plain-ascii.txt"
}
Dim archivePath = context.PathTo("unicode.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
For index = 0 To names.Length - 1
Dim body = Encoding.UTF8.GetBytes("This entry is named " & names(index))
Using source As New MemoryStream(body, False)
Dim added = writer.AddStream(source, names(index), Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
End Using
Next
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
If archive.Entries.Count <> names.Length Then
context.Say("{0} entries went in and {1} came back", names.Length, archive.Entries.Count)
Return RecipeOutcome.Failed
End If
For index = 0 To names.Length - 1
Dim recovered = archive.Entries(index).Name
context.Say(" {0} {1}", If(String.Equals(recovered, names(index), StringComparison.Ordinal), "same", "DIFFERENT"),
Describe(recovered))
If Not String.Equals(recovered, names(index), StringComparison.Ordinal) Then
context.Say(" went in as " & Describe(names(index)))
Return RecipeOutcome.Failed
End If
Next
' The names have to work as paths too, which is a separate question from surviving the format.
Dim target = context.PathTo("out")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
Dim produced = Directory.GetFiles(target, "*", SearchOption.AllDirectories)
context.Say("{0} files extracted with their names intact", produced.Length)
If produced.Length <> names.Length Then Return RecipeOutcome.Failed
For Each name In names
Dim expected = Path.Combine(target, name.Replace("/"c, Path.DirectorySeparatorChar))
If Not File.Exists(expected) Then
context.Say("not on disk under the name it was given: " & Describe(name))
Return RecipeOutcome.Failed
End If
Next
End Using
Return RecipeOutcome.Passed
End Function
''' <summary>
''' A name written so it can be compared on any console: printable ASCII as itself, everything else as
''' its code unit. Whether a terminal can draw a character is not the archive's problem.
''' </summary>
Private Function Describe(name As String) As String
Dim builder As New StringBuilder(name.Length + 16)
For Each character In name
Dim code = Convert.ToInt32(character)
If code >= &H20 AndAlso code < &H7F Then
builder.Append(character)
Else
builder.Append(String.Format(CultureInfo.InvariantCulture, "\u{0:X4}", code))
End If
Next
Return builder.ToString()
End Function
End Module
End NamespaceC#
using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Keep non-ASCII entry names exactly as they were given.
/// </summary>
/// <remarks>
/// The format's own name encoding is IBM code page 437, which has no room for most of the world. The way
/// out is general purpose bit 11, the "language encoding flag": set it and the name is UTF-8. This library
/// sets it for any name that needs it and leaves it clear for a name that does not, so an ASCII archive
/// stays byte-identical to what an old tool would have written.
/// Reading is the harder half, because an archive written by an old tool has no flag and no declaration.
/// <c>ArchiveOpenOptions.FallbackCodePage</c> is what says "treat unflagged names as code page 932", which
/// is the only honest answer: the information is not in the file, so it has to come from the caller.
/// Names are compared and printed here by code point rather than by eye, because whether a console can
/// render an emoji says nothing about whether the archive is right.
/// </remarks>
internal static class UnicodeNamesRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
// Latin with diacritics, Greek, Cyrillic, Japanese, an emoji outside the basic plane, and one
// plain ASCII name to show that the flag is set per entry rather than per archive.
var names = new[]
{
"documents/résumé-außergewöhnlich.txt",
"documents/Ελληνικά.txt",
"документы/отчёт.txt",
"書類/報告書.txt",
"emoji/" + char.ConvertFromUtf32(0x1F4E6) + "-parcel.txt",
"plain-ascii.txt"
};
var archivePath = context.PathTo("unicode.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
foreach (var name in names)
{
var body = Encoding.UTF8.GetBytes("This entry is named " + name);
using (var source = new MemoryStream(body, false))
{
var added = writer.AddStream(source, name, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
if (archive.Entries.Count != names.Length)
{
context.Say("{0} entries went in and {1} came back", names.Length, archive.Entries.Count);
return RecipeOutcome.Failed;
}
for (var index = 0; index < names.Length; index++)
{
var recovered = archive.Entries[index].Name;
var same = string.Equals(recovered, names[index], StringComparison.Ordinal);
context.Say(" {0} {1}", same ? "same" : "DIFFERENT", Describe(recovered));
if (!same)
{
context.Say(" went in as " + Describe(names[index]));
return RecipeOutcome.Failed;
}
}
// The names have to work as paths too, which is a separate question from surviving the format.
var target = context.PathTo("out");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
var produced = Directory.GetFiles(target, "*", SearchOption.AllDirectories);
context.Say("{0} files extracted with their names intact", produced.Length);
if (produced.Length != names.Length) return RecipeOutcome.Failed;
foreach (var name in names)
{
var expected = Path.Combine(target, name.Replace('/', Path.DirectorySeparatorChar));
if (!File.Exists(expected))
{
context.Say("not on disk under the name it was given: " + Describe(name));
return RecipeOutcome.Failed;
}
}
}
return RecipeOutcome.Passed;
}
/// <summary>
/// A name written so it can be compared on any console: printable ASCII as itself, everything else as
/// its code unit. Whether a terminal can draw a character is not the archive's problem.
/// </summary>
private static string Describe(string name)
{
var builder = new StringBuilder(name.Length + 16);
foreach (var character in name)
{
var code = Convert.ToInt32(character);
if (code >= 0x20 && code < 0x7F)
{
builder.Append(character);
}
else
{
builder.Append(string.Format(CultureInfo.InvariantCulture, "\\u{0:X4}", code));
}
}
return builder.ToString();
}
}
}Add, rename and remove by rebuilding into a new file.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Add, rename and remove by rebuilding into a new file.
''' </summary>
''' <remarks>
''' There is no in-place update, on purpose. An <see cref="ArchiveUpdate"/> describes
''' what should change and <c>Apply</c> writes a new archive; the source is opened read-only and comes out
''' byte for byte identical. What that buys is that a power cut during an update costs you the update rather
''' than the archive.
''' An entry the update does not touch is copied across still compressed and still encrypted — never
''' decoded and re-encoded — so renaming one file in a large archive costs a file copy, not a recompression.
''' A replacement keeps the position the original held, so an update does not reshuffle an archive.
''' </remarks>
Friend Module UpdateArchiveRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim archivePath = context.PathTo("original.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim before = File.ReadAllBytes(archivePath)
Dim newFile = context.PathTo("licence.txt")
File.WriteAllText(newFile, "Bastion Software Solutions Ltd — sample licence text.")
Dim update As New ArchiveUpdate(archivePath)
update.Remove("data.bin")
update.Rename("readme.txt", "docs/readme.txt")
update.AddOrReplaceFile(newFile, "licence.txt")
update.AddOrReplaceDirectory("docs/")
Dim targetPath = context.PathTo("updated.zip")
Dim applied = update.Apply(targetPath, Nothing)
If Not applied.Succeeded Then
context.Say(applied.ToString())
Return RecipeOutcome.Failed
End If
context.Say("rebuilt in {0:N0} ms", applied.Elapsed.TotalMilliseconds)
' The source is untouched. This is the claim worth checking, so it is checked rather than asserted.
If Not RecipeContext.SameBytes(before, File.ReadAllBytes(archivePath)) Then
context.Say("the source archive changed, which it must never do")
Return RecipeOutcome.Failed
End If
context.Say("the source archive is byte for byte what it was")
' Applying over an existing file is refused, like every other write.
Dim refused = update.Apply(targetPath, Nothing)
If refused.Succeeded Then
context.Say("applying over an existing file should have been refused")
Return RecipeOutcome.Failed
End If
context.Say("applying over an existing file returned " & refused.ErrorCode.ToString())
Dim opened = Archive.Open(targetPath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim hasRemoved = False
Dim hasRenamed = False
Dim hasAdded = False
For Each entry In archive.Entries
If String.Equals(entry.Name, "data.bin", StringComparison.Ordinal) Then hasRemoved = True
If String.Equals(entry.Name, "docs/readme.txt", StringComparison.Ordinal) Then hasRenamed = True
If String.Equals(entry.Name, "licence.txt", StringComparison.Ordinal) Then hasAdded = True
Next
context.Say("{0} entries: removed={1} renamed={2} added={3}",
archive.Entries.Count, Not hasRemoved, hasRenamed, hasAdded)
If hasRemoved OrElse Not hasRenamed OrElse Not hasAdded Then Return RecipeOutcome.Failed
Dim target = context.PathTo("out")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
Dim carried = File.ReadAllText(Path.Combine(target, "docs", "readme.txt"))
context.Say("the renamed entry still reads: " & carried)
If carried.Length = 0 Then Return RecipeOutcome.Failed
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Add, rename and remove by rebuilding into a new file.
/// </summary>
/// <remarks>
/// There is no in-place update, on purpose. An <see cref="ArchiveUpdate"/> describes
/// what should change and <c>Apply</c> writes a new archive; the source is opened read-only and comes out
/// byte for byte identical. What that buys is that a power cut during an update costs you the update rather
/// than the archive.
/// An entry the update does not touch is copied across still compressed and still encrypted — never
/// decoded and re-encoded — so renaming one file in a large archive costs a file copy, not a recompression.
/// A replacement keeps the position the original held, so an update does not reshuffle an archive.
/// </remarks>
internal static class UpdateArchiveRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var archivePath = context.PathTo("original.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var before = File.ReadAllBytes(archivePath);
var newFile = context.PathTo("licence.txt");
File.WriteAllText(newFile, "Bastion Software Solutions Ltd — sample licence text.");
var update = new ArchiveUpdate(archivePath);
update.Remove("data.bin");
update.Rename("readme.txt", "docs/readme.txt");
update.AddOrReplaceFile(newFile, "licence.txt");
update.AddOrReplaceDirectory("docs/");
var targetPath = context.PathTo("updated.zip");
var applied = update.Apply(targetPath, null);
if (!applied.Succeeded)
{
context.Say(applied.ToString());
return RecipeOutcome.Failed;
}
context.Say("rebuilt in {0:N0} ms", applied.Elapsed.TotalMilliseconds);
// The source is untouched. This is the claim worth checking, so it is checked rather than asserted.
if (!RecipeContext.SameBytes(before, File.ReadAllBytes(archivePath)))
{
context.Say("the source archive changed, which it must never do");
return RecipeOutcome.Failed;
}
context.Say("the source archive is byte for byte what it was");
// Applying over an existing file is refused, like every other write.
var refused = update.Apply(targetPath, null);
if (refused.Succeeded)
{
context.Say("applying over an existing file should have been refused");
return RecipeOutcome.Failed;
}
context.Say("applying over an existing file returned " + refused.ErrorCode);
var opened = Archive.Open(targetPath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var hasRemoved = false;
var hasRenamed = false;
var hasAdded = false;
foreach (var entry in archive.Entries)
{
if (string.Equals(entry.Name, "data.bin", StringComparison.Ordinal)) hasRemoved = true;
if (string.Equals(entry.Name, "docs/readme.txt", StringComparison.Ordinal)) hasRenamed = true;
if (string.Equals(entry.Name, "licence.txt", StringComparison.Ordinal)) hasAdded = true;
}
context.Say("{0} entries: removed={1} renamed={2} added={3}",
archive.Entries.Count, !hasRemoved, hasRenamed, hasAdded);
if (hasRemoved || !hasRenamed || !hasAdded) return RecipeOutcome.Failed;
var target = context.PathTo("out");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
var carried = File.ReadAllText(Path.Combine(target, "docs", "readme.txt"));
context.Say("the renamed entry still reads: " + carried);
if (carried.Length == 0) return RecipeOutcome.Failed;
}
return RecipeOutcome.Passed;
}
}
}Write a Windows image (<c>.wim</c>) and read it back, then read one Microsoft made.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write a Windows image (<c>.wim</c>) and read it back, then read one Microsoft made.
''' </summary>
''' <remarks>
''' A path ending <c>.wim</c> writes a one-image WIM with every resource stored, as 7-Zip writes it. WIM keeps
''' each distinct content once, found by its SHA-1, so ten copies of a file cost one — the recipe shows it by
''' writing the sample tree twice over and comparing sizes. Reading is the same <c>Archive.Open</c> as for any
''' other format, and Windows ships a WIM compressed with XPRESS in System32 that serves as a second example.
''' </remarks>
Friend Module WimCreateRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
Dim once = Write(context, "once.wim", source, 1)
Dim twice = Write(context, "twice.wim", source, 2)
If once < 0 OrElse twice < 0 Then Return RecipeOutcome.Failed
context.Say("the tree once: {0}; twice over: {1} — the second copy costs only its directory entries",
RecipeContext.Readable(once), RecipeContext.Readable(twice))
If twice - once > 4096 Then Return RecipeOutcome.Failed
Dim opened = Archive.Open(context.PathTo("twice.wim"))
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
context.Say("opened as {0} with {1} entries", archive.Format, archive.Entries.Count)
Dim target = context.PathTo("unpacked")
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
If Not SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "copy2"), context) Then Return RecipeOutcome.Failed
End Using
' Microsoft's own WIM, on Windows: XPRESS-compressed, read by the library's own decoder.
Dim shipped = Path.Combine(Environment.SystemDirectory, "DrtmAuthTxt.wim")
If File.Exists(shipped) Then
Dim theirs = Archive.Open(shipped)
If Not theirs.Succeeded Then
context.Say(theirs.ToString())
Return RecipeOutcome.Failed
End If
Using archive = theirs.Archive
Dim tested = archive.Test(Nothing)
context.Say("{0}: {1} entries, method {2}, test {3}", Path.GetFileName(shipped), archive.Entries.Count,
archive.Entries(archive.Entries.Count - 1).Method, tested.ErrorCode)
If Not tested.Succeeded Then Return RecipeOutcome.Failed
End Using
End If
Return RecipeOutcome.Passed
End Function
''' <summary>Writes the tree <paramref name="copies"/> times over into one WIM and returns its size, or -1.</summary>
Private Function Write(context As RecipeContext, name As String, source As String, copies As Integer) As Long
Dim path = context.PathTo(name)
Dim started = ArchiveWriter.Create(path)
If Not started.Succeeded Then
context.Say(started.ToString())
Return -1
End If
Using writer = started.Writer
For copy = 1 To copies
Dim added = writer.AddDirectory(source, "copy" & copy, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return -1
End If
Next
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return -1
End If
End Using
Return New FileInfo(path).Length
End Function
End Module
End NamespaceC#
using System;
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write a Windows image (<c>.wim</c>) and read it back, then read one Microsoft made.
/// </summary>
/// <remarks>
/// A path ending <c>.wim</c> writes a one-image WIM with every resource stored, as 7-Zip writes it. WIM keeps
/// each distinct content once, found by its SHA-1, so ten copies of a file cost one — the recipe shows it by
/// writing the sample tree twice over and comparing sizes. Reading is the same <c>Archive.Open</c> as for any
/// other format, and Windows ships a WIM compressed with XPRESS in System32 that serves as a second example.
/// </remarks>
internal static class WimCreateRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
var once = Write(context, "once.wim", source, 1);
var twice = Write(context, "twice.wim", source, 2);
if (once < 0 || twice < 0) return RecipeOutcome.Failed;
context.Say("the tree once: {0}; twice over: {1} — the second copy costs only its directory entries",
RecipeContext.Readable(once), RecipeContext.Readable(twice));
if (twice - once > 4096) return RecipeOutcome.Failed;
var opened = Archive.Open(context.PathTo("twice.wim"));
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
context.Say("opened as {0} with {1} entries", archive.Format, archive.Entries.Count);
var target = context.PathTo("unpacked");
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
if (!SevenZipCreateRecipe.SameTree(source, Path.Combine(target, "copy2"), context)) return RecipeOutcome.Failed;
}
// Microsoft's own WIM, on Windows: XPRESS-compressed, read by the library's own decoder.
var shipped = Path.Combine(Environment.SystemDirectory, "DrtmAuthTxt.wim");
if (File.Exists(shipped))
{
var theirs = Archive.Open(shipped);
if (!theirs.Succeeded)
{
context.Say(theirs.ToString());
return RecipeOutcome.Failed;
}
using (var archive = theirs.Archive)
{
var tested = archive.Test(null);
context.Say("{0}: {1} entries, method {2}, test {3}", Path.GetFileName(shipped), archive.Entries.Count,
archive.Entries[archive.Entries.Count - 1].Method, tested.ErrorCode);
if (!tested.Succeeded) return RecipeOutcome.Failed;
}
}
return RecipeOutcome.Passed;
}
/// <summary>Writes the tree <paramref name="copies"/> times over into one WIM and returns its size, or -1.</summary>
private static long Write(RecipeContext context, string name, string source, int copies)
{
var path = context.PathTo(name);
var started = ArchiveWriter.Create(path);
if (!started.Succeeded)
{
context.Say(started.ToString());
return -1;
}
using (var writer = started.Writer)
{
for (var copy = 1; copy <= copies; copy++)
{
var added = writer.AddDirectory(source, "copy" + copy, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return -1;
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return -1;
}
}
return new FileInfo(path).Length;
}
}
}Read the two things a WIM records that no other format here does: each file's Windows security descriptor, and its NTFS alternate data streams.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Linq
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Read the two things a WIM records that no other format here does: each file's Windows security descriptor,
''' and its NTFS alternate data streams.
''' </summary>
''' <remarks>
''' <c>ArchiveEntry.GetSecurityDescriptor()</c> hands back the self-relative <c>SECURITY_DESCRIPTOR</c> the
''' image stored, in the form <c>SetFileSecurity</c> takes — this library does not interpret it, and returns a
''' fresh copy each call. An alternate data stream is an entry of its own named <c>file:stream</c>, marked by
''' <c>IsAlternateStream</c>; it can be read like any other entry, and <c>ExtractAll</c> skips it with a warning
''' rather than writing it, because a stream can only be written beside a file that already exists.
''' <para>
''' The WIM Windows ships in System32 carries a descriptor for every one of its files, so this recipe needs no
''' producer of its own; where it is not present, the recipe says so instead of pretending.
''' </para>
''' </remarks>
Friend Module WimSecurityRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim shipped = Path.Combine(Environment.SystemDirectory, "DrtmAuthTxt.wim")
If Not File.Exists(shipped) Then
context.Say("{0} is not on this machine, so there is no WIM here carrying security descriptors.", shipped)
Return RecipeOutcome.Passed
End If
Dim opened = Archive.Open(shipped)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim secured = archive.Entries.Where(Function(e) e.GetSecurityDescriptor() IsNot Nothing).ToArray()
context.Say("{0} of {1} entries carry a security descriptor", secured.Length, archive.Entries.Count)
If secured.Length = 0 Then
context.Say("this image was written without them; nothing further to show.")
Return RecipeOutcome.Passed
End If
Dim descriptor = secured(0).GetSecurityDescriptor()
' Byte 0 is the revision, always 1; bit 0x80 of byte 3 is SE_SELF_RELATIVE, which says the four
' offsets that follow are offsets into these same bytes rather than pointers.
context.Say("'{0}': {1} bytes, revision {2}, self-relative {3}",
secured(0).Name, descriptor.Length, descriptor(0), (descriptor(3) And &H80) <> 0)
If descriptor(0) <> 1 Then Return RecipeOutcome.Failed
' On Windows the bytes go straight to the API: new RawSecurityDescriptor(descriptor, 0) reads it,
' and File.SetAccessControl applies it. Nothing here needs a reference to do that.
Dim streams = archive.Entries.Where(Function(e) e.IsAlternateStream).ToArray()
If streams.Length = 0 Then
context.Say("no alternate data streams in this image — 7z a -twim -sns writes one that looks like 'readme.txt:meta'")
Else
For Each stream In streams
Using content As New MemoryStream()
Dim read = archive.ExtractToStream(stream, content, Nothing)
If Not read.Succeeded Then
context.Say(read.ToString())
Return RecipeOutcome.Failed
End If
context.Say("stream '{0}': {1} bytes read", stream.Name, content.Length)
End Using
Next
End If
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using System.Linq;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Read the two things a WIM records that no other format here does: each file's Windows security descriptor,
/// and its NTFS alternate data streams.
/// </summary>
/// <remarks>
/// <c>ArchiveEntry.GetSecurityDescriptor()</c> hands back the self-relative <c>SECURITY_DESCRIPTOR</c> the
/// image stored, in the form <c>SetFileSecurity</c> takes — this library does not interpret it, and returns a
/// fresh copy each call. An alternate data stream is an entry of its own named <c>file:stream</c>, marked by
/// <c>IsAlternateStream</c>; it can be read like any other entry, and <c>ExtractAll</c> skips it with a warning
/// rather than writing it, because a stream can only be written beside a file that already exists.
/// <para>
/// The WIM Windows ships in System32 carries a descriptor for every one of its files, so this recipe needs no
/// producer of its own; where it is not present, the recipe says so instead of pretending.
/// </para>
/// </remarks>
internal static class WimSecurityRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var shipped = Path.Combine(Environment.SystemDirectory, "DrtmAuthTxt.wim");
if (!File.Exists(shipped))
{
context.Say("{0} is not on this machine, so there is no WIM here carrying security descriptors.", shipped);
return RecipeOutcome.Passed;
}
var opened = Archive.Open(shipped);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var secured = archive.Entries.Where(e => e.GetSecurityDescriptor() != null).ToArray();
context.Say("{0} of {1} entries carry a security descriptor", secured.Length, archive.Entries.Count);
if (secured.Length == 0)
{
context.Say("this image was written without them; nothing further to show.");
return RecipeOutcome.Passed;
}
var descriptor = secured[0].GetSecurityDescriptor();
// Byte 0 is the revision, always 1; bit 0x80 of byte 3 is SE_SELF_RELATIVE, which says the four
// offsets that follow are offsets into these same bytes rather than pointers.
context.Say("'{0}': {1} bytes, revision {2}, self-relative {3}",
secured[0].Name, descriptor.Length, descriptor[0], (descriptor[3] & 0x80) != 0);
if (descriptor[0] != 1) return RecipeOutcome.Failed;
// On Windows the bytes go straight to the API: new RawSecurityDescriptor(descriptor, 0) reads it,
// and File.SetAccessControl applies it. Nothing here needs a reference to do that.
var streams = archive.Entries.Where(e => e.IsAlternateStream).ToArray();
if (streams.Length == 0)
{
context.Say("no alternate data streams in this image — 7z a -twim -sns writes one that looks like 'readme.txt:meta'");
}
else
{
foreach (var stream in streams)
{
using (var content = new MemoryStream())
{
var read = archive.ExtractToStream(stream, content, null);
if (!read.Succeeded)
{
context.Say(read.ToString());
return RecipeOutcome.Failed;
}
context.Say("stream '{0}': {1} bytes read", stream.Name, content.Length);
}
}
}
}
return RecipeOutcome.Passed;
}
}
}Write an <c>.xz</c> file in blocks, then jump straight to the middle of it.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.Formats.Xz
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write an <c>.xz</c> file in blocks, then jump straight to the middle of it.
''' </summary>
''' <remarks>
''' xz is the format that learned from gzip and bzip2, and blocks are what it learned. A file written in
''' blocks carries an index of them at the end, and the index is what turns a compressed file into one you
''' can read from the middle: a seek costs **one block**, not one file.
''' <para>
''' That is reached through the ordinary <see cref="System.IO.Stream"/> contract rather than an API of its
''' own. <c>CanSeek</c> becomes true when the source can seek and the index can be read,
''' and then <c>Position</c>, <c>Seek</c> and <c>Length</c> do what they do anywhere else — so a caller
''' who wants the tenth megabyte writes <c>Position = 10485760</c> and never learns that this format has an
''' index. A file written as one block seeks no faster than it reads, which is the writer's choice; a
''' source that cannot seek, such as a pipe, stays forward-only rather than failing to open.
''' </para>
''' </remarks>
Friend Module XzStreamUsageRecipe
''' <summary>A megabyte and a half, so several blocks are worth having.</summary>
Private Const ContentLength As Integer = 1500000
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim content = Patterned(ContentLength)
Dim archivePath = context.PathTo("payload.xz")
Dim blocks As Integer
Using file As New FileStream(archivePath, FileMode.Create, FileAccess.Write)
' Level 6, a CRC-64 after every block, and a block every 256 KiB.
Dim writer As New XzEncoderStream(file, 6, XzCheck.Crc64, 262144L, leaveOpen:=True)
Try
writer.Write(content, 0, content.Length)
Finally
writer.Dispose()
End Try
' Asked after disposing, not before: the last block is short and is not written until the
' stream is finished, so a count taken inside the block is one too few.
blocks = writer.BlockCount
End Using
context.Say("{0} in, {1} out, in {2} blocks with a CRC-64 each",
RecipeContext.Readable(content.Length),
RecipeContext.Readable(New FileInfo(archivePath).Length), blocks)
Using file As New FileStream(archivePath, FileMode.Open, FileAccess.Read)
Using reader As New XzDecoderStream(file, leaveOpen:=True)
Using produced As New MemoryStream()
GZipStreamUsageRecipe.CopyAll(reader, produced)
If Not RecipeContext.SameBytes(content, produced.ToArray()) Then
context.Say("the bytes that came back are not the bytes that went in")
Return RecipeOutcome.Failed
End If
context.Say("read back whole: {0} blocks, check {1}", reader.BlockCount, reader.Check)
End Using
End Using
End Using
Using file As New FileStream(archivePath, FileMode.Open, FileAccess.Read)
Using reader As New XzDecoderStream(file, leaveOpen:=True)
If Not reader.CanSeek Then
context.Say("this file has no readable index, so it can only be read forwards")
Return RecipeOutcome.Failed
End If
context.Say("the index says the content is {0}", RecipeContext.Readable(reader.Length))
' Offsets in no particular order, including one backwards, because that is the case a
' forward-only reader cannot do at all.
For Each at In New Long() {1200000L, 300000L, 262144L, 262143L, 0L}
reader.Position = at
Dim window(63) As Byte
Dim filled = 0
Do While filled < window.Length
Dim taken = reader.Read(window, filled, window.Length - filled)
If taken <= 0 Then Exit Do
filled += taken
Loop
For index = 0 To filled - 1
If content(CInt(at) + index) <> window(index) Then
context.Say("seeking to {0} gave the wrong byte at {1}", at, index)
Return RecipeOutcome.Failed
End If
Next
Next
context.Say("five seeks, forwards and back, every byte where it should be")
End Using
End Using
Return RecipeOutcome.Passed
End Function
''' <summary>
''' Content that compresses well and is still different everywhere, so a seek landing in the wrong
''' place is caught rather than matching by luck.
''' </summary>
Private Function Patterned(length As Integer) As Byte()
Dim builder As New StringBuilder(length + 64)
Dim line = 0
Do While builder.Length < length
builder.Append("line ")
builder.Append(line.ToString(Globalization.CultureInfo.InvariantCulture))
builder.Append(": the quick brown fox jumps over the lazy dog")
builder.Append(Environment.NewLine)
line += 1
Loop
Dim all = Encoding.ASCII.GetBytes(builder.ToString())
Dim result(length - 1) As Byte
System.Buffer.BlockCopy(all, 0, result, 0, length)
Return result
End Function
End Module
End NamespaceC#
using System;
using System.Globalization;
using System.IO;
using System.Text;
using Bastion.Archive.Formats.Xz;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write an <c>.xz</c> file in blocks, then jump straight to the middle of it.
/// </summary>
/// <remarks>
/// xz is the format that learned from gzip and bzip2, and blocks are what it learned. A file written in
/// blocks carries an index of them at the end, and the index is what turns a compressed file into one you
/// can read from the middle: a seek costs <b>one block</b>, not one file.
/// <para>
/// That is reached through the ordinary <see cref="Stream"/> contract rather than an API of its own
///. <c>CanSeek</c> becomes true when the source can seek and the index can be read, and
/// then <c>Position</c>, <c>Seek</c> and <c>Length</c> do what they do anywhere else — so a caller who
/// wants the tenth megabyte writes <c>Position = 10485760</c> and never learns that this format has an
/// index. A file written as one block seeks no faster than it reads, which is the writer's choice; a
/// source that cannot seek, such as a pipe, stays forward-only rather than failing to open.
/// </para>
/// </remarks>
internal static class XzStreamUsageRecipe
{
/// <summary>A megabyte and a half, so several blocks are worth having.</summary>
private const int ContentLength = 1500000;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var content = Patterned(ContentLength);
var archivePath = context.PathTo("payload.xz");
int blocks;
using (var file = new FileStream(archivePath, FileMode.Create, FileAccess.Write))
{
// Level 6, a CRC-64 after every block, and a block every 256 KiB.
var writer = new XzEncoderStream(file, 6, XzCheck.Crc64, 262144L, leaveOpen: true);
try
{
writer.Write(content, 0, content.Length);
}
finally
{
writer.Dispose();
}
// Asked after disposing, not before: the last block is short and is not written until the
// stream is finished, so a count taken inside the block is one too few.
blocks = writer.BlockCount;
}
context.Say("{0} in, {1} out, in {2} blocks with a CRC-64 each",
RecipeContext.Readable(content.Length),
RecipeContext.Readable(new FileInfo(archivePath).Length), blocks);
using (var file = new FileStream(archivePath, FileMode.Open, FileAccess.Read))
using (var reader = new XzDecoderStream(file, leaveOpen: true))
using (var produced = new MemoryStream())
{
GZipStreamUsageRecipe.CopyAll(reader, produced);
if (!RecipeContext.SameBytes(content, produced.ToArray()))
{
context.Say("the bytes that came back are not the bytes that went in");
return RecipeOutcome.Failed;
}
context.Say("read back whole: {0} blocks, check {1}", reader.BlockCount, reader.Check);
}
using (var file = new FileStream(archivePath, FileMode.Open, FileAccess.Read))
using (var reader = new XzDecoderStream(file, leaveOpen: true))
{
if (!reader.CanSeek)
{
context.Say("this file has no readable index, so it can only be read forwards");
return RecipeOutcome.Failed;
}
context.Say("the index says the content is {0}", RecipeContext.Readable(reader.Length));
// Offsets in no particular order, including one backwards, because that is the case a
// forward-only reader cannot do at all.
foreach (var at in new[] { 1200000L, 300000L, 262144L, 262143L, 0L })
{
reader.Position = at;
var window = new byte[64];
var filled = 0;
while (filled < window.Length)
{
var taken = reader.Read(window, filled, window.Length - filled);
if (taken <= 0) break;
filled += taken;
}
for (var index = 0; index < filled; index++)
{
if (content[(int)at + index] != window[index])
{
context.Say("seeking to {0} gave the wrong byte at {1}", at, index);
return RecipeOutcome.Failed;
}
}
}
context.Say("five seeks, forwards and back, every byte where it should be");
}
return RecipeOutcome.Passed;
}
/// <summary>
/// Content that compresses well and is still different everywhere, so a seek landing in the wrong
/// place is caught rather than matching by luck.
/// </summary>
private static byte[] Patterned(int length)
{
var builder = new StringBuilder(length + 64);
var line = 0;
while (builder.Length < length)
{
builder.Append("line ");
builder.Append(line.ToString(CultureInfo.InvariantCulture));
builder.Append(": the quick brown fox jumps over the lazy dog");
builder.Append(Environment.NewLine);
line++;
}
var all = Encoding.ASCII.GetBytes(builder.ToString());
var result = new byte[length];
Buffer.BlockCopy(all, 0, result, 0, length);
return result;
}
}
}Cross the four-gibibyte line that needs Zip64.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Globalization
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Cross the four-gibibyte line that needs Zip64.
''' </summary>
''' <remarks>
''' The original ZIP record layout holds a size in 32 bits, so 4 GiB − 1 is the largest entry it can
''' describe. Zip64 adds 64-bit sizes in an extra field, and the value <c>0xFFFFFFFF</c> in the old field is
''' what redirects a reader to it. That last detail is the one everybody gets wrong: <c>0xFFFFFFFF</c> is a
''' <em>reserved redirection</em>, not a size, so an entry of exactly 4 294 967 295 bytes needs Zip64 too.
''' Every comparison in this library is therefore <c>>=</c> rather than <c>></c>, and the four-case
''' boundary fixture in the test suite exists because the first version of that code used <c>></c> and
''' produced an archive no reader could open.
''' Nothing has to be switched on. The writer uses Zip64 for an entry that needs it and not for one that
''' does not, so an ordinary archive stays readable by an ordinary reader.
''' </remarks>
Friend Module Zip64LargeFileRecipe
''' <summary>One byte past the largest size the 32-bit fields can describe.</summary>
Private Const OverTheLine As Long = 4294967296L + 1024L
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
If Not context.IsFullRun Then
context.Say("skipped: proving this needs an entry of at least {0}, which takes minutes and",
RecipeContext.Readable(4294967295L))
context.Say("several gibibytes of scratch disk. Run 'ArchiveCookbook Zip64LargeFile --full' for the")
context.Say("real thing. The boundary is covered on every build by Zip64BoundaryTests, which")
context.Say("checks 4 GiB - 1, 4 GiB, 4 GiB + 1 and the 0xFFFFFFFF sentinel itself, and by the")
context.Say("Size=Large oracle fixtures that hand the result to the 7-Zip command line.")
Return RecipeOutcome.Skipped
End If
Dim archivePath = context.PathTo("large.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
context.Say("writing one entry of {0} ({1:N0} bytes) — this is the slow part",
RecipeContext.Readable(OverTheLine), OverTheLine)
Dim lastReport = DateTime.UtcNow
Using writer = started.Writer
AddHandler writer.Monitor.Progress,
Sub(sender, e)
If (DateTime.UtcNow - lastReport).TotalSeconds < 15 Then Return
lastReport = DateTime.UtcNow
context.Say(" {0:N1}% — {1}", e.TotalPercent, RecipeContext.Readable(e.ProcessedBytes))
End Sub
' A generated stream, so nothing this large is ever written to disk uncompressed. It reports its
' own Length, which is how the writer knows at the time it writes the local header that this
' entry needs Zip64 sizes in it.
Using source As New RepeatingStream(OverTheLine)
Dim added = writer.AddStream(source, "large.bin", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
context.Say("the archive itself is {0} on disk", RecipeContext.Readable(New FileInfo(archivePath).Length))
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim entry = archive.Entries(0)
context.Say("read back: {0}, {1:N0} bytes", entry.Name, entry.Size)
If entry.Size <> OverTheLine Then
context.Say("the size did not survive the round trip")
Return RecipeOutcome.Failed
End If
' Verifying it means reading all of it and checking the CRC, which Test does without writing.
Dim tested = archive.Test(Nothing)
If Not tested.Succeeded Then
context.Say(tested.ToString())
Return RecipeOutcome.Failed
End If
context.Say("verified in {0:N0} s", tested.Elapsed.TotalSeconds)
End Using
Return RecipeOutcome.Passed
End Function
''' <summary>
''' A read-only stream of a fixed length that produces a repeating pattern. It exists so a recipe can
''' hand the writer several gibibytes without any of them existing anywhere.
''' </summary>
Private NotInheritable Class RepeatingStream
Inherits Stream
Private ReadOnly _pattern As Byte()
Private ReadOnly _length As Long
Private _position As Long
Public Sub New(length As Long)
If length < 0 Then Throw New ArgumentOutOfRangeException(NameOf(length))
_length = length
' Compressible, but not a single byte repeated: a run of zeros would say nothing about the
' encoder, and this is still small enough that the archive stays a few megabytes.
_pattern = RecipeContext.PseudoRandom(4096, 31)
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return True
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return True
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return False
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Return _length
End Get
End Property
Public Overrides Property Position As Long
Get
Return _position
End Get
Set(value As Long)
If value < 0 Then Throw New ArgumentOutOfRangeException(NameOf(value))
_position = value
End Set
End Property
Public Overrides Function Read(buffer As Byte(), offset As Integer, count As Integer) As Integer
If buffer Is Nothing Then Throw New ArgumentNullException(NameOf(buffer))
Dim remaining = _length - _position
If remaining <= 0 Then Return 0
Dim taken = CInt(Math.Min(CLng(count), remaining))
For index = 0 To taken - 1
buffer(offset + index) = _pattern(CInt((_position + index) Mod _pattern.Length))
Next
_position += taken
Return taken
End Function
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Select Case origin
Case SeekOrigin.Begin
Position = offset
Case SeekOrigin.Current
Position = _position + offset
Case Else
Position = _length + offset
End Select
Return _position
End Function
Public Overrides Sub Flush()
End Sub
Public Overrides Sub SetLength(value As Long)
Throw New NotSupportedException("This stream is read-only.")
End Sub
Public Overrides Sub Write(buffer As Byte(), offset As Integer, count As Integer)
Throw New NotSupportedException("This stream is read-only.")
End Sub
End Class
End Module
End NamespaceC#
using System;
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Cross the four-gibibyte line that needs Zip64.
/// </summary>
/// <remarks>
/// The original ZIP record layout holds a size in 32 bits, so 4 GiB − 1 is the largest entry it can
/// describe. Zip64 adds 64-bit sizes in an extra field, and the value <c>0xFFFFFFFF</c> in the old field is
/// what redirects a reader to it. That last detail is the one everybody gets wrong: <c>0xFFFFFFFF</c> is a
/// <em>reserved redirection</em>, not a size, so an entry of exactly 4 294 967 295 bytes needs Zip64 too.
/// Every comparison in this library is therefore <c>>=</c> rather than <c>></c>, and the four-case
/// boundary fixture in the test suite exists because the first version of that code used <c>></c> and
/// produced an archive no reader could open.
/// Nothing has to be switched on. The writer uses Zip64 for an entry that needs it and not for one that
/// does not, so an ordinary archive stays readable by an ordinary reader.
/// </remarks>
internal static class Zip64LargeFileRecipe
{
/// <summary>One kibibyte past the largest size the 32-bit fields can describe.</summary>
private const long OverTheLine = 4294967296L + 1024L;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
if (!context.IsFullRun)
{
context.Say("skipped: proving this needs an entry of at least {0}, which takes minutes and",
RecipeContext.Readable(4294967295L));
context.Say("several gibibytes of scratch disk. Run 'ArchiveCookbookCs Zip64LargeFile --full' for");
context.Say("the real thing. The boundary is covered on every build by Zip64BoundaryTests, which");
context.Say("checks 4 GiB - 1, 4 GiB, 4 GiB + 1 and the 0xFFFFFFFF sentinel itself, and by the");
context.Say("Size=Large oracle fixtures that hand the result to the 7-Zip command line.");
return RecipeOutcome.Skipped;
}
var archivePath = context.PathTo("large.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
context.Say("writing one entry of {0} ({1:N0} bytes) — this is the slow part",
RecipeContext.Readable(OverTheLine), OverTheLine);
var lastReport = DateTime.UtcNow;
using (var writer = started.Writer)
{
writer.Monitor.Progress += (sender, e) =>
{
if ((DateTime.UtcNow - lastReport).TotalSeconds < 15) return;
lastReport = DateTime.UtcNow;
context.Say(" {0:N1}% — {1}", e.TotalPercent, RecipeContext.Readable(e.ProcessedBytes));
};
// A generated stream, so nothing this large is ever written to disk uncompressed. It reports its
// own Length, which is how the writer knows at the time it writes the local header that this
// entry needs Zip64 sizes in it.
using (var source = new RepeatingStream(OverTheLine))
{
var added = writer.AddStream(source, "large.bin", null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
context.Say("the archive itself is {0} on disk", RecipeContext.Readable(new FileInfo(archivePath).Length));
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var entry = archive.Entries[0];
context.Say("read back: {0}, {1:N0} bytes", entry.Name, entry.Size);
if (entry.Size != OverTheLine)
{
context.Say("the size did not survive the round trip");
return RecipeOutcome.Failed;
}
// Verifying it means reading all of it and checking the CRC, which Test does without writing.
var tested = archive.Test(null);
if (!tested.Succeeded)
{
context.Say(tested.ToString());
return RecipeOutcome.Failed;
}
context.Say("verified in {0:N0} s", tested.Elapsed.TotalSeconds);
}
return RecipeOutcome.Passed;
}
/// <summary>
/// A read-only stream of a fixed length that produces a repeating pattern. It exists so a recipe can
/// hand the writer several gibibytes without any of them existing anywhere.
/// </summary>
private sealed class RepeatingStream : Stream
{
private readonly byte[] _pattern;
private readonly long _length;
private long _position;
public RepeatingStream(long length)
{
if (length < 0) throw new ArgumentOutOfRangeException(nameof(length));
_length = length;
// Compressible, but not a single byte repeated: a run of zeros would say nothing about the
// encoder, and this is still small enough that the archive stays a few megabytes.
_pattern = RecipeContext.PseudoRandom(4096, 31);
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _length;
public override long Position
{
get { return _position; }
set
{
if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
_position = value;
}
}
public override int Read(byte[] buffer, int offset, int count)
{
if (buffer == null) throw new ArgumentNullException(nameof(buffer));
var remaining = _length - _position;
if (remaining <= 0) return 0;
var taken = (int)Math.Min(count, remaining);
for (var index = 0; index < taken; index++)
{
buffer[offset + index] = _pattern[(int)((_position + index) % _pattern.Length)];
}
_position += taken;
return taken;
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
Position = offset;
break;
case SeekOrigin.Current:
Position = _position + offset;
break;
default:
Position = _length + offset;
break;
}
return _position;
}
public override void Flush()
{
}
public override void SetLength(long value)
{
throw new NotSupportedException("This stream is read-only.");
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException("This stream is read-only.");
}
}
}
}Stop a decompression bomb with a total-byte budget.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System.Globalization
Imports System.IO
Imports Bastion.Archive
Imports Bastion.Archive.Security
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Stop a decompression bomb with a total-byte budget.
''' </summary>
''' <remarks>
''' The guards are on by default and are the caller's to tighten: total bytes produced, entry count, ratio,
''' nesting depth and path length all live on <see cref="ExtractionPolicy"/>.
''' Which guard matters is worth understanding, because the obvious one is the weaker one. A ratio limit
''' cannot catch an honest Deflate stream at all: RFC 1951 caps a single stream at about 1032:1, and eight
''' mebibytes of zeros only reaches about 840:1, well under the 100 000:1 default. What a ratio limit
''' catches is a producer <em>lying</em> in the header about how much data is coming. The guard that stops a
''' large honest entry, or a thousand of them, is the total-byte budget — which is why this recipe sets that
''' one, and why the default is 1 TiB rather than unlimited.
''' The quoted-overlap bombs of the Fifield class are refused earlier still, at open time: every quoted copy
''' has to point at a local header bearing another entry's name, and the reader cross-checks the two.
''' </remarks>
Friend Module ZipBombGuardRecipe
''' <summary>Eight mebibytes of zeros, which Deflate takes down to a few kilobytes.</summary>
Private Const PayloadSize As Integer = 8 * 1024 * 1024
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim archivePath = context.PathTo("compressible.zip")
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Dim payload(PayloadSize - 1) As Byte
Using writer = started.Writer
For index = 0 To 3
Using source As New MemoryStream(payload, False)
Dim added = writer.AddStream(source, "zeros-" & index.ToString("D2", CultureInfo.InvariantCulture) & ".bin", Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
End Using
Next
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim archiveSize = New FileInfo(archivePath).Length
Dim declared = 4L * PayloadSize
context.Say("{0} on disk claims to hold {1} — a ratio of about {2:N0}:1",
RecipeContext.Readable(archiveSize), RecipeContext.Readable(declared), declared \ archiveSize)
' A budget of 10 MiB against 32 MiB of content: the extraction stops rather than filling the disk.
Dim frugal As New ExtractionOptions()
frugal.Policy.MaxTotalBytes = 10L * 1024L * 1024L
frugal.Policy.Overwrite = OverwriteMode.Overwrite
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim target = context.PathTo("out-budgeted")
Dim stopped = archive.ExtractAll(target, frugal)
If stopped.Succeeded Then
context.Say("a 10 MiB budget should not have extracted 32 MiB")
Return RecipeOutcome.Failed
End If
context.Say("with a 10 MiB budget: {0} — {1}", stopped.ErrorCode, stopped.ErrorDescription)
If stopped.ErrorCode <> ErrorCode.LimitExceeded Then
context.Say("expected LimitExceeded")
Return RecipeOutcome.Failed
End If
Dim produced = 0L
If Directory.Exists(target) Then
For Each filePath In Directory.GetFiles(target, "*", SearchOption.AllDirectories)
produced += New FileInfo(filePath).Length
Next
End If
context.Say("it stopped after producing {0}, not {1}",
RecipeContext.Readable(produced), RecipeContext.Readable(declared))
If produced > frugal.Policy.MaxTotalBytes Then
context.Say("more was written than the budget allowed")
Return RecipeOutcome.Failed
End If
' An entry-count limit is the same idea for an archive with a million tiny files.
Dim fewEntries As New ExtractionOptions()
fewEntries.Policy.MaxEntryCount = 2
fewEntries.Policy.Overwrite = OverwriteMode.Overwrite
Dim refused = archive.ExtractAll(context.PathTo("out-counted"), fewEntries)
If refused.Succeeded Then
context.Say("an entry limit of 2 should not have extracted 4 entries")
Return RecipeOutcome.Failed
End If
context.Say("with an entry limit of 2: {0}", refused.ErrorCode)
' With a budget that fits, the same archive extracts normally: the guard is a limit, not a veto.
Dim generous As New ExtractionOptions()
generous.Policy.MaxTotalBytes = 64L * 1024L * 1024L
Dim allowed = archive.ExtractAll(context.PathTo("out-allowed"), generous)
If Not allowed.Succeeded Then
context.Say(allowed.ToString())
Return RecipeOutcome.Failed
End If
context.Say("with a 64 MiB budget the same archive extracted in {0:N0} ms",
allowed.Elapsed.TotalMilliseconds)
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System.Globalization;
using System.IO;
using Bastion.Archive.Security;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Stop a decompression bomb with a total-byte budget.
/// </summary>
/// <remarks>
/// The guards are on by default and are the caller's to tighten: total bytes produced, entry count, ratio,
/// nesting depth and path length all live on <see cref="ExtractionPolicy"/>.
/// Which guard matters is worth understanding, because the obvious one is the weaker one. A ratio limit
/// cannot catch an honest Deflate stream at all: RFC 1951 caps a single stream at about 1032:1, and eight
/// mebibytes of zeros only reaches about 840:1, well under the 100 000:1 default. What a ratio limit
/// catches is a producer <em>lying</em> in the header about how much data is coming. The guard that stops a
/// large honest entry, or a thousand of them, is the total-byte budget — which is why this recipe sets that
/// one, and why the default is 1 TiB rather than unlimited.
/// The quoted-overlap bombs of the Fifield class are refused earlier still, at open time: every quoted copy
/// has to point at a local header bearing another entry's name, and the reader cross-checks the two.
/// </remarks>
internal static class ZipBombGuardRecipe
{
/// <summary>Eight mebibytes of zeros, which Deflate takes down to a few kilobytes.</summary>
private const int PayloadSize = 8 * 1024 * 1024;
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var archivePath = context.PathTo("compressible.zip");
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
var payload = new byte[PayloadSize];
using (var writer = started.Writer)
{
for (var index = 0; index < 4; index++)
{
using (var source = new MemoryStream(payload, false))
{
var name = "zeros-" + index.ToString("D2", CultureInfo.InvariantCulture) + ".bin";
var added = writer.AddStream(source, name, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
}
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var archiveSize = new FileInfo(archivePath).Length;
var declared = 4L * PayloadSize;
context.Say("{0} on disk claims to hold {1} — a ratio of about {2:N0}:1",
RecipeContext.Readable(archiveSize), RecipeContext.Readable(declared), declared / archiveSize);
// A budget of 10 MiB against 32 MiB of content: the extraction stops rather than filling the disk.
var frugal = new ExtractionOptions();
frugal.Policy.MaxTotalBytes = 10L * 1024L * 1024L;
frugal.Policy.Overwrite = OverwriteMode.Overwrite;
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var target = context.PathTo("out-budgeted");
var stopped = archive.ExtractAll(target, frugal);
if (stopped.Succeeded)
{
context.Say("a 10 MiB budget should not have extracted 32 MiB");
return RecipeOutcome.Failed;
}
context.Say("with a 10 MiB budget: {0} — {1}", stopped.ErrorCode, stopped.ErrorDescription);
if (stopped.ErrorCode != ErrorCode.LimitExceeded)
{
context.Say("expected LimitExceeded");
return RecipeOutcome.Failed;
}
var produced = 0L;
if (Directory.Exists(target))
{
foreach (var filePath in Directory.GetFiles(target, "*", SearchOption.AllDirectories))
{
produced += new FileInfo(filePath).Length;
}
}
context.Say("it stopped after producing {0}, not {1}",
RecipeContext.Readable(produced), RecipeContext.Readable(declared));
if (produced > frugal.Policy.MaxTotalBytes)
{
context.Say("more was written than the budget allowed");
return RecipeOutcome.Failed;
}
// An entry-count limit is the same idea for an archive with a million tiny files.
var fewEntries = new ExtractionOptions();
fewEntries.Policy.MaxEntryCount = 2;
fewEntries.Policy.Overwrite = OverwriteMode.Overwrite;
var refused = archive.ExtractAll(context.PathTo("out-counted"), fewEntries);
if (refused.Succeeded)
{
context.Say("an entry limit of 2 should not have extracted 4 entries");
return RecipeOutcome.Failed;
}
context.Say("with an entry limit of 2: {0}", refused.ErrorCode);
// With a budget that fits, the same archive extracts normally: the guard is a limit, not a veto.
var generous = new ExtractionOptions();
generous.Policy.MaxTotalBytes = 64L * 1024L * 1024L;
var allowed = archive.ExtractAll(context.PathTo("out-allowed"), generous);
if (!allowed.Succeeded)
{
context.Say(allowed.ToString());
return RecipeOutcome.Failed;
}
context.Say("with a 64 MiB budget the same archive extracted in {0:N0} ms",
allowed.Elapsed.TotalMilliseconds);
}
return RecipeOutcome.Passed;
}
}
}Add a whole directory tree under a prefix.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Add a whole directory tree under a prefix.
''' </summary>
''' <remarks>
''' <c>AddDirectory</c> walks the tree in a stable order, so the same tree gives the same archive on every
''' framework and every operating system. The names it writes use forward slashes, whatever
''' the platform's separator is, because that is what the format says and what every other reader expects.
''' A filter is a plain <c>Func(Of String, Boolean)</c> over the relative path, which is enough to express
''' "no logs", "only source" or an entire include/exclude language of the caller's own.
''' </remarks>
Friend Module ZipFolderRecursiveRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
' Two files the filter should leave out, one of them inside a subdirectory.
File.WriteAllText(Path.Combine(source, "build.log"), "noise")
File.WriteAllText(Path.Combine(source, "documents", "notes.log"), "more noise")
Dim archivePath = context.PathTo("tree.zip")
Dim options As New AdditionOptions() With {
.Recursive = True,
.IncludeDirectoryEntries = True,
.Filter = Function(relative) Not relative.EndsWith(".log", StringComparison.OrdinalIgnoreCase)
}
Dim started = ArchiveWriter.Create(archivePath)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Using writer = started.Writer
' The prefix is how a tree lands inside a folder in the archive rather than at its root.
Dim added = writer.AddDirectory(source, "project", options)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
End Using
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim directories = 0
For Each entry In archive.Entries
context.Say(" {0}{1}", entry.Name, If(entry.IsDirectory, " (directory entry)", ""))
If entry.IsDirectory Then directories += 1
If Not entry.Name.StartsWith("project/", StringComparison.Ordinal) Then
context.Say("an entry escaped the prefix: " & entry.Name)
Return RecipeOutcome.Failed
End If
If entry.Name.IndexOf("\"c) >= 0 Then
context.Say("a backslash reached the archive: " & entry.Name)
Return RecipeOutcome.Failed
End If
If entry.Name.EndsWith(".log", StringComparison.OrdinalIgnoreCase) Then
context.Say("the filter let a log file through: " & entry.Name)
Return RecipeOutcome.Failed
End If
Next
If directories = 0 Then
context.Say("no directory entry was written, so an empty folder would not survive")
Return RecipeOutcome.Failed
End If
context.Say("{0} entries, {1} of them directories, all under 'project/'", archive.Entries.Count, directories)
End Using
Return RecipeOutcome.Passed
End Function
End Module
End NamespaceC#
using System;
using System.IO;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Add a whole directory tree under a prefix.
/// </summary>
/// <remarks>
/// <c>AddDirectory</c> walks the tree in a stable order, so the same tree gives the same archive on every
/// framework and every operating system. The names it writes use forward slashes, whatever
/// the platform's separator is, because that is what the format says and what every other reader expects.
/// A filter is a plain <c>Func<string, bool></c> over the relative path, which is enough to express
/// "no logs", "only source" or an entire include/exclude language of the caller's own.
/// </remarks>
internal static class ZipFolderRecursiveRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
// Two files the filter should leave out, one of them inside a subdirectory.
File.WriteAllText(Path.Combine(source, "build.log"), "noise");
File.WriteAllText(Path.Combine(source, "documents", "notes.log"), "more noise");
var archivePath = context.PathTo("tree.zip");
var options = new AdditionOptions
{
Recursive = true,
IncludeDirectoryEntries = true,
Filter = relative => !relative.EndsWith(".log", StringComparison.OrdinalIgnoreCase)
};
var started = ArchiveWriter.Create(archivePath);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
using (var writer = started.Writer)
{
// The prefix is how a tree lands inside a folder in the archive rather than at its root.
var added = writer.AddDirectory(source, "project", options);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
}
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var directories = 0;
foreach (var entry in archive.Entries)
{
context.Say(" {0}{1}", entry.Name, entry.IsDirectory ? " (directory entry)" : "");
if (entry.IsDirectory) directories++;
if (!entry.Name.StartsWith("project/", StringComparison.Ordinal))
{
context.Say("an entry escaped the prefix: " + entry.Name);
return RecipeOutcome.Failed;
}
if (entry.Name.IndexOf('\\') >= 0)
{
context.Say("a backslash reached the archive: " + entry.Name);
return RecipeOutcome.Failed;
}
if (entry.Name.EndsWith(".log", StringComparison.OrdinalIgnoreCase))
{
context.Say("the filter let a log file through: " + entry.Name);
return RecipeOutcome.Failed;
}
}
if (directories == 0)
{
context.Say("no directory entry was written, so an empty folder would not survive");
return RecipeOutcome.Failed;
}
context.Say("{0} entries, {1} of them directories, all under 'project/'", archive.Entries.Count, directories);
}
return RecipeOutcome.Passed;
}
}
}Write the same files with bzip2, LZMA and XZ, and see what each one costs.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports Bastion.Archive
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Write the same files with bzip2, LZMA and XZ, and see what each one costs.
''' </summary>
''' <remarks>
''' Deflate is what every ZIP reader implements and what this library writes unless told otherwise. The
''' later methods — bzip2 (12), LZMA (14) and XZ (95) — compress better, sometimes much better, at the price
''' of a reader that has to implement them. A store-and-Deflate reader will open such an archive, list its
''' entries and then fail to extract them, which is a worse experience than being refused outright.
''' That is what WinZip's <c>.zipx</c> extension is for, and
''' <see cref="CompressionSettings.RecommendedFileExtension"/> says which name a given set of settings calls
''' for. The extension changes nothing inside the archive: the bytes are a ZIP either way and each entry's
''' header carries its method as always. The writer also reports the choice as an interoperability warning,
''' so a caller who did not think about it finds out anyway.
''' Which to pick depends on the data, and this recipe prints the evidence rather than asserting a ranking.
''' On the documentation-like prose it generates, bzip2 wins comfortably — a block sort suits text with
''' little long-range structure, and the reference bzip2 and LZMA rank the same way round on these bytes —
''' while LZMA and XZ come in a little under Deflate. On a source tree or a log file the order is usually
''' the other way about, which is the point: measure your own data.
''' One thing worth knowing about the levels. From five up the encoder chooses what to emit by **price**,
''' pricing every candidate through the range coder and taking the cheapest path; below five it takes the
''' longest match it can find, which is about six times faster and, on prose, some forty per cent worse.
''' 7-Zip draws the line in the same place.
''' </remarks>
Friend Module ZipxMethodsRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim source = context.CreateSampleTree()
' A quarter of a mebibyte of prose, because on a handful of small files the per-entry overhead of
' the later methods swamps what they save and the comparison says nothing. This is the size at
' which the choice starts to matter.
File.WriteAllBytes(Path.Combine(source, "manual.txt"), Prose(256 * 1024))
Dim expected = File.ReadAllBytes(Path.Combine(source, "manual.txt"))
' Deflate first, as the baseline every reader can manage.
Dim baseline = WriteWith(context, source, CompressionMethod.Deflate, "baseline")
If baseline < 0 Then Return RecipeOutcome.Failed
context.Say("Deflate: {0:N0} bytes, recommended name .zip", baseline)
For Each method In New CompressionMethod() {CompressionMethod.BZip2, CompressionMethod.Lzma,
CompressionMethod.Xz}
Dim settings As New CompressionSettings() With {.Method = method, .Level = 9}
' The name the convention calls for, which is advice rather than something done behind the
' caller's back: this recipe uses it to name the file.
Dim extension = settings.RecommendedFileExtension
If extension <> ".zipx" Then
context.Say("{0} should recommend .zipx, not {1}", method, extension)
Return RecipeOutcome.Failed
End If
Dim archivePath = context.PathTo(method.ToString().ToLowerInvariant() & extension)
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return RecipeOutcome.Failed
End If
Dim warned = False
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return RecipeOutcome.Failed
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return RecipeOutcome.Failed
End If
For Each warning In writer.InteropWarnings
If warning.Feature.IndexOf(".zipx", StringComparison.Ordinal) >= 0 Then warned = True
Next
End Using
If Not warned Then
context.Say("{0} should have reported the naming convention as an interoperability warning", method)
Return RecipeOutcome.Failed
End If
' And it has to read back, which is the only claim that matters.
Dim opened = Archive.Open(archivePath)
If Not opened.Succeeded Then
context.Say(opened.ToString())
Return RecipeOutcome.Failed
End If
Using archive = opened.Archive
Dim target = context.PathTo("out-" & method.ToString())
Dim extracted = archive.ExtractAll(target)
If Not extracted.Succeeded Then
context.Say(extracted.ToString())
Return RecipeOutcome.Failed
End If
Dim recovered = File.ReadAllBytes(Path.Combine(target, "manual.txt"))
If Not RecipeContext.SameBytes(expected, recovered) Then
context.Say("{0} did not give back what it was given", method)
Return RecipeOutcome.Failed
End If
End Using
Dim size = New FileInfo(archivePath).Length
context.Say("{0}: {1:N0} bytes ({2:P0} of Deflate), read back byte for byte, named {3}",
method, size, size / CDbl(baseline), Path.GetFileName(archivePath))
Next
Return RecipeOutcome.Passed
End Function
''' <summary>
''' Prose of about the given length: sentences drawn from a small vocabulary, which compresses the way
''' documentation does rather than the way a single repeated byte does.
''' </summary>
Private Function Prose(length As Integer) As Byte()
Dim words = New String() {"archive", "entry", "method", "deflate", "dictionary", "window", "match",
"literal", "checksum", "header", "volume", "stream", "reader", "writer",
"compressed", "uncompressed", "the", "a", "and", "of", "that", "which"}
Dim random As New Random(20260911)
Dim builder As New System.Text.StringBuilder(length + 64)
While builder.Length < length
Dim wordsInSentence = 6 + random.Next(12)
For index = 0 To wordsInSentence - 1
If index > 0 Then builder.Append(" "c)
builder.Append(words(random.Next(words.Length)))
Next
builder.Append(". ")
If random.Next(6) = 0 Then builder.Append(Environment.NewLine)
End While
Return System.Text.Encoding.UTF8.GetBytes(builder.ToString().Substring(0, length))
End Function
''' <summary>Writes the tree with one method and returns the archive's size, or -1 on failure.</summary>
Private Function WriteWith(context As RecipeContext, source As String, method As CompressionMethod,
name As String) As Long
Dim settings As New CompressionSettings() With {.Method = method, .Level = 9}
Dim archivePath = context.PathTo(name & settings.RecommendedFileExtension)
Dim started = ArchiveWriter.Create(archivePath, settings)
If Not started.Succeeded Then
context.Say(started.ToString())
Return -1
End If
Using writer = started.Writer
Dim added = writer.AddDirectory(source, Nothing, Nothing)
If Not added.Succeeded Then
context.Say(added.ToString())
Return -1
End If
Dim finished = writer.Complete()
If Not finished.Succeeded Then
context.Say(finished.ToString())
Return -1
End If
End Using
Return New FileInfo(archivePath).Length
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using System.Text;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Write the same files with bzip2, LZMA and XZ, and see what each one costs.
/// </summary>
/// <remarks>
/// Deflate is what every ZIP reader implements and what this library writes unless told otherwise. The
/// later methods — bzip2 (12), LZMA (14) and XZ (95) — compress better, sometimes much better, at the price
/// of a reader that has to implement them. A store-and-Deflate reader will open such an archive, list its
/// entries and then fail to extract them, which is a worse experience than being refused outright.
/// That is what WinZip's <c>.zipx</c> extension is for, and
/// <see cref="CompressionSettings.RecommendedFileExtension"/> says which name a given set of settings calls
/// for. The extension changes nothing inside the archive: the bytes are a ZIP either way and each entry's
/// header carries its method as always. The writer also reports the choice as an interoperability warning,
/// so a caller who did not think about it finds out anyway.
/// Which to pick depends on the data, and this recipe prints the evidence rather than asserting a ranking.
/// On the documentation-like prose it generates, bzip2 wins comfortably — a block sort suits text with
/// little long-range structure, and the reference bzip2 and LZMA rank the same way round on these bytes —
/// while LZMA and XZ come in a little under Deflate. On a source tree or a log file the order is usually
/// the other way about, which is the point: measure your own data.
/// One thing worth knowing about the levels. From five up the encoder chooses what to emit by **price**,
/// pricing every candidate through the range coder and taking the cheapest path; below five it takes the
/// longest match it can find, which is about six times faster and, on prose, some forty per cent worse.
/// 7-Zip draws the line in the same place.
/// </remarks>
internal static class ZipxMethodsRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var source = context.CreateSampleTree();
// A quarter of a mebibyte of prose, because on a handful of small files the per-entry overhead of
// the later methods swamps what they save and the comparison says nothing. This is the size at
// which the choice starts to matter.
File.WriteAllBytes(Path.Combine(source, "manual.txt"), Prose(256 * 1024));
var expected = File.ReadAllBytes(Path.Combine(source, "manual.txt"));
// Deflate first, as the baseline every reader can manage.
var baseline = WriteWith(context, source, CompressionMethod.Deflate, "baseline");
if (baseline < 0)
{
return RecipeOutcome.Failed;
}
context.Say("Deflate: {0:N0} bytes, recommended name .zip", baseline);
foreach (var method in new[] { CompressionMethod.BZip2, CompressionMethod.Lzma, CompressionMethod.Xz })
{
var settings = new CompressionSettings { Method = method, Level = 9 };
// The name the convention calls for, which is advice rather than something done behind the
// caller's back: this recipe uses it to name the file.
var extension = settings.RecommendedFileExtension;
if (extension != ".zipx")
{
context.Say("{0} should recommend .zipx, not {1}", method, extension);
return RecipeOutcome.Failed;
}
var archivePath = context.PathTo(method.ToString().ToLowerInvariant() + extension);
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return RecipeOutcome.Failed;
}
var warned = false;
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return RecipeOutcome.Failed;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return RecipeOutcome.Failed;
}
foreach (var warning in writer.InteropWarnings)
{
if (warning.Feature.IndexOf(".zipx", StringComparison.Ordinal) >= 0)
{
warned = true;
}
}
}
if (!warned)
{
context.Say("{0} should have reported the naming convention as an interoperability warning", method);
return RecipeOutcome.Failed;
}
// And it has to read back, which is the only claim that matters.
var opened = Archive.Open(archivePath);
if (!opened.Succeeded)
{
context.Say(opened.ToString());
return RecipeOutcome.Failed;
}
using (var archive = opened.Archive)
{
var target = context.PathTo("out-" + method);
var extracted = archive.ExtractAll(target);
if (!extracted.Succeeded)
{
context.Say(extracted.ToString());
return RecipeOutcome.Failed;
}
var recovered = File.ReadAllBytes(Path.Combine(target, "manual.txt"));
if (!RecipeContext.SameBytes(expected, recovered))
{
context.Say("{0} did not give back what it was given", method);
return RecipeOutcome.Failed;
}
}
var size = new FileInfo(archivePath).Length;
context.Say("{0}: {1:N0} bytes ({2:P0} of Deflate), read back byte for byte, named {3}",
method, size, size / (double)baseline, Path.GetFileName(archivePath));
}
return RecipeOutcome.Passed;
}
/// <summary>
/// Sentences drawn from a small vocabulary: text that compresses the way documentation does rather than
/// the way a single repeated byte does.
/// </summary>
private static byte[] Prose(int length)
{
var words = new[]
{
"archive", "entry", "method", "deflate", "dictionary", "window", "match", "literal", "checksum",
"header", "volume", "stream", "reader", "writer", "compressed", "uncompressed", "the", "a",
"and", "of", "that", "which"
};
var random = new Random(20260911);
var builder = new StringBuilder(length + 64);
while (builder.Length < length)
{
var wordsInSentence = 6 + random.Next(12);
for (var index = 0; index < wordsInSentence; index++)
{
if (index > 0)
{
builder.Append(' ');
}
builder.Append(words[random.Next(words.Length)]);
}
builder.Append(". ");
if (random.Next(6) == 0)
{
builder.Append(Environment.NewLine);
}
}
return Encoding.UTF8.GetBytes(builder.ToString().Substring(0, length));
}
/// <summary>Writes the tree with one method and returns the archive's size, or -1 on failure.</summary>
private static long WriteWith(RecipeContext context, string source, CompressionMethod method, string name)
{
var settings = new CompressionSettings { Method = method, Level = 9 };
var archivePath = context.PathTo(name + settings.RecommendedFileExtension);
var started = ArchiveWriter.Create(archivePath, settings);
if (!started.Succeeded)
{
context.Say(started.ToString());
return -1;
}
using (var writer = started.Writer)
{
var added = writer.AddDirectory(source, null, null);
if (!added.Succeeded)
{
context.Say(added.ToString());
return -1;
}
var finished = writer.Complete();
if (!finished.Succeeded)
{
context.Say(finished.ToString());
return -1;
}
}
return new FileInfo(archivePath).Length;
}
}
}Compress and decompress Zstandard, including the frames a reader is expected to walk past.
VB.NET
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.IO
Imports System.Text
Imports Bastion.Archive
Imports Bastion.Archive.Codecs
Namespace Bastion.Archive.Samples.Cookbook
''' <summary>
''' Compress and decompress Zstandard, including the frames a reader is expected to walk past.
''' </summary>
''' <remarks>
''' Zstandard is one class each way and no ceremony, so what this recipe is really about is the three
''' things about the format that surprise people.
''' <para>
''' A <c>.zst</c> file is **frames concatenated**, exactly as gzip is members concatenated, and one kind of
''' frame is not content at all: a **skippable frame** carries whatever an application wants to put beside
''' the data, and a decoder is required to step over it without comment. Tools use them for their own
''' metadata, so a reader that choked on one would fail on files that are perfectly sound.
''' </para>
''' <para>
''' A frame's header names the window it needs, and a file is free to ask for far more than the machine
''' has; the decoder therefore takes a cap and refuses anything above it rather than trying
''' and falling over.
''' </para>
''' <para>
''' And the frame ends with a checksum of what it produced, which is the XXH64 of the content cut down to
''' four bytes. It is optional, and writing it is the sensible default.
''' </para>
''' </remarks>
Friend Module ZstdDecodeRecipe
''' <summary>Runs the recipe.</summary>
Public Function Run(context As RecipeContext) As RecipeOutcome
Dim content = Encoding.UTF8.GetBytes(
GZipStreamUsageRecipe.Repeat("the quick brown fox jumps over the lazy dog. ", 2000))
Dim singlePath = context.PathTo("payload.zst")
Dim blocks As Integer
Using file As New FileStream(singlePath, FileMode.Create, FileAccess.Write)
Dim writer As New ZstdEncoderStream(file, 5, True, leaveOpen:=True)
Try
writer.Write(content, 0, content.Length)
Finally
writer.Dispose()
End Try
blocks = writer.BlockCount
End Using
context.Say("{0} in, {1} out in {2} block(s), window {3}",
RecipeContext.Readable(content.Length),
RecipeContext.Readable(New FileInfo(singlePath).Length), blocks,
RecipeContext.Readable(1024 * 1024))
Using file As New FileStream(singlePath, FileMode.Open, FileAccess.Read)
Using reader As New ZstdDecoderStream(file, leaveOpen:=True)
Using produced As New MemoryStream()
GZipStreamUsageRecipe.CopyAll(reader, produced)
If Not RecipeContext.SameBytes(content, produced.ToArray()) Then
context.Say("the bytes that came back are not the bytes that went in")
Return RecipeOutcome.Failed
End If
context.Say("read back {0} from {1} frame(s) of {2} block(s), window {3}",
RecipeContext.Readable(produced.Length), reader.FrameCount,
reader.BlockCount, RecipeContext.Readable(reader.WindowBytes))
End Using
End Using
End Using
' Two content frames with a skippable one between them, which a reader must step over in silence.
Dim joinedPath = context.PathTo("joined.zst")
Dim first = Encoding.UTF8.GetBytes("the first frame" & Environment.NewLine)
Dim second = Encoding.UTF8.GetBytes("the second frame" & Environment.NewLine)
Using file As New FileStream(joinedPath, FileMode.Create, FileAccess.Write)
WriteFrame(file, first)
Dim aside = SkippableFrame(Encoding.ASCII.GetBytes("metadata some other tool left here"))
file.Write(aside, 0, aside.Length)
WriteFrame(file, second)
End Using
Using file As New FileStream(joinedPath, FileMode.Open, FileAccess.Read)
Using reader As New ZstdDecoderStream(file, leaveOpen:=True)
Using produced As New MemoryStream()
GZipStreamUsageRecipe.CopyAll(reader, produced)
Dim text = Encoding.UTF8.GetString(produced.ToArray())
Dim expected = Encoding.UTF8.GetString(first) & Encoding.UTF8.GetString(second)
If Not String.Equals(text, expected, StringComparison.Ordinal) Then
context.Say("the joined file read back as: " & text.Replace(Environment.NewLine, "|"))
Return RecipeOutcome.Failed
End If
context.Say("two frames read as one stream, with a skippable frame stepped over between them")
End Using
End Using
End Using
' The memory cap, refused rather than attempted.
Using file As New FileStream(singlePath, FileMode.Open, FileAccess.Read)
Try
Dim taken As Integer
Using reader As New ZstdDecoderStream(file, leaveOpen:=True, maximumWindowBytes:=1024)
Dim scratch(255) As Byte
taken = reader.Read(scratch, 0, scratch.Length)
End Using
context.Say("a 1 KiB window cap gave back {0} bytes from a frame that needs more, " &
"which it must not", taken)
Return RecipeOutcome.Failed
Catch ex As ArchiveException
context.Say("a 1 KiB window cap refused the file: {0}", ex.ErrorCode)
End Try
End Using
Return RecipeOutcome.Passed
End Function
''' <summary>Writes one complete frame of content.</summary>
Private Sub WriteFrame(destination As Stream, content As Byte())
Using writer As New ZstdEncoderStream(destination, 5, True, leaveOpen:=True)
writer.Write(content, 0, content.Length)
writer.Complete()
End Using
End Sub
''' <summary>
''' A skippable frame: a magic number in the reserved range, a length, and whatever the application
''' wanted to carry. It holds no content and every decoder is required to walk past it.
''' </summary>
Private Function SkippableFrame(payload As Byte()) As Byte()
Dim frame(8 + payload.Length - 1) As Byte
' 0x184D2A50 through 0x184D2A5F are all skippable; the low nibble is the application's to choose.
frame(0) = &H50
frame(1) = &H2A
frame(2) = &H4D
frame(3) = &H18
For index = 0 To 3
frame(4 + index) = CByte((CLng(payload.Length) >> (8 * index)) And &HFFL)
Next
System.Buffer.BlockCopy(payload, 0, frame, 8, payload.Length)
Return frame
End Function
End Module
End NamespaceC#
using System;
using System.IO;
using System.Text;
using Bastion.Archive.Codecs;
namespace Bastion.Archive.Samples.Cookbook
{
/// <summary>
/// Compress and decompress Zstandard, including the frames a reader is expected to walk past.
/// </summary>
/// <remarks>
/// Zstandard is one class each way and no ceremony, so what this recipe is really about is the three
/// things about the format that surprise people.
/// <para>
/// A <c>.zst</c> file is <b>frames concatenated</b>, exactly as gzip is members concatenated, and one kind
/// of frame is not content at all: a <b>skippable frame</b> carries whatever an application wants to put
/// beside the data, and a decoder is required to step over it without comment. Tools use them for their
/// own metadata, so a reader that choked on one would fail on files that are perfectly sound.
/// </para>
/// <para>
/// A frame's header names the window it needs, and a file is free to ask for far more than the machine
/// has; the decoder therefore takes a cap and refuses anything above it rather than trying
/// and falling over.
/// </para>
/// <para>
/// And the frame ends with a checksum of what it produced, which is the XXH64 of the content cut down to
/// four bytes. It is optional, and writing it is the sensible default.
/// </para>
/// </remarks>
internal static class ZstdDecodeRecipe
{
/// <summary>Runs the recipe.</summary>
public static RecipeOutcome Run(RecipeContext context)
{
var content = Encoding.UTF8.GetBytes(
GZipStreamUsageRecipe.Repeat("the quick brown fox jumps over the lazy dog. ", 2000));
var singlePath = context.PathTo("payload.zst");
int blocks;
using (var file = new FileStream(singlePath, FileMode.Create, FileAccess.Write))
{
var writer = new ZstdEncoderStream(file, 5, true, leaveOpen: true);
try
{
writer.Write(content, 0, content.Length);
}
finally
{
writer.Dispose();
}
blocks = writer.BlockCount;
}
context.Say("{0} in, {1} out in {2} block(s), window {3}",
RecipeContext.Readable(content.Length),
RecipeContext.Readable(new FileInfo(singlePath).Length), blocks,
RecipeContext.Readable(1024 * 1024));
using (var file = new FileStream(singlePath, FileMode.Open, FileAccess.Read))
using (var reader = new ZstdDecoderStream(file, leaveOpen: true))
using (var produced = new MemoryStream())
{
GZipStreamUsageRecipe.CopyAll(reader, produced);
if (!RecipeContext.SameBytes(content, produced.ToArray()))
{
context.Say("the bytes that came back are not the bytes that went in");
return RecipeOutcome.Failed;
}
context.Say("read back {0} from {1} frame(s) of {2} block(s), window {3}",
RecipeContext.Readable(produced.Length), reader.FrameCount,
reader.BlockCount, RecipeContext.Readable(reader.WindowBytes));
}
// Two content frames with a skippable one between them, which a reader must step over in silence.
var joinedPath = context.PathTo("joined.zst");
var first = Encoding.UTF8.GetBytes("the first frame" + Environment.NewLine);
var second = Encoding.UTF8.GetBytes("the second frame" + Environment.NewLine);
using (var file = new FileStream(joinedPath, FileMode.Create, FileAccess.Write))
{
WriteFrame(file, first);
var aside = SkippableFrame(Encoding.ASCII.GetBytes("metadata some other tool left here"));
file.Write(aside, 0, aside.Length);
WriteFrame(file, second);
}
using (var file = new FileStream(joinedPath, FileMode.Open, FileAccess.Read))
using (var reader = new ZstdDecoderStream(file, leaveOpen: true))
using (var produced = new MemoryStream())
{
GZipStreamUsageRecipe.CopyAll(reader, produced);
var text = Encoding.UTF8.GetString(produced.ToArray());
var expected = Encoding.UTF8.GetString(first) + Encoding.UTF8.GetString(second);
if (!string.Equals(text, expected, StringComparison.Ordinal))
{
context.Say("the joined file read back as: " + text.Replace(Environment.NewLine, "|"));
return RecipeOutcome.Failed;
}
context.Say("two frames read as one stream, with a skippable frame stepped over between them");
}
// The memory cap, refused rather than attempted.
using (var file = new FileStream(singlePath, FileMode.Open, FileAccess.Read))
{
try
{
int taken;
using (var reader = new ZstdDecoderStream(file, leaveOpen: true, maximumWindowBytes: 1024))
{
var scratch = new byte[256];
taken = reader.Read(scratch, 0, scratch.Length);
}
context.Say("a 1 KiB window cap gave back {0} bytes from a frame that needs more, " +
"which it must not", taken);
return RecipeOutcome.Failed;
}
catch (ArchiveException ex)
{
context.Say("a 1 KiB window cap refused the file: {0}", ex.ErrorCode);
}
}
return RecipeOutcome.Passed;
}
/// <summary>Writes one complete frame of content.</summary>
private static void WriteFrame(Stream destination, byte[] content)
{
using (var writer = new ZstdEncoderStream(destination, 5, true, leaveOpen: true))
{
writer.Write(content, 0, content.Length);
writer.Complete();
}
}
/// <summary>
/// A skippable frame: a magic number in the reserved range, a length, and whatever the application
/// wanted to carry. It holds no content and every decoder is required to walk past it.
/// </summary>
private static byte[] SkippableFrame(byte[] payload)
{
var frame = new byte[8 + payload.Length];
// 0x184D2A50 through 0x184D2A5F are all skippable; the low nibble is the application's to choose.
frame[0] = 0x50;
frame[1] = 0x2A;
frame[2] = 0x4D;
frame[3] = 0x18;
for (var index = 0; index < 4; index++)
{
frame[4 + index] = (byte)((payload.Length >> (8 * index)) & 0xFF);
}
Buffer.BlockCopy(payload, 0, frame, 8, payload.Length);
return frame;
}
}
}Public types and members only, grouped by namespace. Signatures are shown in Visual Basic.
The archive API: Archive to open and extract, ArchiveWriter to create, ArchiveUpdate to change an existing archive, and the options, results and enumerations they take.
| Type | Summary |
|---|---|
AdditionOptions Class | How to add files to an archive. Everything is optional, and a new instance follows the archive's own settings, so a caller adding a folder has only to name it. |
Archive Class | An open archive: what is in it, and how to get it out. This is the type a caller starts from. |
ArchiveEntry Class | One entry of an open archive, as a caller sees it: everything the directory says about the entry and nothing that would let the caller read it behind the archive's back. |
ArchiveException Class | The only exception the codec Stream classes in Bastion.Archive.Codecs throw for archive or data conditions. A Stream.Read has no result channel, and a stream that returned zero bytes on corrupt input would silently truncate data. Archive-level operations never throw this; they return an OperationResult. |
ArchiveFormat Enum | Container formats the library recognises. Which capabilities (detect, list, extract, test, create, update) each one has reached is listed in Format support; a value here does not imply support. |
ArchiveOpenOptions Class | How to open an archive. Everything is optional, and a new instance opens an archive the safe way, so a caller that only has a path never has to construct one. |
ArchiveOpenResult Class | The outcome of opening an archive, and the archive itself when it opened. Archive-level operations never throw into the caller, so the factory returns a result and carries the object rather than taking an output parameter. |
ArchiveUpdate Class | Changes to make to an existing archive, applied by rebuilding it. Describe every change, then call Apply; nothing is written until then, and the original is never modified. |
ArchiveWriter Class | Builds a new archive. Add what should go in it, then call Complete; the archive is only finished, and only valid, once that returns. |
ArchiveWriterResult Class | The outcome of starting a new archive, and the writer itself when it started. The same pattern as ArchiveOpenResult, for the same reason: the operation cannot throw, so it carries the object it made. |
CompressionMethod Enum | Compression methods a writer can be asked for. Which formats accept which is listed in Format support. |
CompressionSettings Class | What a writer should produce: level, method, encryption, solid mode, dictionary and word sizes, threading, timestamps and ordering. A new instance matches 7-Zip's defaults for the format at level 5. |
EncryptionMethod Enum | Encryption a writer applies when Password is set. |
EntryOrdering Enum | The order in which a writer emits entries (ordering is an explicit setting). |
ErrorCode Enum | Identifies why an archive operation failed. None means success. Every archive-level operation reports through an OperationResult rather than throwing. |
ExtractionOptions Class | How to extract. Everything is optional, and a new instance extracts the safe way: refuse to overwrite, refuse a path that escapes the target, skip links, verify every checksum, and write through a temporary file so a crash cannot leave a half-written file where a good one was. |
InteropWarning Class | Reports that an archive was written with a feature some common readers cannot open: for example AES-encrypted zip on macOS Archive Utility, or PPMd-in-zip in Windows Explorer. The archive is valid; the warning tells the developer which audience will need another tool. |
OperationResult Class | The outcome of an archive-level operation. Archive operations never throw into the caller; they return this type or a subclass carrying operation-specific data. Check Succeeded or ErrorCode before using any other member of a subclass. |
OperationWarning Class | A non-fatal condition observed during an operation. |
SolidMode Enum | Whether a 7z writer packs entries into shared solid blocks. |
ThreadingSettings Class | How much parallelism a writer or reader may use. Serial and parallel runs produce byte-identical archives; these settings trade memory for speed, never output. |
TimestampMode Enum | Which timestamps a writer records (timestamps are explicit settings, never ambient). |
WarningCode Enum | Classifies a non-fatal condition reported in Warnings. |
Bastion.Archive
How to add files to an archive. Everything is optional, and a new instance follows the archive's own settings, so a caller adding a folder has only to name it.
| Constructor | Summary |
|---|---|
New() | Creates options that follow the archive's settings. |
| Member | Type | Summary |
|---|---|---|
Filter | Func(Of String, Boolean) | Decides which files to add, given each one's path relative to the directory being added, with forward slashes. Return False to leave a file out. Nothing adds everything. |
IncludeDirectoryEntries | Boolean | True to write an entry for each directory walked, as well as for the files in it. Default True, because a directory entry is the only way an empty directory survives. |
IncludeHidden | Boolean | True to include hidden and system files when adding a directory. Default False. |
Level | Integer | The level for these entries, 0 to 9, or -1 to follow the archive's settings. Useful for adding already-compressed files at level 0 in an archive otherwise written at level 6. Throws ArgumentOutOfRangeException when the value is not -1 and not between 0 and 9. |
Method | CompressionMethod | The method for these entries, or Automatic to follow the archive's settings. |
Recursive | Boolean | True to walk subdirectories when adding a directory. Default True. |
Bastion.Archive · Implements IDisposable
An open archive: what is in it, and how to get it out. This is the type a caller starts from.
Nothing here throws into the caller. Every operation returns an OperationResult, and an argument that is Nothing or plainly wrong still throws, because that is a mistake in the calling code rather than something an archive did. Lifecycle events, Opened through Completed, are raised on this object. Progress, entry and error events live on Monitor, which already marshals them through the synchronisation context it captured, so a user interface can update a progress bar from a handler without arranging that itself.
| Member | Type | Summary |
|---|---|---|
Comment read-only | String | The archive comment, or an empty string. |
EmbeddedStubSize read-only | Long | Bytes before the archive, such as a self-extractor stub. Zero for an ordinary archive. |
Entries read-only | IReadOnlyList(Of ArchiveEntry) | The entries, in directory order. |
Format read-only | ArchiveFormat | The format detected from the archive's own signature. |
Monitor read-only | OperationMonitor | Where progress, entry, log and error events go, and where cancellation comes from. |
SelfExtractor read-only | SelfExtractorInfo | For an archive that is also a Windows program — a self-extracting archive — what the program is: its length and, for one this library wrote, its settings. Nothing for an archive that is only an archive. |
TrailingDataSize read-only | Long | Bytes after the end of the archive structure, which were ignored. |
VolumeCount read-only | Integer | Volumes in the set; 1 for a single-file archive. |
| Member | Returns | Summary |
|---|---|---|
Close() | OperationResult | Closes the archive and reports the outcome. A Closing handler can abandon the close by setting Cancel, which Dispose deliberately ignores. |
Dispose() | — | Closes the archive. Never throws, and never reports: a caller that wants the outcome of closing calls Close instead, or subscribes to Closed. |
Extract(entries As IEnumerable(Of ArchiveEntry), targetDirectory As String, options As ExtractionOptions) | OperationResult | Extracts the entries given, and no others.entries — Entries from Entries.targetDirectory — Where they go; created if it does not exist.options — How to extract, or Nothing for the secure defaults.Throws ArgumentNullException when an argument is Nothing. |
ExtractAll(targetDirectory As String) | OperationResult | Extracts every entry under a directory, with the secure defaults.targetDirectory — Where the entries go; created if it does not exist.Throws ArgumentNullException when targetDirectory is Nothing. |
ExtractAll(targetDirectory As String, options As ExtractionOptions) | OperationResult | Extracts every entry under a directory.targetDirectory — Where the entries go; created if it does not exist.options — How to extract, or Nothing for the secure defaults.Throws ArgumentNullException when targetDirectory is Nothing. |
ExtractAllAsync(targetDirectory As String, Optional options As ExtractionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Extracts every entry on the thread pool; the twin of ExtractAll.targetDirectory — Where the entries go; created if it does not exist.options — How to extract, or Nothing for the secure defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Events are raised on the monitor's SynchronizationContext, so await the task rather than blocking on it from that context.Throws ArgumentNullException when targetDirectory is Nothing. |
ExtractAsync(entries As IEnumerable(Of ArchiveEntry), targetDirectory As String, Optional options As ExtractionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Extracts the entries given on the thread pool; the twin of Extract.entries — Entries from Entries.targetDirectory — Where they go; created if it does not exist.options — How to extract, or Nothing for the secure defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when an argument is Nothing. |
ExtractToStream(entry As ArchiveEntry, target As Stream, options As ExtractionOptions) | OperationResult | Writes one entry's data to a stream, verifying it as it goes. The caller owns the stream.entry — An entry from Entries.target — Where the data goes.options — Chiefly for the password, or Nothing.Throws ArgumentNullException when an argument is Nothing. |
ExtractToStreamAsync(entry As ArchiveEntry, target As Stream, Optional options As ExtractionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Decompresses one entry into a stream through WriteAsync alone; the twin of ExtractToStream for a destination that refuses synchronous writes, such as an ASP.NET Core response body.entry — The entry, from Entries.target — Where the bytes go. It is left open, and flushed before the task completes.options — Password and limits, or Nothing for the secure defaults.cancellationToken — Stops the operation, and is passed to every write to target.Throws ArgumentNullException when an argument is Nothing. |
Open(path As String) Shared | ArchiveOpenResult | Opens the archive at a path, detecting the format and reading its directory. A split archive opens from any one of its volumes.path — The archive, or any volume of a split set.Throws ArgumentNullException when path is Nothing; ArgumentException when path is empty. |
Open(path As String, options As ArchiveOpenOptions) Shared | ArchiveOpenResult | Opens the archive at a path with options. A split archive opens from any one of its volumes, whichever of the two naming schemes it uses.path — The archive, or any volume of a split set.options — How to open it, or Nothing for the secure defaults.Throws ArgumentNullException when path is Nothing; ArgumentException when path is empty. |
Open(stream As Stream, options As ArchiveOpenOptions) Shared | ArchiveOpenResult | Opens an archive already in a stream, which must be readable and seekable.stream — The archive.options — How to open it, or Nothing for the secure defaults.Throws ArgumentNullException when stream is Nothing. |
OpenAsync(path As String, Optional options As ArchiveOpenOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of ArchiveOpenResult) Shared | — | Opens an archive on the thread pool; the twin of Open.path — Any volume of the archive.options — How to open it, or Nothing for the secure defaults.cancellationToken — Checked before the archive is read; once it is open, the token on its monitor governs.Throws ArgumentNullException when path is Nothing. |
OpenAsync(stream As Stream, Optional options As ArchiveOpenOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of ArchiveOpenResult) Shared | — | Opens an archive in a stream on the thread pool; the twin of Open that also takes a stream which cannot seek.stream — The archive. A seekable stream is read where it is; one that cannot seek — a request body, a network stream — is first copied with ReadAsync to a temporary file, deleted when the archive closes.options — How to open it, or Nothing for the secure defaults.cancellationToken — Stops the copy of a stream that cannot seek; the result then says Cancelled.The copy is as large as the archive, and nothing here limits it: bound the source instead (for ASP.NET Core, its request body size limit). ZIP needs the end of the archive before the start, which is why a stream that cannot seek is copied rather than read as it arrives. Throws ArgumentNullException when stream is Nothing. |
Test(options As ExtractionOptions) | OperationResult | Reads every entry to the end without writing anything, which verifies each checksum and each authentication code. This is what 7z t does.options — Chiefly for the password, or Nothing. |
TestAsync(Optional options As ExtractionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Tests every entry on the thread pool; the twin of Test.options — Password and limits, or Nothing for the secure defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled. |
| Event | Handler | Summary |
|---|---|---|
Closed | EventHandler(Of ArchiveClosedEventArgs) | Raised once the archive has closed, carrying the outcome. |
Closing | EventHandler(Of ArchiveClosingEventArgs) | Raised before the archive closes, while it can still be read. |
Completed | EventHandler(Of OperationCompletedEventArgs) | Raised when an operation finishes, carrying the same result the method returned. |
Opened | EventHandler(Of ArchiveOpenedEventArgs) | Raised once the directory has been read, before any data moves. |
Bastion.Archive
One entry of an open archive, as a caller sees it: everything the directory says about the entry and nothing that would let the caller read it behind the archive's back.
Deliberately without a stream of its own. Extraction goes through Archive, so the archive stays in charge of the policy limits, the progress reporting and the order things are read in, none of which it could guarantee if an entry handed out its own reader.
| Member | Type | Summary |
|---|---|---|
Comment read-only | String | The entry comment, or an empty string. |
CompressedSize read-only | Long | Compressed size in bytes, including any encryption overhead. |
Crc32 read-only | Long | CRC-32 of the uncompressed data, or 0 for an entry that authenticates instead. |
CreationUtc read-only | Date | Creation time, in UTC, or LastWriteUtc when the archive records none. |
DosAttributes read-only | Integer | MS-DOS attribute bits, or 0 when the producer stored none. |
Encryption read-only | EncryptionMethod | The encryption the entry uses, or None. |
HostSystem read-only | Integer | The host system code the producer stamped: 0 for FAT, 3 for Unix. |
Index read-only | Integer | Zero-based position in the archive's directory. |
IsAlternateStream read-only | Boolean | True when the entry is an NTFS alternate data stream of the entry whose name its own name begins with, up to the colon — readme.txt:meta is the meta stream of readme.txt.Only a WIM records these. Extraction to a stream reads one like any other entry; extraction to disk refuses the colon unless RejectAlternateStreams is turned off. |
IsDirectory read-only | Boolean | True when the entry names a directory and carries no data. |
IsEncrypted read-only | Boolean | True when the entry's data is encrypted. |
IsSymbolicLink read-only | Boolean | True when the entry is a symbolic link, whose data is its target path. |
LastAccessUtc read-only | Date | Last access time, in UTC, or LastWriteUtc when the archive records none. |
LastWriteUtc read-only | Date | Last modification time, in UTC. |
Method read-only | CompressionMethod | The compression method the data really uses. |
Name read-only | String | The entry name, with forward slashes, and a trailing slash on a directory. |
Size read-only | Long | Uncompressed size in bytes. |
UnixMode read-only | Integer | The Unix mode including the file type, or 0 when the producer was not Unix-hosted. |
VolumeNumber read-only | Integer | Zero-based volume the entry starts on; 0 unless the archive is split. |
| Member | Returns | Summary |
|---|---|---|
GetSecurityDescriptor() As Byte() | — | The Windows security descriptor the archive recorded for this entry, in the self-relative form GetFileSecurity and SetFileSecurity use, or Nothing when none was recorded.Only a WIM records these, and only when it was written to: 7z a -sni and DISM both do. This library does not interpret the bytes or apply them to extracted files; it hands them over as they are, because a caller who wants them wants them in the form the Windows API takes. A fresh copy is returned each call, so a caller cannot change what the archive says. |
ToString() | String | Returns a readable description of the value. |
Bastion.Archive · Inherits Exception
The only exception the codec Stream classes in Bastion.Archive.Codecs throw for archive or data conditions. A Stream.Read has no result channel, and a stream that returned zero bytes on corrupt input would silently truncate data. Archive-level operations never throw this; they return an OperationResult.
| Constructor | Summary |
|---|---|
New() | Creates an exception with a default message. |
New(message As String) | Creates an exception with a message.message — Description ending in a full stop. |
New(message As String, innerException As Exception) | Creates an exception wrapping another.message — Description ending in a full stop.innerException — The underlying exception. |
New(message As String, errorCode As ErrorCode) | Creates an exception with a classified error code.message — Description ending in a full stop.errorCode — The classification. |
New(message As String, errorCode As ErrorCode, innerException As Exception) | Creates an exception with a classified error code, wrapping another.message — Description ending in a full stop.errorCode — The classification.innerException — The underlying exception. |
| Member | Type | Summary |
|---|---|---|
ErrorCode read-only | ErrorCode | The classification of the failure, using the same codes as ErrorCode. |
Bastion.Archive
Container formats the library recognises. Which capabilities (detect, list, extract, test, create, update) each one has reached is listed in Format support; a value here does not imply support.
| Name | Value | Summary |
|---|---|---|
Unknown | 0 | Not an archive the registered handlers recognise. |
Zip | 1 | ZIP (PKWARE APPNOTE), including zipx method extensions and WinZip AES. |
SevenZip | 2 | 7-Zip 7z container. |
Tar | 3 | POSIX tar (ustar, GNU, pax). |
GZip | 4 | gzip (RFC 1952) member stream. |
BZip2 | 5 | bzip2 stream. |
Xz | 6 | XZ container. |
Lzma | 7 | Raw .lzma (LZMA-alone) stream. |
Wim | 8 | Windows Imaging Format. |
Cab | 9 | Microsoft Cabinet. |
Iso | 10 | ISO 9660 / Joliet / Rock Ridge image. |
Udf | 11 | OSTA Universal Disk Format image. |
Cpio | 12 | cpio (odc, newc, crc, bin). |
Ar | 13 | Unix ar archive, including Debian packages. |
Rpm | 14 | RPM package. |
Xar | 15 | Apple xar archive. |
Z | 16 | Unix compress (.Z, LZW). |
Zstd | 17 | Zstandard frame (.zst). |
Lz4 | 18 | LZ4 frame. |
Brotli | 19 | Brotli stream. |
Vhd | 20 | Virtual Hard Disk (VHD). |
Vhdx | 21 | Hyper-V Virtual Hard Disk v2 (VHDX). |
Vmdk | 22 | VMware Virtual Machine Disk (VMDK). |
Qcow2 | 23 | QEMU Copy-On-Write v2/v3 (QCOW2). |
Vdi | 24 | VirtualBox Disk Image (VDI). |
Fat | 25 | FAT12/16/32 file system image. |
Ntfs | 26 | NTFS file system image. |
Ext | 27 | ext2/3/4 file system image. |
Hfs | 28 | HFS / HFS+ file system image. |
Apfs | 29 | Apple File System image (unencrypted). |
Gpt | 30 | GUID Partition Table container. |
Mbr | 31 | Master Boot Record partition container. |
SquashFs | 32 | SquashFS image. |
CramFs | 33 | CramFS image. |
Uefi | 34 | UEFI firmware volume or capsule. |
Ihex | 35 | Intel HEX. |
Lzh | 36 | LHA/LZH archive. |
Arj | 37 | ARJ archive. |
Chm | 38 | Microsoft Compiled HTML Help (ITSS). |
Nsis | 39 | Nullsoft Scriptable Install System installer. |
Dmg | 40 | Apple Disk Image (UDIF). |
Compound | 41 | Compound File Binary (OLE2), including MSI. |
Pe | 42 | Portable Executable sections and resources. |
Elf | 43 | ELF sections. |
MachO | 44 | Mach-O segments. |
Split | 45 | Numeric split set (.001, .002 …) reassembled as one stream. |
Rar | 46 | RAR 2/3/4/5/7 (extract only). |
Sparse | 47 | Android sparse image (simg), as fastboot flashes. |
MsLz | 48 | Microsoft SZDD compression, the .??_ files of old install media. |
Apm | 49 | Apple Partition Map, the partition scheme of pre-Intel Macs. |
Base64 | 50 | Base64 text (RFC 4648) wrapping another file. |
Flv | 51 | Flash Video (FLV), separated into its audio and video streams. |
Mub | 52 | A Mach-O universal (fat) binary, holding one image per architecture. |
Coff | 53 | A COFF object file (.obj), as a compiler writes before linking. |
Swf | 54 | A Flash movie (SWF), plain or with a compressed body. |
Lzip | 55 | The lzip container (.lz): LZMA with framing of its own. |
Snappy | 56 | The Snappy framing format (.sz): chunked Snappy with a CRC-32C per chunk. |
Srecord | 57 | A Motorola S-record file (.s19, .s28, .s37, .srec, .mot). |
Lzo | 58 | An lzop file (.lzo): blocked LZO1X with a checksum per block. |
MacBinary | 59 | A MacBinary file (.bin, .macbin): a Macintosh file's forks and Finder metadata. |
BinHex | 60 | A BinHex 4.0 file (.hqx): a Macintosh file as six-bit text. |
AppleSingle | 61 | An AppleSingle file: a Macintosh file's forks and metadata in one file. |
AppleDouble | 62 | An AppleDouble header file (._name): the resource fork and metadata beside the data file. |
UImage | 63 | A U-Boot legacy uImage, as mkimage writes it. |
Cue | 64 | A CUE sheet (.cue) naming the disc image beside it. |
Luks | 65 | A standalone LUKS version 1 encrypted volume. |
Bastion.Archive
How to open an archive. Everything is optional, and a new instance opens an archive the safe way, so a caller that only has a path never has to construct one.
| Constructor | Summary |
|---|---|
New() | Creates options with the secure defaults. |
| Member | Type | Summary |
|---|---|---|
FallbackCodePage | Integer | Code page for entry names that carry no UTF-8 marker, or 0 for IBM 437, which is what the format specifies. Set this only for archives from a producer known to have used something else. Throws ArgumentOutOfRangeException when the value is negative. |
LeaveStreamOpen | Boolean | True to leave a caller-supplied stream open when the archive closes. Ignored when the archive was opened from a path, since the archive owns that stream either way. |
Monitor | OperationMonitor | Where progress, entry, log and error events go, or Nothing for a monitor of the archive's own. Supplying one lets a caller subscribe before the archive exists, and lets one monitor follow several operations. |
Password | String | The password for an encrypted archive, or Nothing. Never logged, never placed in a result or a support report. |
Policy | ExtractionPolicy | The limits and refusals to apply while reading, or Nothing for the secure defaults. Reading it never returns Nothing. |
Bastion.Archive · Inherits OperationResult
The outcome of opening an archive, and the archive itself when it opened. Archive-level operations never throw into the caller, so the factory returns a result and carries the object rather than taking an output parameter.
| Member | Type | Summary |
|---|---|---|
Archive read-only | Archive | The open archive, or Nothing when the operation failed. The caller owns it and should dispose it, normally with a Using block. |
Bastion.Archive
Changes to make to an existing archive, applied by rebuilding it. Describe every change, then call Apply; nothing is written until then, and the original is never modified.
For a ZIP, an entry nobody touched is carried across still compressed and still encrypted, so an update needs no password and preserves a method this library cannot itself write. The other side of that bargain is that a ZIP update never recompresses or re-encrypts what it did not touch: changing an archive's level or password means reading it and writing a new one. A 7z source is detected by its signature and handled differently, because a solid 7z compresses its entries together and removing one changes the bytes of everything after it. So every entry kept is decoded — its CRC-32 checked on the way — and written again, the settings passed to Apply govern the whole result, and their password is also what opens an encrypted source.
A 7z source is detected by its signature and handled differently, because a solid 7z compresses its entries together and removing one changes the bytes of everything after it. So every entry kept is decoded — its CRC-32 checked on the way — and written again, the settings passed to Apply govern the whole result, and their password is also what opens an encrypted source.
| Constructor | Summary |
|---|---|
New(archivePath As String) | Describes changes to the archive at a path.archivePath — The archive to update. It is read, never written.Throws ArgumentNullException when archivePath is Nothing; ArgumentException when archivePath is empty. |
New(source As Stream) | Describes changes to an archive already in a stream, which must be readable and seekable.source — The archive to update. It is read, never written.Throws ArgumentNullException when source is Nothing. |
| Member | Type | Summary |
|---|---|---|
IsEmpty read-only | Boolean | True when nothing at all is being changed. |
Monitor | OperationMonitor | Where progress, entry, log and error events go, or Nothing for one of its own. |
| Member | Returns | Summary |
|---|---|---|
AddOrReplaceDirectory(entryName As String) | — | Adds a directory entry, or replaces one of the same name.entryName — The directory name; a trailing slash is added if missing.Throws ArgumentNullException when entryName is Nothing. |
AddOrReplaceFile(sourcePath As String, entryName As String) | — | Adds a file, or replaces the entry of the same name. A replacement keeps the position the original held, so an update does not reshuffle an archive; a genuinely new entry goes at the end.sourcePath — The file to add.entryName — Its name in the archive, or Nothing for the file's own name.Throws ArgumentNullException when sourcePath is Nothing. |
Apply(targetPath As String, settings As CompressionSettings) | OperationResult | Rebuilds the archive into targetPath with the changes applied. The target must not already exist, and the source is never modified.targetPath — Where the rebuilt archive goes.settings — Level, method, encryption and timestamps for added and replaced entries in a ZIP, or for the whole rebuilt archive in a 7z, whose password also opens the source; Nothing for the defaults.Throws ArgumentNullException when targetPath is Nothing; ArgumentException when targetPath is empty. |
ApplyAsync(targetPath As String, Optional settings As CompressionSettings = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Writes the updated archive on the thread pool; the twin of Apply.targetPath — Where the new archive goes; it must not exist.settings — How new and re-encoded entries are compressed, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when targetPath is Nothing. |
Remove(entryName As String) | — | Leaves an entry out of the rebuilt archive.entryName — The entry name as the reader reports it, with forward slashes.Throws ArgumentNullException when entryName is Nothing; ArgumentException when the name is empty, or the entry is already being renamed. |
Rename(entryName As String, newName As String) | — | Carries an entry over under a different name; in a ZIP without recompressing it.entryName — The entry name as the reader reports it.newName — The name it should have in the rebuilt archive.Throws ArgumentNullException when an argument is Nothing; ArgumentException when a name is empty, or the entry is already being removed or renamed. |
Bastion.Archive · Implements IDisposable
Builds a new archive. Add what should go in it, then call Complete; the archive is only finished, and only valid, once that returns.
Nothing here throws into the caller. When the output is a path, the archive is written to a temporary neighbour and moved into place by Complete, so a crash part-way through leaves the previous file untouched and never produces a half-written archive that looks whole. Abandoning a writer without completing it therefore leaves nothing behind.
| Member | Type | Summary |
|---|---|---|
FillEachMedium const | Long | Passed to CreateSpanned as the volume size: fill each medium with as much as it has room for. |
| Member | Type | Summary |
|---|---|---|
Comment | String | The archive comment, written when the archive is completed. |
EntryCount read-only | Integer | Entries written so far. |
InteropWarnings read-only | IReadOnlyList(Of InteropWarning) | Interoperability notes for features some readers cannot open. |
Monitor read-only | OperationMonitor | Where progress, entry, log and error events go, and where cancellation comes from. |
| Member | Returns | Summary |
|---|---|---|
AddDirectory(sourceDirectory As String, entryPrefix As String, options As AdditionOptions) | OperationResult | Adds a directory tree. Files are added in a stable order, so the same tree gives the same archive.sourceDirectory — The directory to add.entryPrefix — A prefix for every name written, or Nothing to add at the root.options — Recursion, filtering, method and level, or Nothing for the defaults.Throws ArgumentNullException when sourceDirectory is Nothing. |
AddDirectoryAsync(sourceDirectory As String, entryPrefix As String, Optional options As AdditionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Adds a directory tree on the thread pool; the twin of AddDirectory.sourceDirectory — The directory to add.entryPrefix — A prefix for every name written, or Nothing to add at the root.options — Recursion, filtering, method and level, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when sourceDirectory is Nothing. |
AddEmptyDirectory(entryName As String) | OperationResult | Adds a directory entry, which is how an empty directory survives a round trip.entryName — The directory name; a trailing slash is added if missing.Throws ArgumentNullException when entryName is Nothing; ArgumentException when entryName is empty. |
AddFile(sourcePath As String, entryName As String) | OperationResult | Adds one file under the name given.sourcePath — The file to add.entryName — Its name in the archive, with forward slashes, or Nothing for the file's own name.Throws ArgumentNullException when sourcePath is Nothing. |
AddFile(sourcePath As String, entryName As String, options As AdditionOptions) | OperationResult | Adds one file under the name given.sourcePath — The file to add.entryName — Its name in the archive, with forward slashes, or Nothing for the file's own name.options — Method, level and filtering, or Nothing to follow the archive's settings.Throws ArgumentNullException when sourcePath is Nothing. |
AddFileAsync(sourcePath As String, entryName As String, Optional options As AdditionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Adds one file on the thread pool; the twin of AddFile.sourcePath — The file to add.entryName — Its name in the archive, with forward slashes, or Nothing for the file's own name.options — Method and level for this entry, or Nothing for the archive's settings.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when sourcePath is Nothing. |
AddStream(source As Stream, entryName As String, options As AdditionOptions) | OperationResult | Adds the contents of a stream as one entry.source — The data; read from its current position to the end.entryName — Its name in the archive, with forward slashes.options — Method and level, or Nothing to follow the archive's settings.Throws ArgumentNullException when an argument is Nothing; ArgumentException when entryName is empty. |
AddStreamAsync(source As Stream, entryName As String, Optional options As AdditionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Adds a stream's content on the thread pool; the twin of AddStream.source — The content, read from its current position to its end.entryName — Its name in the archive, with forward slashes.options — Method and level for this entry, or Nothing for the archive's settings.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when source or entryName is Nothing. |
Complete() | OperationResult | Finishes the archive: writes the directory, closes the output and, when writing to a path, moves the finished file into place. Safe to call twice. |
CompleteAsync(Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Finishes the archive on the thread pool; the twin of Complete. For a writer from CreateAsync, it returns once the destination has taken and flushed every byte.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled. |
Create(archivePath As String) Shared | ArchiveWriterResult | Starts a new archive at a path, with the format defaults.archivePath — Where the archive goes. It must not already exist.Throws ArgumentNullException when archivePath is Nothing; ArgumentException when archivePath is empty. |
Create(archivePath As String, settings As CompressionSettings) Shared | ArchiveWriterResult | Starts a new archive at a path.archivePath — Where the archive goes. It must not already exist.settings — Level, method, encryption and timestamps, or Nothing for the defaults.Throws ArgumentNullException when archivePath is Nothing; ArgumentException when archivePath is empty. |
Create(stream As Stream, settings As CompressionSettings, monitor As OperationMonitor) Shared | ArchiveWriterResult | Starts a new archive in a caller-supplied stream, which the writer never disposes.stream — Where the archive goes; writable.settings — Level, method, encryption and timestamps, or Nothing for the defaults.monitor — Where progress and errors go, or Nothing for one of the writer's own.Throws ArgumentNullException when stream is Nothing. |
Create(archivePath As String, settings As CompressionSettings, monitor As OperationMonitor) Shared | ArchiveWriterResult | Starts a new archive at a path, with a monitor subscribed before anything is written.archivePath — Where the archive goes. It must not already exist.settings — Level, method, encryption and timestamps, or Nothing for the defaults.monitor — Where progress and errors go, or Nothing for one of the writer's own.Throws ArgumentNullException when archivePath is Nothing; ArgumentException when archivePath is empty. |
CreateAsync(stream As Stream, settings As CompressionSettings, monitor As OperationMonitor, Optional cancellationToken As CancellationToken = Nothing) As Task(Of ArchiveWriterResult) Shared | — | Starts a new archive written to a stream through WriteAsync alone, for a destination that refuses synchronous writes — an ASP.NET Core response body, say.stream — Where the archive goes. It is written forwards only, so ZIP entries carry data descriptors.settings — Level, method, encryption and timestamps, or Nothing for the defaults. ZIP only: 7z needs to seek.monitor — Where progress and errors go, or Nothing for one of the writer's own.cancellationToken — Passed to every write to stream.The archive is compressed on the thread pool by the Async methods and handed to the stream in 64 KiB chunks, at most four waiting, so memory stays bounded. CompleteAsync returns once the stream has taken and flushed every byte; the stream itself is left open. Await each call before the next: a writer does one thing at a time.Throws ArgumentNullException when stream is Nothing. |
CreateSelfExtracting(executablePath As String, options As SelfExtractorOptions, settings As CompressionSettings, monitor As OperationMonitor) Shared | ArchiveWriterResult | Starts a self-extracting archive: a Windows program that, when run, offers to extract the ZIP archive it carries.executablePath — Where the program goes, normally ending in .exe. It must not already exist.options — What the program's window says and does, or Nothing for the defaults.settings — Level, method, encryption and timestamps, or Nothing for the defaults. ZIP only.monitor — Where progress and errors go, or Nothing for one of the writer's own.Add entries and complete the writer as for any archive. The program is the library's own stub — a .NET Framework 4.6.2 program, so it runs on every Windows 10 and 11 machine as it comes, natively on x64 and on ARM64 — followed by its settings and then the archive. Every offset in the archive counts from the start of the file, so 7-Zip, WinZip, Info-ZIP and Windows itself open the program as the ZIP archive it also is. Extraction in the program is this library's own, with its safeguards on: names are canonicalised, the limits of ExtractionPolicy apply, and a program downloaded from the internet passes its Mark of the Web on to every file it extracts.Throws ArgumentNullException when executablePath is Nothing; ArgumentException when executablePath is empty. |
CreateSpanned(archivePath As String, volumeSize As Long, settings As CompressionSettings, monitor As OperationMonitor) Shared | ArchiveWriterResult | Starts a new ZIP archive spanned across volumes the PKWARE way: name.z01, name.z02 and so on, with the last volume carrying the archive's own name — or name.zx01 onwards for a .zipx, as WinZip names them.archivePath — The archive name, which the last volume takes once the archive is complete.volumeSize — Bytes per volume, at least 65 536; or FillEachMedium to put as much on each medium as it has room for, which is what spanning removable disks means.settings — Level, method, encryption and timestamps, or Nothing for the defaults. ZIP only.monitor — Where progress and errors go, and where VolumeNeeded is raised before each volume after the first, so that an application can ask for the next disk or send the volume elsewhere.Unlike CreateSplit, this writes strictly forwards and closes each volume as it fills, so the volumes can be on removable media that is changed between them. Entry sizes therefore go in a data descriptor after each entry's data, as PKZIP did when spanning disks. Every volume is written under a .zNN name and the last is renamed only once the archive is complete, so an interrupted write never leaves anything carrying the archive's name. Nothing that exists already is ever replaced.Throws ArgumentNullException when archivePath is Nothing; ArgumentOutOfRangeException when volumeSize is neither FillEachMedium nor at least 65 536. |
CreateSplit(archivePath As String, volumeSize As Long, settings As CompressionSettings, monitor As OperationMonitor) Shared | ArchiveWriterResult | Starts a new archive split into volumes of a fixed size, named name.zip.001 or name.7z.001 onwards, which is what 7-Zip writes and what every modern tool reads.archivePath — The archive name; the volumes take its name with a number appended.volumeSize — Bytes per volume; at least 65 536.settings — Level, method, encryption and timestamps, or Nothing for the defaults.monitor — Where progress and errors go, or Nothing for one of the writer's own.Throws ArgumentNullException when archivePath is Nothing; ArgumentOutOfRangeException when volumeSize is below 65 536. |
Dispose() | — | Completes the archive if that has not happened, and otherwise throws away what was written. Never throws; a caller that wants the outcome calls Complete. |
| Event | Handler | Summary |
|---|---|---|
Completed | EventHandler(Of OperationCompletedEventArgs) | Raised when an operation finishes, carrying the same result the method returned. |
Bastion.Archive · Inherits OperationResult
The outcome of starting a new archive, and the writer itself when it started. The same pattern as ArchiveOpenResult, for the same reason: the operation cannot throw, so it carries the object it made.
| Member | Type | Summary |
|---|---|---|
Writer read-only | ArchiveWriter | The writer, or Nothing when the operation failed. The caller owns it and must complete or dispose it; an archive whose writer is abandoned is left unfinished on purpose, so a crash cannot produce something that looks whole. |
Bastion.Archive
Compression methods a writer can be asked for. Which formats accept which is listed in Format support.
| Name | Value | Summary |
|---|---|---|
Automatic | 0 | The format's default for the chosen level (Deflate for zip, LZMA2 for 7z), matching 7-Zip. |
Store | 1 | No compression. |
Deflate | 2 | Deflate (RFC 1951). |
Deflate64 | 3 | Deflate64 (PKWARE enhanced deflate). |
BZip2 | 4 | bzip2. |
Lzma | 5 | LZMA. |
Lzma2 | 6 | LZMA2. |
Ppmd | 7 | PPMd (var.H in 7z, var.I in zip). |
Xz | 8 | XZ container (zip method 95). |
Zstd | 9 | Zstandard (decode only in v1). |
Xpress | 10 | XPRESS, [MS-XCA] LZ77+Huffman, as WIM uses it (read only; 7-Zip writes no compressed WIM either). |
Lzx | 11 | LZX ([MS-PATCH]), as WIM and cabinets use it (read only). |
Lzms | 12 | LZMS, WIM's "recovery" compression in ESD files (read only). |
Quantum | 13 | Quantum, a cabinet method with no published description: named so that it can be reported, not decoded. |
Lzh | 14 | LZH's static Huffman methods -lh4- to -lh7- (read only). |
Bastion.Archive
What a writer should produce: level, method, encryption, solid mode, dictionary and word sizes, threading, timestamps and ordering. A new instance matches 7-Zip's defaults for the format at level 5.
| Constructor | Summary |
|---|---|
New() | Creates settings at level 5 with the format defaults. |
| Member | Type | Summary |
|---|---|---|
DictionarySize | Long | Dictionary size in bytes for LZ-family methods. Default 0 means the level's size. Capped by MemoryLimitBytes.Throws ArgumentOutOfRangeException when the value is negative. |
EncryptHeaders | Boolean | Encrypt archive headers (entry names and sizes) where the format supports it (7z). Default False. |
Encryption | EncryptionMethod | The encryption applied when Password is set. Default Automatic. |
FixedTimestampUtc | Date | The timestamp written to every entry under Fixed. Default 1980-01-01 00:00:00 UTC, the DOS epoch. Must be UTC.Throws ArgumentException when the value's Kind is not Utc. |
Format | ArchiveFormat | Which format to write, or Unknown to take it from the file name.Left alone, writing to backup.7z writes 7z and anything else writes ZIP, which is what a caller means by the name they chose. Set it when there is no name to read — the stream overloads — or to be explicit in the face of an unusual extension. Only Zip and SevenZip can be written; the rest of the enumeration is for formats this library reads. |
Level | Integer | Compression level 0 (store) to 9 (ultra), mapped to 7-Zip's per-level table for the format. Default 5. Throws ArgumentOutOfRangeException when the value is outside 0 to 9. |
MemoryLimitBytes | Long | Cap on dictionary memory for the operation. Default 0 means an automatic cap; the effective cap is reported in the result detail. Throws ArgumentOutOfRangeException when the value is negative. |
Method | CompressionMethod | The method, or Automatic for the format's default. |
Ordering | EntryOrdering | The order entries are written in. Default AsProvided. |
Password | String | The password, or Nothing for no encryption. Never logged, never placed in a result or a support report. |
RecommendedFileExtension read-only | String | The file extension WinZip's convention gives an archive written with these settings: .zipx where the method is one of the later ones, .zip otherwise.WinZip introduced .zipx for archives whose entries use a method older readers cannot decode — Deflate64, bzip2, LZMA, PPMd, XZ, Zstandard and the recompression methods — so that a user is not offered a file the tool they have will open and then refuse to extract. The extension changes nothing inside the archive: the bytes are a ZIP either way, the method is in each entry's header as always, and a reader that implements the method reads either name happily. It is advice about what to call the file, which is why this is a property to read rather than something applied to a path behind the caller's back. |
Solid | SolidMode | Solid-block behaviour for formats that have it (7z). Default Automatic. |
Threading | ThreadingSettings | Parallelism settings. Never Nothing.Throws ArgumentNullException when the value is Nothing. |
Timestamps | TimestampMode | Which timestamps are written. Default Preserve. |
WordSize | Integer | Word (fast bytes) size for LZ-family methods. Default 0 means the level's size. Throws ArgumentOutOfRangeException when the value is negative. |
Bastion.Archive
Encryption a writer applies when Password is set.
| Name | Value | Summary |
|---|---|---|
Automatic | 0 | The format's strongest widely readable method: WinZip AE-2 with AES-256 for zip, 7zAES for 7z. |
None | 1 | No encryption even if a password is set; the password is ignored. |
Aes128 | 2 | AES-128 (zip: WinZip AE-2). |
Aes192 | 3 | AES-192 (zip: WinZip AE-2). |
Aes256 | 4 | AES-256 (zip: WinZip AE-2; 7z: 7zAES). |
ZipCrypto | 5 | Traditional PKWARE encryption. Broken since 1994 (known-plaintext recovery of the key stream); use only when the reader cannot open AES. The ObsoleteAttribute makes every use a compile-time warning. |
Bastion.Archive
The order in which a writer emits entries (ordering is an explicit setting).
| Name | Value | Summary |
|---|---|---|
AsProvided | 0 | The order the caller supplied them; the caller is responsible for reproducibility. |
OrdinalByPath | 1 | Ordinal (byte-wise) order of the canonical entry path, independent of culture and file system. |
Bastion.Archive
Identifies why an archive operation failed. None means success. Every archive-level operation reports through an OperationResult rather than throwing.
Values are grouped by hundreds so new codes can be added to a group without renumbering.
| Name | Value | Summary |
|---|---|---|
None | 0 | The operation succeeded. |
Unknown | 1 | An unclassified failure; Detail carries the specifics. |
Cancelled | 2 | The operation was cancelled through the caller's cancellation token. |
InvalidSetting | 100 | A setting value is outside the range the operation accepts. |
InvalidOperation | 101 | The object is not in a state that allows the operation (for example, writing after close). |
UnsupportedFormat | 200 | No registered handler recognises the input as an archive. |
UnsupportedMethod | 201 | The archive uses a compression or encryption method this library does not implement. |
UnsupportedFeature | 202 | The archive uses a feature of an otherwise supported format that this library does not implement. |
FileNotFound | 300 | The archive, volume or input file does not exist. |
AccessDenied | 301 | The operating system refused access to a file or directory. |
IOError | 302 | An I/O error occurred reading or writing a stream. |
OutputExists | 303 | The output already exists and the policy is fail-if-exists. |
VolumeMissing | 304 | A required volume of a split or spanned archive is missing. |
EntryNotFound | 305 | The archive holds no entry with the requested name. |
DiskFull | 306 | The destination ran out of room, or offered less than the smallest volume a spanned archive may have. |
CorruptArchive | 400 | The archive structure violates its format specification. |
ChecksumMismatch | 401 | A CRC, hash or MAC check failed; data was not exposed to the caller. |
TruncatedArchive | 402 | The archive ends before its declared structures do. |
DuplicateEntry | 403 | Two entries claim the same path or the same local header. |
OverlappingEntry | 404 | Two entries' data ranges overlap. |
PasswordRequired | 500 | The entry is encrypted and no password was supplied. |
PasswordInvalid | 501 | The supplied password did not verify. |
UnsafePath | 600 | An entry path failed canonicalisation (absolute, parent traversal, drive, UNC, alternate stream, reserved name). |
LimitExceeded | 601 | An ExtractionPolicy limit (bytes, entries, ratio, depth) was reached. |
LinkRejected | 602 | A symbolic or hard link was refused by the ExtractionPolicy. |
InternalError | 900 | A defect inside the library. Please report it with the support report. |
Bastion.Archive
How to extract. Everything is optional, and a new instance extracts the safe way: refuse to overwrite, refuse a path that escapes the target, skip links, verify every checksum, and write through a temporary file so a crash cannot leave a half-written file where a good one was.
| Constructor | Summary |
|---|---|
New() | Creates options with the secure defaults. |
| Member | Type | Summary |
|---|---|---|
FlattenPaths | Boolean | True to write every entry into the target directory itself, discarding the directories in its name. Off by default, because it turns two entries in different folders into one file. |
Password | String | The password for encrypted entries, or Nothing to use the one the archive was opened with. |
Policy | ExtractionPolicy | The limits and refusals to apply, or Nothing for the secure defaults. Reading it never returns Nothing. When the archive was opened with a policy of its own, whichever is set here wins. |
RestoreAttributes | Boolean | True to apply each entry's attributes to the file written. Default True. |
RestoreTimestamps | Boolean | True to apply each entry's timestamps to the file written. Default True. |
TargetPlatform | Nullable(Of PathPlatform) | The platform whose naming rules apply to the target, or Nothing for the one this process is running on. Set it explicitly when extracting to a share that another platform reads, so a name legal here but hostile there is still refused. Nullable rather than an Automatic enum member, because PathPlatform is also what the canonicaliser takes and that needs a real platform. |
Bastion.Archive
Reports that an archive was written with a feature some common readers cannot open: for example AES-encrypted zip on macOS Archive Utility, or PPMd-in-zip in Windows Explorer. The archive is valid; the warning tells the developer which audience will need another tool.
| Constructor | Summary |
|---|---|
New(feature As String, affectedReaders As String, message As String) | Creates an interoperability warning.feature — The feature used, for example "AES-256 encryption".affectedReaders — The readers known not to support it, for example "macOS Archive Utility".message — Guidance for the developer, ending in a full stop.Throws ArgumentNullException when any argument is Nothing. |
| Member | Type | Summary |
|---|---|---|
AffectedReaders read-only | String | The readers known not to support the feature. |
Feature read-only | String | The feature that limits interoperability. |
Message read-only | String | Guidance for the developer. |
| Member | Returns | Summary |
|---|---|---|
ToString() | String | Returns a readable description of the value. |
Bastion.Archive
The outcome of an archive-level operation. Archive operations never throw into the caller; they return this type or a subclass carrying operation-specific data. Check Succeeded or ErrorCode before using any other member of a subclass.
Argument-contract violations (a Nothing stream, a negative limit) are programming errors and do throw ArgumentException derivatives, as every .NET API does. Everything that can go wrong with an archive, a file system or a password is reported here instead.
| Member | Type | Summary |
|---|---|---|
Cause read-only | Exception | The underlying exception, when the failure originated in one; otherwise Nothing. |
CompletedUtc read-only | Date | When the operation ended, in UTC. |
Detail read-only | String | Technical detail for the developer (file offsets, structure names, method identifiers); never credentials or content. |
Elapsed read-only | TimeSpan | Wall-clock duration of the operation. |
ErrorCode read-only | ErrorCode | The outcome; None means success. |
ErrorDescription read-only | String | Human-readable description of the failure; empty on success. |
InteropWarnings read-only | IReadOnlyList(Of InteropWarning) | Interoperability notes about features some readers cannot open. Never Nothing. |
StartedUtc read-only | Date | When the operation began, in UTC. |
Succeeded read-only | Boolean | True when ErrorCode is None. Warnings do not affect this. |
Warnings read-only | IReadOnlyList(Of OperationWarning) | Non-fatal conditions observed during the operation. Never Nothing. |
| Member | Returns | Summary |
|---|---|---|
ToString() | String | One-line summary: "OK", or the error code and description, followed by the warning counts. |
Bastion.Archive
A non-fatal condition observed during an operation.
| Constructor | Summary |
|---|---|
New(code As WarningCode, message As String, entryPath As String) | Creates a warning.code — The warning classification.message — Human-readable description, ending in a full stop.entryPath — The archive entry the warning concerns, or Nothing for the archive as a whole.Throws ArgumentNullException when message is Nothing. |
| Member | Type | Summary |
|---|---|---|
Code read-only | WarningCode | The warning classification. |
EntryPath read-only | String | The archive entry concerned, or Nothing for the archive as a whole. |
Message read-only | String | Human-readable description. |
| Member | Returns | Summary |
|---|---|---|
ToString() | String | Returns a readable description of the value. |
Bastion.Archive
Whether a 7z writer packs entries into shared solid blocks.
| Name | Value | Summary |
|---|---|---|
Automatic | 0 | 7-Zip's rule for the level: solid blocks sized by the level's formula. |
NonSolid | 1 | Every entry in its own block; random access without decoding neighbours. |
Solid | 2 | One solid block for everything, best ratio. |
Bastion.Archive
How much parallelism a writer or reader may use. Serial and parallel runs produce byte-identical archives; these settings trade memory for speed, never output.
| Constructor | Summary |
|---|---|
New() | Creates settings that use every logical processor within an automatic memory budget. |
| Member | Type | Summary |
|---|---|---|
MaxThreads | Integer | Upper bound on worker threads. Default 0 means the logical processor count. Throws ArgumentOutOfRangeException when the value is negative. |
MemoryBudgetBytes | Long | Memory the operation may commit to dictionaries and block buffers across all threads. Default 0 means an automatic cap derived from the settings. The thread count is reduced to stay inside it. Throws ArgumentOutOfRangeException when the value is negative. |
ParallelBlocks | Boolean | Split one stream into blocks compressed on separate threads (LZMA2, BCJ2). Default True. |
ParallelEntries | Boolean | Compress independent entries on separate threads (zip Deflate and BZip2). Default True. |
| Member | Returns | Summary |
|---|---|---|
EffectiveThreadCount() | Integer | The thread count the operation will start with: MaxThreads, or the logical processor count when that is 0. |
Bastion.Archive
Which timestamps a writer records (timestamps are explicit settings, never ambient).
| Name | Value | Summary |
|---|---|---|
Preserve | 0 | Record each source item's own timestamps. |
Fixed | 1 | Record FixedTimestampUtc on every entry, for reproducible archives. |
None | 2 | Record no timestamps where the format allows; otherwise the format's epoch. |
Bastion.Archive
Classifies a non-fatal condition reported in Warnings.
| Name | Value | Summary |
|---|---|---|
None | 0 | No warning. |
TrailingData | 1 | Bytes follow the end of the archive structure; they were ignored (7-Zip kpidTailSize). |
EmbeddedStub | 2 | The archive is preceded by a self-extractor stub or other data (7-Zip kpidEmbeddedStubSize). |
EntrySkipped | 3 | An entry was skipped, by policy or by the caller's decision in an error event. |
TimestampTruncated | 4 | A timestamp could not be represented exactly in the target format and was truncated. |
AttributeDropped | 5 | An attribute or permission has no representation in the target format or platform and was dropped. |
SpecificationDeviation | 6 | The archive declares a structure the reader tolerated but the specification forbids. |
Compression codecs and branch filters as Stream classes, for use without an archive around them.
| Type | Summary |
|---|---|
Base64DecoderStream Class | Base64 (RFC 4648) as a decoding stream, for the .b64 files that carry an archive through something that will only pass text. |
Bcj2DecoderStream Class | Undoes BCJ2: the x86 branch filter that splits its output into four streams instead of one. |
Bcj2EncoderStream Class | Applies BCJ2: the x86 branch filter that splits its output into four streams instead of one. |
Bcj86DecoderStream Class | Undoes the x86 branch filter: call and jump targets back from absolute to relative. |
Bcj86EncoderStream Class | Applies the x86 branch filter: call and jump targets from relative to absolute. |
BranchArchitecture Enum | The instruction sets the branch filters know how to rewrite call and jump targets for. |
BranchFilterDecoderStream Class | Undoes a fixed-width branch filter: call targets back from absolute to relative. |
BranchFilterEncoderStream Class | Applies a fixed-width branch filter: call targets from relative to absolute. |
BrotliDecoderStream Class | Reads a brotli stream (RFC 7932), producing the bytes that went into it. |
BZip2DecoderStream Class | Decodes a bzip2 stream, block by block, from the published format description. |
BZip2EncoderStream Class | Writes a bzip2 stream, from the published format description. |
CompressDecoderStream Class | Reads a Unix compress stream (.Z, LZW as in the original compress utility). |
DclExplodeDecoderStream Class | Reads the PKWARE Data Compression Library's implode format, which is ZIP method 10 and the payload of a family of installers and game archives. |
DeflateDecoderStream Class | Decompresses a raw Deflate stream (RFC 1951) with no zlib or gzip wrapper: the format of ZIP method 8 and the payload of gzip and zlib. Reads only as much input as it needs and produces output through a 32 KiB sliding window, so memory does not grow with the stream. |
DeflateEncoderStream Class | Compresses to a raw Deflate stream (RFC 1951) with no zlib or gzip wrapper: the format of ZIP method 8. Levels 0 to 9 follow 7-Zip's fast-bytes and pass counts; level 0 stores. Output is deterministic: the same input at the same level produces the same bytes on every target, serially or not. |
DeltaDecoderStream Class | Undoes the delta filter: each byte plus the one a fixed distance behind it. |
DeltaEncoderStream Class | Applies the delta filter: each byte less the one a fixed distance behind it. |
ImplodeDecoderStream Class | Decodes ZIP method 6, "imploded": PKWARE's LZ77 with Shannon-Fano codes. |
Lz4DecoderStream Class | Reads an LZ4 frame (the .lz4 file format), producing the bytes that went into it. |
LzipDecoderStream Class | The lzip container (.lz): LZMA with framing of its own, designed for long-term archival. |
Lzma2DecoderStream Class | Decodes an LZMA2 stream: a sequence of chunks, each either raw bytes or an LZMA body of its own. |
Lzma2EncoderStream Class | Writes an LZMA2 stream: a sequence of chunks, each either raw bytes or an LZMA body of its own. |
LzmaDecoderStream Class | Decodes an LZMA stream: the range-coded body, without any container header. |
LzmaEncoderStream Class | Writes an LZMA stream: the range-coded body, without any container header. |
LzopDecoderStream Class | Reads an lzop file (.lzo), producing the bytes that went into it. |
PpmdDecoderStream Class | Decompresses a PPMd variant H stream as 7z stores it. |
PpmdEncoderStream Class | Compresses to a PPMd variant H stream as 7z stores it. |
ReduceDecoderStream Class | Reads ZIP methods 2 to 5, PKWARE's Reduce: the method PKZIP 0.90 and 0.92 wrote in 1989 and nothing has written since. |
RiscVDecoderStream Class | Undoes the RISC-V branch filter: jump and address-formation targets back from absolute to relative. |
RiscVEncoderStream Class | Applies the RISC-V branch filter: jump and address-formation targets from relative to absolute. |
ShrinkDecoderStream Class | Decodes ZIP method 1, "shrunk": PKWARE's LZW with an explicit code width and a partial table clear. |
SnappyDecoderStream Class | Reads the Snappy framing format (the .sz file format), producing the bytes that went into it. |
SzddDecoderStream Class | Microsoft's SZDD compression — the .??_ files that install media carried from the late 1980s until CAB replaced them, written by COMPRESS.EXE and expanded by EXPAND.EXE. |
ZstdDecoderStream Class | Decodes a Zstandard stream: the frames a .zst file holds and ZIP method 93 carries. |
ZstdEncoderStream Class | Compresses a stream into the Zstandard format (RFC 8878). |
Bastion.Archive.Codecs · Inherits Stream
Base64 (RFC 4648) as a decoding stream, for the .b64 files that carry an archive through something that will only pass text.
Decoding is done four input characters at a time into three output bytes, with white space of every kind skipped wherever it appears — real files are wrapped at 64 or 76 columns and the line ending depends on which machine wrote them. Padding is accepted at the end and, once seen, nothing but more padding and white space may follow. An unexpected character is an error rather than something to skip. Silently ignoring the odd stray byte is how a corrupt file decodes to plausible rubbish, and this library would rather say which byte and where.
An unexpected character is an error rather than something to skip. Silently ignoring the odd stray byte is how a corrupt file decodes to plausible rubbish, and this library would rather say which byte and where.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Wraps source, which holds base64 text.Throws ArgumentNullException when source is Nothing. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
LooksLikeBase64(head As Byte(), count As Integer) Shared | Boolean | Whether head looks like base64 text: the alphabet, padding and white space only.Base64 has no signature — that is the whole point of it — so a file can only be recognised by its name and then checked for plausibility. This is the check: it cannot prove a file is base64, but it refuses one that certainly is not, so a mislabelled file is reported rather than decoded into nonsense. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when the text holds a character that is not base64. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Undoes BCJ2: the x86 branch filter that splits its output into four streams instead of one.
The plain x86 filter has to guess. It sees a byte that could begin a call and decides from the four bytes after it, gets it wrong sometimes, and carries a mask to limit the damage — and because it rewrites in place, a wrong guess costs compression on both sides of the decision. BCJ2 does not guess, because the encoder knows. It writes the bytes with every converted operand removed into a main stream, the absolute addresses of calls into a call stream and of jumps into a jump stream, and one range-coded bit per candidate into a fourth stream saying whether it was converted. Separating addresses from code is most of the win: four-byte addresses compress far better gathered together than scattered through instructions. So decoding is: copy from main, and at every byte that could start a call or a jump, ask the range coder. If it says converted, take four big-endian bytes from the call or jump stream, subtract the position after the operand, and write them back little-endian. The probability the coder consults is chosen by what the instruction is — and for a call, by the byte before it, which is what makes the prediction good: the byte before a real call is not random. The range coder is LZMA's, with eleven-bit probabilities and a five-bit adaptation, so the arithmetic is the decoder this library already has rather than a second one. Ported from the LZMA SDK's Bcj2.c. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(main As Stream, call As Stream, jump As Stream, rangeCoded As Stream, declaredLength As Long, leaveOpen As Boolean) | Creates a decoder over the four streams of a BCJ2 coder.main — The bytes, with converted operands removed.call — The absolute targets of calls, big-endian.jump — The absolute targets of jumps, big-endian.rangeCoded — One coded bit per candidate.declaredLength — How many bytes the folder says this produces.leaveOpen — False to dispose the sources with this stream.Throws ArgumentNullException when an argument is Nothing; ArgumentOutOfRangeException when declaredLength is negative. |
| Member | Type | Summary |
|---|---|---|
StreamCount const | Integer | Streams BCJ2 reads: the bytes, the call targets, the jump targets, and the decisions. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | The length the folder declared. |
Position | Long | Bytes given out so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads the reassembled bytes. Returns. How many were given, or zero at the declared length. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed; Bastion.Archive.ArchiveException when a stream ends before the declared length, or the coded stream is malformed. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Applies BCJ2: the x86 branch filter that splits its output into four streams instead of one.
The inverse of Bcj2DecoderStream, and the only filter here that is not one stream in and one stream out — which is the whole point of it. The plain x86 filter has to guess. It sees a byte that could begin a call, decides from the four bytes after it, gets it wrong sometimes, and carries a mask to limit the damage. BCJ2 does not guess, because the encoder knows: it writes the bytes with every converted operand removed into a main stream, the absolute addresses of calls into a call stream and of jumps into a jump stream, and one range-coded bit per candidate into a fourth saying whether it was converted. Separating the addresses from the code is most of the win — four-byte addresses compress far better gathered together than scattered through instructions — and the bit costs a fraction of one, because the model is good. Two details decide whether an encoder is right, and neither shows up in a round trip: What the context is after a conversion. The byte the next decision treats as "the one before" is the operand's high byte, not the marker — because the decoder, reconstructing the operand, will have that byte in hand at the same point. Using the marker instead differs only when an address ends in 0F and is followed by 8x, which is rare and silent. When to convert at all. A displacement further than about 251 MB is left alone, because an address that large is more likely to be data that looks like a call than a call. That is a threshold rather than a rule, so it has to be the same threshold the reference uses or the streams diverge. The range coder is LZMA's, with eleven-bit probabilities and a five-bit adaptation. Ported from the LZMA SDK's Bcj2Enc.c. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destinations As Stream(), leaveOpen As Boolean) | Creates an encoder writing to the four streams BCJ2 produces.destinations — Four streams, in the order a folder lists them: main, call, jump and the range-coded decisions.leaveOpen — False to dispose the destinations with this stream.Throws ArgumentNullException when destinations or one of its members is Nothing; ArgumentException when there are not four of them, or one cannot be written. |
New(destinations As Stream(), leaveOpen As Boolean, fileLength As Long) | Creates an encoder that knows how long the run it is filtering will be.destinations — Four streams, in the order a folder lists them: main, call, jump and the range-coded decisions.leaveOpen — False to dispose the destinations with this stream.fileLength — How many bytes will be written, or UnknownLength if that is not known.Throws ArgumentNullException when destinations or one of its members is Nothing; ArgumentException when there are not four of them, or one cannot be written; ArgumentOutOfRangeException when fileLength is negative and not UnknownLength. |
| Member | Type | Summary |
|---|---|---|
StreamCount const | Integer | Streams BCJ2 writes: the bytes, the call targets, the jump targets, and the decisions. |
UnknownLength const | Long | What _fileLength holds when the length is not known. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes taken so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Passes on what has been gathered, but does not finish the filter. The held tail and the range coder's own four bytes cannot go out here: a candidate near the end has not been decided, and the coder's last bytes are not known until there are no more decisions. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Takes the original bytes and splits them across the four streams.buffer — The bytes.offset — Where they start.count — How many.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Bastion.Archive.Codecs · Inherits Stream
Undoes the x86 branch filter: call and jump targets back from absolute to relative.
An x86 call or jmp stores its target as a displacement from the instruction after it, so the same function called from two places is two different byte sequences and a compressor cannot match them. The filter rewrites each displacement as an absolute address before compression, which makes those sequences identical; undoing it subtracts the position back out. It is worth a great deal on executables and nothing at all on anything else, which is why 7-Zip applies it by extension. Three details decide whether an implementation is right, and all three are why this is gated against real 7-Zip output rather than reasoned about: What counts as an instruction. A byte of E8 or E9 starts one, and only when the most significant byte of the four that follow is 00 or FF — that test is what keeps the filter from rewriting data that merely looks like a call. The running mask. A false start leaves a trace in a three-bit mask so that overlapping candidates a few bytes apart are not each converted; the filter and its inverse have to agree about it exactly, and the mask survives across reads. The tail. A conversion needs five bytes, so the last four of whatever has arrived cannot be decided until more does. They are held back rather than guessed at, and at the end of the stream they pass through unchanged, which is what the filter's own encoder does. The conversion itself is in Bcj86Converter, shared with the encoder: the two directions differ in one operator and nothing else, so keeping two copies of the mask logic would mean two places for a correction to reach. What is here is the buffering. The algorithm is the LZMA SDK's Bra86.c. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a decoder over filtered bytes.source — The filtered bytes.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes given out so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads filtered bytes and gives back the originals. Returns. How many were given, or zero at the end. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Applies the x86 branch filter: call and jump targets from relative to absolute.
The inverse of Bcj86DecoderStream, sharing the conversion with it rather than restating it — see Bcj86Converter, where the two directions differ in a single operator. What is here is the write-side buffering. This is the filter that matters most in practice. On a native executable it finds tens of thousands of conversion sites where the fixed-width filters find a handful, and turning each call's distance into its target's address is what makes the same function called from a hundred places compress as one sequence rather than a hundred. On anything that is not x86 code it is worse than useless, which is why 7-Zip applies it by file extension and why this is asked for rather than chosen. Two pieces of state cross a write boundary, and both are the reason this cannot be done a buffer at a time independently: the running mask of recent false starts, and the tail. A conversion needs five bytes, so the last four of whatever has arrived wait for the next write; at the end of the stream they go out unchanged, which is exactly what the decoder leaves alone on the way back. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream, leaveOpen As Boolean) | Creates an encoder writing to a stream.destination — Where the filtered bytes go.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes taken so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Flushes the destination, but not the filter. The held tail cannot go out here. It is held precisely because it may turn out to be the front of a call, and writing it early would mean either converting a half-seen one or leaving an unconverted gap in the middle of the stream. Only the end settles that. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Takes the original bytes and writes on the filtered ones.buffer — The bytes.offset — Where they start.count — How many.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Bastion.Archive.Codecs
The instruction sets the branch filters know how to rewrite call and jump targets for.
x86 is not here: its instructions are variable length, so finding a call means scanning rather than stepping, and it carries state between candidates. That makes it a different algorithm rather than a case of this one, and it lives in Bcj86DecoderStream.
| Name | Value | Summary |
|---|---|---|
Arm | 0 | 32-bit ARM, four-byte instructions, the branch-with-link opcode. |
ArmThumb | 1 | ARM Thumb, pairs of 16-bit halves making one long branch. |
PowerPc | 2 | PowerPC, four-byte big-endian instructions. |
Sparc | 3 | SPARC, four-byte big-endian instructions. |
Arm64 | 4 | 64-bit ARM, four-byte instructions. Two forms are converted rather than one: the branch-with-link, and the page-address instruction whose immediate is split across the word. |
Ia64 | 5 | IA-64, whose instructions come three to a sixteen-byte bundle rather than one to a word. The odd one out here. An IA-64 bundle holds a five-bit template and three 41-bit instruction slots, and the template says which slots can hold a long branch at all — so finding one means reading the template first, and a slot's bits straddle byte boundaries at an offset that differs per slot. It is still a fixed-width stepping filter, which is why it belongs here rather than with x86, but it steps sixteen bytes at a time and converts up to three instructions per step. |
Bastion.Archive.Codecs · Inherits Stream
Undoes a fixed-width branch filter: call targets back from absolute to relative.
The same idea as the x86 filter and a far simpler one. On these instruction sets every instruction is the same width and sits at a fixed alignment, so a branch is found by stepping rather than scanning, and there is no state to carry between candidates — which is why one class covers four architectures and x86 needs its own. What differs between them is only where the displacement sits in the word, how it is scaled, and what the target is measured from. Each of those is stated once, in the one place that converts, so a reader can check an architecture against its manual without reading around it. The tail is held back for the same reason as x86: a conversion needs a whole instruction, so bytes beyond the last complete one wait for the next read, and at the end of the stream they pass through unchanged. Alignment survives that because what is consumed is always a whole number of steps. The conversions themselves are in BranchConverter, shared with the encoder: the two directions differ in one operator and nothing else, so keeping two copies would mean two places for a correction to reach. What is here is the buffering — the tail, the running offset, the stream contract. The algorithms are the LZMA SDK's Bra.c. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, architecture As BranchArchitecture, leaveOpen As Boolean) | Creates a decoder for one architecture's filter.source — The filtered bytes.architecture — Which instruction set the filter was applied for.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; ArgumentOutOfRangeException when architecture is not one this knows. |
| Member | Type | Summary |
|---|---|---|
Architecture read-only | BranchArchitecture | The architecture in use. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes given out so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads filtered bytes and gives back the originals. Returns. How many were given, or zero at the end. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Applies a fixed-width branch filter: call targets from relative to absolute.
The inverse of BranchFilterDecoderStream, and it shares the arithmetic with it rather than restating it — see BranchConverter, where the two directions differ in a single operator. What is here is the write-side buffering. Why a branch filter is worth anything: a routine called from a hundred places is a hundred different byte sequences, because each call carries its own distance to the target. Rewriting the distance as the target's absolute address makes them a hundred identical sequences, and identical sequences are the only thing a compressor can charge less for. The tail is held back rather than filtered: a conversion needs a whole instruction, so bytes beyond the last complete one wait for the next write, and whatever is still short at the end goes out unchanged — which is what the decoder passes through untouched on the way back. Alignment survives that because what is consumed is always a whole number of steps. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream, architecture As BranchArchitecture, leaveOpen As Boolean) | Creates an encoder for one architecture's filter.destination — Where the filtered bytes go.architecture — Which instruction set to filter for.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when architecture is not one this knows. |
| Member | Type | Summary |
|---|---|---|
Architecture read-only | BranchArchitecture | The architecture in use. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes taken so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Flushes the destination, but not the filter. The held tail cannot go out here. It is held precisely because it may turn out to be the front of an instruction, and writing it early would mean either filtering a half-read one or leaving an unfiltered gap in the middle of the stream. Only the end of the stream settles the question, which is what Dispose does. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Takes the original bytes and writes on the filtered ones.buffer — The bytes.offset — Where they start.count — How many.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Bastion.Archive.Codecs · Inherits Stream
Reads a brotli stream (RFC 7932), producing the bytes that went into it.
Brotli is deflate's shape — literals and backward copies — with four things added, and the four are where the work is. Context modelling. Which prefix code reads the next literal depends on the two bytes just produced, so a stream may carry hundreds of literal codes and switch between them per byte. Block switching. Literals, commands and distances each run in blocks with their own type, and a block-switch command in the stream changes which set of codes is in use. A shared dictionary. A distance past the start of the window names a word in the 122 KiB static dictionary, optionally transformed, which is how brotli compresses a few hundred bytes of HTML well. Distance reuse. The last four distances are kept, and sixteen distance symbols name them or small variations of them rather than coding a number. Output is produced into a ring buffer the size of the stream's declared window, and the decoder stops as soon as that is full of bytes the caller has not taken — so memory is bounded by the window the stream asked for and not by what it decodes to.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a decoder over a brotli stream.source — The compressed stream. It need not be seekable.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads decoded bytes. Returns. How many were given, or zero at the end of the stream. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed; Bastion.Archive.ArchiveException when the stream is malformed or truncated. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Decodes a bzip2 stream, block by block, from the published format description.
Each block is undone in the reverse of the order it was built: Huffman decode with tables that alternate every fifty symbols, then the run-length coding of the zero symbol, then move-to-front, then the inverse Burrows-Wheeler transform, then the run-length coding of the original bytes. Only the last of those can be done lazily, so a block's worth of memory is held while it is being served — which is why the level digit in the header, and nothing about the file's size, decides what this costs. Concatenated streams are read as one, which is what bzip2 -d does and what cat a.bz2 b.bz2 produces. Being a Stream, this type throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream) | Creates a decoder over source, leaving it open when this stream is disposed.source — The compressed input; read forward only.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
New(source As Stream, leaveOpen As Boolean) | Creates a decoder over source.source — The compressed input; read forward only.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
BytesConsumed read-only | Long | Compressed bytes consumed so far. Exact once IsFinished is True. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
IsFinished read-only | Boolean | True once the final stream footer has been read. |
Length read-only | Long | Overrides Stream.Length. |
Level read-only | Integer | The level digit of the stream being read, 1 to 9, or 0 before the header is seen. |
MaximumBlockLength read-only | Integer | The most a block of this stream may hold, which is what the decoder's memory is bounded by. |
Position | Long | Bytes produced so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes into buffer, returning 0 only at the end of the last stream.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; Bastion.Archive.ArchiveException when the stream is not bzip2, is truncated, or fails a checksum. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Writes a bzip2 stream, from the published format description.
The five stages of a block, in the order they are applied: the run-length coding of the original bytes, the Burrows-Wheeler transform, move-to-front, the run-length coding of the zero symbol, and Huffman coding with two to six tables that alternate every fifty symbols. The tables are chosen the way the format's own description does it — start from a split of the alphabet by frequency, then repeatedly assign each group of fifty symbols to whichever table codes it most cheaply and rebuild the tables from the groups that chose them. Memory is one block plus the transform's five index arrays, so the level decides it and the input size does not: WorkingMemoryBytes says how much. Output is deterministic — the same input and level always give the same bytes. Being a Stream, this type throws ArchiveException rather than returning a result. The final block and the stream footer are written by Complete, which Dispose calls.
| Constructor | Summary |
|---|---|
New(destination As Stream) | Creates an encoder at level 9, which is bzip2's own default.destination — Where the compressed bytes go.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, leaveOpen As Boolean) | Creates an encoder.destination — Where the compressed bytes go.level — Block size in hundreds of thousands of bytes, 1 to 9.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 1 to 9. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Level read-only | Integer | The level in use, which is the block size in hundreds of thousands of bytes. |
Position | Long | Uncompressed bytes accepted so far. |
WorkingMemoryBytes read-only | Long | Working memory this encoder holds, which the level alone decides. The input's size does not enter into it. |
| Member | Returns | Summary |
|---|---|---|
Complete() | — | Finishes the stream: the last block, the footer and the stream checksum. Safe to call twice. Throws Bastion.Archive.ArchiveException when a block could not be coded. |
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Accepts uncompressed bytes. Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; InvalidOperationException when the stream has already been completed. |
Bastion.Archive.Codecs · Inherits Stream
Reads a Unix compress stream (.Z, LZW as in the original compress utility).
Three bytes of header — 1F 9D and a flags byte whose low five bits are the largest code width the stream will reach and whose 0x80 bit says the dictionary may be cleared — and then codes, packed least significant bit first at a width that grows from 9 bits as the dictionary fills. Two things here are peculiar to this format rather than to LZW in general, and both are invisible until a real file exercises them. The KwKwK case. A code may refer to the entry that is about to be added, which happens when the encoder meets a sequence whose first character repeats — the decoder resolves it as the previous string plus its own first character. Files without a doubled character never reach it. The padding after a clear or a width increase. The encoder packs codes into a buffer of exactly eight codes — which at n bits is n whole bytes — and empties it whenever the dictionary is cleared or the codes get wider, whatever is left in it. Nothing in the data announces the gap. A decoder that reads straight on is correct on every stream too small to clear or to outgrow nine bits, and wrong from that point on for every stream that is not, which is why the gate uses a payload large enough to do both several times over.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a decoder over a .Z stream.source — The compressed stream. It need not be seekable.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads decoded bytes. Returns. How many were given, or zero at the end of the stream. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed; Bastion.Archive.ArchiveException when the stream is malformed or truncated. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Reads the PKWARE Data Compression Library's implode format, which is ZIP method 10 and the payload of a family of installers and game archives.
Not ZIP's own Implode (method 6), despite the name: this is PKWARE's commercial library, an LZ77 with fixed prefix codes, whose stream starts with two bytes — 0 or 1 for raw or coded literals, then 4, 5 or 6 for a 1, 2 or 4 KiB window — and ends with a reserved length code rather than at a known size. PKWARE never published the format. It is written here from Ben Rudiak-Gould's description, posted to comp.compression on 13 August 2001 and the source every reader since has worked from; no implementation was read. The description's prose was followed and its worked example was not: the example reads an offset's low bits most significant first, contradicting the prose, and PKZIP 4.00's own output decodes only the prose's way — as the independent decoder blast agrees, rejecting the example as a copy from before the start. Gated against PKWARE's own software: PKZIP 4.00 for Windows writes method 10 in all six variants and tests what it writes, and blast reads the raw streams as a second opinion. Like every codec stream here it honours the BCL contract and throws ArchiveException.
PKWARE never published the format. It is written here from Ben Rudiak-Gould's description, posted to comp.compression on 13 August 2001 and the source every reader since has worked from; no implementation was read. The description's prose was followed and its worked example was not: the example reads an offset's low bits most significant first, contradicting the prose, and PKZIP 4.00's own output decodes only the prose's way — as the independent decoder blast agrees, rejecting the example as a copy from before the start.
Gated against PKWARE's own software: PKZIP 4.00 for Windows writes method 10 in all six variants and tests what it writes, and blast reads the raw streams as a second opinion. Like every codec stream here it honours the BCL contract and throws ArchiveException.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Opens a DCL implode stream, which carries its own end marker.source — The compressed bytes, header first.leaveOpen — Whether to leave source open when this is disposed.Throws ArgumentNullException when source is Nothing. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Decompresses a raw Deflate stream (RFC 1951) with no zlib or gzip wrapper: the format of ZIP method 8 and the payload of gzip and zlib. Reads only as much input as it needs and produces output through a 32 KiB sliding window, so memory does not grow with the stream.
This is a Stream, so it throws rather than returning a result: only ArchiveException, with CorruptArchive for a malformed stream and TruncatedArchive for one that ends early. A caller that wants a result object uses the archive API instead. Reading past the final block returns 0 for ever; BytesConsumed then says how much input the stream used, which is how a zip reader finds the entry's compressed size when no size was declared.
| Constructor | Summary |
|---|---|
New(source As Stream) | Creates a decoder over source, leaving it open when this stream is disposed.source — The compressed input; read forward only.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
New(source As Stream, leaveOpen As Boolean) | Creates a decoder over source.source — The compressed input; read forward only.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
BytesConsumed read-only | Long | Compressed bytes consumed so far. Exact once IsFinished is True. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
IsFinished read-only | Boolean | True once the final block has been decoded. |
Length read-only | Long | Not supported: the uncompressed length is not known until the stream ends. |
Position | Long | Uncompressed bytes produced so far; cannot be set. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when the compressed data is malformed or ends early. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
TakeUnconsumedBytes() As Byte() | — | Hands back the input that was fetched and never used, once the stream has been decoded. A Deflate stream carries no length, so it is decoded by fetching input in blocks — and the last block fetched usually reaches past the final bit into whatever follows. A container that has something after the compressed data, as gzip does, needs those bytes back rather than a seekable source to wind. Meaningless before IsFinished, and the caller should be done reading when it asks. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Compresses to a raw Deflate stream (RFC 1951) with no zlib or gzip wrapper: the format of ZIP method 8. Levels 0 to 9 follow 7-Zip's fast-bytes and pass counts; level 0 stores. Output is deterministic: the same input at the same level produces the same bytes on every target, serially or not.
Input is gathered into 64 KiB blocks (65 535 bytes, what one stored block holds). Each block is parsed into literals and matches, then written in whichever of the three block forms is smallest: stored, fixed-Huffman or dynamic-Huffman. At levels 7 and above the block is parsed several times, each pass pricing matches with the code lengths the previous pass produced, and the smallest result wins. Being a Stream, this type throws rather than returning a result. The final block is written by Complete, which Dispose calls; a stream that is abandoned without either produces an incomplete Deflate stream, which is why the archive API always disposes it.
| Constructor | Summary |
|---|---|
New(destination As Stream) | Creates an encoder at level 5, the default of CompressionSettings and of 7-Zip.destination — Where the compressed bytes go.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, leaveOpen As Boolean) | Creates an encoder.destination — Where the compressed bytes go.level — Compression level 0 to 9; 0 stores without compressing.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 0 to 9. |
| Member | Type | Summary |
|---|---|---|
BytesWritten read-only | Long | Compressed bytes written so far. Final once Complete has run. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Not supported: the compressed length is not known until the stream is complete. |
Level read-only | Integer | The compression level in use. |
Position | Long | Uncompressed bytes accepted so far; cannot be set. |
| Member | Returns | Summary |
|---|---|---|
Complete() | — | Writes the final block and flushes the output. Called by Dispose; calling it twice is harmless. No data may be written afterwards. |
Flush() | — | Pushes the bytes of every block written so far to the destination. A partially filled block is not written, because ending a block early costs ratio and only the caller knows whether that matters; Complete writes it. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Undoes the delta filter: each byte plus the one a fixed distance behind it.
The filter an encoder applies before compressing data whose values change little from one sample to the next — uncompressed audio, bitmaps, tables of similar numbers. Subtracting the byte a sample-width back turns a slowly changing series into small numbers, which a compressor does far better with. Undoing it is the addition, and the only subtlety is that it carries across reads: the last distance bytes given out are what the next ones are added to, so the state has to outlive a single call. Ported from the LZMA SDK's Delta.c. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, distance As Integer, leaveOpen As Boolean) | Creates a decoder for a given sample distance.source — The filtered bytes.distance — How far back each byte refers, 1 to 256.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; ArgumentOutOfRangeException when distance is outside 1 to 256. |
| Member | Type | Summary |
|---|---|---|
MaximumDistance const | Integer | The largest distance the filter allows, which its one property byte can spell. |
MinimumDistance const | Integer | The smallest distance the filter allows. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Distance read-only | Integer | The sample distance in use. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes given out so far. |
| Member | Returns | Summary |
|---|---|---|
DistanceFromProperties(properties As Byte()) Shared | Integer | Reads the distance a container's one property byte spells, which is the distance less one.properties — The filter properties.Throws ArgumentNullException when properties is Nothing; Bastion.Archive.ArchiveException when the properties are not the single byte the filter takes. |
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads filtered bytes and gives back the originals. Returns. How many were given, or zero at the end. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Applies the delta filter: each byte less the one a fixed distance behind it.
The exact inverse of DeltaDecoderStream, and the first filter in this library that runs forwards. Every other one decodes only, which is enough to read an archive and not enough to write one. What it is for: data whose values change little from one sample to the next — uncompressed audio, a bitmap, a table of similar numbers. Subtracting the byte a sample-width back turns a slowly changing series into small numbers clustered round zero, and a compressor does far better with those than with the originals. On anything else it is worse than useless, which is why 7-Zip applies it only when asked rather than guessing. The state carries across writes, exactly as it does across reads on the way back: the last distance bytes as they arrived are what the next ones are subtracted from. Note as they arrived — the history holds the original bytes, not the filtered ones, which is the detail that makes this the inverse of a decoder whose history holds the bytes it just produced. From the LZMA SDK's Delta.c. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream, distance As Integer, leaveOpen As Boolean) | Creates an encoder for a given sample distance.destination — Where the filtered bytes go.distance — How far back each byte refers, 1 to 256.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when distance is outside 1 to 256. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Distance read-only | Integer | The sample distance in use. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes taken so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Properties() As Byte() | — | The single property byte a container records for this filter, which is the distance less one. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Takes the originals and writes on the differences.buffer — The bytes.offset — Where they start.count — How many.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Bastion.Archive.Codecs · Inherits Stream
Decodes ZIP method 6, "imploded": PKWARE's LZ77 with Shannon-Fano codes.
Two of the entry's general purpose bits are part of the format rather than of the container: bit 1 says the dictionary is 8 KiB rather than 4, and bit 2 says there are three trees rather than two. Both change how the body is read — the dictionary size decides how many offset bits are read straight from the stream, and the presence of a literal tree decides both how a literal is coded and that the shortest match is three bytes rather than two. A reader that ignored either would decode plausible rubbish. A packet is one bit: set for a literal, clear for a match. A match reads the low offset bits directly, takes the high bits from the distance tree, takes the length from the length tree, and adds the minimum; the last length symbol means "and eight more bits of length". No current tool writes this format, so the behaviour here was settled by measurement: fixtures built field by field and handed to Info-ZIP unzip 6.00 and 7-Zip 26.03, which agree with each other on every construct — the tree encoding, the code assignment, both dictionary sizes, both tree counts, the minimum match length in each, and the extended length. The one place this deliberately differs from both is a match that reaches back before the start of the entry: they pad with zeroes, this refuses. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, declaredLength As Long, largeDictionary As Boolean, threeTrees As Boolean, leaveOpen As Boolean) | Opens an imploded entry and reads its trees.source — The imploded bytes, positioned at the first tree.declaredLength — Bytes the entry declares, or -1 to read until the source runs out. The format has no end marker, so the declared size is what really ends it.largeDictionary — General purpose bit 1: an 8 KiB dictionary rather than 4 KiB.threeTrees — General purpose bit 2: a literal tree is present as well.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; Bastion.Archive.ArchiveException when a tree is malformed, or the entry ends inside one. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
DictionaryBytes read-only | Integer | The dictionary this entry declared, 4 KiB or 8 KiB. |
Length read-only | Long | Overrides Stream.Length. |
MinimumMatchLength read-only | Integer | The shortest match this entry can describe, which the tree count decides. |
Position | Long | Bytes produced so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes into buffer, returning 0 at the end of the entry.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; Bastion.Archive.ArchiveException when the entry is corrupt or ends early. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Reads an LZ4 frame (the .lz4 file format), producing the bytes that went into it.
A frame is a header, a run of blocks each with its own length, an end mark of four zero bytes, and optionally a checksum of everything that came out. Three things about it are easy to get wrong and are worth naming. Blocks may be linked. The frame header says whether each block stands alone or may match back into the one before it, and the tool's own default is linked — so a decoder that resets its history each block reads files written with the flag set and produces rubbish for the ones written without it. A 64 KiB history is therefore carried across blocks, which is the reach LZ4 allows. A block may be stored. The top bit of the length means the block is not compressed at all, which is what the encoder does when compression made it bigger. Frames concatenate, like gzip members, and a skippable frame may sit between them carrying anything at all. Stopping at the first end mark reads almost every file correctly and silently truncates the rest. Checksums are XXH32 and all three are verified: the header's, each block's where the frame asks for them, and the content's. Verified before the bytes are handed out — a checksum checked afterwards tells a caller that what they have already used was wrong. The legacy frame (magic 184C2102) is refused by name rather than guessed at.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a decoder over a frame.source — The compressed stream. It need not be seekable.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads decoded bytes. Returns. How many were given, or zero at the end of the last frame. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed; Bastion.Archive.ArchiveException when the frame is malformed, truncated or fails a checksum. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
The lzip container (.lz): LZMA with framing of its own, designed for long-term archival.
A member is six bytes of header — the magic LZIP, a version, and one byte giving the dictionary size — then an LZMA stream ended by its own marker, then twenty bytes of trailer: the CRC-32 of what came out, the uncompressed length, and the length of the whole member. The coding parameters are fixed by the format rather than carried in it: literal context 3, literal position 0, position 2 — the byte 0x5D that a .lzma file would write out. Only the dictionary size varies, and it is coded as a power of two with an optional fraction subtracted, so that sizes between the powers can be named. A file may hold several members one after another, and they are read as one stream, the way lzip itself concatenates them. Each member's CRC and length are checked as it ends, so a file that decodes to the wrong bytes fails rather than being handed over.
The coding parameters are fixed by the format rather than carried in it: literal context 3, literal position 0, position 2 — the byte 0x5D that a .lzma file would write out. Only the dictionary size varies, and it is coded as a power of two with an optional fraction subtracted, so that sizes between the powers can be named.
A file may hold several members one after another, and they are read as one stream, the way lzip itself concatenates them. Each member's CRC and length are checked as it ends, so a file that decodes to the wrong bytes fails rather than being handed over.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Wraps source, positioned at the first member.Throws ArgumentNullException when source is Nothing. |
| Member | Type | Summary |
|---|---|---|
HeaderBytes const | Integer | Header bytes before the coded data. |
Magic Shared read-only | Byte() | The four-byte magic every member begins with. |
TrailerBytes const | Integer | Trailer bytes after it: CRC-32, uncompressed size, member size. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
DictionaryBytes(coded As Byte) Shared | Long | The dictionary size a header byte names: a power of two, less a sixteenth of it for each step in the top three bits, which is how sizes between the powers are written. |
Flush() | — | Overrides Stream.Flush. |
HasMagic(head As Byte(), count As Integer) Shared | Boolean | Whether head begins with a member this reader will take. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when a member is malformed, cut short, or fails its CRC. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
TotalLength(stream As Stream) Shared | Long | The uncompressed length of every member in stream, by reading the trailers.Walking the members backwards from the end is what lets a listing state a size without decoding anything: each trailer says how long its own member is, so the one before it can be found. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Decodes an LZMA2 stream: a sequence of chunks, each either raw bytes or an LZMA body of its own.
LZMA2 exists to make LZMA resumable and parallelisable, and the chunk header is where that happens. Each chunk says whether to reset the probability model, whether to take new properties, and whether to forget the dictionary — independently, so a stream can carry on exactly where it left off, start fresh, or anything between. It also allows a chunk to be stored rather than coded, which is what keeps incompressible data from growing. A chunk holds at most 2 MiB of output in at most 64 KiB of input. The dictionary size is not in the stream: it comes from whatever container carries it — the filter properties of an XZ block, or a 7z coder's properties — so the caller supplies it here. Written from the specification the LZMA SDK ships. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, dictionaryBytes As Integer, leaveOpen As Boolean) | Creates a decoder over an LZMA2 stream.source — The chunks; read forward only.dictionaryBytes — The dictionary the container declared.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; Bastion.Archive.ArchiveException when the dictionary asked for is larger than this decoder allows. |
New(source As Stream, dictionaryBytes As Integer, leaveOpen As Boolean, maximumDictionaryBytes As Integer) | Creates a decoder with an explicit cap on the dictionary it will allocate.source — The chunks; read forward only.dictionaryBytes — The dictionary the container declared.leaveOpen — False to dispose source with this stream.maximumDictionaryBytes — The largest dictionary to allocate.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; ArgumentOutOfRangeException when maximumDictionaryBytes is below one; Bastion.Archive.ArchiveException when the dictionary asked for is larger than this decoder allows. |
| Member | Type | Summary |
|---|---|---|
DefaultMaximumDictionaryBytes const | Integer | The largest dictionary accepted unless the caller says otherwise: 256 MiB. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
DictionaryBytes read-only | Integer | The dictionary this stream allocated. |
IsFinished read-only | Boolean | True once the end-of-stream control byte has been read. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes produced so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes into buffer, returning 0 at the end of the stream.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; Bastion.Archive.ArchiveException when the stream is corrupt or ends early. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Writes an LZMA2 stream: a sequence of chunks, each either raw bytes or an LZMA body of its own.
The model and the dictionary live in LzmaEncoderCore and persist across every chunk, so a chunk boundary costs only the header and the five bytes a range coder ends with — the probabilities and the repeated distances carry straight on, which is what reset mode 0 in the chunk header means. That is also why a chunk is not tied to the encoder's window: a chunk stays open across as many window slides as it takes to fill, because sealing one early at a small level would multiply that per-chunk cost. Two limits decide where a chunk ends: it may hold at most 2 MiB of output in at most 64 KiB of input, so the coder is watched as it writes and the chunk is sealed at the last whole packet before either runs out. A match is measured against the chunk's end as well, because a match may not span chunks. Data that does not compress is stored instead. That decision cannot be made in advance — it takes encoding the chunk to find out — so the attempt is made into a buffer, and where the result is no smaller than the input the attempt is thrown away and the bytes are written as a stored chunk. The decoder never saw those bits, and a stored chunk leaves it with a reset coder, so the model is reset to match and the next coded chunk carries new properties. Only a chunk still inside a stored chunk's 64 KiB can go that way, which is why the raw bytes are kept only that far: past it the coded form is smaller than storing by arithmetic, since the coded form can never exceed 64 KiB. The dictionary size is not in the stream: whatever container carries it records it, and DictionaryBytes is what it must record. Written from the specification the LZMA SDK ships. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream) | Creates an encoder at level 5.destination — Where the compressed bytes go.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, leaveOpen As Boolean) | Creates an encoder.destination — Where the compressed bytes go.level — Compression level 1 to 9.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 1 to 9. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
DictionaryBytes read-only | Integer | The dictionary a reader must allocate, which the container carrying this stream has to record — an XZ block's filter properties, or a 7z coder's. |
Length read-only | Long | Overrides Stream.Length. |
Level read-only | Integer | The level in use. |
Position | Long | Uncompressed bytes accepted so far. |
WorkingMemoryBytes read-only | Long | Working memory this encoder holds, which the level alone decides. |
| Member | Returns | Summary |
|---|---|---|
Complete() | — | Finishes the stream: the last chunk, then the control byte that says there are no more. |
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Accepts uncompressed bytes. Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; InvalidOperationException when the stream has already been completed. |
Bastion.Archive.Codecs · Inherits Stream
Decodes an LZMA stream: the range-coded body, without any container header.
Written from the specification the LZMA SDK ships. The model and the dictionary live in LzmaModel, because LZMA2 drives the same model across many chunks and a decoder that owned it could not express that; what is here is the container-free framing — read the properties, run one range coder to the end, stop at the declared length or the end marker. Memory is the dictionary, whose size the stream's own properties declare and which MaximumDictionaryBytes caps, so a hostile header cannot ask for a gigabyte. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, properties As Byte(), declaredLength As Long, leaveOpen As Boolean) | Creates a decoder from the five properties bytes an LZMA header carries.source — The range-coded body.properties — Five bytes: the lc/lp/pb packing, then the dictionary size, little endian.declaredLength — Bytes the stream should produce, or -1 when only an end marker says.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when an argument is Nothing; ArgumentException when properties is not five bytes, or the source is not readable; Bastion.Archive.ArchiveException when the properties are not a valid packing, or ask for more dictionary than allowed. |
New(source As Stream, properties As Byte(), declaredLength As Long, leaveOpen As Boolean, maximumDictionaryBytes As Integer) | Creates a decoder with an explicit cap on the dictionary it will allocate.source — The range-coded body.properties — Five bytes: the lc/lp/pb packing, then the dictionary size, little endian.declaredLength — Bytes the stream should produce, or -1 when only an end marker says.leaveOpen — False to dispose source with this stream.maximumDictionaryBytes — The largest dictionary to allocate for a stream that asks.Throws ArgumentNullException when an argument is Nothing; ArgumentException when properties is not five bytes, or the source is not readable; ArgumentOutOfRangeException when maximumDictionaryBytes is below one; Bastion.Archive.ArchiveException when the properties are not a valid packing, or ask for more dictionary than allowed. |
| Member | Type | Summary |
|---|---|---|
DefaultMaximumDictionaryBytes const | Integer | The largest dictionary accepted unless the caller says otherwise: 256 MiB. |
| Member | Type | Summary |
|---|---|---|
BytesConsumed read-only | Long | Compressed bytes consumed so far. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
DictionaryBytes read-only | Integer | The dictionary this stream allocated, which its own properties asked for. |
IsFinished read-only | Boolean | True once the end marker or the declared length has been reached. |
Length read-only | Long | Overrides Stream.Length. |
MaximumDictionaryBytes read-only | Integer | The cap this decoder checked the stream's declared dictionary size against. |
Position | Long | Bytes produced so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes into buffer, returning 0 at the end of the stream.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; Bastion.Archive.ArchiveException when the stream is corrupt or ends early. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Writes an LZMA stream: the range-coded body, without any container header.
The model, the window and the choice of what to encode live in LzmaEncoderCore, because LZMA2 drives the same model across many chunks and an encoder that owned it could not express that; what is here is the container-free framing — one range coder from the first byte to the last, then the end marker if the caller wants one. Memory is the dictionary and the match finder's chains, so the level decides it and the input size does not: WorkingMemoryBytes says how much. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream) | Creates an encoder at level 5 with an end-of-stream marker.destination — Where the compressed bytes go.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, writeEndMarker As Boolean, leaveOpen As Boolean) | Creates an encoder.destination — Where the compressed bytes go.level — Compression level 1 to 9.writeEndMarker — True to finish with the end-of-stream marker, which lets a reader stop without knowing the length. A ZIP entry that sets general purpose bit 1 says the marker is there.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 1 to 9. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Level read-only | Integer | The level in use. |
Position | Long | Uncompressed bytes accepted so far. |
WorkingMemoryBytes read-only | Long | Working memory this encoder holds, which the level alone decides. |
| Member | Returns | Summary |
|---|---|---|
Complete() | — | Finishes the stream: the last positions, the end marker if asked for, and the range coder. |
Flush() | — | Overrides Stream.Flush. |
Properties() As Byte() | — | The five properties bytes a container must record for this stream to be readable. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Accepts uncompressed bytes. Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; InvalidOperationException when the stream has already been completed. |
Bastion.Archive.Codecs · Inherits Stream
Reads an lzop file (.lzo), producing the bytes that went into it.
After the header (LzopHeader) the file is a run of blocks, each one the decompressed length, the compressed length, whatever checksums the flags call for, and the data. A block whose two lengths are equal was stored rather than compressed, which is what the compressor does when compression made it bigger, and which a decoder that always decompresses turns into an error on the first incompressible file it meets. A decompressed length of zero ends the file. Blocks are independent — a match never reaches out of the block it is in — so this reader holds one block's worth of memory and no more, however large the file. lzop's own default block is 256 KiB, and a block claiming more than BlockCeiling is refused rather than allocated, so a corrupt length cannot be turned into an allocation. Each block's checksum is verified before any of its bytes are handed out, against whichever of Adler-32 or CRC-32 the header's flags chose.
Blocks are independent — a match never reaches out of the block it is in — so this reader holds one block's worth of memory and no more, however large the file. lzop's own default block is 256 KiB, and a block claiming more than BlockCeiling is refused rather than allocated, so a corrupt length cannot be turned into an allocation.
Each block's checksum is verified before any of its bytes are handed out, against whichever of Adler-32 or CRC-32 the header's flags chose.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Wraps source, reading its header at once.Throws ArgumentNullException when source is Nothing; Bastion.Archive.ArchiveException when the header is malformed or asks for something this reader does not do. |
| Member | Type | Summary |
|---|---|---|
BlockCeiling const | Integer | The largest block this reader will allocate for: eight times lzop's own default. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when a block is malformed, cut short, or fails its checksum. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Decompresses a PPMd variant H stream as 7z stores it.
PPMd predicts each byte from the bytes before it rather than referring back to matches, which is what makes it far better than LZ methods on text and far worse on binaries with long repeats. The whole model lives in one allocation of a size the archive states; when it fills, the model forgets everything and begins again, which is bounded and predictable and is why memory here never depends on how much has been decoded. The model is the expensive part and it is built while decoding, so this stream is strictly forward-only — there is no seeking back to a byte already given out, because the model that produced it has moved on. This is 7z's framing of PPMd specifically. The same model inside a ZIP archive (method 98) is variant I wrapped in a different range coder, and is a separate job. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, properties As Byte(), declaredLength As Long, leaveOpen As Boolean) | Creates a decoder for a stream carrying the given coder properties.source — The coded bytes.properties — Five bytes: the order, then the model size, little endian.declaredLength — Bytes the stream should produce, or -1 when only its end says.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when the source or the properties are Nothing; ArgumentException when source is not readable; Bastion.Archive.ArchiveException when the properties lie outside the format. |
New(source As Stream, properties As Byte(), declaredLength As Long, leaveOpen As Boolean, maximumMemoryBytes As Integer) | Creates a decoder with an explicit cap on the model it will allocate.source — The coded bytes.properties — Five bytes: the order, then the model size, little endian.declaredLength — Bytes the stream should produce, or -1 when only its end says.leaveOpen — False to dispose source with this stream.maximumMemoryBytes — The most the caller will allow the model to allocate, which bounds what a stated size can cost.Throws ArgumentNullException when the source or the properties are Nothing; ArgumentException when source is not readable; ArgumentOutOfRangeException when the memory cap is not positive; Bastion.Archive.ArchiveException when the properties lie outside the format, or ask for more memory than the caller allows. |
| Member | Type | Summary |
|---|---|---|
DefaultMaximumMemoryBytes const | Integer | The largest model accepted unless the caller says otherwise: 256 MiB. |
MaximumOrder const | Integer | The deepest model the format allows. |
MinimumMemoryBytes const | Integer | The smallest model memory the format allows. |
MinimumOrder const | Integer | The smallest model the format allows. |
PropertiesLength const | Integer | Bytes of coder properties 7z stores for this method: an order and a memory size. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
MemoryBytes read-only | Long | Bytes the model may use. |
Order read-only | Integer | How many bytes of history the deepest context holds. |
Position | Long | Bytes given out so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
MemoryBytesFromProperties(properties As Byte()) Shared | Long | Reads the model size a container's coder properties state, in bytes, little-endian.properties — The coder properties: an order byte, then a memory size.Throws ArgumentNullException when properties is Nothing; Bastion.Archive.ArchiveException when the properties are not the five bytes the method takes. |
OrderFromProperties(properties As Byte()) Shared | Integer | Reads the order a container's coder properties state.properties — The coder properties: an order byte, then a memory size.Throws ArgumentNullException when properties is Nothing; Bastion.Archive.ArchiveException when the properties are not the five bytes the method takes. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes the next bytes of the stream. Returns. How many were given, or zero at the end of the stream. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed; Bastion.Archive.ArchiveException when the coded stream is not one this model can have produced. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Compresses to a PPMd variant H stream as 7z stores it.
The inverse of PpmdDecoderStream. PPMd predicts each byte from the bytes before it rather than referring back to matches, which makes it far better than LZ methods on text and far worse on binaries with long repeats — so it is a method a caller chooses for a known kind of data, not a default. The model is the expensive part and both sides build it as they go, from the same bytes in the same order. That is what makes the encoder cheap to be confident about and expensive to get subtly wrong: the contexts, frequencies and escape estimates here are the same code the decoder runs and is already gated on, so what is new is only the coding of the slice each symbol occupies. The two settings are the order — how many bytes of history the deepest context holds — and the model size, which is all the memory it will ever use: when the model fills it forgets everything and starts again, which is bounded and predictable and why memory here never depends on how much has been written. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream, leaveOpen As Boolean) | Creates an encoder with the default order and model size.destination — Where the coded bytes go.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, order As Integer, memoryBytes As Integer, leaveOpen As Boolean) | Creates an encoder with an explicit order and model size.destination — Where the coded bytes go.order — How many bytes of history the deepest context holds, 2 to 64.memoryBytes — All the memory the model will use. A larger one forgets less often and compresses better; it never grows beyond this, whatever is written.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when the order or the model size is outside the format. |
| Member | Type | Summary |
|---|---|---|
DefaultMemoryBytes const | Integer | The model size 7-Zip uses when it is not told one: 16 MiB. |
DefaultOrder const | Integer | The order 7-Zip uses when it is not told one. |
PropertiesLength const | Integer | Bytes of coder properties 7z stores for this method: an order and a memory size. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
MemoryBytes read-only | Integer | All the memory the model will use. |
Order read-only | Integer | How many bytes of history the deepest context holds. |
Position | Long | Bytes taken so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Flushes the destination, but not the coder. The range coder's last bytes are not known until there are no more symbols, so nothing of the coded stream can be settled here. That is the format rather than this implementation: a PPMd stream has no flush point at all. |
Properties() As Byte() | — | The five property bytes a container records for this coder: the order, then the model size. Little endian for the size, which is the one place this format is, and the same five bytes PpmdDecoderStream reads back. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Codes bytes into the stream.buffer — The bytes.offset — Where they start.count — How many.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Bastion.Archive.Codecs · Inherits Stream
Reads ZIP methods 2 to 5, PKWARE's Reduce: the method PKZIP 0.90 and 0.92 wrote in 1989 and nothing has written since.
Written from PKWARE's own specification — the APPNOTE.TXT that shipped inside PKZIP 0.92 on 6 March 1989 — which describes the method completely, so this is a clean-room implementation. No other implementation was read. Reduce is two algorithms chained. The first stage undoes a probabilistic coding: for every byte value there is a "follower set" of up to 32 bytes that tend to come after it, stored at the head of the entry, and each following byte is sent either as a short index into that set or as a literal. The second stage undoes a simple repeated-sequence coding, where the byte 144 (DLE) introduces a length and a distance back into what has already been produced. The four compression factors — the four method numbers — differ only in how many bits of the length byte are lent to the distance, which is why the window grows from 512 bytes at factor 1 to 4 KiB at factor 4. Gated against the original producer and two original readers, there being no living ones: the fixtures were written by PKZIP 0.92 itself and tested by PKUNZIP 0.92 and PKUNZIP 2.04g, run under a DOS emulator. Info-ZIP's unzip cannot serve: its Reduce support was withdrawn for copyright reasons and every build, however configured, contains a stub. Like every other codec stream here it honours the BCL contract and throws ArchiveException rather than returning a result.
Reduce is two algorithms chained. The first stage undoes a probabilistic coding: for every byte value there is a "follower set" of up to 32 bytes that tend to come after it, stored at the head of the entry, and each following byte is sent either as a short index into that set or as a literal. The second stage undoes a simple repeated-sequence coding, where the byte 144 (DLE) introduces a length and a distance back into what has already been produced. The four compression factors — the four method numbers — differ only in how many bits of the length byte are lent to the distance, which is why the window grows from 512 bytes at factor 1 to 4 KiB at factor 4.
Gated against the original producer and two original readers, there being no living ones: the fixtures were written by PKZIP 0.92 itself and tested by PKUNZIP 0.92 and PKUNZIP 2.04g, run under a DOS emulator. Info-ZIP's unzip cannot serve: its Reduce support was withdrawn for copyright reasons and every build, however configured, contains a stub.
Like every other codec stream here it honours the BCL contract and throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, declaredLength As Long, method As Integer, leaveOpen As Boolean) | Opens a Reduce stream of a known length.source — The compressed bytes.declaredLength — The entry's uncompressed size, which is the only thing that ends it.method — The ZIP method, 2 to 5, which is the compression factor plus one.leaveOpen — Whether to leave source open when this is disposed.Throws ArgumentNullException when source is Nothing; ArgumentOutOfRangeException when the method is not 2 to 5, or the length is negative. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Undoes the RISC-V branch filter: jump and address-formation targets back from absolute to relative.
RISC-V has no single call instruction with a wide reach. A short jump is JAL, with twenty bits of displacement; a long one is a pair — AUIPC forming the high twenty bits of an address in a register, then a second instruction adding the low twelve. The filter rewrites both so that the same target compresses the same way wherever it is called from, and undoing it means recognising both shapes and subtracting the position back out. That pairing is why this filter does not live with the fixed-width ones. Its step is not constant: a candidate that turns out to be nothing advances two bytes, a JAL four, a rejected pair four or six, and a converted pair eight. So it scans like the x86 filter rather than stepping, and like that one it carries its work across reads. Two details decide whether an implementation is right. What counts as a candidate. Both shapes are found by one test on the first sixteen bits, which catches the four encodings xx6f, xxef, xx17 and xx97 at once — and then the register fields decide which shape it really is, because AUIPC is only converted for particular registers and only when the instruction after it agrees. The tail. A pair spans eight bytes and the scan looks ahead beyond that, so the last several bytes of whatever has arrived cannot be decided until more does. They are held back rather than guessed at, and at the end of the stream they pass through unchanged — which is what the filter's own encoder leaves alone too, so the reserve is part of the format rather than an artefact of buffering. The conversion itself is in RiscVConverter, shared with the encoder — which for this filter is not a matter of one operator, because the filter moves the register as well as the address and the two directions' branches therefore cross. That is exactly why one class holds both rather than two streams each holding half of it. The algorithm is the LZMA SDK's Bra.c. That implementation offers several paths chosen by what the CPU can do — unaligned loads, sixteen-bit loads, byte swapping — and this is a port of the portable one. The paths are written to agree, and the places where they look as though they would not are noted where they arise. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a decoder over a filtered stream.source — The filtered bytes.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes given out so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads filtered bytes and gives back the originals. Returns. How many were given, or zero at the end. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Seek(offset As Long, origin As SeekOrigin) | Long | Converts in place and returns how many bytes are settled, leaving anything that needs bytes not yet seen. The scan tests sixteen bits at a time. (value XOR 16) + 1 having no bits of &H77 set is true for exactly the four encodings the filter cares about, which is a cheaper way of asking than four comparisons; the value that test produces is then reused as the instruction's fields rather than reloaded, which is why it is carried forward rather than recomputed. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Applies the RISC-V branch filter: jump and address-formation targets from relative to absolute.
The inverse of RiscVDecoderStream, sharing the conversion with it rather than restating it — see RiscVConverter, which holds both directions because for this filter they are not a single operator apart: it moves the register as well as the address, so the two directions' branches cross. What is here is the write-side buffering. The tail held back is larger than any other filter's and that is the format rather than this implementation: a long call is a pair of instructions spanning eight bytes and the scan looks ahead beyond them, so six bytes at the end of anything that has arrived cannot be decided until more does. At the end of the stream they go out unchanged, which is what the decoder leaves alone on the way back. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream, leaveOpen As Boolean) | Creates an encoder writing to a stream.destination — Where the filtered bytes go.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes taken so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Flushes the destination, but not the filter. The held tail cannot go out here: it may turn out to be the front of a pair, and writing it early would mean either converting half of one or leaving an unconverted gap in the middle. Only the end of the stream settles that. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Takes the original bytes and writes on the filtered ones.buffer — The bytes.offset — Where they start.count — How many.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Bastion.Archive.Codecs · Inherits Stream
Decodes ZIP method 1, "shrunk": PKWARE's LZW with an explicit code width and a partial table clear.
Two things make this different from every other LZW. The code width is explicit: code 256 is an escape, and the code after it says either "widen by one bit" or "clear". Nothing is implied by how full the table is, and there is no padding at a width change — the bits simply carry on. And the clear is partial: it frees only the codes that are not the prefix of another code still in use, and it does not disturb the running link between one code and the next, so the entry added after a clear takes its prefix from the code that came before it. No current tool writes this format, so the behaviour here was settled by measurement rather than by reading prose: fixtures built code by code and handed to Info-ZIP unzip 6.00 and 7-Zip 26.03, which agree on every construct a real encoder emits. Where they disagree — a stream that references a code the clear has just freed, a clear before anything has been coded, or a table that fills without a clear — the phase log records what each does and which reading this takes. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a decoder that reads until the source runs out.source — The shrunk bytes.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
New(source As Stream, declaredLength As Long, leaveOpen As Boolean) | Creates a decoder that stops at a known length.source — The shrunk bytes.declaredLength — Bytes the stream should produce, or -1 to read until the source runs out. The format has no end marker, so an entry's declared size is what really ends it; the padding bits that finish the last byte cannot form a code, since the narrowest is nine bits and at most seven are left over.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
CodeBits read-only | Integer | The code width in use, which the stream itself changes. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes produced so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes into buffer, returning 0 at the end of the stream.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; Bastion.Archive.ArchiveException when the stream is corrupt. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Reads the Snappy framing format (the .sz file format), producing the bytes that went into it.
A stream is a run of chunks, each one a type byte then three little-endian bytes of length. The first must be a stream identifier — type 0xFF carrying the six bytes sNaPpY — and after that the types that matter are 0x00 for a compressed chunk and 0x01 for a stored one, each beginning with a masked CRC-32C of what the chunk decodes to. Two rules about unknown chunk types are the whole reason the framing format exists, and a reader that treats them alike is wrong in both directions: types 0x02–0x7F are reserved unskippable and must be an error, while 0x80–0xFE are reserved skippable and must be stepped over without complaint. Refusing the second breaks files a future producer is entitled to write; accepting the first hands over data that is missing whatever the unknown chunk was supposed to contribute. The checksum is CRC-32C, and then masked — rotated right fifteen bits and offset by 0xA282EAD8 — because the format is embedded in systems that already CRC their own payloads and the mask stops the two agreeing by accident. It is verified before any byte of the chunk is handed out. A chunk decodes to at most 64 KiB, which the format guarantees, so this reader needs one buffer of that size and no more however long the stream is. Matches never reach outside the chunk they are in, which is what makes that possible and is the difference between this and the LZ4 frame.
Two rules about unknown chunk types are the whole reason the framing format exists, and a reader that treats them alike is wrong in both directions: types 0x02–0x7F are reserved unskippable and must be an error, while 0x80–0xFE are reserved skippable and must be stepped over without complaint. Refusing the second breaks files a future producer is entitled to write; accepting the first hands over data that is missing whatever the unknown chunk was supposed to contribute.
The checksum is CRC-32C, and then masked — rotated right fifteen bits and offset by 0xA282EAD8 — because the format is embedded in systems that already CRC their own payloads and the mask stops the two agreeing by accident. It is verified before any byte of the chunk is handed out.
A chunk decodes to at most 64 KiB, which the format guarantees, so this reader needs one buffer of that size and no more however long the stream is. Matches never reach outside the chunk they are in, which is what makes that possible and is the difference between this and the LZ4 frame.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Wraps source, positioned at the stream identifier.Throws ArgumentNullException when source is Nothing. |
| Member | Type | Summary |
|---|---|---|
ChunkLimit const | Integer | The largest a chunk may decode to, which the format fixes. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
HasMagic(head As Byte(), count As Integer) Shared | Boolean | Whether head begins with a stream identifier chunk.The chunk header is checked as well as the letters, not just the letters: the identifier's length field is fixed at six by the format, so a file carrying the right letters behind the wrong length is not this format. It settles for eight bytes rather than the whole ten, because eight is all the sniffing buffer in Archive holds — it is sized by the longest signature that needed it, and asking for more silently never matches. Four fixed header bytes and four of the six letters are decisive enough to choose a reader; the remaining two are verified by that reader before it decodes anything, so nothing is taken on trust.It settles for eight bytes rather than the whole ten, because eight is all the sniffing buffer in Archive holds — it is sized by the longest signature that needed it, and asking for more silently never matches. Four fixed header bytes and four of the six letters are decisive enough to choose a reader; the remaining two are verified by that reader before it decodes anything, so nothing is taken on trust. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when a chunk is malformed, cut short, reserved-unskippable, or fails its CRC. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
TotalLength(stream As Stream) Shared | Long | The length stream decodes to, by reading chunk headers and preambles only.A compressed chunk states its own decoded length in the varint that opens it, so the total is arithmetic over the chunk headers rather than a decode — the same bargain lzip's trailers and xz's index offer. Returns -1 when the stream is not walkable, and the caller then lists no size rather than a guess. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Microsoft's SZDD compression — the .??_ files that install media carried from the late 1980s until CAB replaced them, written by COMPRESS.EXE and expanded by EXPAND.EXE.
A fourteen-byte header — the eight-byte signature SZDD 88 F0 27 33, a mode byte that is always A, the character the producer removed from the file's name to make room for the underscore, and the uncompressed length — then plain LZSS. The LZSS is the textbook arrangement with one detail that has to be right or nothing decodes: the four-kilobyte window starts filled with spaces and the write position starts at 4096 - 16, not at zero. A match near the beginning of a file therefore legitimately refers to window bytes that were never written, and must read spaces rather than zeros or a refusal. Getting this wrong produces output that is right for most of a file and wrong at the start, which is the shape of bug that survives a careless test. Flag bits are consumed low bit first; a one means a literal byte follows, a zero means two bytes follow giving a window position and a length. The declared length is what stops the stream, because the format carries no end marker and a truncated file is otherwise indistinguishable from a complete one.
The LZSS is the textbook arrangement with one detail that has to be right or nothing decodes: the four-kilobyte window starts filled with spaces and the write position starts at 4096 - 16, not at zero. A match near the beginning of a file therefore legitimately refers to window bytes that were never written, and must read spaces rather than zeros or a refusal. Getting this wrong produces output that is right for most of a file and wrong at the start, which is the shape of bug that survives a careless test.
Flag bits are consumed low bit first; a one means a literal byte follows, a zero means two bytes follow giving a window position and a length. The declared length is what stops the stream, because the format carries no end marker and a truncated file is otherwise indistinguishable from a complete one.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Reads the header from source and prepares to decode the rest.source — The compressed stream, positioned at the signature.leaveOpen — Whether to leave source open when this is disposed.Throws ArgumentNullException when source is Nothing; Bastion.Archive.ArchiveException when the header is missing, short or not SZDD. |
| Member | Type | Summary |
|---|---|---|
HeaderBytes const | Integer | The whole header: signature, mode, missing character, and the length. |
Signature Shared read-only | Byte() | The eight-byte signature. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
DeclaredLength read-only | Long | The uncompressed length the header declares. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Overrides Stream.Position. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
HasSignature(head As Byte(), count As Integer) Shared | Boolean | Whether head begins with the SZDD signature. |
MissingCharacter(header As Byte()) Shared | Byte | The header's missing-name character, for reconstructing the original file name. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when the stream ends before the length its header declares. |
RestoredName(fileName As String, missing As Byte) Shared | String | The name the producer compressed, worked out from the archive's own name and the header. The convention is that the last character of the name is replaced by an underscore and kept in the header, so README.TXT becomes README.TX_ with T at offset 9. A producer that does not know the character writes zero, and then the underscore is simply dropped — which is what mscompress does and what 7-Zip shows for its output. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Decodes a Zstandard stream: the frames a .zst file holds and ZIP method 93 carries.
A frame is a header, a run of blocks and an optional checksum over everything the frame produced. Blocks are raw, one byte repeated, or compressed — and a compressed block is literals plus sequences, with the entropy tables and the recent match distances carried from one block to the next, which is what ZstdBlockDecoder holds. Several frames may sit one after another and are read as one stream, and a frame whose magic marks it skippable is stepped over by the length it declares — that is how metadata rides alongside compressed data without any decoder having to know what it means. Memory is the window the frame declares plus one block, capped by MaximumWindowBytes so a header asking for a gibibyte is refused rather than allocated. A frame that needs a dictionary is refused by name, because decoding without one gives plausible rubbish rather than an error. Written from RFC 8878. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Opens a Zstandard stream.source — The frames; read forward only.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
New(source As Stream, leaveOpen As Boolean, maximumWindowBytes As Integer) | Opens a Zstandard stream with an explicit cap on the window a frame may ask for.source — The frames; read forward only.leaveOpen — False to dispose source with this stream.maximumWindowBytes — The largest window to allocate for a frame that asks.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; ArgumentOutOfRangeException when maximumWindowBytes is below one. |
| Member | Type | Summary |
|---|---|---|
DefaultMaximumWindowBytes const | Integer | The largest window accepted unless the caller says otherwise: 256 MiB. |
| Member | Type | Summary |
|---|---|---|
BlockCount read-only | Integer | Blocks read to the end so far, across every frame. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
FrameCount read-only | Integer | Frames read to the end so far, which is more than one for concatenated input. |
Length read-only | Long | Overrides Stream.Length. |
MaximumWindowBytes read-only | Integer | The cap a frame's declared window size is checked against. |
Position | Long | Bytes produced so far. |
WindowBytes read-only | Integer | The window the frame being read asked for, or zero before the first one. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes into buffer, returning 0 at the end of the last frame.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; Bastion.Archive.ArchiveException when the stream is corrupt, ends early, or fails its checksum. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Codecs · Inherits Stream
Compresses a stream into the Zstandard format (RFC 8878).
Written from RFC 8878 and against this library's own decoder, which is the same specification already read once and already gated against the reference tool. Nothing was ported: the pieces an encoder needs that are hard to get right — the predefined entropy distributions, the length and offset code tables, the direction of the bitstream — were all already here and already proven, and the encoder is those run backwards. The buffer is two windows long and slides by exactly one, which is what lets the match finder keep its chain the size of a window: every position drops by the window size, so the index a position hashes to is unchanged and only the stored values move. Memory is therefore a function of the level alone and never of the input, and what it comes to is reported. Each block is offered three shapes and takes the smallest: compressed, a single repeated byte, or the bytes as they came. That last one is what keeps the format's promise that compressing cannot make data meaningfully larger — an incompressible block costs three bytes of header and nothing else.
The buffer is two windows long and slides by exactly one, which is what lets the match finder keep its chain the size of a window: every position drops by the window size, so the index a position hashes to is unchanged and only the stored values move. Memory is therefore a function of the level alone and never of the input, and what it comes to is reported.
Each block is offered three shapes and takes the smallest: compressed, a single repeated byte, or the bytes as they came. That last one is what keeps the format's promise that compressing cannot make data meaningfully larger — an incompressible block costs three bytes of header and nothing else.
| Constructor | Summary |
|---|---|
New(destination As Stream, leaveOpen As Boolean) | Creates an encoder at the default level.destination — Where the compressed bytes go.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, writeChecksum As Boolean, leaveOpen As Boolean) | Creates an encoder.destination — Where the compressed bytes go.level — Compression level 1 to 9.writeChecksum — True to end the frame with the XXH64 of its content.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 1 to 9. |
| Member | Type | Summary |
|---|---|---|
BlockCount read-only | Integer | How many blocks have been written. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length.Throws Bastion.Archive.ArchiveException when always: the length of a stream being written is not known. |
Level read-only | Integer | The level in use. |
Position | Long | Overrides Stream.Position.Throws Bastion.Archive.ArchiveException when on setting: the stream cannot seek. |
WindowBytes read-only | Integer | The window this encoder searches, which is also what the frame header declares. |
WorkingMemoryBytes read-only | Long | Working memory this encoder holds, which the level alone decides. |
| Member | Returns | Summary |
|---|---|---|
Complete() | — | Finishes the frame: the last block, and the checksum if the header promised one. A frame must hold at least one block, so content of no bytes at all still writes one — an empty raw block marked last, which is what an empty file looks like in this format. |
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when always: this stream cannot be read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek.Throws Bastion.Archive.ArchiveException when always: the stream cannot seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength.Throws Bastion.Archive.ArchiveException when always: the length is decided by what is written. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write.Throws Bastion.Archive.ArchiveException when the stream is finished. |
OperationMonitor and the event arguments for progress, entries, prompts, logging and errors.
| Type | Summary |
|---|---|
ArchiveClosedEventArgs Class | Raised once an archive has closed, carrying the outcome. Closing a newly written archive is where the last bytes reach the disk, so it is the one close whose result is worth looking at. |
ArchiveClosingEventArgs Class | Raised before an archive closes, while it can still be read. A handler that is part-way through something can ask for the close to be abandoned by setting Cancel, which a caller that called Close explicitly will see in the result. |
ArchiveOpenedEventArgs Class | Raised once an archive's directory has been read, which is the first moment anything is known about it. A user interface uses this to size its progress bars before any data moves. |
EntryEventArgs Class | Raised when work on one archive entry starts or finishes. |
ErrorDecision Enum | What the caller wants the operation to do after an ErrorOccurred event. |
ExistsDecision Enum | What to do about an item whose destination already exists, answered through ItemExists. |
InvalidPasswordEventArgs Class | Raised by InvalidPassword when an entry, or a 7z archive's encrypted header, needs a password it was not given or refuses the one it was — the moment to ask the user. |
ItemExistsEventArgs Class | Raised by ItemExists when an item is about to be written where something already is and Ask is in force. |
LogLevel Enum | Severity of a message raised through LogMessage. |
LogMessageEventArgs Class | A diagnostic message from the library, for the caller's own log window or log file. |
OperationCompletedEventArgs Class | Raised when an operation finishes, whether it succeeded or not, carrying the same result the method returned. The result holds the start and end times, so a caller that wants to display how long something took does not have to time it itself. |
OperationErrorEventArgs Class | Raised when an operation hits an error it can report before deciding what to do. The handler sets Decision; the operation result still carries the error or a warning, so the event is a chance to influence the outcome, never a substitute for checking the result. |
OperationMonitor Class | Optional companion to every long-running operation: cancellation, progress, per-entry events, a log feed and the error event. Create one per operation, subscribe, and pass it as the operation's last argument. |
PreviewItemEventArgs Class | Raised by PreviewItem for each item just before it is extracted, added or copied, so a caller can leave it out. |
ProgressEventArgs Class | A progress snapshot for the whole operation and for the entry currently being processed. Totals are zero when the archive does not declare them (a non-seekable stream, for example), and the matching percentage is then zero. |
ScanningFolderEventArgs Class | Raised by ScanningFolder as a folder is walked before its content is added or copied, so a caller can show that something is happening before there is a total to show progress against. |
VolumeNeededEventArgs Class | Raised by VolumeNeeded when a split or spanned archive needs a volume that is not where it was expected, or is about to start writing the next one. |
VolumeNeededReason Enum | Why VolumeNeeded was raised. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised once an archive has closed, carrying the outcome. Closing a newly written archive is where the last bytes reach the disk, so it is the one close whose result is worth looking at.
| Constructor | Summary |
|---|---|
New(result As OperationResult) | Creates the arguments.result — The outcome of closing.Throws ArgumentNullException when result is Nothing. |
| Member | Type | Summary |
|---|---|---|
Result read-only | OperationResult | The outcome of closing, including how long the archive was open. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised before an archive closes, while it can still be read. A handler that is part-way through something can ask for the close to be abandoned by setting Cancel, which a caller that called Close explicitly will see in the result.
Cancelling has no effect when the close came from Dispose, because a Using block has already decided. The property is honoured only on the explicit path, and the documentation on Archive.Close says so.
| Constructor | Summary |
|---|---|
New(isDisposing As Boolean) | Creates the arguments.isDisposing — True when the close came from Dispose rather than Close. |
| Member | Type | Summary |
|---|---|---|
Cancel | Boolean | Set to True to abandon an explicit close. Ignored while IsDisposing. |
IsDisposing read-only | Boolean | True when the close came from Dispose, in which case Cancel is ignored. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised once an archive's directory has been read, which is the first moment anything is known about it. A user interface uses this to size its progress bars before any data moves.
| Constructor | Summary |
|---|---|
New(format As ArchiveFormat, entryCount As Integer, totalUncompressedSize As Long, volumeCount As Integer, isEncrypted As Boolean, comment As String) | Creates the arguments.format — The format detected.entryCount — Entries in the directory, directories included.totalUncompressedSize — What the entries say they will expand to, in bytes.volumeCount — Volumes in the set; 1 for a single-file archive.isEncrypted — True when at least one entry is encrypted.comment — The archive comment, or an empty string. |
| Member | Type | Summary |
|---|---|---|
Comment read-only | String | The archive comment, or an empty string. |
EntryCount read-only | Integer | Entries in the directory, directories included. |
Format read-only | ArchiveFormat | The format detected. |
IsEncrypted read-only | Boolean | True when at least one entry is encrypted, so a caller knows to ask for a password. |
TotalUncompressedSize read-only | Long | What the entries say they will expand to. A declaration and not a fact: a hostile archive can claim anything, which is why the limits exist. |
VolumeCount read-only | Integer | Volumes in the set; 1 for a single-file archive. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised when work on one archive entry starts or finishes.
| Constructor | Summary |
|---|---|
New(entryPath As String, entryIndex As Long, size As Long, outcome As ErrorCode) | Creates the arguments.entryPath — The entry's path inside the archive.entryIndex — Zero-based position of the entry in the operation.size — Uncompressed size of the entry, or 0 if unknown.outcome — For EntryCompleted, how the entry ended; None when starting.Throws ArgumentNullException when entryPath is Nothing; ArgumentOutOfRangeException when entryIndex or size is negative. |
| Member | Type | Summary |
|---|---|---|
EntryIndex read-only | Long | Zero-based position of the entry in the operation. |
EntryPath read-only | String | The entry's path inside the archive. |
Outcome read-only | ErrorCode | How the entry ended; None for success or when the entry is only starting. |
Size read-only | Long | Uncompressed size of the entry, or 0 if unknown. |
Bastion.Archive.Diagnostics
What the caller wants the operation to do after an ErrorOccurred event.
| Name | Value | Summary |
|---|---|---|
Abort | 0 | Stop the operation; the result reports the error. This is the default. |
Skip | 1 | Skip the affected entry and carry on; the result records an EntrySkipped warning. |
Proceed | 2 | Carry on with the affected entry where the library can (for example, after a checksum mismatch during a test). |
Retry | 3 | Try the affected entry again, where CanRetry says that can help — a file another program held, a volume put back. Repeated failures stop after twenty attempts. |
Bastion.Archive.Diagnostics
What to do about an item whose destination already exists, answered through ItemExists.
| Name | Value | Summary |
|---|---|---|
Fail | 0 | Stop with OutputExists, leaving the existing item alone. This is the default. |
Skip | 1 | Leave the existing item and go on; the result records a EntrySkipped warning. |
Overwrite | 2 | Replace the existing item, through a temporary file swapped into place. |
Rename | 3 | Write the item beside the existing one, under NewName. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised by InvalidPassword when an entry, or a 7z archive's encrypted header, needs a password it was not given or refuses the one it was — the moment to ask the user.
| Constructor | Summary |
|---|---|
New(entryPath As String, passwordWasGiven As Boolean, attempt As Integer) | Creates the arguments.entryPath — The entry concerned, or Nothing when it is the archive's header.passwordWasGiven — Whether a password was tried and refused, rather than missing.attempt — Which attempt this is, counting from 1.Throws ArgumentOutOfRangeException when attempt is below 1. |
| Member | Type | Summary |
|---|---|---|
Attempt read-only | Integer | Which attempt this is. After twenty the operation stops asking and fails. |
EntryPath read-only | String | The entry concerned, or Nothing for the archive's encrypted header. |
NewPassword | String | The password to try next, or Nothing to give up, which fails or skips the entry as the error event decides. |
PasswordWasGiven read-only | Boolean | True when a password was tried and refused; False when none was given. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised by ItemExists when an item is about to be written where something already is and Ask is in force.
| Constructor | Summary |
|---|---|
New(itemPath As String, targetPath As String) | Creates the arguments.itemPath — The item being written, as the archive or source names it.targetPath — Where it would go, which already exists.Throws ArgumentNullException when an argument is Nothing. |
| Member | Type | Summary |
|---|---|---|
Decision | ExistsDecision | What to do. Defaults to Fail. |
ItemPath read-only | String | The item being written. |
NewName | String | For Rename: the file name to use instead, in the same folder. A plain name with no folder in it; anything else — a separator, "..", a drive — is refused as an unsafe path. |
TargetPath read-only | String | The destination that already exists. |
Bastion.Archive.Diagnostics
Severity of a message raised through LogMessage.
| Name | Value | Summary |
|---|---|---|
Trace | 0 | Finest detail: per-block and per-header events. Very high volume. |
Debug | 1 | Developer detail: per-entry decisions, method selection. |
Information | 2 | Significant lifecycle events: archive opened, entry written, operation completed. |
Warning | 3 | A recoverable anomaly; also surfaced in Warnings. |
Failure | 4 | A failed operation or entry; also surfaced in the OperationResult. |
Bastion.Archive.Diagnostics · Inherits EventArgs
A diagnostic message from the library, for the caller's own log window or log file.
Messages never contain passwords, keys or entry content; they may contain entry paths.
| Constructor | Summary |
|---|---|
New(timestampUtc As Date, level As LogLevel, message As String) | Creates the arguments.timestampUtc — When the message was produced, in UTC.level — Severity.message — The message text.Throws ArgumentNullException when message is Nothing. |
| Member | Type | Summary |
|---|---|---|
Level read-only | LogLevel | Severity. |
Message read-only | String | The message text. |
TimestampUtc read-only | Date | When the message was produced, in UTC. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised when an operation finishes, whether it succeeded or not, carrying the same result the method returned. The result holds the start and end times, so a caller that wants to display how long something took does not have to time it itself.
| Constructor | Summary |
|---|---|
New(operation As String, result As OperationResult) | Creates the arguments.operation — What finished, for a log line: "Extract", "Test", "Add".result — The outcome.Throws ArgumentNullException when an argument is Nothing. |
| Member | Type | Summary |
|---|---|---|
Duration read-only | TimeSpan | How long the operation took. |
Operation read-only | String | What finished, for a log line. |
Result read-only | OperationResult | The outcome, including the start and end times and any warnings. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised when an operation hits an error it can report before deciding what to do. The handler sets Decision; the operation result still carries the error or a warning, so the event is a chance to influence the outcome, never a substitute for checking the result.
| Constructor | Summary |
|---|---|
New(errorCode As ErrorCode, description As String, entryPath As String, cause As Exception, canSkip As Boolean, canProceed As Boolean) | Creates the arguments.errorCode — The classification.description — Human-readable description ending in a full stop.entryPath — The entry concerned, or Nothing for the archive as a whole.cause — The underlying exception, or Nothing.canSkip — Whether Skip is a valid decision here.canProceed — Whether Proceed is a valid decision here.Throws ArgumentNullException when description is Nothing; ArgumentException when errorCode is None. |
New(errorCode As ErrorCode, description As String, entryPath As String, cause As Exception, canSkip As Boolean, canProceed As Boolean, canRetry As Boolean, attempt As Integer) | Creates the arguments for an error that may also be retried.errorCode — What went wrong; not None.description — What to tell the user.entryPath — The entry or item concerned, or Nothing.cause — The exception behind it, or Nothing.canSkip — Whether Skip is allowed.canProceed — Whether Proceed is allowed.canRetry — Whether Retry is allowed.attempt — Which attempt failed, counting from 1.Throws ArgumentNullException when description is Nothing; ArgumentException when errorCode is None; ArgumentOutOfRangeException when attempt is below 1. |
| Member | Type | Summary |
|---|---|---|
Attempt read-only | Integer | Which attempt at the item failed, counting from 1. |
CanProceed read-only | Boolean | Whether Proceed may be chosen. |
CanRetry read-only | Boolean | Whether Retry is allowed: the failure is one that trying again can cure. |
CanSkip read-only | Boolean | Whether Skip may be chosen. |
Cause read-only | Exception | The underlying exception, or Nothing. |
Decision | ErrorDecision | The caller's decision. Defaults to Abort.Throws InvalidOperationException when the decision is not permitted here (see CanSkip and CanProceed). |
Description read-only | String | Human-readable description. |
EntryPath read-only | String | The entry concerned, or Nothing for the archive as a whole. |
ErrorCode read-only | ErrorCode | The classification. |
Bastion.Archive.Diagnostics
Optional companion to every long-running operation: cancellation, progress, per-entry events, a log feed and the error event. Create one per operation, subscribe, and pass it as the operation's last argument.
Events are raised on the SynchronizationContext captured when the monitor was created (the UI thread in a WinForms or WPF application), synchronously, so a handler's Decision reaches the operation. With no context the events fire on the worker thread. Progress events are throttled to one per ProgressInterval, except the final one.
| Constructor | Summary |
|---|---|
New() | Creates a monitor that raises events on the current thread's SynchronizationContext, if any. |
New(synchronizationContext As SynchronizationContext) | Creates a monitor that raises events on the given context.synchronizationContext — The context to marshal events to, or Nothing to raise them on the worker thread. |
| Member | Type | Summary |
|---|---|---|
CancellationToken | CancellationToken | Token the operation polls; cancelling it ends the operation with Cancelled. |
IsCancellationRequested read-only | Boolean | True once CancellationToken has been cancelled, or the token passed to the Async method now running. |
MinimumLogLevel | LogLevel | Messages below this level are not raised. Default Information. |
ProgressInterval | TimeSpan | Minimum time between two Progress events. Default 100 ms. Zero disables throttling.Throws ArgumentOutOfRangeException when the value is negative. |
SynchronizationContext read-only | SynchronizationContext | The context events are marshalled to, or Nothing for the worker thread. |
| Member | Returns | Summary |
|---|---|---|
CreateSupportReport() | String | A structural description of this monitor and the runtime for a support request: library version, target framework, operating system, processor count, event counts. Never entry names, passwords or content. |
Log(level As LogLevel, message As String) | — | Raises LogMessage if level is at or above MinimumLogLevel. Public so that an application can put its own lines into the same stream as the library's, which is what makes a single log window show the whole story.level — Severity.message — The text. Never a credential, a token or a payload.Throws ArgumentNullException when message is Nothing. |
| Event | Handler | Summary |
|---|---|---|
EntryCompleted | EventHandler(Of EntryEventArgs) | Raised when work on an entry ends, with its outcome. |
EntryStarted | EventHandler(Of EntryEventArgs) | Raised when work on an entry begins. |
ErrorOccurred | EventHandler(Of OperationErrorEventArgs) | Raised when an error occurs that the caller may skip or proceed past. With no handler the operation aborts. |
InvalidPassword | EventHandler(Of InvalidPasswordEventArgs) | Raised when a password is missing or refused; set NewPassword to try another. |
ItemExists | EventHandler(Of ItemExistsEventArgs) | Raised when an item would be written where something already is and Ask is in force; set Decision. |
LogMessage | EventHandler(Of LogMessageEventArgs) | Raised for each diagnostic message at or above MinimumLogLevel. |
PreviewItem | EventHandler(Of PreviewItemEventArgs) | Raised for each item just before it is extracted, added or copied; clear Include to leave it out. |
Progress | EventHandler(Of ProgressEventArgs) | Raised at most once per ProgressInterval with overall and current-entry progress. |
ScanningFolder | EventHandler(Of ScanningFolderEventArgs) | Raised for each folder as a tree is walked before being added or copied. |
VolumeNeeded | EventHandler(Of VolumeNeededEventArgs) | Raised when a split or spanned archive needs a volume: one that is missing when reading, one on removable media that has to be inserted again, or the next one about to be written. Set Path or Cancel. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised by PreviewItem for each item just before it is extracted, added or copied, so a caller can leave it out.
| Constructor | Summary |
|---|---|
New(itemPath As String, size As Long, isFolder As Boolean) | Creates the arguments.itemPath — The item, as the archive or source names it.size — Its size in bytes, or -1 when unknown.isFolder — Whether it is a folder.Throws ArgumentNullException when itemPath is Nothing. |
| Member | Type | Summary |
|---|---|---|
Include | Boolean | Whether to process the item. Defaults to True; set False to leave it out, which the result records as a EntrySkipped warning. |
IsFolder read-only | Boolean | Whether it is a folder. |
ItemPath read-only | String | The item. |
Size read-only | Long | Its size in bytes, or -1 when unknown. |
Bastion.Archive.Diagnostics · Inherits EventArgs
A progress snapshot for the whole operation and for the entry currently being processed. Totals are zero when the archive does not declare them (a non-seekable stream, for example), and the matching percentage is then zero.
| Constructor | Summary |
|---|---|
New(totalBytes As Long, processedBytes As Long, totalItems As Long, processedItems As Long, currentEntryPath As String, currentEntryTotalBytes As Long, currentEntryProcessedBytes As Long) | Creates a snapshot.totalBytes — Bytes the whole operation will process, or 0 if unknown.processedBytes — Bytes processed so far.totalItems — Entries the whole operation will process, or 0 if unknown.processedItems — Entries completed so far.currentEntryPath — Path of the entry in progress, or Nothing between entries.currentEntryTotalBytes — Size of the entry in progress, or 0 if unknown.currentEntryProcessedBytes — Bytes of the entry in progress processed so far.Throws ArgumentOutOfRangeException when any count is negative. |
| Member | Type | Summary |
|---|---|---|
CurrentEntryPath read-only | String | Path of the entry in progress, or Nothing between entries. |
CurrentEntryPercent read-only | Double | Completion of the current entry from 0 to 100; 0 when its size is unknown. |
CurrentEntryProcessedBytes read-only | Long | Bytes of the entry in progress processed so far. |
CurrentEntryTotalBytes read-only | Long | Size of the entry in progress, or 0 if unknown. |
ProcessedBytes read-only | Long | Bytes processed so far. |
ProcessedItems read-only | Long | Entries completed so far. |
TotalBytes read-only | Long | Bytes the whole operation will process, or 0 if unknown. |
TotalItems read-only | Long | Entries the whole operation will process, or 0 if unknown. |
TotalPercent read-only | Double | Overall completion from 0 to 100, by bytes; 0 when TotalBytes is unknown. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised by ScanningFolder as a folder is walked before its content is added or copied, so a caller can show that something is happening before there is a total to show progress against.
| Constructor | Summary |
|---|---|
New(folderPath As String, itemsFound As Long) | Creates the arguments.folderPath — The folder being read.itemsFound — How many items the walk has found so far, this folder's included.Throws ArgumentNullException when folderPath is Nothing; ArgumentOutOfRangeException when itemsFound is negative. |
| Member | Type | Summary |
|---|---|---|
FolderPath read-only | String | The folder being read. |
ItemsFound read-only | Long | How many items the walk has found so far. |
Bastion.Archive.Diagnostics · Inherits EventArgs
Raised by VolumeNeeded when a split or spanned archive needs a volume that is not where it was expected, or is about to start writing the next one.
This is how removable-media spanning works: the handler prompts the user to insert the disk asked for and answers once it is in, and every volume can live on a different disk under the same path. It is also how a set whose volumes were renamed or scattered is opened, since the handler can say where each one is.
| Constructor | Summary |
|---|---|
New(reason As VolumeNeededReason, volumeNumber As Integer, volumeCount As Integer, expectedPath As String, attempt As Integer) | Creates the arguments.reason — Why the volume is needed.volumeNumber — The volume, counting from 1; 0 when it is the last volume and its number is not yet known.volumeCount — How many volumes the set has, or 0 while that is not known.expectedPath — Where the set's naming puts the volume.attempt — How many times this volume has been asked for, counting from 1.Throws ArgumentNullException when expectedPath is Nothing. |
| Member | Type | Summary |
|---|---|---|
Attempt read-only | Integer | How many times this volume has been asked for, counting from 1. The operation gives up after 20. |
Cancel | Boolean | Set to stop the operation, which then ends with Cancelled. |
ExpectedPath read-only | String | Where the set's naming puts the volume. |
Path | String | Where the volume is, or where it should be written. Starts as ExpectedPath; leaving it there after the user has inserted a disk is the normal answer for removable media. |
Reason read-only | VolumeNeededReason | Why the volume is needed. |
VolumeCount read-only | Integer | How many volumes the set has, or 0 while that is not known. |
VolumeNumber read-only | Integer | The volume, counting from 1; 0 when it is the last volume and its number is not yet known. |
Bastion.Archive.Diagnostics
Why VolumeNeeded was raised.
| Name | Value | Summary |
|---|---|---|
Missing | 0 | Reading: the volume is not where the set's naming says it should be. Set Path to where it is, or Cancel. Unanswered, the operation stops with VolumeMissing. |
InsertVolume | 1 | Reading: the volume is on removable media that has since been changed — the same path serves more than one volume — so it has to be put back. Answer once it is in place. Unanswered, the path is read as it stands, and a volume of the wrong length is refused with VolumeMissing. |
NextVolume | 2 | Writing: the previous volume is full and the next is about to start at ExpectedPath. Change Path to put it somewhere else — another folder, or a disk the user has just inserted. Unanswered, it goes where expected. |
NotEnoughSpace | 3 | Writing a set that fills each medium: the place offered for the next volume has less room than the smallest volume allowed. Offer another. Unanswered, the operation stops with DiskFull. |
A file-system abstraction for copying between disk, memory and archives with one set of calls.
| Type | Summary |
|---|---|
AbstractFile Class | A file in any store: its size, its bytes, and copying or moving it to any folder. |
AbstractFolder Class | A folder in any store. Copying and moving work between any two folders — disk to archive, archive to archive, memory to disk — which is how zipping and unzipping are done in this model. |
ArchivedFile Class | A file inside an archive. |
ArchivedFolder Class | A folder inside an archive, recorded by the archive or implied by the paths of the files in it. |
ArchiveFolder Class | An archive seen as a folder: its entries are files and folders that can be listed, read, and copied to or from any other folder. |
ArchiveFolderOptions Class | How an ArchiveFolder reads its archive, and how it writes it again when it changes. |
DiskFile Class | A file on disk. |
DiskFolder Class | A folder on disk. |
FileSystemCopyOptions Class | How a copy or move between folders behaves. |
FileSystemItem Class | A file or a folder, wherever it lives: on disk, in memory, or inside an archive. |
FileSystemListResult Class | The outcome of listing a folder, carrying the items when it succeeded. |
FileSystemStreamResult Class | The outcome of opening a file, carrying the stream when it succeeded. |
MemoryFile Class | A file held in memory, inside a MemoryFolder's tree. |
MemoryFolder Class | A folder held in memory: a place to build an archive's content, or to unpack one, without touching disk. |
Bastion.Archive.FileSystem · Inherits FileSystemItem
A file in any store: its size, its bytes, and copying or moving it to any folder.
| Member | Type | Summary |
|---|---|---|
Size read-only | Long | The file's length in bytes, or -1 when it does not exist or cannot be read. |
| Member | Returns | Summary |
|---|---|---|
CopyTo(destination As AbstractFolder, options As FileSystemCopyOptions) | OperationResult | Copies this file into destination under its own name.destination — The folder to copy into; created if it does not exist.options — How to behave, or Nothing for the defaults.Throws ArgumentNullException when destination is Nothing. |
CopyToAsync(destination As AbstractFolder, Optional options As FileSystemCopyOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Copies this file on the thread pool; the twin of CopyTo.destination — The folder to copy into.options — Recursion, overwrite, filter and monitor, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when destination is Nothing. |
MoveTo(destination As AbstractFolder, options As FileSystemCopyOptions) | OperationResult | Moves this file into destination: a copy, then the original removed once the copy is known to be whole.destination — The folder to move into; created if it does not exist.options — How to behave, or Nothing for the defaults.Throws ArgumentNullException when destination is Nothing. |
MoveToAsync(destination As AbstractFolder, Optional options As FileSystemCopyOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Moves this file on the thread pool; the twin of MoveTo.destination — The folder to move into.options — Recursion, overwrite, filter and monitor, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when destination is Nothing. |
OpenRead() abstract | FileSystemStreamResult | Opens the file's bytes for reading. The caller owns the stream. |
OpenWrite(overwrite As Boolean) abstract | FileSystemStreamResult | Opens the file for writing its whole content. Nothing is visible under the file's name until the stream is closed, so an interrupted write never leaves half a file where a whole one was.overwrite — Whether an existing file may be replaced; if not, an existing file fails the open. |
Bastion.Archive.FileSystem · Inherits FileSystemItem
A folder in any store. Copying and moving work between any two folders — disk to archive, archive to archive, memory to disk — which is how zipping and unzipping are done in this model.
| Member | Returns | Summary |
|---|---|---|
CopyFilesTo(destination As AbstractFolder, options As FileSystemCopyOptions) | OperationResult | Copies what is in this folder, but not the folder itself, into destination.destination — The folder to copy into.options — How to behave, or Nothing for the defaults.Throws ArgumentNullException when destination is Nothing. |
CopyFilesToAsync(destination As AbstractFolder, Optional options As FileSystemCopyOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Copies this folder's content on the thread pool; the twin of CopyFilesTo.destination — The folder to copy into.options — Recursion, overwrite, filter and monitor, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when destination is Nothing. |
CopyTo(destination As AbstractFolder, options As FileSystemCopyOptions) | OperationResult | Copies this folder, itself and what is in it, into destination.destination — The folder to copy into; this one arrives as a subfolder of the same name.options — How to behave, or Nothing for the defaults.Throws ArgumentNullException when destination is Nothing. |
CopyToAsync(destination As AbstractFolder, Optional options As FileSystemCopyOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Copies this folder on the thread pool; the twin of CopyTo.destination — The folder to copy into.options — Recursion, overwrite, filter and monitor, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when destination is Nothing. |
Create() abstract | OperationResult | Creates the folder and any missing parents; succeeds if it already exists. |
GetFile(relativePath As String) abstract | AbstractFile | A file inside this folder, by a relative path with forward or back slashes. No I/O.relativePath — The path, relative to this folder. |
GetFolder(relativePath As String) abstract | AbstractFolder | A folder inside this one, by a relative path with forward or back slashes. No I/O.relativePath — The path, relative to this folder. |
GetItems() abstract | FileSystemListResult | The files and folders directly inside this one. |
GetItems(recursive As Boolean) | FileSystemListResult | Every file under this folder, and each subfolder too when recursive is set, in a stable order: folders before the files inside them, names in ordinal order.recursive — Whether to descend into subfolders. |
MoveTo(destination As AbstractFolder, options As FileSystemCopyOptions) | OperationResult | Moves this folder into destination: a copy, then the original removed once every item arrived. Anything skipped leaves the original where it is.destination — The folder to move into.options — How to behave, or Nothing for the defaults.Throws ArgumentNullException when destination is Nothing. |
MoveToAsync(destination As AbstractFolder, Optional options As FileSystemCopyOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Moves this folder on the thread pool; the twin of MoveTo.destination — The folder to move into.options — Recursion, overwrite, filter and monitor, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when destination is Nothing. |
Bastion.Archive.FileSystem · Inherits AbstractFile
A file inside an archive.
Reading streams the entry out, its checksum verified as it goes, with the archive held open only until the stream is closed. Writing collects the new content in a temporary file and puts it into the archive when the stream is closed, which rebuilds the archive — or, inside BeginUpdate, when the update ends.
| Member | Type | Summary |
|---|---|---|
Exists read-only | Boolean | True when the item exists now. A store that cannot be read answers False. |
FullName read-only | String | The archive's full name, then this file's path inside it. |
LastWriteUtc read-only | Date | When the item was last written, in UTC, or MinValue when unknown. |
Name read-only | String | The item's own name, without any folder. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
PathInArchive read-only | String | The file's path inside the archive, with forward slashes. |
Root read-only | ArchiveFolder | The archive this file is in. |
Size read-only | Long | The file's length in bytes, or -1 when it does not exist or cannot be read. |
| Member | Returns | Summary |
|---|---|---|
Delete() | OperationResult | Removes the file from the archive, rebuilding it. |
OpenRead() | FileSystemStreamResult | Streams the entry out; the archive stays open until the stream is closed. |
OpenWrite(overwrite As Boolean) | FileSystemStreamResult | Opens the file for writing. Its content goes into the archive when the stream is closed, rebuilding it; inside BeginUpdate it waits for EndUpdate. |
Bastion.Archive.FileSystem · Inherits AbstractFolder
A folder inside an archive, recorded by the archive or implied by the paths of the files in it.
| Member | Type | Summary |
|---|---|---|
Exists read-only | Boolean | True when the item exists now. A store that cannot be read answers False. |
FullName read-only | String | The archive's full name, then this folder's path inside it. |
LastWriteUtc read-only | Date | The time the archive records for the folder, or MinValue when it records none. |
Name read-only | String | The item's own name, without any folder. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
PathInArchive read-only | String | The folder's path inside the archive, with forward slashes. |
Root read-only | ArchiveFolder | The archive this folder is in. |
| Member | Returns | Summary |
|---|---|---|
Create() | OperationResult | Creates the folder and any missing parents; succeeds if it already exists. |
Delete() | OperationResult | Removes the item; a folder goes with everything in it. Deleting what does not exist succeeds. |
GetFile(relativePath As String) | AbstractFile | A file inside this folder, by a relative path with forward or back slashes. No I/O. Throws ArgumentException when the path contains "." or ".." segments. |
GetFolder(relativePath As String) | AbstractFolder | A folder inside this one, by a relative path with forward or back slashes. No I/O. Throws ArgumentException when the path contains "." or ".." segments. |
GetItems() | FileSystemListResult | Every file under this folder, and each subfolder too when recursive is set, in a stable order: folders before the files inside them, names in ordinal order. |
Bastion.Archive.FileSystem · Inherits AbstractFolder
An archive seen as a folder: its entries are files and folders that can be listed, read, and copied to or from any other folder.
The archive can be any file — on disk, in memory, or inside another archive, which is how nested archives are opened; MaxNestingDepth bounds how deep that goes. ZIP and 7z are read and written; other formats are refused by name.
Every read canonicalises the entry names as extraction does, so a name that would climb out of the folder it is copied to is refused before anything is copied, and the archive is opened for each operation and closed after it. Every change rebuilds the archive into a temporary file and swaps it into place only when whole; bracket many changes with BeginUpdate and EndUpdate to rebuild once.
| Constructor | Summary |
|---|---|
New(archiveFile As AbstractFile) | The archive in archiveFile, with the secure defaults and no password.archiveFile — The archive; it need not exist until something is copied into it.Throws ArgumentNullException when archiveFile is Nothing. |
New(archiveFile As AbstractFile, options As ArchiveFolderOptions) | The archive in archiveFile, read and written as options say.archiveFile — The archive; it need not exist until something is copied into it.options — Password, limits and compression settings, or Nothing for the defaults.Throws ArgumentNullException when archiveFile is Nothing. |
| Member | Type | Summary |
|---|---|---|
ArchiveFile read-only | AbstractFile | The file holding the archive. |
Exists read-only | Boolean | True when the archive file exists. |
FullName read-only | String | The archive file's full name; entries inside it are named beneath it with forward slashes. |
IsUpdating read-only | Boolean | True between BeginUpdate and the matching EndUpdate. |
LastWriteUtc read-only | Date | When the item was last written, in UTC, or MinValue when unknown. |
Name read-only | String | The archive file's name. |
Options read-only | ArchiveFolderOptions | How the archive is read and written. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
| Member | Returns | Summary |
|---|---|---|
BeginUpdate() | — | Starts collecting changes instead of applying each one: until the matching EndUpdate, copies into this archive, writes, creations and deletions are recorded and the archive is rebuilt once at the end. Calls nest; only the outermost EndUpdate applies. Moves into an archive being updated are refused, because their sources could only be removed once the update is applied. |
Create() | OperationResult | Creates an empty archive if there is none; succeeds if there already is one. |
Delete() | OperationResult | Deletes the archive file itself. |
EndUpdate() | OperationResult | Ends an update started by BeginUpdate, rebuilding the archive with everything collected. |
EndUpdate(monitor As OperationMonitor) | OperationResult | Ends an update started by BeginUpdate, rebuilding the archive with everything collected.monitor — Where the rebuild reports progress, or Nothing. |
EndUpdateAsync(Optional monitor As OperationMonitor = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Applies the gathered changes on the thread pool; the twin of EndUpdate.monitor — Where progress goes, or Nothing.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled. |
GetFile(relativePath As String) | AbstractFile | A file inside this folder, by a relative path with forward or back slashes. No I/O. Throws ArgumentException when the path contains "." or ".." segments. |
GetFolder(relativePath As String) | AbstractFolder | A folder inside this one, by a relative path with forward or back slashes. No I/O. Throws ArgumentException when the path contains "." or ".." segments. |
GetItems() | FileSystemListResult | Every file under this folder, and each subfolder too when recursive is set, in a stable order: folders before the files inside them, names in ordinal order. |
Bastion.Archive.FileSystem
How an ArchiveFolder reads its archive, and how it writes it again when it changes.
| Constructor | Summary |
|---|---|
New() | Creates options with the secure defaults and no password. |
| Member | Type | Summary |
|---|---|---|
Password | String | The password for encrypted entries, or Nothing. |
Policy | ExtractionPolicy | The limits and refusals for reading, or Nothing for the secure defaults. Reading it never returns Nothing. Its MaxNestingDepth bounds how deep archives inside archives may be opened, and its MaxTotalBytes bounds what can be read out. |
Settings | CompressionSettings | How the archive is compressed when a change rebuilds it, or Nothing for the defaults. For a 7z the whole archive is rewritten with these; for a ZIP only what is added or replaced is. |
Bastion.Archive.FileSystem · Inherits AbstractFile
A file on disk.
| Constructor | Summary |
|---|---|
New(path As String) | A file at path, which need not exist yet.path — An absolute or relative path; relative paths resolve against the current directory now.Throws ArgumentNullException when path is Nothing; ArgumentException when path is empty or not a valid path. |
| Member | Type | Summary |
|---|---|---|
Exists read-only | Boolean | True when the item exists now. A store that cannot be read answers False. |
FullName read-only | String | The absolute path. |
LastWriteUtc read-only | Date | When the item was last written, in UTC, or MinValue when unknown. |
Name read-only | String | The item's own name, without any folder. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
Size read-only | Long | The file's length in bytes, or -1 when it does not exist or cannot be read. |
| Member | Returns | Summary |
|---|---|---|
Delete() | OperationResult | Deletes the file, read-only or not. A file that does not exist is already deleted. |
OpenRead() | FileSystemStreamResult | Opens the file's bytes for reading. The caller owns the stream. |
OpenWrite(overwrite As Boolean) | FileSystemStreamResult | Opens the file for writing. The bytes go to a temporary neighbour, which takes the file's name in one step when the stream is closed. |
Bastion.Archive.FileSystem · Inherits AbstractFolder
A folder on disk.
| Constructor | Summary |
|---|---|
New(path As String) | A folder at path, which need not exist yet.path — An absolute or relative path; relative paths resolve against the current directory now.Throws ArgumentNullException when path is Nothing; ArgumentException when path is empty or not a valid path. |
| Member | Type | Summary |
|---|---|---|
Exists read-only | Boolean | True when the item exists now. A store that cannot be read answers False. |
FullName read-only | String | The absolute path. |
LastWriteUtc read-only | Date | When the item was last written, in UTC, or MinValue when unknown. |
Name read-only | String | The item's own name, without any folder. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
| Member | Returns | Summary |
|---|---|---|
Create() | OperationResult | Creates the folder and any missing parents; succeeds if it already exists. |
Delete() | OperationResult | Deletes the folder and everything in it, read-only files included. |
GetFile(relativePath As String) | AbstractFile | A file inside this folder, by a relative path with forward or back slashes. No I/O. |
GetFolder(relativePath As String) | AbstractFolder | A folder inside this one, by a relative path with forward or back slashes. No I/O. |
GetItems() | FileSystemListResult | Every file under this folder, and each subfolder too when recursive is set, in a stable order: folders before the files inside them, names in ordinal order. |
Bastion.Archive.FileSystem
How a copy or move between folders behaves.
| Constructor | Summary |
|---|---|
New() | Creates options with the defaults: recursive, failing on anything that already exists. |
| Member | Type | Summary |
|---|---|---|
Filter | Func(Of AbstractFile, String, Boolean) | Decides which files are taken, given the file and its path relative to the folder being copied, with forward slashes; Nothing takes everything. Folders are created as their files need them. |
Monitor | OperationMonitor | Where progress, per-item events and errors go, and where cancellation comes from; or Nothing. |
Overwrite | OverwriteMode | What happens when a file of the same name is already at the destination. The default fails, because replacing something is a decision the caller should have made. |
Recursive | Boolean | Whether a folder's subfolders are copied too. Default True. |
Bastion.Archive.FileSystem
A file or a folder, wherever it lives: on disk, in memory, or inside an archive.
Creating an item does no I/O, so an item for something that does not exist yet is an ordinary thing to hold — it is how a destination is named before anything is copied to it. Everything that touches storage returns an OperationResult or a subclass rather than throwing; only a programming error, such as a Nothing argument, throws.
| Member | Type | Summary |
|---|---|---|
Exists read-only | Boolean | True when the item exists now. A store that cannot be read answers False. |
FullName read-only | String | A name that identifies the item within its store: a path on disk, a path inside an archive. |
LastWriteUtc read-only | Date | When the item was last written, in UTC, or MinValue when unknown. |
Name read-only | String | The item's own name, without any folder. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
| Member | Returns | Summary |
|---|---|---|
Delete() abstract | OperationResult | Removes the item; a folder goes with everything in it. Deleting what does not exist succeeds. |
DeleteAsync(Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) | — | Deletes the item on the thread pool; the twin of Delete, worth having for an item inside an archive, where deleting rebuilds the archive.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled. |
ToString() | String | The full name. |
Bastion.Archive.FileSystem · Inherits OperationResult
The outcome of listing a folder, carrying the items when it succeeded.
| Member | Type | Summary |
|---|---|---|
Items read-only | IReadOnlyList(Of FileSystemItem) | The items, files and folders together; empty when the listing failed. |
Bastion.Archive.FileSystem · Inherits OperationResult
The outcome of opening a file, carrying the stream when it succeeded.
The caller owns Stream. A stream opened for writing makes its content visible only when it is closed: a disk file is written to a temporary neighbour and moved into place then, and a file inside an archive is added when the archive is next rebuilt.
| Member | Type | Summary |
|---|---|---|
Stream read-only | Stream | The opened stream, or Nothing when the open failed. |
Bastion.Archive.FileSystem · Inherits AbstractFile
A file held in memory, inside a MemoryFolder's tree.
| Member | Type | Summary |
|---|---|---|
Exists read-only | Boolean | True when the item exists now. A store that cannot be read answers False. |
FullName read-only | String | The path from the root of its tree, starting with a slash. |
LastWriteUtc read-only | Date | When the item was last written, in UTC, or MinValue when unknown. |
Name read-only | String | The item's own name, without any folder. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
Size read-only | Long | The file's length in bytes, or -1 when it does not exist or cannot be read. |
| Member | Returns | Summary |
|---|---|---|
Delete() | OperationResult | Removes the item; a folder goes with everything in it. Deleting what does not exist succeeds. |
OpenRead() | FileSystemStreamResult | Opens the file's bytes for reading. The caller owns the stream. |
OpenWrite(overwrite As Boolean) | FileSystemStreamResult | Opens the file for writing; its content is replaced when the stream is closed. |
Bastion.Archive.FileSystem · Inherits AbstractFolder
A folder held in memory: a place to build an archive's content, or to unpack one, without touching disk.
New MemoryFolder() makes an empty root; GetFile and GetFolder give handles beneath it that share its content. Everything is held in memory by the caller's choice, so its size is the caller's to bound; this is the one store in the library that is not streamed.
| Constructor | Summary |
|---|---|
New() | An empty folder that is the root of its own tree. |
| Member | Type | Summary |
|---|---|---|
Exists read-only | Boolean | True when the item exists now. A store that cannot be read answers False. |
FullName read-only | String | The path from the root, starting with a slash; the root is "/". |
LastWriteUtc read-only | Date | When the item was last written, in UTC, or MinValue when unknown. |
Name read-only | String | The item's own name, without any folder. |
ParentFolder read-only | AbstractFolder | The folder holding this item, or Nothing for a root. |
| Member | Returns | Summary |
|---|---|---|
Create() | OperationResult | Creates the folder and any missing parents; succeeds if it already exists. |
Delete() | OperationResult | Removes the item; a folder goes with everything in it. Deleting what does not exist succeeds. |
GetFile(relativePath As String) | AbstractFile | A file inside this folder, by a relative path with forward or back slashes. No I/O. Throws ArgumentException when the path contains "." or ".." segments. |
GetFolder(relativePath As String) | AbstractFolder | A folder inside this one, by a relative path with forward or back slashes. No I/O. Throws ArgumentException when the path contains "." or ".." segments. |
GetItems() | FileSystemListResult | Every file under this folder, and each subfolder too when recursive is set, in a stable order: folders before the files inside them, names in ordinal order. |
Format detection and the handler registry.
| Type | Summary |
|---|---|
ArchiveFormatRegistry Class | Finds the handler for an input by signature: first at offset 0, then by scanning forward for an archive embedded after a self-extractor stub or other prefix, up to MaxStubScanBytes. Trailing data after the archive is tolerated and reported as a warning, matching 7-Zip's kpidEmbeddedStubSize and kpidTailSize. File extensions are never consulted. |
DetectionResult Class | The outcome of ArchiveFormatRegistry.Detect: which format was found, where the archive starts (7-Zip kpidEmbeddedStubSize) and how many bytes follow it (kpidTailSize). |
FormatSignature Class | A byte pattern that identifies a format, at a fixed offset from the start of the archive. Detection is signature-based; file extensions are never consulted. |
IArchiveHandler Interface | A reader for one container format. The ArchiveFormatRegistry matches Signatures against the input, then calls Probe so the handler can confirm from the archive's own structures. |
IArchiveWriterHandler Interface | A writer for one container format. Declares what the format can be asked for so settings are validated before any output is produced. |
ProbeResult Class | What a handler found when asked to confirm a signature match by reading the archive's own structures (for zip, the end-of-central-directory record; for 7z, the start header CRC). |
Bastion.Archive.Formats
Finds the handler for an input by signature: first at offset 0, then by scanning forward for an archive embedded after a self-extractor stub or other prefix, up to MaxStubScanBytes. Trailing data after the archive is tolerated and reported as a warning, matching 7-Zip's kpidEmbeddedStubSize and kpidTailSize. File extensions are never consulted.
| Constructor | Summary |
|---|---|
New() | Creates an empty registry. Register the handlers to consider, in priority order. |
| Member | Type | Summary |
|---|---|---|
Handlers read-only | IReadOnlyList(Of IArchiveHandler) | The registered handlers, in the order they are tried. |
MaxStubScanBytes | Long | How far past offset 0 Detect scans for an embedded archive. Default 4 MiB. Zero disables scanning.Throws ArgumentOutOfRangeException when the value is negative or above Int32. |
| Member | Returns | Summary |
|---|---|---|
Detect(stream As Stream) | DetectionResult | Identifies the archive in stream. The stream must be seekable; its position on return is unspecified. Failure to recognise the input is reported as UnsupportedFormat, never thrown.stream — The input.Throws ArgumentNullException when stream is Nothing; ArgumentException when stream is not readable and seekable. |
Register(handler As IArchiveHandler) | — | Adds a handler. Handlers registered earlier win when two signatures match at the same offset.handler — The handler.Throws ArgumentNullException when handler is Nothing; ArgumentException when the handler declares no signatures, or a handler for its format is already registered. |
Bastion.Archive.Formats · Inherits OperationResult
The outcome of ArchiveFormatRegistry.Detect: which format was found, where the archive starts (7-Zip kpidEmbeddedStubSize) and how many bytes follow it (kpidTailSize).
| Member | Type | Summary |
|---|---|---|
EmbeddedStubSize read-only | Long | Bytes before the archive start: a self-extractor stub or other prefix. Zero when the archive starts at offset 0. |
Format read-only | ArchiveFormat | The detected format, or Unknown on failure. |
Handler read-only | IArchiveHandler | The handler that claimed the archive, or Nothing on failure. |
TailSize read-only | Long | Bytes after the archive's structural end, or -1 when the format does not record its own length. |
Bastion.Archive.Formats
A byte pattern that identifies a format, at a fixed offset from the start of the archive. Detection is signature-based; file extensions are never consulted.
| Constructor | Summary |
|---|---|
New(bytes As Byte(), offset As Integer) | Creates a signature.bytes — The pattern; copied.offset — Where the pattern sits relative to the archive start, for example 257 for the tar ustar magic.Throws ArgumentNullException when bytes is Nothing; ArgumentException when bytes is empty; ArgumentOutOfRangeException when offset is negative. |
| Member | Type | Summary |
|---|---|---|
Bytes read-only | IReadOnlyList(Of Byte) | The pattern bytes. |
Offset read-only | Integer | Offset of the pattern from the archive start. |
Reach read-only | Integer | Number of bytes from the archive start needed to test this signature. |
Bastion.Archive.Formats
A reader for one container format. The ArchiveFormatRegistry matches Signatures against the input, then calls Probe so the handler can confirm from the archive's own structures.
| Member | Type | Summary |
|---|---|---|
Format read-only | ArchiveFormat | The format this handler reads. |
Signatures read-only | IReadOnlyList(Of FormatSignature) | Byte patterns that identify the format. At least one; never Nothing. |
| Member | Returns | Summary |
|---|---|---|
Probe(stream As Stream, archiveOffset As Long) | ProbeResult | Confirms or rejects a signature match by reading the archive structures at archiveOffset. The stream is seekable and positioned anywhere; the handler must not assume position 0 is the archive start.Returns. The verdict, with the archive's structural length when the format records one. stream — The input, seekable and readable.archiveOffset — Where the candidate archive starts within the stream. |
Bastion.Archive.Formats
A writer for one container format. Declares what the format can be asked for so settings are validated before any output is produced.
| Member | Type | Summary |
|---|---|---|
Format read-only | ArchiveFormat | The format this handler writes. |
SupportedEncryption read-only | IReadOnlyList(Of EncryptionMethod) | Encryption methods the format accepts, excluding Automatic. Never Nothing; empty when the format cannot encrypt. |
SupportedMethods read-only | IReadOnlyList(Of CompressionMethod) | Compression methods the format accepts, excluding Automatic. Never Nothing. |
SupportsStreamingOutput read-only | Boolean | Whether the format can be written to a non-seekable stream (zip with data descriptors: yes; 7z: no, it seeks back for the header offset). |
Bastion.Archive.Formats
What a handler found when asked to confirm a signature match by reading the archive's own structures (for zip, the end-of-central-directory record; for 7z, the start header CRC).
| Member | Type | Summary |
|---|---|---|
ArchiveLength read-only | Long | Bytes from the archive start to its structural end, or -1 when unknown. |
Detail read-only | String | Detail of structural damage, or empty. |
IsDamaged read-only | Boolean | True when the handler reported damage. |
IsMatch read-only | Boolean | Whether the handler claims the data. |
MatchWithUnknownLength Shared read-only | ProbeResult | The handler recognises the archive but cannot tell where it ends (a stream format with no length field). |
NoMatch Shared read-only | ProbeResult | The handler does not recognise the data at the candidate offset. |
| Member | Returns | Summary |
|---|---|---|
Damaged(detail As String) Shared | ProbeResult | The handler recognises the signature but the structure is damaged; detection reports the format and the detail.detail — What was wrong, for the result detail.Throws ArgumentNullException when detail is Nothing. |
Match(archiveLength As Long) Shared | ProbeResult | The handler recognises the archive and knows where it ends.archiveLength — Bytes from the archive start to its structural end, so trailing data can be measured (7-Zip kpidTailSize).Throws ArgumentOutOfRangeException when archiveLength is negative. |
gzip member streams.
| Type | Summary |
|---|---|
GZipDecoderStream Class | Decompresses a gzip stream, RFC 1952. |
GZipEncoderStream Class | Compresses to a gzip stream, RFC 1952. |
Bastion.Archive.Formats.GZip · Inherits Stream
Decompresses a gzip stream, RFC 1952.
gzip is deflate with a header in front and a check behind: a CRC-32 of the original bytes and their length taken modulo 2^32. Both are verified before the last byte is handed out, which is what separates this from simply decompressing. A gzip file is one or more members, concatenated. Nothing in the format says how many, and the usual way to make one is cat a.gz b.gz > both.gz, which is legal and common — log rotation produces them constantly. So a reader that stops at the first trailer gets the right answer for most files and silently truncates the rest, which is the worst kind of wrong. This reads on until there is nothing that begins a member. Whatever follows the last member and is not a member is ignored, which is what gzip itself does with what it calls trailing garbage. Padding to a block boundary is the usual source of it. The length check is modulo 2^32 because the field is four bytes, so a member holding more than four gibibytes states its length wrapped. That is the format's limitation rather than this one's, and it is why the check is written as a comparison of low words rather than of lengths. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a reader over a gzip stream.source — The compressed bytes. It need not be seekable.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Comment read-only | String | The first member's comment, or Nothing. |
Length read-only | Long | Overrides Stream.Length. |
MemberCount read-only | Integer | How many members have been opened so far. |
ModifiedUnixTime read-only | Long | The first member's modification time in Unix seconds, or 0 where it records none. Widened to a signed 64-bit value on the way out, because the field is unsigned and an unsigned type on a public surface is not CLS-compliant — a C# caller would see it and an F# one might not. Zero means the member recorded no time, which is what gzip -c writes. |
OriginalName read-only | String | The name the first member records for the original file, or Nothing.The first member's rather than the current one's, because a caller asking what the file was called means the file, and a multi-member stream has no single answer. Nothing before anything has been read, and Nothing for a member that carries no name — which is most of them, since gzip -c records none. The name is the original file's and is not a path this library will write to. It comes from inside the file, so treating it as a destination would be exactly the trust extraction exists to refuse. |
Position | Long | Bytes given out so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Gives back the original bytes. Returns. How many were given, or zero at the end. buffer — Where they go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed; Bastion.Archive.ArchiveException when the stream is malformed, truncated, or fails its own check. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Formats.GZip · Inherits Stream
Compresses to a gzip stream, RFC 1952.
One member: a header, a deflate stream, then a CRC-32 of the original bytes and their length taken modulo 2^32. Concatenating two of these gives a valid two-member file, which is how cat a.gz b.gz works and why nothing here needs to know about members beyond writing one properly. What goes in the header is deliberately little. A name and a comment are written only when a caller gives them, and the operating-system byte is left at 255 for unknown rather than claiming a machine this library cannot know it is running on. The modification time is a setting rather than the clock, because the same input and settings must give the same bytes and a timestamp taken from Now would make every archive differ from every other. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream, leaveOpen As Boolean) | Creates a writer at the default level, recording no name or time.destination — Where the compressed bytes go.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, originalName As String, modifiedUnixTime As Long, leaveOpen As Boolean) | Creates a writer, recording what the caller asks it to.destination — Where the compressed bytes go.level — 1 to 9, where 1 is fastest and 9 smallest.originalName — The original file's name to record, or Nothing. Stored as Latin-1, which is what the format says; a name needing more than that cannot be recorded faithfully here.modifiedUnixTime — The modification time in Unix seconds, or 0 to record none. A setting rather than the clock, so that the same input gives the same bytes.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when the level or the time is outside what can be written. |
| Member | Type | Summary |
|---|---|---|
DefaultLevel const | Integer | The level used when a caller does not choose one, which is what gzip itself defaults to. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Length read-only | Long | Overrides Stream.Length. |
Position | Long | Bytes taken so far. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Flushes the destination, but not the member. A gzip member's checksum and length are only known once there is no more input, so nothing of the trailer can be settled here. That is the format rather than this implementation. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Compresses bytes into the member.buffer — The bytes.offset — Where they start.count — How many.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the stream has been disposed. |
Raw .lzma (LZMA-alone) streams.
| Type | Summary |
|---|---|
LzmaAloneDecoderStream Class | Reads a bare .lzma file: the thirteen-byte header, then the range-coded body. |
LzmaAloneEncoderStream Class | Writes a bare .lzma file: the thirteen-byte header the format calls for, then the range-coded body. |
Bastion.Archive.Formats.Lzma · Inherits Stream
Reads a bare .lzma file: the thirteen-byte header, then the range-coded body.
The format has no magic number, so nothing here can confirm that a file is one — only that its header is not impossible. Two fields can be judged. The packing byte holds (pb * 5 + lp) * 9 + lc and so cannot exceed 224, which rejects most bytes outright; and the dictionary size is refused when it asks for more memory than the caller allows, which is the whole of the memory limit for this format, since the header is otherwise free to name four gibibytes. A length of 0xFFFFFFFFFFFFFFFF means the writer did not know it, and the body then ends with an end-of-stream marker. Anything else is the exact number of bytes to produce.
A length of 0xFFFFFFFFFFFFFFFF means the writer did not know it, and the body then ends with an end-of-stream marker. Anything else is the exact number of bytes to produce.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a reader.source — The .lzma file, positioned at its header.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; Bastion.Archive.ArchiveException when the header is truncated or cannot be an LZMA header. |
New(source As Stream, leaveOpen As Boolean, maximumDictionaryBytes As Integer) | Creates a reader with an explicit cap on the dictionary it will allocate.source — The .lzma file, positioned at its header.leaveOpen — False to dispose source with this stream.maximumDictionaryBytes — The largest dictionary this reader will allocate.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; ArgumentOutOfRangeException when maximumDictionaryBytes is below 1; Bastion.Archive.ArchiveException when the header is truncated or cannot be an LZMA header. |
| Member | Type | Summary |
|---|---|---|
HeaderLength const | Integer | The header's size: five properties bytes and an eight-byte length. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
DeclaredLength read-only | Long | The length the header declares, or -1 when it says the length is unknown. |
DictionaryBytes read-only | Integer | The dictionary size the header names, which is what the memory cap is judged against. |
Length read-only | Long | Overrides Stream.Length.Throws Bastion.Archive.ArchiveException when the header did not declare a length. |
Position | Long | Overrides Stream.Position.Throws Bastion.Archive.ArchiveException when on setting: the stream cannot seek. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek.Throws Bastion.Archive.ArchiveException when always: the stream cannot seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength.Throws Bastion.Archive.ArchiveException when always: the stream is read-only. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write.Throws Bastion.Archive.ArchiveException when always: the stream is read-only. |
Bastion.Archive.Formats.Lzma · Inherits Stream
Writes a bare .lzma file: the thirteen-byte header the format calls for, then the range-coded body.
The container is the whole of what this adds over LzmaEncoderStream, and it is four fields in thirteen bytes: one byte packing lc, lp and pb, four bytes of dictionary size, and eight bytes of uncompressed length, all little endian. There is no magic number, no checksum and no name — which is why the format was superseded by .xz, and why a corrupt .lzma file is detected by failing to decode rather than by anything in its header. The length field has two spellings and this writes whichever the caller has earned. A caller who knows the length gets it recorded, and the stream ends when that many bytes have been produced. A caller who does not — anything streaming — gets 0xFFFFFFFFFFFFFFFF, which means "unknown", and the body finishes with an end-of-stream marker instead. Both are what the reference encoder writes, for lzma e and lzma e -eos respectively; writing a length the caller has not actually produced would be worse than writing none, so a declared length that turns out to be wrong is refused at Complete rather than committed to a file.
The length field has two spellings and this writes whichever the caller has earned. A caller who knows the length gets it recorded, and the stream ends when that many bytes have been produced. A caller who does not — anything streaming — gets 0xFFFFFFFFFFFFFFFF, which means "unknown", and the body finishes with an end-of-stream marker instead. Both are what the reference encoder writes, for lzma e and lzma e -eos respectively; writing a length the caller has not actually produced would be worse than writing none, so a declared length that turns out to be wrong is refused at Complete rather than committed to a file.
| Constructor | Summary |
|---|---|
New(destination As Stream, leaveOpen As Boolean) | Creates a writer at level 5 for content whose length is not known in advance.destination — Where the file goes.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, uncompressedLength As Long, leaveOpen As Boolean) | Creates a writer.destination — Where the file goes.level — Compression level 1 to 9.uncompressedLength — How many bytes will be written, or -1 when that is not known. A declared length is recorded in the header and must match what the caller actually writes.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 1 to 9. |
| Member | Type | Summary |
|---|---|---|
HeaderLength const | Integer | The header's size: five properties bytes and an eight-byte length. |
| Member | Type | Summary |
|---|---|---|
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
DeclaredLength read-only | Long | The length recorded in the header, or -1 when the header says it is unknown. |
Length read-only | Long | Overrides Stream.Length.Throws Bastion.Archive.ArchiveException when always: the length of a stream being written is not known. |
Level read-only | Integer | The level in use. |
Position | Long | Overrides Stream.Position.Throws Bastion.Archive.ArchiveException when on setting: the stream cannot seek. |
WorkingMemoryBytes read-only | Long | Working memory this encoder holds, which the level alone decides. |
| Member | Returns | Summary |
|---|---|---|
Complete() | — | Finishes the file: flushes the body and, where the header said the length is unknown, the end-of-stream marker. Throws Bastion.Archive.ArchiveException when fewer bytes were written than the header declares. |
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read.Throws Bastion.Archive.ArchiveException when always: this stream cannot be read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek.Throws Bastion.Archive.ArchiveException when always: the stream cannot seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength.Throws Bastion.Archive.ArchiveException when always: the length is decided by what is written. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write.Throws Bastion.Archive.ArchiveException when the stream is finished, or more was written than was declared. |
tar reading and writing, including forward-only streams.
| Type | Summary |
|---|---|
TarArchive Class | .tar, .tar.gz, .tar.bz2, .tar.xz and .tar.zst as single operations. |
TarArchiveOptions Class | What TarArchive should do: which compressor, which tar dialect, and what to do about a file that is already there. |
TarCompression Enum | What a tar is wrapped in: nothing, or one of the stream compressors that conventionally carry one. |
TarEntry Class | One entry in a tar archive, after every extension carrying its name and times has been applied. |
TarEntryType Enum | What a tar entry is, from the single type byte in its header. |
TarFormat Enum | Which dialect of tar to write, for the things the original format cannot express. |
TarReader Class | Reads a tar archive forwards, one entry at a time. |
TarWriter Class | Writes a tar archive forwards, one entry at a time. |
Bastion.Archive.Formats.Tar
.tar, .tar.gz, .tar.bz2, .tar.xz and .tar.zst as single operations.
A convenience over parts that already work rather than anything new: a .tar.gz is a tar written into a gzip, and both halves stream, so composing them needs no buffer and no seek. What this adds is the part every caller would otherwise write again — walking a directory, choosing the compressor from the file's name, and getting the safety right on the way back out. Extraction applies the same path checks through the same PathCanonicaliser the ZIP extractor uses, so an entry called ../../etc/passwd is refused here exactly as it is there. Creation is atomic: the archive is written to a temporary neighbour and moved into place only once it is complete, and an existing file is left alone unless the caller says otherwise. Nothing here throws; every operation returns an OperationResult.
| Member | Returns | Summary |
|---|---|---|
CreateFromDirectory(sourceDirectory As String, archivePath As String) Shared | OperationResult | Creates an archive from a directory, choosing the compressor from the archive's name. Returns. The outcome; this never throws. sourceDirectory — The directory to archive; its contents go in, not the directory itself.archivePath — Where to write, whose extension chooses the compressor. |
CreateFromDirectory(sourceDirectory As String, archivePath As String, options As TarArchiveOptions) Shared | OperationResult | Creates an archive from a directory. Returns. The outcome; this never throws. sourceDirectory — The directory to archive; its contents go in, not the directory itself.archivePath — Where to write.options — What to do, or Nothing for the defaults. |
CreateFromDirectoryAsync(sourceDirectory As String, archivePath As String, Optional options As TarArchiveOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) Shared | — | Writes a tar of a directory on the thread pool; the twin of CreateFromDirectory.sourceDirectory — The directory to archive.archivePath — The tar to write; it must not exist.options — Format, compression and timestamps, or Nothing for the defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when sourceDirectory or archivePath is Nothing. |
ExtractToDirectory(archivePath As String, targetDirectory As String) Shared | OperationResult | Extracts an archive under a directory, with the secure defaults. Returns. The outcome; this never throws. archivePath — The archive, whose extension chooses the decompressor.targetDirectory — Where the entries go; created if it does not exist. |
ExtractToDirectory(archivePath As String, targetDirectory As String, options As ExtractionOptions) Shared | OperationResult | Extracts an archive under a directory. Returns. The outcome; this never throws. archivePath — The archive, whose extension chooses the decompressor.targetDirectory — Where the entries go; created if it does not exist.options — How to extract, or Nothing for the secure defaults. |
ExtractToDirectoryAsync(archivePath As String, targetDirectory As String, Optional options As ExtractionOptions = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of OperationResult) Shared | — | Extracts a tar on the thread pool; the twin of ExtractToDirectory.archivePath — The tar to read.targetDirectory — Where its entries go; created if it does not exist.options — How to extract, or Nothing for the secure defaults.cancellationToken — Stops the operation at the next boundary; the result then says Cancelled.Throws ArgumentNullException when archivePath or targetDirectory is Nothing. |
Bastion.Archive.Formats.Tar
What TarArchive should do: which compressor, which tar dialect, and what to do about a file that is already there.
Every default is the safe or conventional one. The compression is taken from the archive's name unless it is set here, because a caller writing backup.tar.gz means gzip and should not have to say so twice; the format is pax, which is what GNU tar writes by default and the only one of the three that carries a non-ASCII name faithfully; and an existing file is left alone rather than overwritten.
| Constructor | Summary |
|---|---|
New() | Creates options with the safe defaults. |
| Member | Type | Summary |
|---|---|---|
Compression | Nullable(Of TarCompression) | Which compressor to wrap the tar in, or Nothing to take it from the archive's name. |
CompressionLevel | Integer | How hard the compressor should work, 1 to 9, or a negative number for each compressor's default. |
FixedModifiedUnixTime | Long | A modification time in Unix seconds to give every entry, or a negative number to use each file's own. The way to get a byte-identical archive from the same tree twice. tar records a time in every header, so an archive built from the clock differs from itself on every run, and anything comparing two builds by hash needs a way to say "not from the clock". |
Format | TarFormat | Which tar dialect to write. Default Pax. |
IncludeHiddenFiles | Boolean | True to include files the file system marks hidden. Default False. |
Overwrite | Boolean | True to replace an archive that already exists. Default False. |
Bastion.Archive.Formats.Tar
What a tar is wrapped in: nothing, or one of the stream compressors that conventionally carry one.
A .tar.gz is not a format of its own. It is a tar inside a gzip, and every one of these is the same arrangement with a different compressor — which is why the composite operations are a convenience over two things that already work rather than a new reader and writer. Zstd can be read and not written, because this library decodes Zstandard and does not yet encode it. Asking to create one is refused by name rather than silently given something else.
| Name | Value | Summary |
|---|---|---|
None | 0 | A plain .tar. |
GZip | 1 | gzip: .tar.gz or .tgz. |
BZip2 | 2 | bzip2: .tar.bz2, .tbz2 or .tbz. |
Xz | 3 | xz: .tar.xz or .txz. |
Zstd | 4 | Zstandard: .tar.zst or .tzst. Read only. |
Bastion.Archive.Formats.Tar
One entry in a tar archive, after every extension carrying its name and times has been applied.
What a caller sees, rather than what a block said. A path longer than a hundred bytes may have arrived in a ustar prefix, in a GNU long-name entry or in a pax keyword; a time may have arrived as whole seconds in the header or as a fraction in a pax keyword. By the time an entry exists, all of that has been resolved, and nothing here says which route a field took.
| Member | Type | Summary |
|---|---|---|
DeviceMajor | Long | Which device a character or block device entry names; zero for every other type. A device entry has no content, so this pair is the whole of what it says. Carried as Int64 because Linux has used 32-bit device numbers since 2.6 and the header field is octal text, which holds far more than the 8-bit pair the numbers began as. |
DeviceMinor | Long | The minor device number of a character or block device entry; zero for every other type. |
EntryType | TarEntryType | What kind of thing the entry is. |
GroupId | Long | The numeric group, and the group's name where the archive records one. |
GroupName | String | The group's name where the archive records one, which ustar and later formats do. |
HasContent read-only | Boolean | Whether the entry has content following its header. |
Length | Long | How many bytes of content the entry has. |
LinkName | String | What a link points at, or Nothing for anything that is not a link. |
Mode | Integer | The permission bits. |
ModifiedUtc | DateTimeOffset | The modification time, in Unix seconds and whatever fraction the archive recorded. A header records whole seconds; a pax mtime keyword records a decimal that may carry nanoseconds. Held as a DateTimeOffset in UTC so that the fraction survives, because rounding it away here would make a round trip through this library lose information the archive actually held. |
Name | String | The entry's path within the archive, with forward slashes. |
UserId | Long | The numeric owner, and the owner's name where the archive records one. |
UserName | String | The owner's name where the archive records one, which ustar and later formats do. |
Bastion.Archive.Formats.Tar
What a tar entry is, from the single type byte in its header.
Four of these are not entries at all but instructions about the entry that follows: GNU's long name and long link name, and pax's per-entry and global headers. A reader that treated them as files would produce archives with strange extra members called ././@LongLink and PaxHeaders/…, which is exactly what a reader that does not know about them produces.
| Name | Value | Summary |
|---|---|---|
File | 0 | An ordinary file. |
Directory | 1 | A directory. |
HardLink | 2 | A hard link to another entry in the same archive. |
SymbolicLink | 3 | A symbolic link, whose target is text and may point anywhere. |
CharacterDevice | 4 | A character device. |
BlockDevice | 5 | A block device. |
Fifo | 6 | A named pipe. |
GnuLongName | 7 | GNU's carrier for a name too long for the header's 100 bytes. |
GnuLongLinkName | 8 | GNU's carrier for a link target too long for the header's 100 bytes. |
PaxEntryHeader | 9 | A pax header giving keywords for the entry that follows it. |
PaxGlobalHeader | 10 | A pax header giving keywords for every entry after it. |
Unsupported | 11 | A type byte this library has no meaning for. |
Bastion.Archive.Formats.Tar
Which dialect of tar to write, for the things the original format cannot express.
A tar header holds a 100-byte name, a 12-byte octal size and a whole number of seconds. Everything beyond that — a longer path, a file over 8 GiB, a time with a fraction — needs an extension, and there are two in use. They are not compatible and both are read everywhere, so which to write is a choice rather than a detail.
| Name | Value | Summary |
|---|---|---|
Pax | 0 | POSIX pax: anything the header cannot hold goes in a keyword record in front of the entry. The modern default and what GNU tar 1.35 writes unless told otherwise. It is the only one of the two that specifies an encoding — UTF-8 — so it is the only way a name outside ASCII survives a round trip through a tar archive with its characters intact. |
Gnu | 1 | GNU: a long name travels as a whole extra entry of its own in front of the real one. Older and still ubiquitous. Worth writing when the reader is known to be old, and worth avoiding otherwise: it has no encoding, and a reader that does not know the convention sees entries called ././@LongLink. |
Ustar | 2 | Plain POSIX ustar: no extensions at all, and a path that will not fit is refused. The most portable and the least capable. A path up to 255 bytes fits if it can be split at a slash so that the last part is under 100 and the rest under 155; anything else cannot be written, and is refused rather than truncated — a silently shortened path is a worse outcome than an error, because nothing downstream can tell it happened. |
Bastion.Archive.Formats.Tar · Implements IDisposable
Reads a tar archive forwards, one entry at a time.
tar has no index and no central directory. An archive is headers and data blocks one after another, and the only way to find the tenth entry is to walk the nine before it — which is why this is a forward-only reader over a stream that need not seek, and why a tar inside a gzip works at all. Most of the work is reconciling the ways a long path can arrive, because there are three and an archive may use any of them: ustar prefix — a 155-byte field joined to the 100-byte name with a slash, handled in the header. GNU long name — a whole extra entry of type L whose content is the real name, sitting in front of the entry it belongs to. Type K does the same for a link target. pax keywords — an entry of type x whose content is a list of length key=value records, which can carry a path, a link path, a size beyond what the octal field holds, and times with a fraction. Type g carries the same for every entry after it. A reader that handles none of these still reads most archives, and produces entries called ././@LongLink and PaxHeaders.0/something alongside truncated paths. That is the failure this is shaped to avoid. The end is two zero blocks. One is not an ending: a single zero block in the middle of a file is a corrupt header, and treating it as the end would silently drop everything after it. Sparse files are put back together here rather than handed outwards: a caller sees the file it would have got from disk, at its full length, with the holes as zeros. Three spellings exist and two are readable — GNU's own, and pax 1.0 and 0.1. pax 0.0 is refused by name; see ApplyPaxSparse for why that is a refusal and not an oversight.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Creates a reader over an archive.source — The archive. It need not be seekable.leaveOpen — False to dispose source with this reader.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable. |
| Member | Type | Summary |
|---|---|---|
Current read-only | TarEntry | The entry now open, or Nothing before the first and after the last. |
| Member | Returns | Summary |
|---|---|---|
Dispose() | — | Releases the archive, where the reader was given it to own. |
MoveNext() | Boolean | Moves to the next entry, skipping whatever is left of the one before. Returns. False when the archive has ended.Throws ObjectDisposedException when the reader has been disposed; Bastion.Archive.ArchiveException when the archive is malformed or truncated. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Reads the current entry's content. Returns. How many were given, or zero at the end of the entry. buffer — Where the bytes go.offset — Where to start writing.count — The most to give.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; ObjectDisposedException when the reader has been disposed; Bastion.Archive.ArchiveException when the archive ends inside the entry. |
Bastion.Archive.Formats.Tar · Implements IDisposable
Writes a tar archive forwards, one entry at a time.
The mirror of TarReader and the simpler half: there is no index to go back and fix, so the destination need not seek and a tar can be written straight into a gzip. What takes the thought is everything the header cannot hold. A name over 100 bytes, a size over 8 GiB, a time with a fraction — each needs an extension, and which one depends on TarFormat. In pax the answer is a keyword record in an x entry in front of the real one; in GNU it is a whole L entry whose content is the name; in ustar there is no answer at all beyond the prefix, and a path that will not fit is refused rather than truncated. A silently shortened path is worse than an error, because nothing downstream can tell it happened. Times are a setting rather than the clock, so that the same input and settings give the same bytes. An archive whose contents are identical but whose timestamps came from Now differs from itself on every run, which makes it useless for anything that compares builds. The end is two zero blocks, then padding to the blocking factor — twenty blocks, which is what every tar writes and what some readers still expect.
| Constructor | Summary |
|---|---|
New(destination As Stream, leaveOpen As Boolean) | Creates a writer in pax format, which is what GNU tar writes by default.destination — Where the archive goes. It need not seek.leaveOpen — False to dispose destination with this writer.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, format As TarFormat, leaveOpen As Boolean) | Creates a writer in a chosen format.destination — Where the archive goes. It need not seek.format — Which dialect to write.leaveOpen — False to dispose destination with this writer.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when format is not one this writes. |
| Member | Returns | Summary |
|---|---|---|
AddBlockDevice(name As String, deviceMajor As Long, deviceMinor As Long, modifiedUnixTime As Long, mode As Integer) | — | Adds a block device, which is a name and a pair of numbers.name — The device@@s path within the archive.deviceMajor — The major number, which says which driver.deviceMinor — The minor number, which says which device of that driver.modifiedUnixTime — The modification time in Unix seconds.mode — The permission bits, or a negative number for 0666.Throws ArgumentNullException when name is Nothing; ArgumentException when the name is empty; ArgumentOutOfRangeException when either number is negative; InvalidOperationException when the archive has been finished. |
AddCharacterDevice(name As String, deviceMajor As Long, deviceMinor As Long, modifiedUnixTime As Long, mode As Integer) | — | Adds a character device, which is a name and a pair of numbers.name — The device@@s path within the archive.deviceMajor — The major number, which says which driver.deviceMinor — The minor number, which says which device of that driver.modifiedUnixTime — The modification time in Unix seconds.mode — The permission bits, or a negative number for 0666.Throws ArgumentNullException when name is Nothing; ArgumentException when the name is empty; ArgumentOutOfRangeException when either number is negative; InvalidOperationException when the archive has been finished. |
AddDirectory(name As String, modifiedUnixTime As Long, mode As Integer) | — | Adds a directory, which has a name and no content.name — The directory's path within the archive.modifiedUnixTime — The modification time in Unix seconds.mode — The permission bits, or -1 for a sensible default.Throws ArgumentNullException when name is Nothing; ArgumentException when name is empty; InvalidOperationException when the archive has been finished. |
AddFifo(name As String, modifiedUnixTime As Long, mode As Integer) | — | Adds a named pipe, which has a name, a mode and nothing else.name — The pipe@@s path within the archive.modifiedUnixTime — The modification time in Unix seconds.mode — The permission bits, or a negative number for 0666.Throws ArgumentNullException when name is Nothing; ArgumentException when the name is empty; InvalidOperationException when the archive has been finished. |
AddFile(name As String, content As Stream, length As Long, modifiedUnixTime As Long, mode As Integer) | — | Adds a file, reading its content to the end.name — The entry's path within the archive, with forward slashes.content — The bytes, or Nothing for a file that has none.length — How many bytes content will give. tar states a length in the header before the content, so it has to be known in advance — that is the format, not a limitation here.modifiedUnixTime — The modification time in Unix seconds.mode — The permission bits, or -1 for a sensible default.Throws ArgumentNullException when name is Nothing; ArgumentException when name is empty; ArgumentOutOfRangeException when length is negative; InvalidOperationException when the archive has been finished; Bastion.Archive.ArchiveException when the format cannot express the name, or the content is shorter than the length given. |
AddHardLink(name As String, target As String, modifiedUnixTime As Long, mode As Integer) | — | Adds a hard link, which names a file already in the archive.name — The link@@s path within the archive.target — The name, as written in this archive, of the entry it is another name for.modifiedUnixTime — The modification time in Unix seconds.mode — The permission bits, or a negative number for 0644.Not a copy and not a path: a hard link is the same file under a second name, so the entry carries no content at all and the target must be an entry written earlier in this archive. A reader that has not seen it has nothing to link to — which is why the target is a name from the archive rather than a path on disk, and why writing the link before the file it names produces an archive that every tar refuses to extract in full. Throws ArgumentNullException when the name or the target is Nothing; ArgumentException when the name or the target is empty; InvalidOperationException when the archive has been finished. |
AddSymbolicLink(name As String, target As String, modifiedUnixTime As Long) | — | Adds a symbolic link, whose target is text and is not followed.name — The link's path within the archive.target — What it points at.modifiedUnixTime — The modification time in Unix seconds.Throws ArgumentNullException when the name or the target is Nothing; ArgumentException when the name or the target is empty; InvalidOperationException when the archive has been finished. |
Dispose() | — | Releases the destination, where the writer was given it to own. |
Finish() | — | Finishes the archive: two zero blocks, then padding out to the blocking factor. Two blocks rather than one, because one is not an ending — a reader that treated a single zero block as the end would stop at the first corrupt header instead of reporting it. |
XZ container streams.
| Type | Summary |
|---|---|
XzCheck Enum | The integrity check an XZ stream carries after each block. |
XzDecoderStream Class | Decodes an XZ stream: the container .xz files hold and ZIP method 95 carries. |
XzEncoderStream Class | Writes an XZ stream: the container .xz files hold and ZIP method 95 carries. |
Bastion.Archive.Formats.Xz
The integrity check an XZ stream carries after each block.
The format reserves sixteen check identifiers in four size classes and names four of them. A reader must know the size of a check it does not recognise, because the size is what lets it skip one, so the identifiers a decoder refuses are still decoded far enough to be named. xz writes Crc64 unless told otherwise, and so does this library.
| Name | Value | Summary |
|---|---|---|
None | 0 | No check at all, which the format allows and no producer here writes. |
Crc32 | 1 | CRC-32, four bytes, little endian. |
Crc64 | 4 | CRC-64 as the format defines it (ECMA-182 reflected), eight bytes, little endian. |
Sha256 | 10 | SHA-256, thirty-two bytes of digest. |
Bastion.Archive.Formats.Xz · Inherits Stream
Decodes an XZ stream: the container .xz files hold and ZIP method 95 carries.
The container is a header, a run of blocks, an index and a footer, and almost all of it exists to be checked. Every part carries its own CRC-32 — the stream flags, each block header, the index — the footer repeats the flags the header gave and the size of the index so the file can be read backwards, and the index repeats every block's size so a reader that went forwards can prove it saw the same stream. All of that is verified here rather than trusted, and the block's own check is verified before the last of its bytes is handed over. Several streams may sit one after another, separated by null padding, and are read as one — which is how xz --cat output and parallel compressors both come out. Only a lone LZMA2 filter is implemented. A block that chains a BCJ or Delta filter is refused by name, because a filter skipped would decode to plausible rubbish rather than to an error. Written from the published xz-file-format.txt, version 1.2.1. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Opens an XZ stream and reads its header.source — The stream; read forward only.leaveOpen — False to dispose source with this stream.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; Bastion.Archive.ArchiveException when the stream is not an XZ stream, or its header is corrupt. |
New(source As Stream, leaveOpen As Boolean, maximumDictionaryBytes As Integer) | Opens an XZ stream with an explicit cap on the dictionary a block may ask for.source — The stream; read forward only.leaveOpen — False to dispose source with this stream.maximumDictionaryBytes — The largest dictionary to allocate for a block that asks.Throws ArgumentNullException when source is Nothing; ArgumentException when source is not readable; ArgumentOutOfRangeException when maximumDictionaryBytes is below one; Bastion.Archive.ArchiveException when the stream is not an XZ stream, or its header is corrupt. |
| Member | Type | Summary |
|---|---|---|
DefaultMaximumDictionaryBytes const | Integer | The largest dictionary accepted unless the caller says otherwise: 256 MiB. |
| Member | Type | Summary |
|---|---|---|
BlockCount read-only | Integer | Blocks read to the end so far, across every stream. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Whether this stream can seek, which it can when the source seeks and the file has an index. Deliberately the ordinary Stream contract rather than an API of its own. A caller that wants the tenth megabyte of a .xz writes Position = 10485760, and everything already written against Stream — copying, readers, callers that probe Length — gets the benefit without knowing this format has an index at all. A seek costs one block, not one file: the index names the block holding the offset, the source moves to it, and only the bytes from the block's start to the offset are decoded and dropped. Blocks are what an encoder chose, so a file written as one block seeks no faster than it reads. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Check read-only | XzCheck | The check the stream being read declares after each of its blocks. |
Length read-only | Long | The uncompressed length, where the index says what it is. Throws NotSupportedException when the file has no index this stream could read. |
Position | Long | Bytes produced so far, and where reading continues from. Throws NotSupportedException when this stream cannot seek; ArgumentOutOfRangeException when the position is negative. |
StreamCount read-only | Integer | Streams read to the end so far, which is more than one for concatenated input. |
| Member | Returns | Summary |
|---|---|---|
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Decodes into buffer, returning 0 at the end of the last stream.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; Bastion.Archive.ArchiveException when the stream is corrupt, ends early, or fails one of its checks. |
Seek(offset As Long, origin As SeekOrigin) | Long | Moves to an uncompressed offset, decoding only the block that holds it. Throws NotSupportedException when this stream cannot seek; ArgumentOutOfRangeException when the target is before the start; Bastion.Archive.ArchiveException when the file is corrupt at the block sought to. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Overrides Stream.Write. |
Bastion.Archive.Formats.Xz · Inherits Stream
Writes an XZ stream: the container .xz files hold and ZIP method 95 carries.
One block holding one LZMA2 stream by default, which is what xz itself writes unless asked for a block size. Ask for one and the stream is cut into blocks of that many uncompressed bytes, each with its own header, its own LZMA2 stream and its own check, exactly as xz --block-size does. That is what lets a reader start in the middle of a stream or decode blocks in parallel, and it is the precondition for encoding them in parallel, which this does when asked for more than one worker. It costs ratio, because every block starts with an empty dictionary and cannot refer to anything before it. Parallel output is byte-identical to serial output, which the design is chosen to make obvious rather than merely likely: the boundaries come from the block size and never from how the work was divided, each block is encoded by a fresh codec that shares nothing with any other, and the blocks are written in index order however they finished. The block header leaves out both optional sizes, as 7-Zip's does, because neither is known when the header has to be written and buffering a whole block to find out would give up streaming for nothing: the index at the end of the stream carries both sizes regardless, so a reader loses no information. Written from the published xz-file-format.txt, version 1.2.1. Being a Stream, this throws ArchiveException rather than returning a result.
| Constructor | Summary |
|---|---|
New(destination As Stream) | Creates an encoder at level 5 with a CRC-64 check, which is what xz writes.destination — Where the compressed bytes go.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable. |
New(destination As Stream, level As Integer, check As XzCheck, leaveOpen As Boolean) | Creates an encoder writing one block, and writes the stream header.destination — Where the compressed bytes go.level — Compression level 1 to 9.check — The integrity check to put after each block.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 1 to 9; Bastion.Archive.ArchiveException when check is one the format reserves. |
New(destination As Stream, level As Integer, check As XzCheck, blockSizeBytes As Long, leaveOpen As Boolean) | Creates an encoder cutting the stream into blocks, and writes the stream header.destination — Where the compressed bytes go.level — Compression level 1 to 9.check — The integrity check to put after each block.blockSizeBytes — Uncompressed bytes per block, or zero for a single block however long the stream is. A block is independently decodable, so a small one buys random access and parallel decoding at the cost of ratio — each block starts with an empty dictionary. Very small values are legal and very wasteful; xz's own default when asked for threads is 64 MiB at level 6.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable; ArgumentOutOfRangeException when level is outside 1 to 9, or blockSizeBytes is negative; Bastion.Archive.ArchiveException when check is one the format reserves. |
New(destination As Stream, level As Integer, check As XzCheck, blockSizeBytes As Long, workerCount As Integer, leaveOpen As Boolean) | Creates an encoder that compresses several blocks at once, and writes the stream header.destination — Where the compressed bytes go.level — Compression level 1 to 9.check — The integrity check to put after each block.blockSizeBytes — Uncompressed bytes per block. Required above one worker, because a block is the unit of work: with one block there is nothing to divide.workerCount — How many blocks to compress at once. One is serial. The output is byte-identical either way, so this trades memory for time and nothing else — and it is memory rather than a little of it, since each worker holds a codec of its own: ask WorkingMemoryBytes before choosing a number.leaveOpen — False to dispose destination with this stream.Throws ArgumentNullException when destination is Nothing; ArgumentException when destination is not writable, or more than one worker was asked for without a block size; ArgumentOutOfRangeException when level is outside 1 to 9, blockSizeBytes is negative, or workerCount is below one; Bastion.Archive.ArchiveException when check is one the format reserves. |
| Member | Type | Summary |
|---|---|---|
BlockCount read-only | Integer | Blocks sealed so far, which is what the index will hold a record for. |
BlockSizeBytes read-only | Long | Uncompressed bytes per block, or zero when the stream is one block. |
CanRead read-only | Boolean | Overrides Stream.CanRead. |
CanSeek read-only | Boolean | Overrides Stream.CanSeek. |
CanWrite read-only | Boolean | Overrides Stream.CanWrite. |
Check read-only | XzCheck | The check written after each block. |
DictionaryBytes read-only | Integer | The dictionary the block header declares, which a reader has to allocate. |
Length read-only | Long | Overrides Stream.Length. |
Level read-only | Integer | The level in use. |
Position | Long | Uncompressed bytes accepted so far. |
WorkerCount read-only | Integer | How many blocks are compressed at once; one is serial. |
WorkingMemoryBytes read-only | Long | Working memory this encoder holds, which the level alone decides. It is nothing until the first write, because the codec that holds it is made then. |
| Member | Returns | Summary |
|---|---|---|
Complete() | — | Finishes the stream: the last block's check, then the index and the footer. |
Flush() | — | Overrides Stream.Flush. |
Read(buffer As Byte(), offset As Integer, count As Integer) | Integer | Overrides Stream.Read. |
Seek(offset As Long, origin As SeekOrigin) | Long | Overrides Stream.Seek. |
SetLength(value As Long) | — | Overrides Stream.SetLength. |
Write(buffer As Byte(), offset As Integer, count As Integer) | — | Accepts uncompressed bytes. Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the offset or count lies outside the buffer; InvalidOperationException when the stream has already been completed. |
Forward-only ZIP reading from streams that cannot seek.
| Type | Summary |
|---|---|
ZipStreamEntry Class | One entry of a ZIP being read forwards, as its local file header describes it. |
ZipStreamReader Class | Reads a ZIP forward, entry by entry, from a stream that cannot seek — a socket, a pipe, or a decompressing stream over a download. |
Bastion.Archive.Formats.Zip
One entry of a ZIP being read forwards, as its local file header describes it.
What this reports comes from the local header alone, because a forward-only reader never reaches the central directory. Where an entry states its sizes after its data (general purpose bit 3), the sizes and the CRC-32 here are zero until the entry has been read to its end, at which point the data descriptor fills them in.
| Member | Type | Summary |
|---|---|---|
CompressedSize | Long | Bytes the entry occupies compressed, 0 until a data descriptor supplies it. |
Crc32 | Long | The CRC-32 the archive states, which is 0 until a data descriptor supplies it. |
HasDataDescriptor read-only | Boolean | True when the sizes and the checksum follow the data rather than precede it. |
IsDirectory read-only | Boolean | True when the name ends in a slash, which is how a ZIP marks a directory. |
IsEncrypted read-only | Boolean | True when the entry's data is encrypted. |
Method read-only | Integer | The compression method the entry's data uses. |
Name read-only | String | The entry name, with forward slashes, and a trailing slash on a directory. |
UncompressedSize | Long | Bytes the entry holds uncompressed, 0 until a data descriptor supplies it. |
| Member | Returns | Summary |
|---|---|---|
ToString() | String | Returns a readable description of the value. |
Bastion.Archive.Formats.Zip · Implements IDisposable
Reads a ZIP forward, entry by entry, from a stream that cannot seek — a socket, a pipe, or a decompressing stream over a download.
The ordinary reader (Archive) starts at the central directory, which is at the end of the file, and so needs to seek. This one walks the local file headers instead, in the order the archive was written, and never looks backwards. That is a genuinely different reading of the same format and it costs something: the central directory is where a ZIP records the truth about each entry, and a local header can disagree with it. What this reader reports is what the local headers say. An entry written with a data descriptor (general purpose bit 3) states its sizes after its data rather than before. For a compressed entry that is workable, because the decoder knows where its own stream ends; the descriptor is then read and its CRC-32 checked. For a stored entry it is not, because nothing marks the end of the data, so such an entry is refused by name rather than guessed at by scanning for the next signature. A codec-style type: it throws rather than returning a result, like the stream classes it is built from.
An entry written with a data descriptor (general purpose bit 3) states its sizes after its data rather than before. For a compressed entry that is workable, because the decoder knows where its own stream ends; the descriptor is then read and its CRC-32 checked. For a stored entry it is not, because nothing marks the end of the data, so such an entry is refused by name rather than guessed at by scanning for the next signature.
A codec-style type: it throws rather than returning a result, like the stream classes it is built from.
| Constructor | Summary |
|---|---|
New(source As Stream, leaveOpen As Boolean) | Reads the ZIP arriving on source, which need not be seekable. |
| Member | Type | Summary |
|---|---|---|
Current read-only | ZipStreamEntry | The entry the reader is on, or Nothing before the first MoveNext. |
| Member | Returns | Summary |
|---|---|---|
Dispose() | — | Releases the resources the object holds. |
MoveNext() | Boolean | Moves to the next entry, stepping over whatever of the current one was not read. Returns. False at the central directory, which is where the entries end.Throws Bastion.Archive.ArchiveException when the archive is malformed or holds something this reader refuses. |
OpenEntry() | Stream | Opens the current entry's decoded content. The stream is read once and forwards only. The reader owns the returned stream and finishes it on the next MoveNext, which also steps over whatever was not read. Do not dispose it: the reader still has to find out how much of the input the decoder used before it can look for what follows the entry.Throws Bastion.Archive.ArchiveException when the entry is encrypted, or uses something this reader refuses. |
Checksums and hashes used by the formats, available directly.
| Type | Summary |
|---|---|
Adler32 Class | Adler-32 (RFC 1950 §8.2), the checksum of zlib streams. |
Blake2sp Class | BLAKE2sp: the 8-way parallel BLAKE2s tree mode from the BLAKE2 specification (fanout 8, depth 2, 32-byte inner digests), used by 7-Zip for hashing. Input is dealt to eight leaves in 64-byte blocks round-robin; the root hashes the eight leaf digests. Streaming: Append then Finish. |
Crc32 Class | CRC-32 (IEEE 802.3, polynomial 0x04C11DB7, reflected, initial and final XOR 0xFFFFFFFF) as used by ZIP, gzip, 7z and PNG. Two implementations produce identical results: a slicing-by-16 table path that runs on every target, and a carry-less-multiply folding path (PCLMULQDQ on x64, PMULL on ARM64) selected at run time on .NET Core 3.0 and later when the processor supports it. |
Crc64 Class | CRC-64/XZ (ECMA-182 polynomial 0x42F0E1EBA9EA3693, reflected, initial and final XOR all ones), the check used by the XZ container and reported by 7-Zip as CRC64. Slicing-by-8 table path; runs on every target. |
Sha1Hash Class | SHA-1 over the BCL primitive. Not collision resistant; it exists because WinZip AES authenticates with HMAC-SHA1 and 7-Zip reports SHA1 hashes, never for new designs. |
Sha256Hash Class | SHA-256 over the BCL primitive, with the same streaming shape as the other hashers so callers can treat every digest alike. Used by 7zAES key derivation and the 7-Zip SHA256 hash report. |
Xxh32 Class | XXH32 (xxHash 32-bit, from the published xxHash specification), which is what an LZ4 frame checksums its header, its blocks and its content with. Streaming: call Append any number of times, then Finish. |
Xxh64 Class | XXH64 (xxHash 64-bit, from the published xxHash specification), the frame checksum of Zstandard and LZ4 and a hash 7-Zip reports. Streaming: call Append any number of times, then Finish. |
Bastion.Archive.Hashing
Adler-32 (RFC 1950 §8.2), the checksum of zlib streams.
| Member | Returns | Summary |
|---|---|---|
Append(adler As UInteger, buffer As Byte(), offset As Integer, count As Integer) Shared | UInteger | Continues an Adler-32: adler is the value so far (1 for none).adler — Adler-32 so far, or 1.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Compute(buffer As Byte()) Shared | UInteger | Adler-32 of a whole array.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Bastion.Archive.Hashing
BLAKE2sp: the 8-way parallel BLAKE2s tree mode from the BLAKE2 specification (fanout 8, depth 2, 32-byte inner digests), used by 7-Zip for hashing. Input is dealt to eight leaves in 64-byte blocks round-robin; the root hashes the eight leaf digests. Streaming: Append then Finish.
| Constructor | Summary |
|---|---|
New() | Creates a hasher. |
| Member | Type | Summary |
|---|---|---|
DigestLength const | Integer | Digest length in bytes. |
| Member | Returns | Summary |
|---|---|---|
Append(buffer As Byte(), offset As Integer, count As Integer) | — | Feeds data.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Compute(buffer As Byte()) As Byte() Shared | — | BLAKE2sp of a whole array.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Finish() As Byte() | — | The 32-byte digest of everything appended since the last Reset. Resets the hasher. |
Reset() | — | Returns the hasher to its initial state. |
Bastion.Archive.Hashing
CRC-32 (IEEE 802.3, polynomial 0x04C11DB7, reflected, initial and final XOR 0xFFFFFFFF) as used by ZIP, gzip, 7z and PNG. Two implementations produce identical results: a slicing-by-16 table path that runs on every target, and a carry-less-multiply folding path (PCLMULQDQ on x64, PMULL on ARM64) selected at run time on .NET Core 3.0 and later when the processor supports it.
The folding constants are derived in code from the polynomial (x^n mod P for the fold distances) rather than copied, so the implementation is traceable to the definition alone. Every fold keeps the invariant that the CRC of the data consumed so far equals the CRC of the register bytes followed by the data not yet consumed, which is why the final reduction is simply the table path over the last 16 register bytes.
| Member | Type | Summary |
|---|---|---|
ReflectedPolynomial const | UInteger | The reflected polynomial, 0xEDB88320. |
| Member | Type | Summary |
|---|---|---|
IsHardwareAccelerated Shared read-only | Boolean | True when the carry-less-multiply path is available on this processor and runtime. |
| Member | Returns | Summary |
|---|---|---|
Append(crc As UInteger, buffer As Byte(), offset As Integer, count As Integer) Shared | UInteger | Continues a CRC-32: crc is the value returned for the data so far (0 for none), and the result is the CRC-32 of that data followed by the range. Final values, not internal state.crc — CRC-32 so far, or 0.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Compute(buffer As Byte()) Shared | UInteger | CRC-32 of a whole array.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Compute(buffer As Byte(), offset As Integer, count As Integer) Shared | UInteger | CRC-32 of a range.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Bastion.Archive.Hashing
CRC-64/XZ (ECMA-182 polynomial 0x42F0E1EBA9EA3693, reflected, initial and final XOR all ones), the check used by the XZ container and reported by 7-Zip as CRC64. Slicing-by-8 table path; runs on every target.
| Member | Type | Summary |
|---|---|---|
ReflectedPolynomial const | ULong | The reflected polynomial, 0xC96C5795D7870F42. |
| Member | Returns | Summary |
|---|---|---|
Append(crc As ULong, buffer As Byte(), offset As Integer, count As Integer) Shared | ULong | Continues a CRC-64: crc is the value returned for the data so far (0 for none).crc — CRC-64 so far, or 0.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Compute(buffer As Byte()) Shared | ULong | CRC-64 of a whole array.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Compute(buffer As Byte(), offset As Integer, count As Integer) Shared | ULong | CRC-64 of a range.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Bastion.Archive.Hashing · Implements IDisposable
SHA-1 over the BCL primitive. Not collision resistant; it exists because WinZip AES authenticates with HMAC-SHA1 and 7-Zip reports SHA1 hashes, never for new designs.
| Constructor | Summary |
|---|---|
New() | Creates a hasher. |
| Member | Type | Summary |
|---|---|---|
DigestLength const | Integer | Digest length in bytes. |
| Member | Returns | Summary |
|---|---|---|
Append(buffer As Byte(), offset As Integer, count As Integer) | — | Feeds data.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array; ObjectDisposedException when the hasher has been disposed. |
Compute(buffer As Byte()) As Byte() Shared | — | SHA-1 of a whole array.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Dispose() | — | Releases the resources the object holds. |
Finish() As Byte() | — | The digest of everything appended so far; the hasher is reset for reuse. Throws ObjectDisposedException when the hasher has been disposed. |
Bastion.Archive.Hashing · Implements IDisposable
SHA-256 over the BCL primitive, with the same streaming shape as the other hashers so callers can treat every digest alike. Used by 7zAES key derivation and the 7-Zip SHA256 hash report.
| Constructor | Summary |
|---|---|
New() | Creates a hasher. |
| Member | Type | Summary |
|---|---|---|
DigestLength const | Integer | Digest length in bytes. |
| Member | Returns | Summary |
|---|---|---|
Append(buffer As Byte(), offset As Integer, count As Integer) | — | Feeds data.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array; ObjectDisposedException when the hasher has been disposed. |
Compute(buffer As Byte()) As Byte() Shared | — | SHA-256 of a whole array.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Dispose() | — | Releases the resources the object holds. |
Finish() As Byte() | — | The digest of everything appended so far; the hasher is reset for reuse. Throws ObjectDisposedException when the hasher has been disposed. |
Bastion.Archive.Hashing
XXH32 (xxHash 32-bit, from the published xxHash specification), which is what an LZ4 frame checksums its header, its blocks and its content with. Streaming: call Append any number of times, then Finish.
Not a narrowing of Xxh64. The two are separate functions with different primes, a different stripe (16 bytes against 32), a different set of rotations and a different avalanche, and neither agrees with the other on any input. LZ4 uses this one and Zstandard uses the other, so the library needs both.
| Constructor | Summary |
|---|---|
New() | Creates a hasher with seed 0, the seed LZ4 uses. |
New(seed As UInteger) | Creates a hasher with an explicit seed.seed — The seed. |
| Member | Returns | Summary |
|---|---|---|
Append(buffer As Byte(), offset As Integer, count As Integer) | — | Adds bytes to the message.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Compute(buffer As Byte()) Shared | UInteger | XXH32 of a whole array with seed 0.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Compute(buffer As Byte(), offset As Integer, count As Integer, seed As UInteger) Shared | UInteger | XXH32 of a range, with a seed.buffer — The data.offset — First byte.count — Number of bytes.seed — The seed.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Finish() | UInteger | The hash of everything appended so far. Does not disturb the state, so appending may continue afterwards. |
Reset() | — | Returns the hasher to its starting state, ready for another message. |
Bastion.Archive.Hashing
XXH64 (xxHash 64-bit, from the published xxHash specification), the frame checksum of Zstandard and LZ4 and a hash 7-Zip reports. Streaming: call Append any number of times, then Finish.
| Constructor | Summary |
|---|---|
New() | Creates a hasher with seed 0, the seed every archive format uses. |
New(seed As ULong) | Creates a hasher with an explicit seed.seed — The seed. |
| Member | Returns | Summary |
|---|---|---|
Append(buffer As Byte(), offset As Integer, count As Integer) | — | Feeds data.buffer — The data.offset — First byte.count — Number of bytes.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Compute(buffer As Byte()) Shared | ULong | XXH64 of a whole array with seed 0.buffer — The data.Throws ArgumentNullException when buffer is Nothing. |
Compute(buffer As Byte(), offset As Integer, count As Integer, seed As ULong) Shared | ULong | XXH64 of a range with a seed.buffer — The data.offset — First byte.count — Number of bytes.seed — The seed.Throws ArgumentNullException when buffer is Nothing; ArgumentOutOfRangeException when the range is outside the array. |
Finish() | ULong | The hash of everything appended since the last Reset. The hasher may keep being fed afterwards. |
Reset() | — | Returns the hasher to its initial state. |
Named item properties exposed by format handlers.
| Type | Summary |
|---|---|
ItemProperty Enum | The metadata an archive entry or archive can expose, one member per 7-Zip kpid (RESEARCH-01 §5), so a listing from any handler can be compared with 7z l -slt. Numeric values follow 7-Zip's PropID.h order; ItemPropertyNames gives the 7-Zip spelling of each member. |
ItemPropertyNames Class | Maps each ItemProperty to its 7-Zip kpid spelling, for listings compared against 7z l -slt. |
Bastion.Archive.Properties
The metadata an archive entry or archive can expose, one member per 7-Zip kpid (RESEARCH-01 §5), so a listing from any handler can be compared with 7z l -slt. Numeric values follow 7-Zip's PropID.h order; ItemPropertyNames gives the 7-Zip spelling of each member.
| Name | Value | Summary |
|---|---|---|
NoProperty | 0 | No property (7-Zip kpidNoProperty). |
MainSubfile | 1 | Index of the archive's principal sub-file (kpidMainSubfile). |
HandlerItemIndex | 2 | Handler's own index for the item (kpidHandlerItemIndex). |
Path | 3 | Path inside the archive (kpidPath). |
Name | 4 | File name without directory (kpidName). |
Extension | 5 | File extension (kpidExtension). |
IsDirectory | 6 | Whether the item is a directory (kpidIsDir). |
Size | 7 | Uncompressed size (kpidSize). |
PackedSize | 8 | Compressed size (kpidPackSize). |
Attributes | 9 | Windows attributes, with Unix mode in the high 16 bits where present (kpidAttrib). |
CreationTime | 10 | Creation time (kpidCTime). |
LastAccessTime | 11 | Last access time (kpidATime). |
LastWriteTime | 12 | Last write time (kpidMTime). |
Solid | 13 | Whether the item is in a solid block (kpidSolid). |
Commented | 14 | Whether the item has a comment (kpidCommented). |
Encrypted | 15 | Whether the item is encrypted (kpidEncrypted). |
SplitBefore | 16 | Item continues from a previous volume (kpidSplitBefore). |
SplitAfter | 17 | Item continues into the next volume (kpidSplitAfter). |
DictionarySize | 18 | Dictionary size used (kpidDictionarySize). |
Crc | 19 | CRC of the data (kpidCRC). |
Type | 20 | Archive or item type name (kpidType). |
IsAnti | 21 | Anti-item that deletes on extract (kpidIsAnti). |
Method | 22 | Method string, 7-Zip style (kpidMethod). |
HostOS | 23 | Host operating system (kpidHostOS). |
FileSystem | 24 | File system name (kpidFileSystem). |
User | 25 | Owner user name (kpidUser). |
Group | 26 | Owner group name (kpidGroup). |
Block | 27 | Block index (kpidBlock). |
Comment | 28 | Comment text (kpidComment). |
Position | 29 | Position (kpidPosition). |
Prefix | 30 | Path prefix (kpidPrefix). |
SubdirectoryCount | 31 | Number of sub-directories (kpidNumSubDirs). |
SubfileCount | 32 | Number of sub-files (kpidNumSubFiles). |
UnpackVersion | 33 | Version needed to unpack (kpidUnpackVer). |
Volume | 34 | Volume number (kpidVolume). |
IsVolume | 35 | Whether the item is a volume (kpidIsVolume). |
Offset | 36 | Offset of the item's data (kpidOffset). |
Links | 37 | Link count (kpidLinks). |
BlockCount | 38 | Number of blocks (kpidNumBlocks). |
VolumeCount | 39 | Number of volumes (kpidNumVolumes). |
TimeType | 40 | Time representation type (kpidTimeType). |
Is64Bit | 41 | 64-bit image (kpidBit64). |
BigEndian | 42 | Big-endian data (kpidBigEndian). |
Cpu | 43 | Target processor (kpidCpu). |
PhysicalSize | 44 | Physical size of the archive (kpidPhySize). |
HeadersSize | 45 | Size of the headers (kpidHeadersSize). |
Checksum | 46 | Checksum (kpidChecksum). |
Characteristics | 47 | PE characteristics (kpidCharacts). |
VirtualAddress | 48 | Virtual address (kpidVa). |
Id | 49 | Identifier (kpidId). |
ShortName | 50 | 8.3 short name (kpidShortName). |
CreatorApplication | 51 | Creating application (kpidCreatorApp). |
SectorSize | 52 | Sector size (kpidSectorSize). |
PosixAttributes | 53 | POSIX mode bits (kpidPosixAttrib). |
SymbolicLink | 54 | Symbolic link target (kpidSymLink). |
ErrorMessage | 55 | Error text (kpidError). |
TotalSize | 56 | Total size of a volume (kpidTotalSize). |
FreeSpace | 57 | Free space (kpidFreeSpace). |
ClusterSize | 58 | Cluster size (kpidClusterSize). |
VolumeName | 59 | Volume label (kpidVolumeName). |
LocalName | 60 | Local name (kpidLocalName). |
Provider | 61 | Provider (kpidProvider). |
NtSecurity | 62 | NT security descriptor (kpidNtSecure). |
IsAlternateStream | 63 | NTFS alternate data stream (kpidIsAltStream). |
IsAuxiliary | 64 | Auxiliary item (kpidIsAux). |
IsDeleted | 65 | Deleted item (kpidIsDeleted). |
IsTree | 66 | Tree node (kpidIsTree). |
Sha1 | 67 | SHA-1 digest (kpidSha1). |
Sha256 | 68 | SHA-256 digest (kpidSha256). |
ErrorType | 69 | Error type (kpidErrorType). |
ErrorCount | 70 | Number of errors (kpidNumErrors). |
ErrorFlags | 71 | Error flags (kpidErrorFlags). |
WarningFlags | 72 | Warning flags (kpidWarningFlags). |
Warning | 73 | Warning text (kpidWarning). |
StreamCount | 74 | Number of streams (kpidNumStreams). |
AlternateStreamCount | 75 | Number of alternate streams (kpidNumAltStreams). |
AlternateStreamsSize | 76 | Total size of alternate streams (kpidAltStreamsSize). |
VirtualSize | 77 | Virtual size (kpidVirtualSize). |
UnpackSize | 78 | Unpacked size of the archive (kpidUnpackSize). |
TotalPhysicalSize | 79 | Total physical size across volumes (kpidTotalPhySize). |
VolumeIndex | 80 | Volume index (kpidVolumeIndex). |
SubType | 81 | Sub-type name (kpidSubType). |
ShortComment | 82 | Short comment (kpidShortComment). |
CodePage | 83 | Code page of names (kpidCodePage). |
IsNotArchiveType | 84 | Data is not of the archive type (kpidIsNotArcType). |
PhysicalSizeCannotBeDetected | 85 | Physical size cannot be determined (kpidPhySizeCantBeDetected). |
ZerosTailIsAllowed | 86 | Zero tail is permitted (kpidZerosTailIsAllowed). |
TailSize | 87 | Bytes after the archive end (kpidTailSize). |
EmbeddedStubSize | 88 | Bytes before the archive start, such as an SFX stub (kpidEmbeddedStubSize). |
NtReparsePoint | 89 | NT reparse point data (kpidNtReparse). |
HardLink | 90 | Hard link target (kpidHardLink). |
Inode | 91 | Inode number (kpidINode). |
StreamId | 92 | Stream identifier (kpidStreamId). |
IsReadOnly | 93 | Read-only flag (kpidReadOnly). |
OutputName | 94 | Output name (kpidOutName). |
CopyLink | 95 | Copy link (kpidCopyLink). |
ArchiveFileName | 96 | Archive file name (kpidArcFileName). |
IsHash | 97 | Item is a hash record (kpidIsHash). |
ChangeTime | 98 | Metadata change time (kpidChangeTime). |
UserId | 99 | Owner user id (kpidUserId). |
GroupId | 100 | Owner group id (kpidGroupId). |
DeviceMajor | 101 | Device major number (kpidDeviceMajor). |
DeviceMinor | 102 | Device minor number (kpidDeviceMinor). |
DeviceNodeMajor | 103 | Device node major number (kpidDevMajor). |
DeviceNodeMinor | 104 | Device node minor number (kpidDevMinor). |
UserDefined | 65536 | First user-defined property (kpidUserDefined = 0x10000). |
Bastion.Archive.Properties
Maps each ItemProperty to its 7-Zip kpid spelling, for listings compared against 7z l -slt.
| Member | Type | Summary |
|---|---|---|
All Shared read-only | IReadOnlyDictionary(Of ItemProperty, String) | Every defined property with its 7-Zip name. |
| Member | Returns | Summary |
|---|---|---|
ToSevenZipName(itemProperty As ItemProperty) Shared | String | The 7-Zip kpid name of a property, for example "kpidMTime" for LastWriteTime.itemProperty — The property.Throws ArgumentOutOfRangeException when itemProperty is not a defined member. |
ExtractionPolicy and the settings that keep extraction inside its target.
| Type | Summary |
|---|---|
ExtractionPolicy Class | Controls what an extractor will accept from an archive. A new instance carries the secure defaults; loosen individual settings deliberately. Every limit is enforced before data reaches disk. |
LinkPolicy Enum | How an extractor treats symbolic links and hard links found in an archive. |
OverwriteMode Enum | What an extractor does when an output file already exists. |
PathPlatform Enum | The file-system conventions an extracted path must satisfy. |
Bastion.Archive.Security
Controls what an extractor will accept from an archive. A new instance carries the secure defaults; loosen individual settings deliberately. Every limit is enforced before data reaches disk.
Defaults: paths are canonicalised (no absolute paths, parent traversal, drive letters, UNC prefixes, NTFS alternate streams, Windows reserved names, trailing dots or spaces); links are skipped; existing output fails the entry; checksums are verified before data is exposed; overlapping and duplicate entries are refused; a nesting depth of 256, ten million entries, one tebibyte in total and a compression ratio of 100 000:1 (checked once 16 MiB has been produced) are the limits. Mark-of-the-Web propagation is opt-in.
| Constructor | Summary |
|---|---|
New() | Creates a policy with the secure defaults. |
| Member | Type | Summary |
|---|---|---|
AllowAbsolutePaths | Boolean | Accept entry paths that are absolute (rooted). Default False. |
AllowParentTraversal | Boolean | Accept entry paths containing a .. segment. Default False. |
HardLinks | LinkPolicy | Treatment of hard links. Default Skip. |
MaxCompressionRatio | Long | Maximum ratio of produced bytes to consumed bytes, checked once RatioCheckThresholdBytes have been produced. Default 100 000. Zero disables the limit. Zip bombs of the Fifield class exceed 28 000 000.Throws ArgumentOutOfRangeException when the value is negative. |
MaxEntryCount | Long | Maximum entries the operation may process. Default ten million. Zero disables the limit. Throws ArgumentOutOfRangeException when the value is negative. |
MaxNestingDepth | Integer | How many archives-within-archives may be opened automatically. Default 256. Zero means never open a nested archive. The default is deliberately generous, so that browsing through nested archives behaves as an interactive archiver does rather than stopping short of what a caller can plainly see. It is not free: each level is opened by reading the one above it, so depth costs work, and depth is cheap to manufacture — 250 levels of 7z fit in 29 KB. A caller embedding this in a server that accepts archives from strangers should lower it, and zero refuses to open a nested archive at all. Throws ArgumentOutOfRangeException when the value is negative. |
MaxPathLength | Integer | Longest output path accepted, in characters. Default 0 means the platform's own limit. Throws ArgumentOutOfRangeException when the value is negative. |
MaxTotalBytes | Long | Maximum bytes the operation may produce in total. Default 1 TiB. Zero disables the limit. Throws ArgumentOutOfRangeException when the value is negative. |
Overwrite | OverwriteMode | What happens when output already exists. Default FailIfExists. |
PreserveAttributes | Boolean | Apply the archive's attributes and Unix permissions where the platform supports them. Default True. |
PreserveTimestamps | Boolean | Apply the archive's timestamps to extracted files. Default True. |
PropagateMarkOfTheWeb | Boolean | On Windows, copy the archive's Zone.Identifier stream to every extracted file. Default False. |
RatioCheckThresholdBytes | Long | Produced bytes before MaxCompressionRatio is enforced, so tiny highly compressible files are not rejected. Default 16 MiB.Throws ArgumentOutOfRangeException when the value is negative. |
RefuseDuplicateEntries | Boolean | Fail with DuplicateEntry when two entries share a canonical path. Default True. |
RefuseOverlappingEntries | Boolean | Fail with OverlappingEntry when two entries' data ranges overlap. Default True. |
RejectAlternateStreams | Boolean | Refuse entry names containing an NTFS alternate data stream separator. Default True. |
RejectReservedNames | Boolean | Refuse Windows reserved device names (CON, NUL, COM1 …) and names with trailing dots or spaces. Default True. |
SymbolicLinks | LinkPolicy | Treatment of symbolic links. Default Skip. |
VerifyChecksums | Boolean | Verify CRCs, hashes and MACs before exposing data. Default True. Turning this off is only for damaged-archive recovery. |
Bastion.Archive.Security
How an extractor treats symbolic links and hard links found in an archive.
| Name | Value | Summary |
|---|---|---|
Skip | 0 | Do not create the link; record an EntrySkipped warning. This is the default. |
CreateValidated | 1 | Create the link only when its target resolves inside the extraction root; otherwise skip it with a warning. |
Reject | 2 | Fail the operation with LinkRejected on the first link. |
Bastion.Archive.Security
What an extractor does when an output file already exists.
| Name | Value | Summary |
|---|---|---|
FailIfExists | 0 | Fail the entry with OutputExists. This is the default. |
Skip | 1 | Leave the existing file and record an EntrySkipped warning. |
Overwrite | 2 | Replace the existing file, writing to a temporary file first and swapping atomically. |
Ask | 3 | Ask through ItemExists: fail, skip, overwrite, or write under another name. With no handler, or no monitor, this fails as FailIfExists does. |
Bastion.Archive.Security
The file-system conventions an extracted path must satisfy.
| Name | Value | Summary |
|---|---|---|
Windows | 0 | Windows: reserved device names, no <>:"|?* or control characters, no trailing dots or spaces, : opens an alternate stream. |
Unix | 1 | Linux and macOS: only / and NUL are forbidden in a name. |
Self-extracting archive options and information.
| Type | Summary |
|---|---|
SelfExtractorInfo Class | What SelfExtractor found at the front of an archive that is also a program: how long the program is and, for one this library wrote, the settings it runs with. |
SelfExtractorOptions Class | How a self-extracting archive behaves when it is run: what its window says, where it offers to extract, whether it asks at all, and what it runs afterwards. |
Bastion.Archive.Sfx
What SelfExtractor found at the front of an archive that is also a program: how long the program is and, for one this library wrote, the settings it runs with.
| Member | Type | Summary |
|---|---|---|
IsOwnStub read-only | Boolean | True when the executable is this library's own stub. |
Options read-only | SelfExtractorOptions | The settings the stub runs with, when it is this library's own; otherwise Nothing. |
PrefixLength read-only | Long | Bytes of executable before the archive begins. |
Bastion.Archive.Sfx
How a self-extracting archive behaves when it is run: what its window says, where it offers to extract, whether it asks at all, and what it runs afterwards.
Every setting travels inside the executable and is read by its stub when it starts. The command line can override them: -s extracts silently, -d "folder" chooses the folder and -p "password" supplies a password. The program exits with 0 when everything was extracted, 1 when something failed and 2 when the user cancelled.
| Constructor | Summary |
|---|---|
New() | Creates the options with the defaults every property below describes. |
| Member | Type | Summary |
|---|---|---|
DefaultDirectory | String | The folder offered, or used when silent. Environment variables such as %TEMP% are expanded when the archive runs. Nothing means a folder beside the executable, named after it. |
IconPath | String | An icon file (.ico) for the executable, shown by Explorer and in the window, or Nothing for the stub's own. It is read when the archive is created and not needed afterwards. |
LicenceText | String | A licence the user must accept before anything is extracted, or Nothing for none. Silent extraction shows no window, so whoever runs a licensed archive silently is taken to have accepted it. |
Overwrite | OverwriteMode | What to do about files that already exist. Ask, the default, asks in the window and fails when silent. |
Prompt | String | A message shown above the folder box, or Nothing for none. |
RunAfterExtraction | String | A file to open once everything is extracted — a setup program, a read-me — given as a path inside the extracted folder, or Nothing for none. A path that would lead outside that folder is refused. |
RunArguments | String | Arguments for RunAfterExtraction, or Nothing. |
Silent | Boolean | Extract without showing a window. The command line can ask for this too, with -s. |
Title | String | The window title. Defaults to "Self-extracting archive". |