-
Notifications
You must be signed in to change notification settings - Fork 0
Pipeline
ZArchiveSharp.Pipeline is the shared engine behind ZArchiveTool, the XISO .zar bridges and the CLI: directory pack, archive extract, and parallel batches with progress, pause, cancellation and collision handling. One engine, one set of semantics.
using ZArchiveSharp.Pipeline;
// Minimal: defaults are zstd level 6, Fail policy, 4 workers
string? result = ZarPipeline.Pack(@"C:\mydata");
// Returns the archive path written (after collision resolution),
// or null when Skip policy skipped an existing output.
string? path = ZarPipeline.Pack(
sourceDirectory: @"C:\mydata",
zarPath: @"C:\mydata.zar", // null => <stem>.zar next to input
options: new ZarPipelineOptions { Level = 9 },
progress: new Progress<ZarProgress>(p => Console.WriteLine($"{p.Ratio:P1}")));The source is validated and collected before the output is resolved, so an
Overwrite policy cannot delete a previous archive when the pack cannot even
start (missing/unreadable source). With DeleteSourceOnSuccess, the source is
removed only after the archive was written successfully.
using ZArchiveSharp.Pipeline;
// Returns extracted file paths relative to the archive root ('/' separated)
IReadOnlyList<string> files = ZarPipeline.Extract(
zarPath: @"C:\mydata.zar",
destDir: @"C:\mydata_out",
progress: new Progress<ZarProgress>(p => Console.WriteLine($"{p.Ratio:P1}")));Anything that can enumerate (path, size, content stream) pairs can be packed via IZarPackSource — the directory tree and the XISO walk are both built on it:
using ZArchiveSharp.Pipeline;
ZarPipeline.PackSource(mySource, @"C:\out.zar", options);ZarPipelineOptions controls all pack/extract work. Defaults keep zarchive.exe codec parity (zstd level 6, no checksums) and use 4 workers for batch/block parallelism.
| Property | Type | Default | Meaning |
|---|---|---|---|
Level |
int |
6 | zstd level 1–22 for packing (ignored for extract) |
Checksum |
bool |
false |
Per-block content checksums |
Compressor |
IZarBlockCompressor? |
null |
Explicit compressor; overrides Level/Checksum
|
DeterministicOrder |
bool |
true |
Sort entries ordinally for reproducible archives |
CollisionPolicy |
ZarCollisionPolicy |
Fail |
Output-exists behavior |
MaxDegreeOfParallelism |
int |
4 | Batch workers (clamped ≥ 1; effective = min(workers, items)) plus 64 KiB block fan-out inside a single pack/extract (capped by processor count; byte-identical) |
DeleteSourceOnSuccess |
bool |
false |
Delete pack source after success (off by default — a library must not destroy inputs unless asked) |
Pause |
PauseToken |
default | Pause gate checked alongside the cancellation token |
NameOrder |
IReadOnlyList<string>? |
null |
Pre-seeded name-table order; null = pack order. Pass a source-walk (discovery) order for byte-parity with packers that write names in discovery order |
var options = new ZarPipelineOptions
{
Level = 19,
CollisionPolicy = ZarCollisionPolicy.AutoRename,
MaxDegreeOfParallelism = 8,
DeterministicOrder = true,
};Progress is reported via IProgress<ZarProgress>:
var progress = new Progress<ZarProgress>(p =>
{
Console.WriteLine($"{p.Operation} {p.CurrentFile}: {p.FilesCompleted}/{p.FilesTotal} {p.Ratio:P1}");
});ZarProgress fields:
| Field | Meaning |
|---|---|
Operation |
Pack or Extract
|
SourcePath |
Item being processed |
DestinationPath |
Output path |
CurrentFile |
File within the item (empty between files) |
FilesCompleted / FilesTotal
|
Pre-scanned totals |
BytesCompleted / BytesTotal
|
Byte progress when known |
Ratio |
0–1 fraction (bytes when known, else files) |
Totals are pre-scanned, so Ratio moves monotonically 0→1 within one SourcePath. Batch runs re-base each item's ratio into its 1/n share (completed items plus the in-flight fraction).
using ZArchiveSharp.Pipeline;
using var cts = new CancellationTokenSource();
using var pauseSource = new PauseTokenSource();
var options = new ZarPipelineOptions { Pause = pauseSource.Token };
var task = Task.Run(() =>
ZarPipeline.Pack(@"C:\bigdata", @"C:\big.zar", options, null, cts.Token));
// Pause mid-run (checked at the same points as cancellation)
pauseSource.Pause();
await Task.Delay(5000);
pauseSource.Resume();
// Or cancel entirely — in-flight work stops; incomplete pack outputs are deleted
cts.Cancel();Cancellation deletes incomplete pack outputs (the zarchive.exe delete-incomplete-output contract). PauseTokenSource is IDisposable (since v1.2.0): dispose it after the workers have stopped, while no PauseToken obtained from it can still be waited on.
What happens when an output path already exists:
| Policy | Behavior |
|---|---|
Fail |
Throw IOException (default; preserves the zarchive.exe refuse-overwrite contract) |
Skip |
Skip the item — reported as ZarItemStatus.Skipped
|
Overwrite |
Delete the existing output, then write |
AutoRename |
Write to {stem}_{n}{suffix}, first free n from 1 |
Skip, Overwrite and AutoRename serve batch runs; Fail preserves the native CLI refuse-overwrite contract.
Resolution is race-safe: parallel batch items (or other processes) can claim
the chosen name between resolve and write/move, so the pack and move paths
re-resolve from the requested path and retry, keeping AutoRename suffixes
canonical (game.zar, game_1.zar, …) instead of compounding. Fail-policy
collisions throw IOException prefixed with ZarPackEngine.OutputExistsMessage
(batch callers map a batch whose only failures are these to exit -11).
using ZArchiveSharp.Pipeline;
var results = ZarPipeline.PackBatch(
sourceDirectories: [@"C:\game1", @"C:\game2", @"C:\game3"],
destDir: @"C:\archives", // null => each .zar next to its source
options: new ZarPipelineOptions { MaxDegreeOfParallelism = 4 },
progress: progress);
foreach (var r in results)
{
Console.WriteLine($"{r.SourcePath}: {r.Status} {r.ErrorMessage}");
}Semantics:
- Worker count is
min(MaxDegreeOfParallelism, items)— never spins up more tasks than items -
One item's failure does not stop the others; per-item outcomes come back as
ZarItemResult - Batch progress re-bases per-item ratios into
1/nshares
Each 64 KiB .zar block is an independent zstd frame, so a single
pack/extract also fans out: BlockWorkers() is
min(max(1, MaxDegreeOfParallelism), ProcessorCount), and blocks are
always emitted in input order — parallel bytes equal sequential bytes
at every level, with or without dictionaries/checksums.
-
ZArchiveWritertakes an optional per-workercompressorFactory(oneZstdCompressorper worker); explicitIZarBlockCompressorinstances stay sequential. - Extract decodes bounded waves (
min(workers × 4, 64)blocks) with the sequential corruption contract. - A default options pack now uses up to 4 block workers instead of
1 thread — pass
MaxDegreeOfParallelism = 1for the old profile. Note batch × block workers multiply (PackBatchat DOP 4 can reach ~16 codec threads on many-core hosts).
var results = ZarPipeline.ExtractBatch(
zarPaths: [@"C:\a.zar", @"C:\b.zar"],
destRoot: @"C:\extracted",
options: options);Same-stem archives (a\game.zar, b\game.zar) are given unique destinations
within the batch (game_extracted, game_extracted_1, …) so parallel items
never extract over each other.
ZarPackEngine treats archive entry names as untrusted:
- names must be single plain components —
.., separators, rooted or drive-qualified paths, and Windows reserved device names are rejected (InvalidOperationException); - the resolved destination path is re-validated against the extraction root (zip-slip defense in depth);
- nesting deeper than
ZarPackEngine.MaxExtractDepth(1024) fails catchably instead of risking a stack overflow; - each file is written through a unique
.partscratch file and moved into place only after the byte-count check, so disk-full, corruption, or cancellation never leaves a truncated file that looks complete.
Packing never descends directory symlinks/junctions (reparse points): the link itself is archived as an empty directory entry, so a tree cannot loop forever or pull in content from outside the source root.
For UIs, a ZarBatchRequest models the full input set with modes and collision handling; ZarItemResult/ZarItemStatus carry outcomes (Completed, Skipped, Failed, ...).
SevenZip (ZArchiveSharp.Pipeline) is the library half of the archive-container stage:
FindTool locates the external binary (explicit path, then the standard
Windows install location, then 7z/7zz on PATH) and Extract runs it
via ProcessRunner (x for full paths, -bsp1 for progress, exit 0/1
accepted). PickIsoCandidate selects the first .iso out of the extracted
tree. The CLI owns the orchestration (temp dir, collision-resolved moves,
extract-archive terminal vs. continue-to-.zar, source cleanup), since the
ISO→.zar leg needs the CLI-side XISO bridge.
ZarchiveCli.Run is the callable form of the zarchive.exe input_path [output_path] contract: directory input packs, file input extracts, outputs default to <stem>.zar / <stem>_extracted, existing pack outputs are refused, incomplete outputs are deleted.
using ZArchiveSharp.Pipeline;
int code = ZarchiveCli.Run(
args: ["C:\\mydata", "C:\\mydata.zar"],
options: options,
log: Console.WriteLine); // receives the native stdout chatterExit codes (same negative values main() returns; -2, -5–-9 unused upstream too):
| Code | Constant | Meaning |
|---|---|---|
0 |
Ok |
Success |
-1 |
BadUsage |
Too many paths; input neither file nor directory |
-3 |
OutputNotDirectory |
Extract output exists and is not a directory |
-4 |
OutputDirectoryNotCreated |
Could not create output directory |
-10 |
NotFound |
Archive not found; pack output not a regular file |
-11 |
Refused |
Archive failed to open; pack output already exists |
-12 |
ExtractionFailed |
Corrupt archive or extract I/O |
-13 |
PackFailed |
Archive structure error |
-14 |
ArchiveEntryFailed |
Duplicate or bad path |
-15 |
InputNotReadable |
Could not open an input file |
-16 |
PackOutputFailed |
Output I/O error |
Automation matching on these codes keeps working unchanged.
With log supplied, ZarchiveCli reproduces native chatter:
- Pack:
Outputting to: ...thenAdding <path>per file (OS separators in the display line) - Extract:
Extracting to: ...then per-entry lines with the native leading-/quirk, directories included, preorder — even on mid-archive failure
Where native behavior is a bug, ZArchiveSharp deviates (all tested):
- An unopenable extract output throws (native prints
Unable to write file:then keeps writing into the dead stream) - A mid-file input read error fails the pack with
-16(native treats a short read as EOF and silently packs a truncated file) - Error-string paths use
/on every OS (nativepathEntry.string()prints\on Windows; only theAddingdisplay line converts)
| Exception | When |
|---|---|
ZarArchiveOpenException |
Archive failed to open/validate (maps to -11) |
ZarInputOpenException |
Pack could not open an input file (maps to -15) |
ZarEntryCreateException |
Duplicate or bad archive path (maps to -14) |
IOException |
Output I/O errors (maps to -16); also Fail-policy collisions |
InvalidOperationException |
Archive structure errors (maps to -13) |
OperationCanceledException |
Cancelled — propagated, never swallowed |
-
ZarSettings— AOT-safe JSON settings: defaults, merge-forward load, per-user save location -
ProcessableFiles.Find(dir, ZarProcessMode.Auto)— ARCHIVE/ISO file sets, non-recursive scan, ordinal sort
ProcessRunner is the seam for external tools (e.g., 7z): (\d+)% progress parsing with a 10 FPS throttle, exit 0/1 treated as ok, anything else throws with the last output line, WinError 740 mapped to an elevation message, missing binaries get an AV-hint. Since v1.2.0 stderr is drained after exit (a late-only failure line is still reported) and the child is polled, so one that closed stdout but keeps running is killed on cancellation.
ZArchiveSharp
Getting Started
Core Concepts
API
Advanced
Releases
Links