@t4h.framework/zip 0.0.0-experimental-20260907231524
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/.ai/skills/framework-zip/SKILL.md +154 -0
- package/CHANGELOG.md +34 -0
- package/README.md +208 -0
- package/dist/activities/ZipExtractActivity.d.ts +20 -0
- package/dist/activities/ZipExtractActivity.d.ts.map +1 -0
- package/dist/activities/ZipExtractActivity.js +40 -0
- package/dist/activities/ZipExtractActivity.js.map +1 -0
- package/dist/activities/ZipListActivity.d.ts +19 -0
- package/dist/activities/ZipListActivity.d.ts.map +1 -0
- package/dist/activities/ZipListActivity.js +27 -0
- package/dist/activities/ZipListActivity.js.map +1 -0
- package/dist/helpers/ZipChecksum.d.ts +15 -0
- package/dist/helpers/ZipChecksum.d.ts.map +1 -0
- package/dist/helpers/ZipChecksum.js +28 -0
- package/dist/helpers/ZipChecksum.js.map +1 -0
- package/dist/helpers/ZipSource.d.ts +17 -0
- package/dist/helpers/ZipSource.d.ts.map +1 -0
- package/dist/helpers/ZipSource.js +40 -0
- package/dist/helpers/ZipSource.js.map +1 -0
- package/dist/helpers/extract-entry.d.ts +10 -0
- package/dist/helpers/extract-entry.d.ts.map +1 -0
- package/dist/helpers/extract-entry.js +30 -0
- package/dist/helpers/extract-entry.js.map +1 -0
- package/dist/helpers/open-archive.d.ts +14 -0
- package/dist/helpers/open-archive.d.ts.map +1 -0
- package/dist/helpers/open-archive.js +57 -0
- package/dist/helpers/open-archive.js.map +1 -0
- package/dist/tools/assert-entry-path.d.ts +2 -0
- package/dist/tools/assert-entry-path.d.ts.map +1 -0
- package/dist/tools/assert-entry-path.js +11 -0
- package/dist/tools/assert-entry-path.js.map +1 -0
- package/dist/tools/match-path.d.ts +2 -0
- package/dist/tools/match-path.d.ts.map +1 -0
- package/dist/tools/match-path.js +31 -0
- package/dist/tools/match-path.js.map +1 -0
- package/dist/tools/select-entries.d.ts +7 -0
- package/dist/tools/select-entries.d.ts.map +1 -0
- package/dist/tools/select-entries.js +20 -0
- package/dist/tools/select-entries.js.map +1 -0
- package/dist/tools/to-entry-info.d.ts +4 -0
- package/dist/tools/to-entry-info.d.ts.map +1 -0
- package/dist/tools/to-entry-info.js +12 -0
- package/dist/tools/to-entry-info.js.map +1 -0
- package/dist/tools/zip-format.d.ts +52 -0
- package/dist/tools/zip-format.d.ts.map +1 -0
- package/dist/tools/zip-format.js +178 -0
- package/dist/tools/zip-format.js.map +1 -0
- package/dist/typings/ZipEntry.d.ts +14 -0
- package/dist/typings/ZipEntry.d.ts.map +1 -0
- package/dist/typings/ZipEntry.js +2 -0
- package/dist/typings/ZipEntry.js.map +1 -0
- package/dist/zip.d.ts +5 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +5 -0
- package/dist/zip.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-zip
|
|
3
|
+
description: >-
|
|
4
|
+
Guides correct use of @t4h.framework/zip in workflows: opening a ZIP archive
|
|
5
|
+
that arrives from SFTP, HTTP or storage, listing what is inside it without
|
|
6
|
+
downloading it, and extracting the entries that matter into `Binary` files
|
|
7
|
+
that `read`, `split` and `sftp.put` accept. Use whenever a workflow receives
|
|
8
|
+
a `.zip` (a compressed batch file, a bundle of invoices, a partner's return
|
|
9
|
+
package), whenever the user mentions unzipping, decompressing, inflate,
|
|
10
|
+
deflate, zip entries, or the symbols `unzip`, `list`, `ZipEntry`,
|
|
11
|
+
`ZipExtractActivity`, and above all when someone is about to hand-roll a
|
|
12
|
+
lazy `Binary` that inflates on `stream()` in App code. Prefer this skill
|
|
13
|
+
over `node:zlib` in workflow code and over any hand-written ZIP parsing.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# @t4h.framework/zip
|
|
17
|
+
|
|
18
|
+
Reading ZIP archives inside a workflow. Package path: `framework/packages/zip`.
|
|
19
|
+
Peer dependencies: `@t4h.framework/core`, `@t4h.framework/fs`.
|
|
20
|
+
|
|
21
|
+
## Import surface
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { list, unzip } from '@t4h.framework/zip'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Two durable activities, nothing else to wire:
|
|
28
|
+
|
|
29
|
+
- `unzip(archive, options?)` — extracts entries into files, resolves with
|
|
30
|
+
`ZipEntry[]` (`{ path, binary, contentLength, compressedLength,
|
|
31
|
+
compressionMethod, crc32, lastModifiedAt, directory }`), directories
|
|
32
|
+
excluded, in central directory order.
|
|
33
|
+
- `list(archive, options?)` — the same metadata **without** `binary` and
|
|
34
|
+
without transferring content, directories included.
|
|
35
|
+
|
|
36
|
+
`archive` is any `Binary`: `sftp.get`, an HTTP response body, `write`, a slice.
|
|
37
|
+
|
|
38
|
+
## Workflow usage
|
|
39
|
+
|
|
40
|
+
The canonical pipeline — fetch a compressed batch, extract the one file that
|
|
41
|
+
carries the records, fan it out:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { split, Split } from '@t4h.framework/fs'
|
|
45
|
+
import { unzip } from '@t4h.framework/zip'
|
|
46
|
+
|
|
47
|
+
const main = new Workflow(inputSchema, async input => {
|
|
48
|
+
const archive = await sftp.get(input.path)
|
|
49
|
+
|
|
50
|
+
const [file] = await unzip(archive, { include: 'MOVIMENTO.TXT' })
|
|
51
|
+
|
|
52
|
+
if (!file) throw new Error(`${input.path} has no movement file`)
|
|
53
|
+
|
|
54
|
+
return await split(file.binary, processRecord, {
|
|
55
|
+
delimiter: Split.line(),
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Deciding from metadata first, when most runs do not need the content:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const entries = await list(archive)
|
|
64
|
+
|
|
65
|
+
const invoice = entries.find(entry => entry.path.endsWith(`/${id}.txt`))
|
|
66
|
+
|
|
67
|
+
if (!invoice) return { status: 'missing' }
|
|
68
|
+
|
|
69
|
+
const [extracted] = await unzip(archive, { include: invoice.path })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
| Option | Applies to | Default | Behavior |
|
|
75
|
+
| ------------- | --------------- | ------- | ---------------------------------------------------------------------- |
|
|
76
|
+
| `include` | `unzip`, `list` | — | Glob or array of globs an entry must match. |
|
|
77
|
+
| `exclude` | `unzip`, `list` | — | Globs that drop entries, applied after `include`. |
|
|
78
|
+
| `encoding` | `unzip`, `list` | `utf8` | Decoding of entry names not flagged UTF-8 (legacy archives: `latin1`). |
|
|
79
|
+
| `contentType` | `unzip` | — | Content type of the extracted files; omitted, the claim infers it. |
|
|
80
|
+
|
|
81
|
+
Patterns match the whole entry path, case sensitively: `*` and `?` never cross
|
|
82
|
+
a `/`, `**` does (`'**.csv'` reaches any depth, `'invoices/*'` one level). An
|
|
83
|
+
`include` matching nothing resolves with an empty array — always check.
|
|
84
|
+
|
|
85
|
+
## Behaviors you can rely on
|
|
86
|
+
|
|
87
|
+
- **Content never enters the workflow.** Only file references do; the bytes go
|
|
88
|
+
from the archive to storage, inflated in a stream with backpressure.
|
|
89
|
+
- **You pay for what you extract.** A ZIP's index sits at the end of the file,
|
|
90
|
+
so the listing is a ranged read of the tail and each entry is fetched by
|
|
91
|
+
offset — pulling one file out of a 6 MB archive does not download 6 MB. A
|
|
92
|
+
binary the claim cannot slice (SFTP, HTTP) is streamed **once** into storage
|
|
93
|
+
first, then read by range.
|
|
94
|
+
- **Corruption fails the activity.** CRC-32 and size are verified as the entry
|
|
95
|
+
flows to storage.
|
|
96
|
+
- **Hostile paths are refused** before anything is written (`../`, absolute,
|
|
97
|
+
drive qualified).
|
|
98
|
+
- **Format coverage.** Stored and deflate, ZIP64, data descriptors, archive
|
|
99
|
+
comments, UTF-8 and legacy names, empty entries. Encrypted entries and other
|
|
100
|
+
compression methods are refused with a `TypeError`.
|
|
101
|
+
|
|
102
|
+
## Limits
|
|
103
|
+
|
|
104
|
+
- **No writing.** The package reads archives; it does not create them. A file
|
|
105
|
+
to send compressed has to be produced another way.
|
|
106
|
+
- **Deflate and stored only.** bzip2, LZMA and zstd entries are refused.
|
|
107
|
+
- **Extraction is one activity per call.** The result is recorded whole, so a
|
|
108
|
+
`unzip` with no `include` over a 3 000-entry archive puts 3 000 references in
|
|
109
|
+
the history and 3 000 files in storage. Filter.
|
|
110
|
+
- **What is bounded and what is not.** Entry content, the reads over the
|
|
111
|
+
archive and the history are constant whatever the archive weighs. Memory
|
|
112
|
+
grows with the _number_ of entries — the central directory is read whole,
|
|
113
|
+
50 to 100 bytes per entry — not with their size. A zip bomb costs storage,
|
|
114
|
+
not memory: the read stops at the declared compressed size and the inflate is
|
|
115
|
+
aborted the moment the output goes past the declared uncompressed size, so a
|
|
116
|
+
lying entry never writes more than it declares — one that honestly declares
|
|
117
|
+
40 GB still writes 40 GB, and `list` shows `contentLength` beforehand. A
|
|
118
|
+
non-sliceable source adds one durable copy of the archive, and whether that
|
|
119
|
+
copy streams is the `FileSystemClaim`'s contract, as it is for `split`.
|
|
120
|
+
|
|
121
|
+
## Runtime requirements
|
|
122
|
+
|
|
123
|
+
- Claim provider: `FileSystemClaim` (from `@t4h.framework/fs`).
|
|
124
|
+
- Production allowlist: register `ZipExtractActivity` and `ZipListActivity`
|
|
125
|
+
via `runtime.addActivity`.
|
|
126
|
+
|
|
127
|
+
## Anti-patterns
|
|
128
|
+
|
|
129
|
+
- **Hand-rolling a lazy `Binary` that inflates on `stream()`.** The usual shape
|
|
130
|
+
is a `class ZipEntryBinary extends Binary` in App code that parses the local
|
|
131
|
+
header on demand. It passes locally and breaks in production: a `Binary`
|
|
132
|
+
given to a primitive is the input of a durable activity, serialized to
|
|
133
|
+
storage before the activity runs, and rehydrated in a process where the
|
|
134
|
+
closure no longer exists. Its `contentLength` also has to lie — the real size
|
|
135
|
+
lives in the central directory — so every ranged `read` over it truncates.
|
|
136
|
+
`unzip` does the same work inside an activity; that is the supported way.
|
|
137
|
+
- **Inflating in workflow code.** `createInflateRaw` in a workflow re-runs on
|
|
138
|
+
every replay and holds the content in workflow memory. Extraction belongs to
|
|
139
|
+
an activity.
|
|
140
|
+
- **Calling `unzip` per record.** Extract once, high in the workflow, and pass
|
|
141
|
+
the entry `Binary` down to children. `unzip` inside a loop or inside a helper
|
|
142
|
+
every child calls turns one extraction into thousands of activities.
|
|
143
|
+
- **Extracting to read metadata.** If the run only needs a name, a size or a
|
|
144
|
+
date, `list` answers without transferring content.
|
|
145
|
+
- **Trusting `include` to find something.** It is a filter: no match is an
|
|
146
|
+
empty array, not an error.
|
|
147
|
+
|
|
148
|
+
## Related skills
|
|
149
|
+
|
|
150
|
+
- `framework-fs-split` — `split`/`join`/`read`: what to do with the entry once
|
|
151
|
+
it is a `Binary`.
|
|
152
|
+
- `framework-sftp` — `sftp.get` is where most archives come from.
|
|
153
|
+
- `framework-transforms` — JSON inside the archive: normalize to NDJSON before
|
|
154
|
+
fanning out.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @t4h.framework/zip
|
|
2
|
+
|
|
3
|
+
## 0.0.0-experimental-20260907231524
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#163](https://github.com/tech4humans-brasil/framework/pull/163) [`ff14777`](https://github.com/tech4humans-brasil/framework/commit/ff147776f44b5cb6ebb4310f44fd57aecf1df79e) Thanks [@gusteycamargo](https://github.com/gusteycamargo)! - Adds `@t4h.framework/zip`: reading ZIP archives from inside a workflow.
|
|
8
|
+
|
|
9
|
+
`unzip(archive, options?)` extracts the entries into files and resolves with
|
|
10
|
+
`ZipEntry[]` — `{ path, binary, contentLength, compressedLength,
|
|
11
|
+
compressionMethod, crc32, lastModifiedAt, directory }` — and
|
|
12
|
+
`list(archive, options?)` returns the same metadata without transferring any
|
|
13
|
+
content. `include`/`exclude` globs select what is extracted or listed,
|
|
14
|
+
`encoding` decodes legacy entry names and `contentType` labels the extracted
|
|
15
|
+
files.
|
|
16
|
+
|
|
17
|
+
Both read the archive back to front through `FileSystemClaim.slice`: the tail
|
|
18
|
+
of the file gives the whole listing (about a kilobyte on an archive without a
|
|
19
|
+
comment) and each entry is fetched by byte range, inflated in a stream and
|
|
20
|
+
written through `FileSystemClaim.write` with backpressure — content never
|
|
21
|
+
enters workflow memory or the history, and extracting one file out of a large
|
|
22
|
+
archive does not download the archive. A binary the claim cannot slice (SFTP,
|
|
23
|
+
HTTP) is streamed once into a durable copy first, the same trade `split` makes.
|
|
24
|
+
|
|
25
|
+
Every entry is verified against its CRC-32 and declared size while it flows to
|
|
26
|
+
storage, and the inflate is aborted the moment the output goes past that size,
|
|
27
|
+
so an entry that lies never writes more than it declares. Entry paths that
|
|
28
|
+
escape the extraction directory, encrypted entries and compression methods
|
|
29
|
+
other than stored and deflate are refused. ZIP64, data descriptors, archive
|
|
30
|
+
comments, UTF-8 and legacy names are handled.
|
|
31
|
+
|
|
32
|
+
This replaces hand-rolled lazy `Binary` subclasses that inflate on `stream()`
|
|
33
|
+
in App code: those are serialized as activity input, cannot be rehydrated after
|
|
34
|
+
a replay in another process, and have to lie about `contentLength`.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# @t4h.framework/zip
|
|
2
|
+
|
|
3
|
+
Reads ZIP archives inside a workflow. `list` tells you what is in an archive
|
|
4
|
+
without transferring its content, and `unzip` unpacks the entries you ask for
|
|
5
|
+
into files — each one a `Binary`, the same reference every other package
|
|
6
|
+
consumes.
|
|
7
|
+
|
|
8
|
+
Both are durable activities: the work happens once, the result is recorded in
|
|
9
|
+
the workflow history, and a replay never re-reads the archive.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @t4h.framework/zip
|
|
15
|
+
# or
|
|
16
|
+
yarn add @t4h.framework/zip
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Peer dependencies: `@t4h.framework/core` and `@t4h.framework/fs` — entries are
|
|
20
|
+
written through the `FileSystemClaim`, so a file system provider must be
|
|
21
|
+
registered in the runtime.
|
|
22
|
+
|
|
23
|
+
## Basic usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { unzip } from '@t4h.framework/zip'
|
|
27
|
+
|
|
28
|
+
const [entry] = await unzip(archive)
|
|
29
|
+
|
|
30
|
+
// entry.binary is a regular Binary: read a window, split it, upload it
|
|
31
|
+
const header = await read(entry.binary, { range: { start: 0, end: 240 } })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`archive` is any `Binary`: an `sftp.get`, an HTTP response body, a file written
|
|
35
|
+
by `write`. `unzip` resolves with one `ZipEntry` per file in the archive, in
|
|
36
|
+
central directory order, directories excluded.
|
|
37
|
+
|
|
38
|
+
## Extract only what you need
|
|
39
|
+
|
|
40
|
+
An archive with three thousand invoices in it does not have to become three
|
|
41
|
+
thousand files:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const [invoice] = await unzip(archive, { include: 'invoices/0001.txt' })
|
|
45
|
+
|
|
46
|
+
const csvs = await unzip(archive, {
|
|
47
|
+
include: '**.csv',
|
|
48
|
+
exclude: '**/draft-*.csv',
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`include` and `exclude` are glob patterns (or arrays of them) matched against
|
|
53
|
+
the whole entry path:
|
|
54
|
+
|
|
55
|
+
| Pattern | Matches |
|
|
56
|
+
| -------------- | --------------------------------------- |
|
|
57
|
+
| `notes.txt` | exactly that path |
|
|
58
|
+
| `*.txt` | `.txt` files at the root of the archive |
|
|
59
|
+
| `invoices/*` | anything directly inside `invoices/` |
|
|
60
|
+
| `**.csv` | `.csv` files at any depth |
|
|
61
|
+
| `report-?.txt` | `report-1.txt`, not `report-42.txt` |
|
|
62
|
+
|
|
63
|
+
Matching is case sensitive, `include` runs before `exclude`, and an `include`
|
|
64
|
+
that matches nothing resolves with an empty array rather than throwing.
|
|
65
|
+
|
|
66
|
+
## Listing without unpacking
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { list } from '@t4h.framework/zip'
|
|
70
|
+
|
|
71
|
+
const entries = await list(archive)
|
|
72
|
+
// [{ path, contentLength, compressedLength, compressionMethod,
|
|
73
|
+
// crc32, lastModifiedAt, directory }, ...]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`list` reads the archive's index (its central directory), which lives in the
|
|
77
|
+
last few kilobytes of the file — so on a sliceable binary a listing costs about
|
|
78
|
+
a kilobyte, whatever the archive weighs. Use it whenever the decision (does
|
|
79
|
+
this archive contain the file I need? how big is it?) can be made from metadata
|
|
80
|
+
alone, and unzip afterwards only if the content is really needed.
|
|
81
|
+
|
|
82
|
+
Unlike `unzip`, `list` reports directory entries too, flagged with
|
|
83
|
+
`directory: true`.
|
|
84
|
+
|
|
85
|
+
## How the bytes move
|
|
86
|
+
|
|
87
|
+
The package never holds an archive, or an entry, in memory.
|
|
88
|
+
|
|
89
|
+
1. **Ranged reads.** A ZIP is read back to front — the central directory sits
|
|
90
|
+
at the end of the file and points backwards at each entry — so every read
|
|
91
|
+
goes through `FileSystemClaim.slice`: the tail for the index, one local
|
|
92
|
+
header per entry, then the entry's compressed range.
|
|
93
|
+
2. **Streaming inflate.** That range is streamed through `inflateRaw` and
|
|
94
|
+
straight into `FileSystemClaim.write`, with backpressure all the way.
|
|
95
|
+
Extracting a thousand entries costs the same memory as extracting one.
|
|
96
|
+
3. **One entry at a time.** Entries are extracted sequentially, on purpose:
|
|
97
|
+
parallel extraction would multiply the memory and the storage pressure for
|
|
98
|
+
no gain on a single source stream.
|
|
99
|
+
4. **Verified content.** Every entry is checked against the CRC-32 and the size
|
|
100
|
+
the archive declares while it flows to storage, so a truncated download or a
|
|
101
|
+
corrupted archive fails the activity instead of quietly producing a shorter
|
|
102
|
+
file.
|
|
103
|
+
|
|
104
|
+
### Binaries that cannot be sliced
|
|
105
|
+
|
|
106
|
+
Ranged reads need a binary the file system claim owns. An `sftp.get` reference
|
|
107
|
+
or an HTTP body is not one: for those, the archive is streamed **once** into
|
|
108
|
+
storage (`zip/<activity-id>.zip`) and the ranges are read from that copy — the
|
|
109
|
+
same trade `split` makes. It costs one pass and one durable copy of the
|
|
110
|
+
archive, and everything downstream behaves identically.
|
|
111
|
+
|
|
112
|
+
Nothing has to be configured for this; it is the fallback, not a mode.
|
|
113
|
+
|
|
114
|
+
## Memory: what scales with what
|
|
115
|
+
|
|
116
|
+
Bounded, whatever the archive weighs: entry content (source range →
|
|
117
|
+
`inflateRaw` → CRC-32 → `FileSystemClaim.write`, one entry at a time), the
|
|
118
|
+
reads over the archive (a ranged read of the tail plus one 30-byte local header
|
|
119
|
+
per entry) and the workflow history (references only).
|
|
120
|
+
|
|
121
|
+
What scales, and with what:
|
|
122
|
+
|
|
123
|
+
- **The central directory is read whole**, so memory grows with the _number_ of
|
|
124
|
+
entries — 50 to 100 bytes each. Three thousand entries is 200 KB; a million
|
|
125
|
+
is around 100 MB.
|
|
126
|
+
- **`unzip` without `include` extracts everything**: one file in storage and
|
|
127
|
+
one reference in the history per entry.
|
|
128
|
+
- **A zip bomb costs storage, not memory.** The read stops at the declared
|
|
129
|
+
compressed size, and the inflate is aborted the moment the output goes past
|
|
130
|
+
the declared uncompressed size — an entry that lies never writes more than it
|
|
131
|
+
declares. One that honestly declares 40 GB still writes 40 GB, so check
|
|
132
|
+
`contentLength` with `list` beforehand.
|
|
133
|
+
- **A non-sliceable source** is streamed once into a durable copy of the
|
|
134
|
+
archive; whether that copy streams is the `FileSystemClaim`'s contract.
|
|
135
|
+
- **Downstream is yours.** `buffer(await entry.binary.stream())` pulls the file
|
|
136
|
+
into workflow memory; `read` is capped at 1 MiB and `split` streams.
|
|
137
|
+
|
|
138
|
+
## Never build a lazy `Binary` by hand
|
|
139
|
+
|
|
140
|
+
The temptation, when a workflow needs one entry out of a zip, is to write a
|
|
141
|
+
`Binary` subclass that inflates on `stream()` and pass it around. It works in
|
|
142
|
+
the dev server and breaks in production:
|
|
143
|
+
|
|
144
|
+
- A `Binary` handed to a primitive becomes the **input of a durable activity**,
|
|
145
|
+
and that input is serialized to storage before the activity runs. A class
|
|
146
|
+
holding a closure over another stream has nothing to serialize into.
|
|
147
|
+
- After a replay the activity may run in **another process**, where the object
|
|
148
|
+
no longer exists.
|
|
149
|
+
- `contentLength` has to lie (the uncompressed size is only in the central
|
|
150
|
+
directory), and every consumer that trusts it — a ranged `read`, a
|
|
151
|
+
`Content-Length` header — is then wrong.
|
|
152
|
+
|
|
153
|
+
`unzip` is the supported answer: it does the same work inside an activity, and
|
|
154
|
+
what travels is a file reference the runtime knows how to rehydrate.
|
|
155
|
+
|
|
156
|
+
## Cost model
|
|
157
|
+
|
|
158
|
+
`unzip` and `list` are activities, so each call is one history entry with the
|
|
159
|
+
work behind it done exactly once. The reference to watch is **how many times
|
|
160
|
+
you call them**, not how big the archive is:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// once per run, however many records follow
|
|
164
|
+
const [entry] = await unzip(archive, { include: 'MOVIMENTO.TXT' })
|
|
165
|
+
|
|
166
|
+
const batch = await split(entry.binary, processRecord, {
|
|
167
|
+
delimiter: Split.line(),
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Calling `unzip` once per record instead — inside a loop, or inside a helper
|
|
172
|
+
called by every child — turns one extraction into thousands of activities, each
|
|
173
|
+
with its own history entry and its own copy of the entry in storage. Extract
|
|
174
|
+
high, pass the `Binary` down.
|
|
175
|
+
|
|
176
|
+
## Limits
|
|
177
|
+
|
|
178
|
+
- **Deflate and stored only** (methods `0` and `8`) — the two an archiver
|
|
179
|
+
produces without being asked. Anything else (bzip2, LZMA, zstd) is refused
|
|
180
|
+
with a `TypeError` naming the method.
|
|
181
|
+
- **No encryption.** An encrypted entry is refused; the ZIP crypto formats are
|
|
182
|
+
either broken (ZipCrypto) or vendor-specific (AES extensions).
|
|
183
|
+
- **No writing.** This package reads archives; it does not create them.
|
|
184
|
+
- **Reading, not extracting to disk.** Entries land wherever the
|
|
185
|
+
`FileSystemClaim` puts them, under `zip/<activity-id>/<entry path>`.
|
|
186
|
+
- **Hostile paths are refused.** An entry whose path is absolute, drive
|
|
187
|
+
qualified or contains `..` fails the whole activity before anything is
|
|
188
|
+
written (the "zip slip" class of archive).
|
|
189
|
+
|
|
190
|
+
ZIP64 archives, entries written with a data descriptor (the streaming writers'
|
|
191
|
+
flavour, where the local header has no sizes), UTF-8 and legacy entry names,
|
|
192
|
+
archive comments and empty entries are all handled.
|
|
193
|
+
|
|
194
|
+
## Errors
|
|
195
|
+
|
|
196
|
+
| Situation | Error |
|
|
197
|
+
| ----------------------------------------------- | ------------- |
|
|
198
|
+
| Not a ZIP, or truncated beyond repair | `SyntaxError` |
|
|
199
|
+
| CRC-32 or size mismatch on an entry | `SyntaxError` |
|
|
200
|
+
| Encrypted entry, unsupported compression method | `TypeError` |
|
|
201
|
+
| Entry path escaping the extraction directory | `TypeError` |
|
|
202
|
+
| Multi-disk archive | `TypeError` |
|
|
203
|
+
|
|
204
|
+
## Runtime requirements
|
|
205
|
+
|
|
206
|
+
- Claim provider: `FileSystemClaim` (from `@t4h.framework/fs`).
|
|
207
|
+
- Production allowlist: register `ZipExtractActivity` and `ZipListActivity`
|
|
208
|
+
through `runtime.addActivity`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Binary, SyncActivity } from '@t4h.framework/core';
|
|
2
|
+
import type { ZipEntry } from '../typings/ZipEntry.js';
|
|
3
|
+
export type UnzipOptions = {
|
|
4
|
+
include?: string | readonly string[];
|
|
5
|
+
exclude?: string | readonly string[];
|
|
6
|
+
encoding?: BufferEncoding;
|
|
7
|
+
contentType?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ZipExtractActivityInput = {
|
|
10
|
+
binary: Binary;
|
|
11
|
+
options: UnzipOptions;
|
|
12
|
+
};
|
|
13
|
+
export declare class ZipExtractActivity extends SyncActivity<readonly [binary: Binary, options: UnzipOptions], ZipExtractActivityInput, ZipEntry[], ZipEntry[]> {
|
|
14
|
+
private readonly claims;
|
|
15
|
+
toInput(binary: Binary, options: UnzipOptions): ZipExtractActivityInput;
|
|
16
|
+
run({ binary, options, }: ZipExtractActivityInput): Promise<ZipEntry[]>;
|
|
17
|
+
toOutput(output: ZipEntry[]): ZipEntry[];
|
|
18
|
+
}
|
|
19
|
+
export declare function unzip(binary: Binary, options?: UnzipOptions): Promise<ZipEntry[]>;
|
|
20
|
+
//# sourceMappingURL=ZipExtractActivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipExtractActivity.d.ts","sourceRoot":"","sources":["../../src/activities/ZipExtractActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAQ/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAEtD,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IACpC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAED,qBAAa,kBAAmB,SAAQ,YAAY,CAClD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,EAChD,uBAAuB,EACvB,QAAQ,EAAE,EACV,QAAQ,EAAE,CACX;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;IAEK,OAAO,CACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,YAAY,GACpB,uBAAuB;IAIb,GAAG,CAAC,EACf,MAAM,EACN,OAAO,GACR,EAAE,uBAAuB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IA0BzC,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE;CAGhD;AAED,wBAAsB,KAAK,CACzB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAErB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Claim, History, SyncActivity } from '@t4h.framework/core';
|
|
2
|
+
import { FileSystemClaim } from '@t4h.framework/fs';
|
|
3
|
+
import { extractEntry } from '../helpers/extract-entry.js';
|
|
4
|
+
import { openArchive } from '../helpers/open-archive.js';
|
|
5
|
+
import { assertEntryPath } from '../tools/assert-entry-path.js';
|
|
6
|
+
import { selectEntries } from '../tools/select-entries.js';
|
|
7
|
+
import { toEntryInfo } from '../tools/to-entry-info.js';
|
|
8
|
+
export class ZipExtractActivity extends SyncActivity {
|
|
9
|
+
claims = new Claim({
|
|
10
|
+
fs: FileSystemClaim,
|
|
11
|
+
});
|
|
12
|
+
toInput(binary, options) {
|
|
13
|
+
return { binary, options };
|
|
14
|
+
}
|
|
15
|
+
async run({ binary, options, }) {
|
|
16
|
+
const { source, entries } = await openArchive(this.claims.fs, binary, {
|
|
17
|
+
encoding: options.encoding ?? 'utf8',
|
|
18
|
+
durablePath: `zip/${this.id}.zip`,
|
|
19
|
+
});
|
|
20
|
+
const selected = selectEntries(entries, options).filter(entry => !entry.directory);
|
|
21
|
+
for (const entry of selected)
|
|
22
|
+
assertEntryPath(entry.path);
|
|
23
|
+
const extracted = [];
|
|
24
|
+
for (const entry of selected) {
|
|
25
|
+
const content = await extractEntry(this.claims.fs, source, entry, {
|
|
26
|
+
path: `zip/${this.id}/${entry.path}`,
|
|
27
|
+
contentType: options.contentType,
|
|
28
|
+
});
|
|
29
|
+
extracted.push({ ...toEntryInfo(entry), binary: content });
|
|
30
|
+
}
|
|
31
|
+
return extracted;
|
|
32
|
+
}
|
|
33
|
+
toOutput(output) {
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function unzip(binary, options = {}) {
|
|
38
|
+
return await History.reconciler(ZipExtractActivity, binary, options);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=ZipExtractActivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipExtractActivity.js","sourceRoot":"","sources":["../../src/activities/ZipExtractActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAevD,MAAM,OAAO,kBAAmB,SAAQ,YAKvC;IACkB,MAAM,GAAG,IAAI,KAAK,CAAC;QAClC,EAAE,EAAE,eAAe;KACpB,CAAC,CAAA;IAEK,OAAO,CACZ,MAAc,EACd,OAAqB;QAErB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IAC5B,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,EACf,MAAM,EACN,OAAO,GACiB;QACxB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE;YACpE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM;YACpC,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,MAAM;SAClC,CAAC,CAAA;QAEF,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,CACrD,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAC1B,CAAA;QAED,KAAK,MAAM,KAAK,IAAI,QAAQ;YAAE,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAEzD,MAAM,SAAS,GAAe,EAAE,CAAA;QAEhC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;gBAChE,IAAI,EAAE,OAAO,IAAI,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE;gBACpC,WAAW,EAAE,OAAO,CAAC,WAAW;aACjC,CAAC,CAAA;YAEF,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;QAC5D,CAAC;QAED,OAAO,SAAS,CAAA;IAClB,CAAC;IAEM,QAAQ,CAAC,MAAkB;QAChC,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,MAAc,EACd,UAAwB,EAAE;IAE1B,OAAO,MAAM,OAAO,CAAC,UAAU,CAAC,kBAAkB,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;AACtE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Binary, SyncActivity } from '@t4h.framework/core';
|
|
2
|
+
import type { ZipEntryInfo } from '../typings/ZipEntry.js';
|
|
3
|
+
export type ZipListOptions = {
|
|
4
|
+
include?: string | readonly string[];
|
|
5
|
+
exclude?: string | readonly string[];
|
|
6
|
+
encoding?: BufferEncoding;
|
|
7
|
+
};
|
|
8
|
+
export type ZipListActivityInput = {
|
|
9
|
+
binary: Binary;
|
|
10
|
+
options: ZipListOptions;
|
|
11
|
+
};
|
|
12
|
+
export declare class ZipListActivity extends SyncActivity<readonly [binary: Binary, options: ZipListOptions], ZipListActivityInput, ZipEntryInfo[], ZipEntryInfo[]> {
|
|
13
|
+
private readonly claims;
|
|
14
|
+
toInput(binary: Binary, options: ZipListOptions): ZipListActivityInput;
|
|
15
|
+
run({ binary, options, }: ZipListActivityInput): Promise<ZipEntryInfo[]>;
|
|
16
|
+
toOutput(output: ZipEntryInfo[]): ZipEntryInfo[];
|
|
17
|
+
}
|
|
18
|
+
export declare function list(binary: Binary, options?: ZipListOptions): Promise<ZipEntryInfo[]>;
|
|
19
|
+
//# sourceMappingURL=ZipListActivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipListActivity.d.ts","sourceRoot":"","sources":["../../src/activities/ZipListActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAM/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IACpC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,CAAA;CACxB,CAAA;AAED,qBAAa,eAAgB,SAAQ,YAAY,CAC/C,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,EAClD,oBAAoB,EACpB,YAAY,EAAE,EACd,YAAY,EAAE,CACf;IACC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;IAEK,OAAO,CACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,cAAc,GACtB,oBAAoB;IAIV,GAAG,CAAC,EACf,MAAM,EACN,OAAO,GACR,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAS1C,QAAQ,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE;CAGxD;AAED,wBAAsB,IAAI,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,EAAE,CAAC,CAEzB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Claim, History, SyncActivity } from '@t4h.framework/core';
|
|
2
|
+
import { FileSystemClaim } from '@t4h.framework/fs';
|
|
3
|
+
import { openArchive } from '../helpers/open-archive.js';
|
|
4
|
+
import { selectEntries } from '../tools/select-entries.js';
|
|
5
|
+
import { toEntryInfo } from '../tools/to-entry-info.js';
|
|
6
|
+
export class ZipListActivity extends SyncActivity {
|
|
7
|
+
claims = new Claim({
|
|
8
|
+
fs: FileSystemClaim,
|
|
9
|
+
});
|
|
10
|
+
toInput(binary, options) {
|
|
11
|
+
return { binary, options };
|
|
12
|
+
}
|
|
13
|
+
async run({ binary, options, }) {
|
|
14
|
+
const { entries } = await openArchive(this.claims.fs, binary, {
|
|
15
|
+
encoding: options.encoding ?? 'utf8',
|
|
16
|
+
durablePath: `zip/${this.id}.zip`,
|
|
17
|
+
});
|
|
18
|
+
return selectEntries(entries, options).map(toEntryInfo);
|
|
19
|
+
}
|
|
20
|
+
toOutput(output) {
|
|
21
|
+
return output;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function list(binary, options = {}) {
|
|
25
|
+
return await History.reconciler(ZipListActivity, binary, options);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=ZipListActivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipListActivity.js","sourceRoot":"","sources":["../../src/activities/ZipListActivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAcvD,MAAM,OAAO,eAAgB,SAAQ,YAKpC;IACkB,MAAM,GAAG,IAAI,KAAK,CAAC;QAClC,EAAE,EAAE,eAAe;KACpB,CAAC,CAAA;IAEK,OAAO,CACZ,MAAc,EACd,OAAuB;QAEvB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IAC5B,CAAC;IAEM,KAAK,CAAC,GAAG,CAAC,EACf,MAAM,EACN,OAAO,GACc;QACrB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE;YAC5D,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM;YACpC,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,MAAM;SAClC,CAAC,CAAA;QAEF,OAAO,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACzD,CAAC;IAEM,QAAQ,CAAC,MAAsB;QACpC,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,MAAc,EACd,UAA0B,EAAE;IAE5B,OAAO,MAAM,OAAO,CAAC,UAAU,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;AACnE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Transform, type TransformCallback } from 'node:stream';
|
|
2
|
+
export type ZipChecksumExpectation = {
|
|
3
|
+
path: string;
|
|
4
|
+
crc32: number;
|
|
5
|
+
contentLength: number;
|
|
6
|
+
};
|
|
7
|
+
export declare class ZipChecksum extends Transform {
|
|
8
|
+
private readonly expected;
|
|
9
|
+
private checksum;
|
|
10
|
+
private length;
|
|
11
|
+
constructor(expected: ZipChecksumExpectation);
|
|
12
|
+
_transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
|
|
13
|
+
_flush(callback: TransformCallback): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=ZipChecksum.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipChecksum.d.ts","sourceRoot":"","sources":["../../src/helpers/ZipChecksum.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAG/D,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,qBAAa,WAAY,SAAQ,SAAS;IAK5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJrC,OAAO,CAAC,QAAQ,CAAI;IAEpB,OAAO,CAAC,MAAM,CAAI;gBAEW,QAAQ,EAAE,sBAAsB;IAI7C,UAAU,CACxB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,iBAAiB,GAC1B,IAAI;IAgBS,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;CAiB1D"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
|
+
import { crc32 } from 'node:zlib';
|
|
3
|
+
export class ZipChecksum extends Transform {
|
|
4
|
+
expected;
|
|
5
|
+
checksum = 0;
|
|
6
|
+
length = 0;
|
|
7
|
+
constructor(expected) {
|
|
8
|
+
super();
|
|
9
|
+
this.expected = expected;
|
|
10
|
+
}
|
|
11
|
+
_transform(chunk, _encoding, callback) {
|
|
12
|
+
const { path, contentLength } = this.expected;
|
|
13
|
+
if (this.length + chunk.length > contentLength)
|
|
14
|
+
return callback(new SyntaxError(`ZIP entry "${path}" unpacked past the ${contentLength} bytes the archive declares`));
|
|
15
|
+
this.checksum = crc32(chunk, this.checksum);
|
|
16
|
+
this.length += chunk.length;
|
|
17
|
+
callback(null, chunk);
|
|
18
|
+
}
|
|
19
|
+
_flush(callback) {
|
|
20
|
+
const { path, crc32: expectedChecksum, contentLength } = this.expected;
|
|
21
|
+
if (this.length !== contentLength)
|
|
22
|
+
return callback(new SyntaxError(`ZIP entry "${path}" unpacked to ${this.length} bytes, the archive declares ${contentLength}`));
|
|
23
|
+
if (this.checksum !== expectedChecksum)
|
|
24
|
+
return callback(new SyntaxError(`ZIP entry "${path}" failed its CRC-32 check`));
|
|
25
|
+
callback();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=ZipChecksum.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipChecksum.js","sourceRoot":"","sources":["../../src/helpers/ZipChecksum.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAA0B,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAQjC,MAAM,OAAO,WAAY,SAAQ,SAAS;IAKX;IAJrB,QAAQ,GAAG,CAAC,CAAA;IAEZ,MAAM,GAAG,CAAC,CAAA;IAElB,YAA6B,QAAgC;QAC3D,KAAK,EAAE,CAAA;QADoB,aAAQ,GAAR,QAAQ,CAAwB;IAE7D,CAAC;IAEe,UAAU,CACxB,KAAa,EACb,SAAyB,EACzB,QAA2B;QAE3B,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAA;QAE7C,IAAI,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,aAAa;YAC5C,OAAO,QAAQ,CACb,IAAI,WAAW,CACb,cAAc,IAAI,uBAAuB,aAAa,6BAA6B,CACpF,CACF,CAAA;QAEH,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC3C,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAA;QAE3B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACvB,CAAC;IAEe,MAAM,CAAC,QAA2B;QAChD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAA;QAEtE,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa;YAC/B,OAAO,QAAQ,CACb,IAAI,WAAW,CACb,cAAc,IAAI,iBAAiB,IAAI,CAAC,MAAM,gCAAgC,aAAa,EAAE,CAC9F,CACF,CAAA;QAEH,IAAI,IAAI,CAAC,QAAQ,KAAK,gBAAgB;YACpC,OAAO,QAAQ,CACb,IAAI,WAAW,CAAC,cAAc,IAAI,2BAA2B,CAAC,CAC/D,CAAA;QAEH,QAAQ,EAAE,CAAA;IACZ,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
import type { Binary } from '@t4h.framework/core';
|
|
3
|
+
import type { FileSystemClaim } from '@t4h.framework/fs';
|
|
4
|
+
export declare class ZipSliceUnavailableError extends Error {
|
|
5
|
+
readonly reason: unknown;
|
|
6
|
+
constructor(reason: unknown);
|
|
7
|
+
}
|
|
8
|
+
export declare class ZipSource {
|
|
9
|
+
private readonly fs;
|
|
10
|
+
readonly binary: Binary;
|
|
11
|
+
constructor(fs: FileSystemClaim, binary: Binary);
|
|
12
|
+
get contentLength(): number;
|
|
13
|
+
read(start: number, end: number): Promise<Buffer>;
|
|
14
|
+
stream(start: number, end: number): Promise<Readable>;
|
|
15
|
+
private slice;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=ZipSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipSource.d.ts","sourceRoot":"","sources":["../../src/helpers/ZipSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD,qBAAa,wBAAyB,SAAQ,KAAK;aACrB,MAAM,EAAE,OAAO;gBAAf,MAAM,EAAE,OAAO;CAK5C;AAED,qBAAa,SAAS;IAElB,OAAO,CAAC,QAAQ,CAAC,EAAE;aACH,MAAM,EAAE,MAAM;gBADb,EAAE,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM;IAGhC,IAAW,aAAa,IAAI,MAAM,CAEjC;IAEY,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAMjD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAWpD,KAAK;CAOpB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { buffer } from 'node:stream/consumers';
|
|
2
|
+
export class ZipSliceUnavailableError extends Error {
|
|
3
|
+
reason;
|
|
4
|
+
constructor(reason) {
|
|
5
|
+
super('The file system claim could not slice the archive');
|
|
6
|
+
this.reason = reason;
|
|
7
|
+
this.name = 'ZipSliceUnavailableError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ZipSource {
|
|
11
|
+
fs;
|
|
12
|
+
binary;
|
|
13
|
+
constructor(fs, binary) {
|
|
14
|
+
this.fs = fs;
|
|
15
|
+
this.binary = binary;
|
|
16
|
+
}
|
|
17
|
+
get contentLength() {
|
|
18
|
+
return this.binary.contentLength;
|
|
19
|
+
}
|
|
20
|
+
async read(start, end) {
|
|
21
|
+
if (end <= start)
|
|
22
|
+
return Buffer.alloc(0);
|
|
23
|
+
return await buffer(await this.stream(start, end));
|
|
24
|
+
}
|
|
25
|
+
async stream(start, end) {
|
|
26
|
+
if (start < 0 || end > this.contentLength)
|
|
27
|
+
throw new SyntaxError(`ZIP archive is truncated: bytes ${start}-${end} are outside a file of ${this.contentLength} bytes`);
|
|
28
|
+
const slice = await this.slice(start, end);
|
|
29
|
+
return await slice.stream();
|
|
30
|
+
}
|
|
31
|
+
async slice(start, end) {
|
|
32
|
+
try {
|
|
33
|
+
return await this.fs.slice(this.binary, { start, end });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new ZipSliceUnavailableError(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=ZipSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ZipSource.js","sourceRoot":"","sources":["../../src/helpers/ZipSource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAK9C,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACrB;IAA5B,YAA4B,MAAe;QACzC,KAAK,CAAC,mDAAmD,CAAC,CAAA;QADhC,WAAM,GAAN,MAAM,CAAS;QAGzC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAA;IACxC,CAAC;CACF;AAED,MAAM,OAAO,SAAS;IAED;IACD;IAFlB,YACmB,EAAmB,EACpB,MAAc;QADb,OAAE,GAAF,EAAE,CAAiB;QACpB,WAAM,GAAN,MAAM,CAAQ;IAC7B,CAAC;IAEJ,IAAW,aAAa;QACtB,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,CAAA;IAClC,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,KAAa,EAAE,GAAW;QAC1C,IAAI,GAAG,IAAI,KAAK;YAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAExC,OAAO,MAAM,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAA;IACpD,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,GAAW;QAC5C,IAAI,KAAK,GAAG,CAAC,IAAI,GAAG,GAAG,IAAI,CAAC,aAAa;YACvC,MAAM,IAAI,WAAW,CACnB,mCAAmC,KAAK,IAAI,GAAG,0BAA0B,IAAI,CAAC,aAAa,QAAQ,CACpG,CAAA;QAEH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;QAE1C,OAAO,MAAM,KAAK,CAAC,MAAM,EAAE,CAAA;IAC7B,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,KAAa,EAAE,GAAW;QAC5C,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAA;QAC3C,CAAC;IACH,CAAC;CACF"}
|