@blamejs/core 0.12.7 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/archive-tar-read.js +418 -0
- package/lib/archive-tar.js +557 -0
- package/lib/archive.js +5 -0
- package/lib/audit.js +22 -7
- package/lib/backup/index.js +196 -12
- package/lib/guard-archive.js +40 -0
- package/lib/safe-archive.js +35 -15
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.8 (2026-05-23) — **`b.archive.tar` + `b.archive.read.tar` — POSIX pax tar format end-to-end + `b.guardArchive.tarEntryPolicy` + `b.backup` tar bundle default.** Tar lands as the second format in the archive family. `b.archive.tar()` builds POSIX pax archives (ustar magic + pax extended headers for >100-char names, >8 GiB sizes, nanosecond mtime); `b.archive.read.tar(adapter)` walks the 512-byte block sequence with the same bomb-cap + path-traversal + entry-type defenses that ZIP read shipped at v0.12.7. Tar's natively-streamable shape means `b.archive.adapters.trustedStream(readable)` is a first-class extract path here (no CD-walk required since tar has no central directory; sequential header-by-header is the canonical adversarial-safe path). `b.guardArchive.tarEntryPolicy` ships as the tar-specific entry-shape policy beyond `entryTypePolicy` — handles typeflag 0/5 (regular/directory) by default, refuses 1/2 (hardlink/symlink) unless `allowDangerous` is set with the realpath-on-link-target dual-check, and refuses 3/4/6/7 (char-device/block-device/FIFO/contiguous-file) unconditionally. `b.backup.bundleAdapterStorage({ format: "tar" })` becomes the default for new bundles — directory-tree format stays available via `format: "directory"` for back-compat with v0.12.7 bundles. `b.backup.migrate(from, to)` one-shot helper converts v0.12.7 directory bundles to v0.12.8 tar bundles transparently. `b.safeArchive.extract({ source, destination, format: "auto" })` now sniffs ustar magic at offset 257 inside the first 512-byte block and dispatches to the tar reader automatically. CVE coverage extends to the tar class: CVE-2026-23745 / 2026-24842 (node-tar symlink+hardlink path resolution), CVE-2025-4517 PATH_MAX TOCTOU (the v0.12.7 dual-check carries through), CVE-2025-11001/11002 (symlink TOCTOU on extract), CVE-2024-12905 / 2025-48387 (tar-fs traversal), CVE-2025-4138/4330 (Python tarfile data filter bypass). **Added:** *`b.archive.tar()` — POSIX pax write builder* — Mirrors `b.archive.zip()`'s contract: `addFile(name, content, opts?)` + `addDirectory(name, opts?)` + `toBuffer()` + `toStream(writable)` + `toAdapter(adapter)` + `digest()`. Emits ustar-magic 512-byte header blocks with the standard 11-field prefix (name / mode / uid / gid / size / mtime / chksum / typeflag / linkname / magic / version / uname / gname / devmajor / devminor / prefix). Names >100 chars + sizes >8 GiB + mtime with nanosecond precision get a pax extended header (typeflag=x) preceding the entry; the extended header records (per POSIX.1-2001 §4.18) carry the `path` / `size` / `mtime` / `atime` / `ctime` fields that overflow ustar's fixed widths. Determinism opts: `{ fixedMtime: 0, ignoreOrder: false }` for reproducible builds (matches the ZIP write side). · *`b.archive.read.tar(adapter, opts)` — sequential + random-access tar reader* — Walks 512-byte header blocks in order. `inspect()` enumerates entries without decompressing; `extract({ destination })` decompresses entry-by-entry with the same bomb-cap + path-traversal + entry-type defenses as ZIP read. Trusted-stream adapters are first-class here — tar has no central directory, so sequential header-by-header walk IS the canonical adversarial-safe path (`b.archive.adapters.trustedStream(readable)` and `b.archive.adapters.fs/buffer/objectStore/http` all flow through the same reader). Per-entry path safety routes through `b.guardFilename.verifyExtractionPath` (the v0.12.7 dual-check). Refuses to overwrite pre-existing destination files (carries the v0.12.7 atomic-rollback contract). · *`b.guardArchive.tarEntryPolicy(opts)` — tar-specific entry-type policy* — Defaults: typeflag 0 (regular file) + 5 (directory) extract; typeflag 1 (hardlink) + 2 (symlink) refused unless `allowDangerous: { symlinks: true, hardlinks: true }` is set; typeflag 3 (char-device) + 4 (block-device) + 6 (FIFO) + 7 (contiguous-file) refused unconditionally. When `allowDangerous` is set, link target is routed through `b.guardFilename.verifyExtractionPath` against the extraction root — the realpath-on-link-target check defends the CVE-2026-23745 / 24842 node-tar class where the safety check and creation logic diverged on path resolution. Pax extended-header (x) + global-header (g) entries consumed by the reader (merged into the following entry's metadata); operators never see them as standalone entries. · *`b.backup.bundleAdapterStorage({ format: "tar" })` — tar bundle becomes default* — New bundles ship as a single tar archive instead of a directory tree. Restore via `b.archive.read.tar` (with the operator-supplied adapter routing the bytes). `format: "directory"` opts back into the v0.12.7 layout for operators with existing bundles. `format: "tar"` is the new default; `b.backup.diskStorage` stays back-compat at the legacy directory-tree format. · *`b.backup.migrate(opts)` — directory → tar bundle migration* — One-shot helper that walks an operator's directory-tree-format bundle (v0.12.7 layout) and writes the same content as a tar-format bundle via the v0.12.8 bundleAdapterStorage. Idempotent: re-running on an already-migrated bundle is a no-op. Source bundle stays in place until the migrate succeeds; operators with explicit transition windows pass `{ deleteSourceOnSuccess: true }` to opt into the inline replace. · *`b.safeArchive.extract({ format: "auto" })` recognizes tar* — Format auto-sniff now dispatches `ustar` magic at offset 257 inside the first 512-byte header block to the tar reader. ZIP magic + tar magic + GZIP magic (v0.12.9) live in the same sniff path; operators with mixed-format pipelines pass `format: "auto"` once + the orchestrator picks the right reader. **Security:** *Symlink + hardlink path resolution (CVE-2026-23745 / CVE-2026-24842 node-tar class)* — node-tar < 7.5.7 / ≤ 7.5.2 shipped a divergence between its hardlink safety check (which used one path resolution) and its hardlink creation logic (which used another). When `allowDangerous: { hardlinks: true }` is set, blamejs routes the link target through `b.guardFilename.verifyExtractionPath` — the SAME primitive that the eventual `link()` call resolves against — so check + create agree by construction. Symlink targets same shape. · *Path traversal (CVE-2024-12905 / CVE-2025-48387 tar-fs + CVE-2025-4138 / 4330 Python tarfile data filter bypass)* — Every entry name passes through `b.guardFilename.verifyExtractionPath` — the v0.12.7 dual-check that refuses pre-resolve names > PATH_MAX (4096 bytes) AND verifies the string-normalize + `fs.realpath` resolutions agree on the same final path. Defends the CVE-2025-4517 / 4138 / 4330 class where the operator's path resolution and the kernel's diverge silently past PATH_MAX. · *Symlink TOCTOU on extract (CVE-2025-11001 / CVE-2025-11002 7-Zip class)* — When `allowDangerous: { symlinks: true }` opts symlinks in, the reader resolves the link target via `verifyExtractionPath` against the extraction root BEFORE calling `fs.symlink` — so the resolved target is inside the trust boundary by construction. The v0.12.7 atomic-rollback contract carries through: any single entry failure aborts the whole extract + cleans up only newly-created files (pre-existing destination files refused at the pre-write check). **Detectors:** *`tar-extract-allow-dangerous-without-link-target-check`* — Flags any `b.archive.read.tar(adapter).extract({ allowDangerous: ... })` call site in `lib/` that doesn't route the link target through `b.guardFilename.verifyExtractionPath` against the extraction root. Forces the dual-check discipline at every allow-dangerous opt-in — operators with hardlink / symlink extract needs see the realpath check at the call site. · *`tar-entry-typeflag-without-policy`* — Flags `lib/archive-tar.js` extract code paths that switch on typeflag without composing `b.guardArchive.tarEntryPolicy` for the type-allowlist decision. Locks the shape: every typeflag dispatch goes through the policy, never inline. · *`backup-migrate-without-source-preserve`* — Flags `b.backup.migrate(opts)` call sites that pass `deleteSourceOnSuccess: true` without an operator-stated justification comment. Default is preserve-source; deletes need an explicit reason. **References:** [POSIX.1-2001 pax extended format (IEEE 1003.1)](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html) · [CVE-2026-23745 — node-tar symlink+hardlink path resolution](https://www.sentinelone.com/vulnerability-database/cve-2026-23745/) · [CVE-2026-24842 — node-tar hardlink path resolution](https://github.com/advisories/GHSA-34x7-hfp2-rc4v) · [CVE-2025-4517 — Python tarfile PATH_MAX bypass (CVSS 9.4)](https://nvd.nist.gov/vuln/detail/CVE-2025-4517) · [CVE-2025-4138 / CVE-2025-4330 — Python tarfile data filter](https://github.com/0xDTC/CVE-2025-4138-4517-POC) · [CVE-2025-11001 / CVE-2025-11002 — 7-Zip symlink TOCTOU on extract](https://www.sentinelone.com/vulnerability-database/cve-2025-11001/) · [CVE-2024-12905 / CVE-2025-48387 — node-tar-fs path traversal](https://vulert.com/vuln-db/debian-11-node-tar-fs-193050)
|
|
12
|
+
|
|
11
13
|
- v0.12.7 (2026-05-23) — **`b.archive.read` — random-access ZIP reader + adapter substrate + `b.safeArchive.extract` orchestrator.** The framework's ZIP primitive grows a read side. `b.archive.read.zip(adapter, opts)` walks the central directory, validates LFH/CD coherence (defeats the malformed-zip / Zip Slip CD-skew class), bounds decompression with operator-declared bomb caps (per-entry size, total bytes, expansion ratio, entry count), and refuses path-traversal + symlink-shaped entries before any byte hits the destination. The adapter contract (`{ size, range(offset, length) }` for random-access; `{ readable }` for the trusted-stream fallback) unifies how operators feed bytes in — local files, `b.objectStore` buckets, HTTP Range fetches, and in-memory buffers all compose the same reader. `b.safeArchive.extract({ source, destination, ... })` ships as the one-liner orchestrator that combines read + guardArchive + path-safety + bomb caps + extract for the common `untar a hostile archive into a quarantine directory` shape. `b.guardArchive` gains `inspect(adapter)` (entry-list enumeration that doesn't decompress), `zipBombPolicy(...)` and `entryTypePolicy(...)` policy-object builders so operators can declare their cap set once + reuse it. `b.guardFilename.verifyExtractionPath(name, root, opts?)` adds the dual-check (string-normalize + `fs.realpath`-agreement) that defends the CVE-2025-4517 PATH_MAX TOCTOU class — refuses paths > 4096 bytes BEFORE the kernel's realpath truncation hits, then verifies the string and fs resolution agree on the same final path. `b.backup` gains `bundleAdapterStorage(adapter, opts)` — the first non-disk transport backend; substrate for the v0.12.8 tar bundle format + v0.12.11 objectStoreStorage. Closes the no-MVP gap from v0.5.15 where `b.archive` shipped write-only and the operator-facing JSDoc explicitly punted reading + extraction to yauzl / `unzip`. **Added:** *`b.archive.read.zip(adapter, opts)` — random-access ZIP reader* — Walks the end-of-central-directory record, validates every CD entry against its LFH (offset / size / CRC / method / name agreement), and emits an iterator of `{ name, size, compressedSize, crc, method, mtime, isEncrypted, externalAttrs, extraFields }` entries. `inspect()` returns the entry list without decompressing — operators wire `b.guardArchive` against the inspect output before paying a single decompress cycle. `extract({ destination, ... })` decompresses entry-by-entry via `node:zlib` raw inflate, routes every path through `b.guardFilename.verifyExtractionPath`, and enforces zip-bomb caps as a streaming abort (the partial extract is fs.rm-ed before the error throws). Composes the existing `lib/archive.js` write-side CRC + signature constants — no duplicated wire-format knowledge. · *Adapter contract — `b.archive.adapters.{fs,objectStore,http,buffer,trustedStream}`* — One shape for source bytes: `{ size: <number>, range(offset, length): Promise<Buffer> }` for random-access, or `{ readable: Readable }` for the explicit trust-stream fallback. `fs(path)` opens a file descriptor + range-reads. `objectStore(client, key)` composes the v0.4.23 `b.objectStore` Range-GET path. `http(url, opts)` composes `b.httpClient` with `Range: bytes=N-M` headers + 206 verification. `buffer(buf)` slices a Buffer in-memory. `trustedStream(readable)` accepts a Node Readable for the rare case the operator can vouch for the source. The same contract feeds `b.safeArchive.extract` and `b.backup.bundleAdapterStorage`. · *`b.safeArchive.extract({ source, destination, ... })` — one-liner safe extraction* — Combines `b.archive.read` + `b.guardArchive` inspect + `b.guardFilename.verifyExtractionPath` + bomb caps + post-extract destination-rebase verification. Refuses the entire archive when any single entry trips a policy (atomic — no half-extracted state on the destination). Operators who want fine-grained control reach for the lower-level primitives directly; `b.safeArchive.extract` covers the 90%-case `extract this hostile-shaped archive into a quarantine directory` workflow. Returns `{ entries: [...], destinationRoot, bytesExtracted, auditTrail }` on success. · *`b.guardArchive.inspect(adapter, opts)` + `zipBombPolicy(...)` + `entryTypePolicy(...)`* — `inspect(adapter)` is the bridge between the read primitive and the existing `validateEntries` gate — runs the read primitive's inspect phase, hands the entry list to `validateEntries`, and returns the merged `{ entries, issues, decisions }` so the caller decides whether to proceed. `zipBombPolicy({ maxEntries, maxEntryDecompressedBytes, maxTotalDecompressedBytes, maxExpansionRatio })` and `entryTypePolicy({ symlinks, hardlinks, devices, fifos, sockets })` are policy-object builders so operators declare the cap set once + reuse across call sites. Each policy carries its own `audit.posture` annotation that propagates through `b.agent.postureChain`. · *`b.guardFilename.verifyExtractionPath(name, root, opts?)` — dual-check path safety* — Companion to the existing `b.guardArchive.checkExtractionPath` (string-only check the gate keeps portable). `verifyExtractionPath` couples to `fs.realpath` deliberately: refuses paths whose pre-resolve string already exceeds 4096 bytes (defends the CVE-2025-4517 PATH_MAX TOCTOU class — `os.path.realpath`-style truncation can't reach the kernel before our refuse fires), then verifies the string-normalized result and the `fs.realpath`-resolved result agree on the same final path. Disagreement throws `guard-filename/extraction-path-toctou`. The string-check stays the canonical portable gate; this primitive is the deeper fs-coupled check the framework's read primitive wires in by default. · *`b.backup.bundleAdapterStorage(adapter, opts)` — adapter-driven storage backend* — First non-disk backend for `b.backup`. Walks the bundle directory file-by-file and writes through the v0.12.7 adapter contract — `fs` adapter behaves identically to `diskStorage` (which stays for back-compat), `buffer` adapter emits the bundle into an in-memory representation, custom adapters can route to anything that satisfies the contract. Substrate for the v0.12.8 tar bundle format (which folds the directory tree into a single tar stream) and the v0.12.11 `objectStoreStorage` (which composes `b.archive.adapters.objectStore` for S3 / MinIO / Azure / GCS-backed backups). Backup manifest layout unchanged — restore code keeps working byte-for-byte against bundles produced by either backend. **Security:** *Zip Slip class (CVE-2025-3445 / 11569 / 23084 / 27210 / 11001 / 11002 / 26960 / 4517 / 4138 / 4330 + 2024 jszip / mholt/archiver / Python tarfile / node-tar / 7-zip)* — Every archive-read entry's name passes through `b.guardFilename.verifyExtractionPath` before any decompression. Path-traversal segments (`..`, leading `/` or `\`, drive-letter prefixes, null bytes, overlong UTF-8) are refused; Windows reserved names + NTFS ADS suffixes are refused; the realpath-agreement check defends the CVE-2025-4517 PATH_MAX TOCTOU class. Symlink and hardlink entries are refused unconditionally under the default `entryTypePolicy`; operators with a legitimate need opt into `allowSymlinks: true` / `allowHardlinks: true` and get the entries routed through an additional `b.guardArchive` realpath-on-target check. · *Decompression-bomb class (CVE-2025-0725, OWASP zip-bomb top-cases)* — `b.archive.read.zip.extract` enforces four caps in parallel: `maxEntries` (entry-count), `maxEntryDecompressedBytes` (per-entry size), `maxTotalDecompressedBytes` (aggregate across the archive), and `maxExpansionRatio` (compressed → decompressed ratio cap, default 100:1). Each cap aborts the extract as soon as the bound is exceeded; the destination directory is `fs.rm`-ed before the error throws so a partial extract never lingers on disk. The `b.safeDecompress` primitive (v0.11.5) is the underlying inflate gate — same defense surface, same audit-trail. · *LFH/CD skew + malformed-ZIP DoS* — The CD walk verifies every entry's local-file-header against the central-directory record (offset / size / CRC / method / name agreement). Mismatches throw `archive/cd-skew` before any byte decompresses. Defends the malformed-zip class where a hostile producer points CD entries at LFH locations that don't match the CD claim — the prior write-only path had no exposure to this class; the new read path closes it. **Detectors:** *`archive-read-without-bomb-caps`* — Flags `b.archive.read.zip(adapter)` call sites in `lib/` that don't pass an explicit `bombPolicy` or `maxTotalDecompressedBytes`. Forces the cap-declaration discipline at call sites — operators see the bomb-cap surface every time they reach for the read primitive. · *`archive-extract-without-guard`* — Flags `b.archive.read.zip(...).extract({ destination, ... })` call sites that don't compose `b.safeArchive.extract` (the orchestrator) AND don't pass an explicit `b.guardArchive.inspect` precheck. Per-file lib/ surface forces operators to either use the orchestrator OR explicitly opt into per-step composition with a written justification. · *`archive-adapter-without-error-path`* — Flags adapter implementations (any export shape matching `{ size, range }` / `{ readable }`) that don't propagate AbortSignal or refuse to throw on partial-read truncation. Forces the cancellation-discipline so a slow / hostile source can't block extraction indefinitely. · *`safe-archive-extract-bypass`* — Flags any composition in `lib/` that builds the safeArchive pipeline (`read.zip(...).extract({ destination, ... })` + bomb caps + guard) inline instead of calling `b.safeArchive.extract`. The orchestrator owns the audit-emission shape; bypassing it means audit gaps. **References:** [APPNOTE.TXT (PKWARE ZIP File Format Specification)](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) · [CVE-2025-3445 — mholt/archiver Zip Slip](https://github.com/advisories/GHSA-7vpp-9cxj-q8gv) · [CVE-2025-4517 — Python tarfile PATH_MAX bypass (CVSS 9.4)](https://nvd.nist.gov/vuln/detail/CVE-2025-4517) · [CVE-2025-11001 / CVE-2025-11002 — 7-Zip directory-traversal RCE](https://www.sentinelone.com/vulnerability-database/cve-2025-11001/) · [CVE-2025-11569 — cross-zip directory traversal](https://security.snyk.io/vuln/SNYK-JS-CROSSZIP-6105396) · [CVE-2026-23745 / CVE-2026-24842 — node-tar symlink + hardlink bypass](https://github.com/advisories/GHSA-34x7-hfp2-rc4v) · [OWASP Zip Slip + zip-bomb reference](https://snyk.io/research/zip-slip-vulnerability) · [USENIX WOOT'19 — A better zip bomb (Fifield)](https://www.usenix.org/conference/woot19/presentation/fifield)
|
|
12
14
|
|
|
13
15
|
- v0.12.6 (2026-05-22) — **`b.observability.otlpExporter` adds OTLP/protobuf-HTTP encoding (`opts.encoding: "protobuf"`).** The OTLP trace exporter now speaks `application/x-protobuf` end-to-end. Operators with high-volume telemetry opt into binary encoding via `opts.encoding: "protobuf"` (`"http/protobuf"` is accepted as a spec-name alias). The protobuf wire format encodes the same `ExportTraceServiceRequest` envelope as the existing JSON path — `ResourceSpans` → `ScopeSpans` → `Span` → `Status` / `Event` / `KeyValue` / `AnyValue` per the opentelemetry-proto repo — but emits 30-50% smaller bodies than the JSON shape on real-world workloads and avoids the JSON-parse cost on the collector side. Default stays `"json"`; collectors that don't speak protobuf keep working unchanged. Composes the existing `lib/protobuf-encoder.js` infrastructure. **Added:** *`opts.encoding: "json" | "protobuf"` on `b.observability.otlpExporter.create`* — When `"protobuf"` (or the spec-name alias `"http/protobuf"`), the exporter encodes batches as binary OTLP `ExportTraceServiceRequest` bytes and POSTs with `Content-Type: application/x-protobuf`. The retry / queue / drop-counter / audit machinery is shared with the JSON path so operators get the same operational primitives across both encodings. Default stays `"json"` — existing collectors keep working without configuration changes. · *Full OTLP trace schema encoded via `lib/protobuf-encoder.js`* — ResourceSpans / ScopeSpans / Span / Event / Status / KeyValue / AnyValue / ArrayValue are emitted per the opentelemetry-proto repo's field numbers + wire types. `trace_id` and `span_id` round-trip as fixed-length bytes (16 + 8 octets respectively). `start_time_unix_nano` / `end_time_unix_nano` use `fixed64` for the nanosecond precision the JSON path's number type lossily encoded. SpanKind enum mapping covers unspecified / internal / server / client / producer / consumer. · *`pb.int64` / `pb.sint64` — signed-integer varint shapes on `lib/protobuf-encoder.js`* — Negative integer attribute values in OTLP `AnyValue` (e.g. retry-after offsets, signed metric deltas) emit as proto3 `int64` — wire-type 0 varint, 10-byte two's-complement reinterpret per the spec. `pb.sint64` adds ZigZag-encoded varint for cases where small negatives dominate. Both accept Number / BigInt / digit-string inputs with explicit `[-2^63, 2^63 - 1]` range validation. · *`pb.fixed64` accepts string-form uint64 values* — OTLP/JSON encodes uint64 as a JSON string (per the proto3 JSON mapping) — the framework's tracer emits `start_time_unix_nano` / `end_time_unix_nano` as digit-string BigInt-to-string conversions so the JSON path stays lossless. `pb.fixed64` now accepts that same digit-string shape on the protobuf path so a single timestamp representation flows through both encodings without a separate coercion step. Refuses non-digit strings and silently-rounded Numbers above `Number.MAX_SAFE_INTEGER`. **Security:** *AnyValue recursion capped at 100 levels (CVE-2024-7254 / CVE-2025-4565 class)* — Both protobufjs (CVE-2024-7254) and protobuf-python (CVE-2025-4565) shipped DoS-via-unbounded-nested-group decoding. The OTLP `AnyValue` type permits a nested `ArrayValue { repeated AnyValue values = 1 }` that an adversarial collector-response could exploit during a future receive path. The encoder caps `_anyValueToProto` recursion at 100 levels — beyond which it emits an empty AnyValue rather than continuing to descend. Today's framework only EMITS (never receives) OTLP — but the cap is in the right place when the receive path lands. **References:** [OTLP §3 — Body encodings (JSON + protobuf)](https://opentelemetry.io/docs/specs/otlp/) · [opentelemetry-proto repo — trace/v1/trace.proto](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto) · [CVE-2024-7254 (protobufjs unbounded nesting DoS)](https://nvd.nist.gov/vuln/detail/CVE-2024-7254) · [CVE-2025-4565 (protobuf-python unbounded nesting DoS)](https://nvd.nist.gov/vuln/detail/CVE-2025-4565)
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* archive-tar-read — POSIX pax tar reader. Sibling of lib/archive-tar.js
|
|
4
|
+
* (write side) the way lib/archive-read.js is to lib/archive.js for ZIP.
|
|
5
|
+
* `b.archive.read.tar` lives here so the @module/@primitive validator
|
|
6
|
+
* can pair `function tar(adapter, opts)` cleanly with the @primitive
|
|
7
|
+
* comment block without colliding with the write-side `function
|
|
8
|
+
* tarBuilder()` (which is exported as `b.archive.tar`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var nodeFs = require("node:fs");
|
|
12
|
+
var nodePath = require("node:path");
|
|
13
|
+
var C = require("./constants");
|
|
14
|
+
var lazyRequire = require("./lazy-require");
|
|
15
|
+
var safeBuffer = require("./safe-buffer");
|
|
16
|
+
var archiveTar = require("./archive-tar");
|
|
17
|
+
|
|
18
|
+
var TarError = archiveTar.TarError;
|
|
19
|
+
var _parseHeader = archiveTar._parseHeader;
|
|
20
|
+
|
|
21
|
+
var guardFilename = lazyRequire(function () { return require("./guard-filename"); });
|
|
22
|
+
var guardArchive = lazyRequire(function () { return require("./guard-archive"); });
|
|
23
|
+
|
|
24
|
+
var BLOCK_SIZE = C.BYTES.bytes(512);
|
|
25
|
+
|
|
26
|
+
var TF_REGULAR = "0";
|
|
27
|
+
var TF_REGULAR_LEGACY = "\u0000";
|
|
28
|
+
var TF_HARDLINK = "1";
|
|
29
|
+
var TF_SYMLINK = "2";
|
|
30
|
+
var TF_CHARDEV = "3";
|
|
31
|
+
var TF_BLOCKDEV = "4";
|
|
32
|
+
var TF_DIRECTORY = "5";
|
|
33
|
+
var TF_FIFO = "6";
|
|
34
|
+
var TF_CONTIGUOUS = "7";
|
|
35
|
+
var TF_PAX_EXTENDED = "x";
|
|
36
|
+
var TF_PAX_GLOBAL = "g";
|
|
37
|
+
|
|
38
|
+
void TF_CHARDEV; void TF_BLOCKDEV; void TF_FIFO; void TF_CONTIGUOUS;
|
|
39
|
+
|
|
40
|
+
var DEFAULT_BOMB_POLICY = Object.freeze({
|
|
41
|
+
maxEntries: 65535, // allow:raw-byte-literal — operator-friendly default ceiling
|
|
42
|
+
maxEntryDecompressedBytes: C.BYTES.mib(128),
|
|
43
|
+
maxTotalDecompressedBytes: C.BYTES.gib(4),
|
|
44
|
+
maxExpansionRatio: 100, // allow:raw-byte-literal — tar has no compression-ratio concept, but keep field for orchestrator policy parity
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
var DEFAULT_ENTRY_TYPE_POLICY = Object.freeze({
|
|
48
|
+
symlinks: false,
|
|
49
|
+
hardlinks: false,
|
|
50
|
+
devices: false,
|
|
51
|
+
fifos: false,
|
|
52
|
+
sockets: false,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function _normalizeBombPolicy(p) {
|
|
56
|
+
if (!p) return DEFAULT_BOMB_POLICY;
|
|
57
|
+
return Object.freeze(Object.assign({}, DEFAULT_BOMB_POLICY, p));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _normalizeEntryTypePolicy(p) {
|
|
61
|
+
if (!p) return DEFAULT_ENTRY_TYPE_POLICY;
|
|
62
|
+
return Object.freeze(Object.assign({}, DEFAULT_ENTRY_TYPE_POLICY, p));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _emitAudit(opts, action, outcome, metadata) {
|
|
66
|
+
if (!opts || !opts.audit || typeof opts.audit.safeEmit !== "function") return;
|
|
67
|
+
try { opts.audit.safeEmit({ action: action, outcome: outcome, metadata: metadata }); }
|
|
68
|
+
catch (_e) { /* drop-silent */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _isZeroBlock(buf) {
|
|
72
|
+
for (var i = 0; i < BLOCK_SIZE; i += 1) {
|
|
73
|
+
if (buf[i] !== 0) return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _parsePaxRecords(buf) {
|
|
79
|
+
var out = Object.create(null);
|
|
80
|
+
var pos = 0;
|
|
81
|
+
var s = buf.toString("utf8");
|
|
82
|
+
while (pos < s.length) {
|
|
83
|
+
var spaceIdx = s.indexOf(" ", pos);
|
|
84
|
+
if (spaceIdx < 0) {
|
|
85
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
86
|
+
"pax record at byte " + pos + " missing length-space delimiter");
|
|
87
|
+
}
|
|
88
|
+
var lenStr = s.slice(pos, spaceIdx);
|
|
89
|
+
var len = parseInt(lenStr, 10);
|
|
90
|
+
if (!Number.isFinite(len) || len <= 0) {
|
|
91
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
92
|
+
"pax record length " + JSON.stringify(lenStr) + " is not a positive integer");
|
|
93
|
+
}
|
|
94
|
+
var record = s.slice(pos, pos + len);
|
|
95
|
+
if (record[record.length - 1] !== "\n") {
|
|
96
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
97
|
+
"pax record at byte " + pos + " not newline-terminated");
|
|
98
|
+
}
|
|
99
|
+
var eqIdx = record.indexOf("=", spaceIdx - pos + 1);
|
|
100
|
+
if (eqIdx < 0) {
|
|
101
|
+
throw new TarError("archive-tar/bad-pax-record",
|
|
102
|
+
"pax record at byte " + pos + " missing key=value delimiter");
|
|
103
|
+
}
|
|
104
|
+
var key = record.slice(spaceIdx - pos + 1, eqIdx);
|
|
105
|
+
var value = record.slice(eqIdx + 1, record.length - 1);
|
|
106
|
+
out[key] = value;
|
|
107
|
+
pos += len;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function _collectAdapterBytes(adapter) {
|
|
113
|
+
if (adapter.kind === "random-access") {
|
|
114
|
+
var size = adapter.size;
|
|
115
|
+
if (size == null && typeof adapter.resolveSize === "function") {
|
|
116
|
+
size = await adapter.resolveSize();
|
|
117
|
+
}
|
|
118
|
+
if (typeof size !== "number" || size === 0) return Buffer.alloc(0);
|
|
119
|
+
return adapter.range(0, size);
|
|
120
|
+
}
|
|
121
|
+
if (adapter.kind === "trusted-sequential") {
|
|
122
|
+
var collector = safeBuffer.boundedChunkCollector({
|
|
123
|
+
maxBytes: C.BYTES.gib(1),
|
|
124
|
+
errorClass: TarError,
|
|
125
|
+
sizeCode: "archive-tar/trusted-stream-too-large",
|
|
126
|
+
});
|
|
127
|
+
for await (var chunk of adapter.readable) {
|
|
128
|
+
collector.push(chunk);
|
|
129
|
+
}
|
|
130
|
+
return collector.result();
|
|
131
|
+
}
|
|
132
|
+
throw new TarError("archive-tar/bad-adapter",
|
|
133
|
+
"read.tar: adapter kind " + adapter.kind + " not supported");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _classifyTypeflag(tf) {
|
|
137
|
+
if (tf === TF_REGULAR || tf === TF_REGULAR_LEGACY) return "file";
|
|
138
|
+
if (tf === TF_DIRECTORY) return "directory";
|
|
139
|
+
if (tf === TF_SYMLINK) return "symlink";
|
|
140
|
+
if (tf === TF_HARDLINK) return "hardlink";
|
|
141
|
+
if (tf === TF_CHARDEV || tf === TF_BLOCKDEV) return "device";
|
|
142
|
+
if (tf === TF_FIFO) return "fifo";
|
|
143
|
+
if (tf === TF_CONTIGUOUS) return "file";
|
|
144
|
+
return "unknown";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @primitive b.archive.read.tar
|
|
149
|
+
* @signature b.archive.read.tar(adapter, opts?)
|
|
150
|
+
* @since 0.12.8
|
|
151
|
+
* @status stable
|
|
152
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
153
|
+
* @related b.archive.read.zip, b.safeArchive.extract, b.guardArchive.tarEntryPolicy
|
|
154
|
+
*
|
|
155
|
+
* POSIX pax tar reader. Walks 512-byte header blocks sequentially +
|
|
156
|
+
* extracts via the same bomb-cap / path-traversal / entry-type policy
|
|
157
|
+
* surface as the v0.12.7 ZIP reader. Random-access and trusted-stream
|
|
158
|
+
* adapters are both first-class (tar has no central directory, so
|
|
159
|
+
* sequential header-by-header is the canonical adversarial-safe
|
|
160
|
+
* path).
|
|
161
|
+
*
|
|
162
|
+
* @opts
|
|
163
|
+
* bombPolicy: { maxEntries, maxEntryDecompressedBytes,
|
|
164
|
+
* maxTotalDecompressedBytes, maxExpansionRatio },
|
|
165
|
+
* entryTypePolicy: { symlinks, hardlinks, devices, fifos, sockets },
|
|
166
|
+
* allowDangerous: { symlinks, hardlinks },
|
|
167
|
+
* guardProfile: "strict" | "balanced" | "permissive",
|
|
168
|
+
* audit: b.audit,
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* var reader = b.archive.read.tar(b.archive.adapters.buffer(Buffer.alloc(0)));
|
|
172
|
+
* var entries = await reader.inspect();
|
|
173
|
+
* void entries;
|
|
174
|
+
*/
|
|
175
|
+
function tar(adapter, opts) {
|
|
176
|
+
if (!adapter || (adapter.kind !== "random-access" && adapter.kind !== "trusted-sequential")) {
|
|
177
|
+
throw new TarError("archive-tar/bad-adapter",
|
|
178
|
+
"read.tar(adapter): adapter must come from b.archive.adapters.*");
|
|
179
|
+
}
|
|
180
|
+
opts = opts || {};
|
|
181
|
+
var bombPolicy = _normalizeBombPolicy(opts.bombPolicy);
|
|
182
|
+
var entryTypePolicy = _normalizeEntryTypePolicy(opts.entryTypePolicy);
|
|
183
|
+
|
|
184
|
+
async function _walk() {
|
|
185
|
+
var bytes = await _collectAdapterBytes(adapter);
|
|
186
|
+
if (bytes.length === 0) return { entries: [], bytes: bytes };
|
|
187
|
+
var pos = 0;
|
|
188
|
+
var entries = [];
|
|
189
|
+
var pendingPax = null;
|
|
190
|
+
var globalPax = null;
|
|
191
|
+
var zeroBlockCount = 0;
|
|
192
|
+
while (pos + BLOCK_SIZE <= bytes.length) {
|
|
193
|
+
var block = bytes.slice(pos, pos + BLOCK_SIZE);
|
|
194
|
+
pos += BLOCK_SIZE;
|
|
195
|
+
if (_isZeroBlock(block)) {
|
|
196
|
+
zeroBlockCount += 1;
|
|
197
|
+
if (zeroBlockCount >= 2) break;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
zeroBlockCount = 0;
|
|
201
|
+
var hdr = _parseHeader(block);
|
|
202
|
+
if (hdr.typeflag === TF_PAX_EXTENDED || hdr.typeflag === TF_PAX_GLOBAL) {
|
|
203
|
+
var bodyEnd = pos + Math.ceil(hdr.size / BLOCK_SIZE) * BLOCK_SIZE;
|
|
204
|
+
if (bodyEnd > bytes.length) {
|
|
205
|
+
throw new TarError("archive-tar/truncated-entry",
|
|
206
|
+
"pax extended header declares size=" + hdr.size +
|
|
207
|
+
" bytes but only " + (bytes.length - pos) +
|
|
208
|
+
" bytes remain after header — archive truncated mid-stream");
|
|
209
|
+
}
|
|
210
|
+
var paxBody = bytes.slice(pos, pos + hdr.size);
|
|
211
|
+
var records = _parsePaxRecords(paxBody);
|
|
212
|
+
if (hdr.typeflag === TF_PAX_EXTENDED) pendingPax = records;
|
|
213
|
+
else globalPax = records;
|
|
214
|
+
pos = bodyEnd;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (globalPax) {
|
|
218
|
+
if (globalPax.path) hdr.name = globalPax.path;
|
|
219
|
+
if (globalPax.size) hdr.size = parseInt(globalPax.size, 10);
|
|
220
|
+
if (globalPax.linkpath) hdr.linkname = globalPax.linkpath;
|
|
221
|
+
}
|
|
222
|
+
if (pendingPax) {
|
|
223
|
+
if (pendingPax.path) hdr.name = pendingPax.path;
|
|
224
|
+
if (pendingPax.size) hdr.size = parseInt(pendingPax.size, 10);
|
|
225
|
+
if (pendingPax.linkpath) hdr.linkname = pendingPax.linkpath;
|
|
226
|
+
pendingPax = null;
|
|
227
|
+
}
|
|
228
|
+
if (entries.length >= bombPolicy.maxEntries) {
|
|
229
|
+
throw new TarError("archive-tar/too-many-entries",
|
|
230
|
+
"archive has more than " + bombPolicy.maxEntries + " entries");
|
|
231
|
+
}
|
|
232
|
+
if (hdr.size > bombPolicy.maxEntryDecompressedBytes) {
|
|
233
|
+
throw new TarError("archive-tar/entry-too-large",
|
|
234
|
+
"entry " + JSON.stringify(hdr.name) +
|
|
235
|
+
" size=" + hdr.size + " exceeds maxEntryDecompressedBytes=" +
|
|
236
|
+
bombPolicy.maxEntryDecompressedBytes);
|
|
237
|
+
}
|
|
238
|
+
var bodyStart = pos;
|
|
239
|
+
var paddedSize = Math.ceil(hdr.size / BLOCK_SIZE) * BLOCK_SIZE;
|
|
240
|
+
// Codex P1 on v0.12.8 PR #159 — refuse truncated archives upfront.
|
|
241
|
+
// The walker advances `pos` by the declared padded block size; if
|
|
242
|
+
// the buffer ends mid-body, extract() would silently slice a
|
|
243
|
+
// partial payload (header says 11 bytes, buffer holds 8 — without
|
|
244
|
+
// this check we'd write the 8-byte prefix as if it were the
|
|
245
|
+
// complete file). Detector: archive-tar-truncated-walker-without-bounds-check.
|
|
246
|
+
if (bodyStart + paddedSize > bytes.length) {
|
|
247
|
+
throw new TarError("archive-tar/truncated-entry",
|
|
248
|
+
"entry " + JSON.stringify(hdr.name) +
|
|
249
|
+
" declares size=" + hdr.size + " (padded=" + paddedSize +
|
|
250
|
+
") but only " + (bytes.length - bodyStart) +
|
|
251
|
+
" bytes remain after header — archive truncated mid-stream");
|
|
252
|
+
}
|
|
253
|
+
hdr._bodyStart = bodyStart;
|
|
254
|
+
hdr._paddedSize = paddedSize;
|
|
255
|
+
entries.push(hdr);
|
|
256
|
+
pos += paddedSize;
|
|
257
|
+
}
|
|
258
|
+
return { entries: entries, bytes: bytes };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function inspect() {
|
|
262
|
+
var walked = await _walk();
|
|
263
|
+
var entries = walked.entries;
|
|
264
|
+
return entries.map(function (e) {
|
|
265
|
+
return {
|
|
266
|
+
name: e.name,
|
|
267
|
+
size: e.size,
|
|
268
|
+
mtime: new Date(e.mtime * C.TIME.seconds(1)),
|
|
269
|
+
mode: e.mode,
|
|
270
|
+
typeflag: e.typeflag,
|
|
271
|
+
linkname: e.linkname,
|
|
272
|
+
uname: e.uname,
|
|
273
|
+
gname: e.gname,
|
|
274
|
+
entryType: _classifyTypeflag(e.typeflag),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function extract(extractOpts) {
|
|
280
|
+
extractOpts = extractOpts || {};
|
|
281
|
+
if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
|
|
282
|
+
throw new TarError("archive-tar/no-destination",
|
|
283
|
+
"extract: opts.destination must be non-empty string");
|
|
284
|
+
}
|
|
285
|
+
var destination = nodePath.resolve(extractOpts.destination);
|
|
286
|
+
if (!nodeFs.existsSync(destination)) {
|
|
287
|
+
nodeFs.mkdirSync(destination, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
var allowDangerous = extractOpts.allowDangerous || {};
|
|
290
|
+
var walked = await _walk();
|
|
291
|
+
var entries = walked.entries;
|
|
292
|
+
var bytes = walked.bytes;
|
|
293
|
+
if (opts.guardProfile !== false) {
|
|
294
|
+
var profile = opts.guardProfile || "balanced";
|
|
295
|
+
var guardEntries = entries.map(function (e) {
|
|
296
|
+
return {
|
|
297
|
+
name: e.name,
|
|
298
|
+
size: e.size,
|
|
299
|
+
compressedSize: e.size,
|
|
300
|
+
isSymlink: e.typeflag === TF_SYMLINK,
|
|
301
|
+
isHardlink: e.typeflag === TF_HARDLINK,
|
|
302
|
+
linkTarget: e.linkname,
|
|
303
|
+
isDirectory: e.typeflag === TF_DIRECTORY,
|
|
304
|
+
isEncrypted: false,
|
|
305
|
+
attrs: { mode: e.mode },
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
309
|
+
if (guardResult && Array.isArray(guardResult.issues)) {
|
|
310
|
+
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
311
|
+
if (critical.length > 0) {
|
|
312
|
+
_emitAudit(opts, "archive.read.tar.extract.refused", "refused", {
|
|
313
|
+
entries: entries.length,
|
|
314
|
+
issues: critical.map(function (i) { return i.ruleId; }),
|
|
315
|
+
});
|
|
316
|
+
throw new TarError("archive-tar/guard-refused",
|
|
317
|
+
"extract refused — " + critical.length + " critical guard issue(s)");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
var written = [];
|
|
322
|
+
var bytesExtracted = 0;
|
|
323
|
+
var totalDecompressed = 0;
|
|
324
|
+
try {
|
|
325
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
326
|
+
var entry = entries[i];
|
|
327
|
+
var type = _classifyTypeflag(entry.typeflag);
|
|
328
|
+
if (type === "device" || type === "fifo" || type === "socket") {
|
|
329
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
330
|
+
"entry " + JSON.stringify(entry.name) + " is a " + type +
|
|
331
|
+
" — refused unconditionally (no application use case)");
|
|
332
|
+
}
|
|
333
|
+
if (type === "symlink" && !(allowDangerous.symlinks || entryTypePolicy.symlinks)) {
|
|
334
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
335
|
+
"entry " + JSON.stringify(entry.name) + " is a symlink — refused by entryTypePolicy " +
|
|
336
|
+
"(opt in via allowDangerous: { symlinks: true })");
|
|
337
|
+
}
|
|
338
|
+
if (type === "hardlink" && !(allowDangerous.hardlinks || entryTypePolicy.hardlinks)) {
|
|
339
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
340
|
+
"entry " + JSON.stringify(entry.name) + " is a hardlink — refused by entryTypePolicy " +
|
|
341
|
+
"(opt in via allowDangerous: { hardlinks: true })");
|
|
342
|
+
}
|
|
343
|
+
var resolvedPath = guardFilename().verifyExtractionPath(entry.name, destination);
|
|
344
|
+
if (type === "directory") {
|
|
345
|
+
nodeFs.mkdirSync(resolvedPath, { recursive: true });
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (nodeFs.existsSync(resolvedPath)) {
|
|
349
|
+
throw new TarError("archive-tar/destination-exists",
|
|
350
|
+
"extract: destination file already exists at " +
|
|
351
|
+
JSON.stringify(resolvedPath) + " — refuse to overwrite");
|
|
352
|
+
}
|
|
353
|
+
if (type === "symlink" || type === "hardlink") {
|
|
354
|
+
guardFilename().verifyExtractionPath(entry.linkname, destination);
|
|
355
|
+
if (type === "symlink") {
|
|
356
|
+
nodeFs.symlinkSync(entry.linkname, resolvedPath);
|
|
357
|
+
} else {
|
|
358
|
+
var hardlinkTarget = nodePath.join(destination, entry.linkname);
|
|
359
|
+
nodeFs.linkSync(hardlinkTarget, resolvedPath);
|
|
360
|
+
}
|
|
361
|
+
written.push({ name: entry.name, bytesWritten: 0, path: resolvedPath });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
var parentDir = nodePath.dirname(resolvedPath);
|
|
365
|
+
if (!nodeFs.existsSync(parentDir)) {
|
|
366
|
+
nodeFs.mkdirSync(parentDir, { recursive: true });
|
|
367
|
+
}
|
|
368
|
+
var body = bytes.slice(entry._bodyStart, entry._bodyStart + entry.size);
|
|
369
|
+
totalDecompressed += body.length;
|
|
370
|
+
if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
|
|
371
|
+
throw new TarError("archive-tar/total-too-large",
|
|
372
|
+
"cumulative uncompressed=" + totalDecompressed +
|
|
373
|
+
" exceeds maxTotalDecompressedBytes during extract");
|
|
374
|
+
}
|
|
375
|
+
var tmpPath = resolvedPath + ".__blamejs-archive-tar-tmp__";
|
|
376
|
+
nodeFs.writeFileSync(tmpPath, body);
|
|
377
|
+
nodeFs.renameSync(tmpPath, resolvedPath);
|
|
378
|
+
written.push({ name: entry.name, bytesWritten: body.length, path: resolvedPath });
|
|
379
|
+
bytesExtracted += body.length;
|
|
380
|
+
}
|
|
381
|
+
} catch (extractErr) {
|
|
382
|
+
try {
|
|
383
|
+
for (var w = 0; w < written.length; w += 1) {
|
|
384
|
+
if (nodeFs.existsSync(written[w].path)) {
|
|
385
|
+
nodeFs.rmSync(written[w].path);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} catch (_e) { /* drop-silent */ }
|
|
389
|
+
_emitAudit(opts, "archive.read.tar.extract.aborted", "failure", {
|
|
390
|
+
entries: entries.length,
|
|
391
|
+
written: written.length,
|
|
392
|
+
error: extractErr && (extractErr.code || extractErr.message),
|
|
393
|
+
});
|
|
394
|
+
throw extractErr;
|
|
395
|
+
}
|
|
396
|
+
_emitAudit(opts, "archive.read.tar.extract.completed", "success", {
|
|
397
|
+
entries: entries.length,
|
|
398
|
+
bytesExtracted: bytesExtracted,
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
entries: written,
|
|
402
|
+
destinationRoot: destination,
|
|
403
|
+
bytesExtracted: bytesExtracted,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
kind: "tar-reader",
|
|
409
|
+
inspect: inspect,
|
|
410
|
+
extract: extract,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
tar: tar,
|
|
416
|
+
DEFAULT_BOMB_POLICY: DEFAULT_BOMB_POLICY,
|
|
417
|
+
DEFAULT_ENTRY_TYPE_POLICY: DEFAULT_ENTRY_TYPE_POLICY,
|
|
418
|
+
};
|