@naturalcycles/nodejs-lib 15.109.0 → 15.111.0
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/dist/zip/index.d.ts +3 -0
- package/dist/zip/index.js +3 -0
- package/dist/zip/zip2.d.ts +37 -0
- package/dist/zip/zip2.js +51 -0
- package/dist/zip/zipInternal.d.ts +41 -0
- package/dist/zip/zipInternal.js +88 -0
- package/dist/zip/zipReader.d.ts +142 -0
- package/dist/zip/zipReader.js +475 -0
- package/dist/zip/zipWriter.d.ts +168 -0
- package/dist/zip/zipWriter.js +579 -0
- package/package.json +3 -3
- package/src/jwt/jwt.service.ts +1 -0
- package/src/stream/transform/worker/workerClassProxy.js +1 -0
- package/src/validation/ajv/jSchema.ts +10 -10
- package/src/zip/index.ts +3 -0
- package/src/zip/zip2.ts +56 -0
- package/src/zip/zipInternal.ts +111 -0
- package/src/zip/zipReader.ts +620 -0
- package/src/zip/zipWriter.ts +792 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
A minimal, dependency-free zip archive writer for Node.js 24+.
|
|
4
|
+
|
|
5
|
+
Adapted from yazl (https://github.com/thejoshwolfe/yazl) by Josh Wolfe (MIT License),
|
|
6
|
+
rewritten in a modern, Promise-based, async/await style:
|
|
7
|
+
|
|
8
|
+
- No external dependencies. `buffer-crc32` is replaced by the built-in `zlib.crc32`
|
|
9
|
+
(used incrementally while streaming), and the deflate is done with `node:zlib`.
|
|
10
|
+
- Promise API instead of callbacks/EventEmitter: `await writer.addFile(...)` etc.,
|
|
11
|
+
finished with `await writer.finalize()`.
|
|
12
|
+
- Streaming entries (`addStream`/`addFile`) are written with a data descriptor
|
|
13
|
+
(general purpose bit 3), so their size and CRC need not be known up front.
|
|
14
|
+
- Buffer entries (`addBuffer`) are written with their sizes and CRC inline.
|
|
15
|
+
|
|
16
|
+
Kept from yazl: ZIP64 output (auto-enabled past the 4 GiB / 0xffff limits, or via
|
|
17
|
+
`forceZip64`), the Info-ZIP extended timestamp (0x5455) for accurate mtimes, DOS
|
|
18
|
+
date/time encoding, file-name validation and per-entry/archive comments.
|
|
19
|
+
|
|
20
|
+
Shares the low-level format details (signatures, sizes, 64-bit and DOS date/time
|
|
21
|
+
helpers, name validation) with the reader via `zipInternal.ts`.
|
|
22
|
+
|
|
23
|
+
*/
|
|
24
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
25
|
+
import fsp from 'node:fs/promises';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { Transform, Writable } from 'node:stream';
|
|
28
|
+
import { finished, pipeline } from 'node:stream/promises';
|
|
29
|
+
import { promisify } from 'node:util';
|
|
30
|
+
import zlib from 'node:zlib';
|
|
31
|
+
import { comparators } from '@naturalcycles/js-lib/array/sort.js';
|
|
32
|
+
import { glob } from 'tinyglobby';
|
|
33
|
+
import { assertSafeZipEntryName, CDFH_SIG, CDFH_SIZE, DATA_DESCRIPTOR_SIG, DEFLATE, EOCDR_SIG, EOCDR_SIZE, LOCAL_FILE_HEADER_SIG, MAX_COMMENT_SIZE, normalizeZipEntryName, STORED, unixToDosDateTime, writeUInt64LE, ZIP64_EOCDL_SIG, ZIP64_EOCDL_SIZE, ZIP64_EOCDR_SIG, ZIP64_EOCDR_SIZE, } from './zipInternal.js';
|
|
34
|
+
// oxlint-disable no-bitwise, unicorn/prefer-math-trunc -- writing the binary ZIP format requires bitwise ops on bit flags, packed DOS date/time fields, and unsigned 32-bit attribute packing
|
|
35
|
+
const deflateRawAsync = promisify(zlib.deflateRaw.bind(zlib));
|
|
36
|
+
/**
|
|
37
|
+
* Create a zip archive on disk from a list of files and/or directories.
|
|
38
|
+
*
|
|
39
|
+
* Each input path may be a file (added directly) or a directory (walked
|
|
40
|
+
* recursively; all nested files are added). By default an entry's name is its
|
|
41
|
+
* path relative to its input's parent directory, so `zipFiles(['./photos'], 'out.zip')`
|
|
42
|
+
* stores entries under `photos/...`. Override the base via {@link ZipPathsOptions.baseDir}.
|
|
43
|
+
*
|
|
44
|
+
* Files discovered by walking a directory are added in deterministic (sorted)
|
|
45
|
+
* order; the explicit input order is otherwise preserved.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* await zipPaths(['./photos'], 'photos.zip') // a whole directory
|
|
49
|
+
* await zipPaths(['a.txt', 'log/b.txt'], 'out.zip') // a list of files
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* On failure the partially-written archive is removed.
|
|
53
|
+
*/
|
|
54
|
+
export async function zipPaths(paths, outputZipFilePath, opt = {}) {
|
|
55
|
+
const { baseDir, ...entryOpt } = opt;
|
|
56
|
+
const inputs = paths.map(p => path.resolve(p));
|
|
57
|
+
const files = await collectFiles(inputs, baseDir);
|
|
58
|
+
// Build the write stream here (rather than via createZip) so a mid-way failure
|
|
59
|
+
// can tear down the stream and remove the half-written archive.
|
|
60
|
+
const out = createWriteStream(outputZipFilePath);
|
|
61
|
+
const writer = new ZipWriter(out);
|
|
62
|
+
try {
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
await writer.addFile(file.absPath, file.name, entryOpt);
|
|
65
|
+
}
|
|
66
|
+
await writer.finalize();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
out.destroy();
|
|
70
|
+
await fsp.rm(outputZipFilePath, { force: true });
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build a zip archive entirely in memory and return it as a Buffer.
|
|
76
|
+
*
|
|
77
|
+
* For large archives or streamed inputs prefer {@link createZip} / {@link ZipWriter}.
|
|
78
|
+
*/
|
|
79
|
+
export async function createZipBuffer(entries) {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
const sink = new Writable({
|
|
82
|
+
write(chunk, _enc, cb) {
|
|
83
|
+
chunks.push(chunk);
|
|
84
|
+
cb();
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const writer = new ZipWriter(sink);
|
|
88
|
+
for (const { name, content, ...opt } of entries) {
|
|
89
|
+
if (content === undefined) {
|
|
90
|
+
await writer.addDirectory(name, opt);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await writer.addBuffer(content, name, opt);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await writer.finalize();
|
|
97
|
+
return Buffer.concat(chunks);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Writes a zip archive to a Node.js {@link Writable} stream.
|
|
101
|
+
*
|
|
102
|
+
* Create one directly over any `Writable`, or use {@link createZip} to write to
|
|
103
|
+
* a file. Add entries sequentially (each add method must be awaited before the
|
|
104
|
+
* next), then call {@link finalize} to write the central directory and close the
|
|
105
|
+
* stream.
|
|
106
|
+
*
|
|
107
|
+
* Implements `AsyncDisposable`, so `await using` finalizes automatically on scope
|
|
108
|
+
* exit:
|
|
109
|
+
*
|
|
110
|
+
* ```ts
|
|
111
|
+
* await using zip = createZip('archive.zip')
|
|
112
|
+
* await zip.addBuffer(Buffer.from('hello'), 'hello.txt')
|
|
113
|
+
* // zip.finalize() is called automatically here
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export class ZipWriter {
|
|
117
|
+
out;
|
|
118
|
+
offset = 0;
|
|
119
|
+
finalized = false;
|
|
120
|
+
streamError;
|
|
121
|
+
entries = [];
|
|
122
|
+
constructor(out) {
|
|
123
|
+
this.out = out;
|
|
124
|
+
// Capture stream errors so a pending/next write rejects instead of the
|
|
125
|
+
// 'error' event going unhandled and crashing the process.
|
|
126
|
+
this.out.once('error', err => {
|
|
127
|
+
this.streamError ??= err;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Add an in-memory buffer as a file entry.
|
|
132
|
+
* The CRC-32 and sizes are computed up front and written inline (no data descriptor).
|
|
133
|
+
*/
|
|
134
|
+
async addBuffer(data, fileName, opt = {}) {
|
|
135
|
+
this.assertWritable();
|
|
136
|
+
const entry = this.createEntry(fileName, false, opt);
|
|
137
|
+
entry.crc32 = zlib.crc32(data);
|
|
138
|
+
entry.uncompressedSize = data.length;
|
|
139
|
+
const stored = entry.method === STORED ? data : await deflateRawAsync(data, { level: entry.level });
|
|
140
|
+
entry.compressedSize = stored.length;
|
|
141
|
+
entry.crcAndFileSizeKnown = true;
|
|
142
|
+
await this.writeKnownEntry(entry, stored);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Add a file from disk, streaming its contents. The entry's mtime and mode
|
|
146
|
+
* default to the file's own (override via `opt`). If `fileName` is omitted,
|
|
147
|
+
* the file's base name is used.
|
|
148
|
+
*/
|
|
149
|
+
async addFile(filePath, fileName, opt = {}) {
|
|
150
|
+
this.assertWritable();
|
|
151
|
+
const stats = await fsp.stat(filePath);
|
|
152
|
+
const entry = this.createEntry(fileName ?? path.basename(filePath), false, {
|
|
153
|
+
mtime: Math.floor(stats.mtimeMs / 1000),
|
|
154
|
+
mode: stats.mode & 0xffff,
|
|
155
|
+
...opt,
|
|
156
|
+
});
|
|
157
|
+
await this.pumpEntry(entry, createReadStream(filePath));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Add a readable stream as a file entry. The size and CRC are computed while
|
|
161
|
+
* streaming and written in a trailing data descriptor.
|
|
162
|
+
*/
|
|
163
|
+
async addStream(stream, fileName, opt = {}) {
|
|
164
|
+
this.assertWritable();
|
|
165
|
+
const entry = this.createEntry(fileName, false, opt);
|
|
166
|
+
await this.pumpEntry(entry, stream);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Add an explicit (empty) directory entry. A trailing `/` is added if missing.
|
|
170
|
+
* Directory entries are optional in zip archives but make empty directories explicit.
|
|
171
|
+
*/
|
|
172
|
+
async addDirectory(fileName, opt = {}) {
|
|
173
|
+
this.assertWritable();
|
|
174
|
+
const entry = this.createEntry(fileName, true, opt);
|
|
175
|
+
await this.writeKnownEntry(entry, EMPTY);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Write the central directory and end-of-central-directory records, then end
|
|
179
|
+
* the underlying stream and wait for it to flush. Idempotent.
|
|
180
|
+
*/
|
|
181
|
+
async finalize(opt = {}) {
|
|
182
|
+
if (this.finalized)
|
|
183
|
+
return;
|
|
184
|
+
const comment = opt.comment ? Buffer.from(opt.comment, 'utf8') : EMPTY;
|
|
185
|
+
if (comment.length > MAX_COMMENT_SIZE) {
|
|
186
|
+
throw new Error(`archive comment is too long: ${comment.length} > ${MAX_COMMENT_SIZE} bytes`);
|
|
187
|
+
}
|
|
188
|
+
// A comment containing this signature would confuse readers that scan
|
|
189
|
+
// backwards for the end-of-central-directory record.
|
|
190
|
+
if (comment.includes(EOCDR_SIG_BYTES)) {
|
|
191
|
+
throw new Error('archive comment must not contain the end-of-central-directory signature');
|
|
192
|
+
}
|
|
193
|
+
this.finalized = true;
|
|
194
|
+
const centralDirectoryOffset = this.offset;
|
|
195
|
+
for (const entry of this.entries) {
|
|
196
|
+
await this.write(buildCentralDirectoryRecord(entry));
|
|
197
|
+
}
|
|
198
|
+
const centralDirectorySize = this.offset - centralDirectoryOffset;
|
|
199
|
+
await this.write(buildEndRecords({
|
|
200
|
+
entryCount: this.entries.length,
|
|
201
|
+
centralDirectoryOffset,
|
|
202
|
+
centralDirectorySize,
|
|
203
|
+
comment,
|
|
204
|
+
zip64EocdrOffset: this.offset,
|
|
205
|
+
forceZip64: opt.forceZip64 ?? false,
|
|
206
|
+
}));
|
|
207
|
+
await this.finishStream();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Called by `await using`; finalizes the archive if not already done. See {@link finalize}.
|
|
211
|
+
*/
|
|
212
|
+
async [Symbol.asyncDispose]() {
|
|
213
|
+
await this.finalize();
|
|
214
|
+
}
|
|
215
|
+
assertWritable() {
|
|
216
|
+
if (this.streamError)
|
|
217
|
+
throw this.streamError;
|
|
218
|
+
if (this.finalized)
|
|
219
|
+
throw new Error('ZipWriter has already been finalized');
|
|
220
|
+
}
|
|
221
|
+
/** Build the in-memory representation of an entry from its name and options. */
|
|
222
|
+
createEntry(fileName, isDirectory, opt) {
|
|
223
|
+
const nameBuf = Buffer.from(normalizeAndValidateEntryName(fileName, isDirectory), 'utf8');
|
|
224
|
+
if (nameBuf.length > MAX_COMMENT_SIZE) {
|
|
225
|
+
throw new Error(`zip entry name is too long: ${nameBuf.length} > ${MAX_COMMENT_SIZE} bytes`);
|
|
226
|
+
}
|
|
227
|
+
const mtime = opt.mtime ?? Math.floor(Date.now() / 1000);
|
|
228
|
+
const { date, time } = unixToDosDateTime(mtime);
|
|
229
|
+
const mode = opt.mode ?? (isDirectory ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE);
|
|
230
|
+
if ((mode & 0xffff) !== mode) {
|
|
231
|
+
throw new Error(`invalid mode: expected 0 <= ${mode} <= 65535`);
|
|
232
|
+
}
|
|
233
|
+
const compress = isDirectory ? false : (opt.compress ?? true);
|
|
234
|
+
const level = compress ? (opt.level ?? DEFAULT_DEFLATE_LEVEL) : 0;
|
|
235
|
+
const commentBuf = opt.comment ? Buffer.from(opt.comment, 'utf8') : EMPTY;
|
|
236
|
+
if (commentBuf.length > MAX_COMMENT_SIZE) {
|
|
237
|
+
throw new Error(`zip entry comment is too long: ${commentBuf.length} > ${MAX_COMMENT_SIZE} bytes`);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
nameBuf,
|
|
241
|
+
isDirectory,
|
|
242
|
+
method: level === 0 ? STORED : DEFLATE,
|
|
243
|
+
level,
|
|
244
|
+
// Directories carry no data, so their (zero) sizes and CRC are known up front.
|
|
245
|
+
crcAndFileSizeKnown: isDirectory,
|
|
246
|
+
crc32: 0,
|
|
247
|
+
uncompressedSize: 0,
|
|
248
|
+
compressedSize: 0,
|
|
249
|
+
relativeOffsetOfLocalHeader: 0,
|
|
250
|
+
lastModFileTime: time,
|
|
251
|
+
lastModFileDate: date,
|
|
252
|
+
mtimeSeconds: clampInt32(mtime),
|
|
253
|
+
// Unix mode packed into the high 16 bits; `>>> 0` keeps it an unsigned uint32.
|
|
254
|
+
externalFileAttributes: (mode << 16) >>> 0,
|
|
255
|
+
commentBuf,
|
|
256
|
+
forceZip64: opt.forceZip64 ?? false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** Write an entry whose CRC and sizes are already known: header, name, data, no descriptor. */
|
|
260
|
+
async writeKnownEntry(entry, data) {
|
|
261
|
+
entry.relativeOffsetOfLocalHeader = this.offset;
|
|
262
|
+
await this.write(buildLocalFileHeader(entry));
|
|
263
|
+
await this.write(entry.nameBuf);
|
|
264
|
+
await this.write(data);
|
|
265
|
+
this.entries.push(entry);
|
|
266
|
+
}
|
|
267
|
+
/** Write a streamed entry: header with bit 3 set, streamed data, then a data descriptor. */
|
|
268
|
+
async pumpEntry(entry, source) {
|
|
269
|
+
entry.relativeOffsetOfLocalHeader = this.offset;
|
|
270
|
+
await this.write(buildLocalFileHeader(entry));
|
|
271
|
+
await this.write(entry.nameBuf);
|
|
272
|
+
const { crc32, uncompressedSize, compressedSize } = await this.pumpData(source, entry.method === DEFLATE, entry.level);
|
|
273
|
+
entry.crc32 = crc32;
|
|
274
|
+
entry.uncompressedSize = uncompressedSize;
|
|
275
|
+
entry.compressedSize = compressedSize;
|
|
276
|
+
await this.write(buildDataDescriptor(entry, useZip64(entry)));
|
|
277
|
+
this.entries.push(entry);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Pipe `source` to the output, computing the CRC-32 and uncompressed size on
|
|
281
|
+
* the way in, optionally deflating, and counting the compressed bytes written.
|
|
282
|
+
*/
|
|
283
|
+
async pumpData(source, compress, level) {
|
|
284
|
+
let crc32 = 0;
|
|
285
|
+
let uncompressedSize = 0;
|
|
286
|
+
let compressedSize = 0;
|
|
287
|
+
// Tap the uncompressed bytes for the CRC-32 and size before they are deflated.
|
|
288
|
+
const tap = new Transform({
|
|
289
|
+
transform(chunk, _enc, cb) {
|
|
290
|
+
crc32 = zlib.crc32(chunk, crc32);
|
|
291
|
+
uncompressedSize += chunk.length;
|
|
292
|
+
cb(null, chunk);
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
// Final pipeline stage: write each (possibly compressed) chunk to the output,
|
|
296
|
+
// counting bytes. Awaiting `write` propagates backpressure up the pipeline.
|
|
297
|
+
const drain = async (src) => {
|
|
298
|
+
for await (const chunk of src) {
|
|
299
|
+
compressedSize += chunk.length;
|
|
300
|
+
await this.write(chunk);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
if (compress) {
|
|
304
|
+
await pipeline(source, tap, zlib.createDeflateRaw({ level }), drain);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
await pipeline(source, tap, drain);
|
|
308
|
+
}
|
|
309
|
+
return { crc32, uncompressedSize, compressedSize };
|
|
310
|
+
}
|
|
311
|
+
/** Write a buffer to the output, tracking the byte offset and respecting backpressure. */
|
|
312
|
+
async write(buf) {
|
|
313
|
+
if (this.streamError)
|
|
314
|
+
throw this.streamError;
|
|
315
|
+
if (buf.length === 0)
|
|
316
|
+
return;
|
|
317
|
+
this.offset += buf.length;
|
|
318
|
+
if (!this.out.write(buf)) {
|
|
319
|
+
await this.waitDrain();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async waitDrain() {
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
const cleanup = () => {
|
|
325
|
+
this.out.off('drain', onDrain);
|
|
326
|
+
this.out.off('error', onError);
|
|
327
|
+
};
|
|
328
|
+
const onDrain = () => {
|
|
329
|
+
cleanup();
|
|
330
|
+
resolve();
|
|
331
|
+
};
|
|
332
|
+
const onError = (err) => {
|
|
333
|
+
cleanup();
|
|
334
|
+
reject(err);
|
|
335
|
+
};
|
|
336
|
+
this.out.once('drain', onDrain);
|
|
337
|
+
this.out.once('error', onError);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async finishStream() {
|
|
341
|
+
this.out.end();
|
|
342
|
+
await finished(this.out);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function buildLocalFileHeader(entry) {
|
|
346
|
+
const buf = Buffer.allocUnsafe(LOCAL_FILE_HEADER_SIZE);
|
|
347
|
+
let generalPurposeBitFlag = FILE_NAME_IS_UTF8;
|
|
348
|
+
let crc32 = 0;
|
|
349
|
+
let compressedSize = 0;
|
|
350
|
+
let uncompressedSize = 0;
|
|
351
|
+
if (entry.crcAndFileSizeKnown) {
|
|
352
|
+
crc32 = entry.crc32;
|
|
353
|
+
compressedSize = entry.compressedSize;
|
|
354
|
+
uncompressedSize = entry.uncompressedSize;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// Sizes/CRC are unknown until the data has streamed; bit 3 says a data
|
|
358
|
+
// descriptor follows the file data.
|
|
359
|
+
generalPurposeBitFlag |= UNKNOWN_CRC32_AND_FILE_SIZES;
|
|
360
|
+
}
|
|
361
|
+
buf.writeUInt32LE(LOCAL_FILE_HEADER_SIG, 0);
|
|
362
|
+
buf.writeUInt16LE(VERSION_NEEDED_UTF8, 4);
|
|
363
|
+
buf.writeUInt16LE(generalPurposeBitFlag, 6);
|
|
364
|
+
buf.writeUInt16LE(entry.method, 8);
|
|
365
|
+
buf.writeUInt16LE(entry.lastModFileTime, 10);
|
|
366
|
+
buf.writeUInt16LE(entry.lastModFileDate, 12);
|
|
367
|
+
buf.writeUInt32LE(crc32, 14);
|
|
368
|
+
buf.writeUInt32LE(compressedSize, 18);
|
|
369
|
+
buf.writeUInt32LE(uncompressedSize, 22);
|
|
370
|
+
buf.writeUInt16LE(entry.nameBuf.length, 26);
|
|
371
|
+
buf.writeUInt16LE(0, 28); // no extra field in the local header
|
|
372
|
+
return buf;
|
|
373
|
+
}
|
|
374
|
+
function buildDataDescriptor(entry, asZip64) {
|
|
375
|
+
if (!asZip64) {
|
|
376
|
+
const buf = Buffer.allocUnsafe(DATA_DESCRIPTOR_SIZE);
|
|
377
|
+
buf.writeUInt32LE(DATA_DESCRIPTOR_SIG, 0);
|
|
378
|
+
buf.writeUInt32LE(entry.crc32, 4);
|
|
379
|
+
buf.writeUInt32LE(entry.compressedSize, 8);
|
|
380
|
+
buf.writeUInt32LE(entry.uncompressedSize, 12);
|
|
381
|
+
return buf;
|
|
382
|
+
}
|
|
383
|
+
const buf = Buffer.allocUnsafe(ZIP64_DATA_DESCRIPTOR_SIZE);
|
|
384
|
+
buf.writeUInt32LE(DATA_DESCRIPTOR_SIG, 0);
|
|
385
|
+
buf.writeUInt32LE(entry.crc32, 4);
|
|
386
|
+
writeUInt64LE(buf, entry.compressedSize, 8);
|
|
387
|
+
writeUInt64LE(buf, entry.uncompressedSize, 16);
|
|
388
|
+
return buf;
|
|
389
|
+
}
|
|
390
|
+
function buildCentralDirectoryRecord(entry) {
|
|
391
|
+
let generalPurposeBitFlag = FILE_NAME_IS_UTF8;
|
|
392
|
+
if (!entry.crcAndFileSizeKnown)
|
|
393
|
+
generalPurposeBitFlag |= UNKNOWN_CRC32_AND_FILE_SIZES;
|
|
394
|
+
const timestampField = buildExtendedTimestampField(entry.mtimeSeconds);
|
|
395
|
+
// When ZIP64 is needed, the 32-bit fields hold the 0xffffffff sentinel and the
|
|
396
|
+
// real values live in the ZIP64 extended information extra field.
|
|
397
|
+
let compressedSize = entry.compressedSize;
|
|
398
|
+
let uncompressedSize = entry.uncompressedSize;
|
|
399
|
+
let localHeaderOffset = entry.relativeOffsetOfLocalHeader;
|
|
400
|
+
let versionNeeded = VERSION_NEEDED_UTF8;
|
|
401
|
+
let zip64Field = EMPTY;
|
|
402
|
+
if (useZip64(entry)) {
|
|
403
|
+
compressedSize = 0xffffffff;
|
|
404
|
+
uncompressedSize = 0xffffffff;
|
|
405
|
+
localHeaderOffset = 0xffffffff;
|
|
406
|
+
versionNeeded = VERSION_NEEDED_ZIP64;
|
|
407
|
+
zip64Field = buildZip64ExtraField(entry);
|
|
408
|
+
}
|
|
409
|
+
const buf = Buffer.allocUnsafe(CDFH_SIZE);
|
|
410
|
+
buf.writeUInt32LE(CDFH_SIG, 0);
|
|
411
|
+
buf.writeUInt16LE(VERSION_MADE_BY, 4);
|
|
412
|
+
buf.writeUInt16LE(versionNeeded, 6);
|
|
413
|
+
buf.writeUInt16LE(generalPurposeBitFlag, 8);
|
|
414
|
+
buf.writeUInt16LE(entry.method, 10);
|
|
415
|
+
buf.writeUInt16LE(entry.lastModFileTime, 12);
|
|
416
|
+
buf.writeUInt16LE(entry.lastModFileDate, 14);
|
|
417
|
+
buf.writeUInt32LE(entry.crc32, 16);
|
|
418
|
+
buf.writeUInt32LE(compressedSize, 20);
|
|
419
|
+
buf.writeUInt32LE(uncompressedSize, 24);
|
|
420
|
+
buf.writeUInt16LE(entry.nameBuf.length, 28);
|
|
421
|
+
buf.writeUInt16LE(timestampField.length + zip64Field.length, 30);
|
|
422
|
+
buf.writeUInt16LE(entry.commentBuf.length, 32);
|
|
423
|
+
buf.writeUInt16LE(0, 34); // disk number start
|
|
424
|
+
buf.writeUInt16LE(0, 36); // internal file attributes
|
|
425
|
+
buf.writeUInt32LE(entry.externalFileAttributes, 38);
|
|
426
|
+
buf.writeUInt32LE(localHeaderOffset, 42);
|
|
427
|
+
return Buffer.concat([buf, entry.nameBuf, timestampField, zip64Field, entry.commentBuf]);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Info-ZIP universal (extended) timestamp extra field (0x5455), central-directory
|
|
431
|
+
* variant: a single 32-bit UTC mtime. Gives 1-second, timezone-independent mtimes,
|
|
432
|
+
* which readers prefer over the coarse local-time DOS fields.
|
|
433
|
+
*/
|
|
434
|
+
function buildExtendedTimestampField(mtimeSeconds) {
|
|
435
|
+
const buf = Buffer.allocUnsafe(INFO_ZIP_TIMESTAMP_FIELD_SIZE);
|
|
436
|
+
buf.writeUInt16LE(0x5455, 0);
|
|
437
|
+
buf.writeUInt16LE(INFO_ZIP_TIMESTAMP_FIELD_SIZE - 4, 2);
|
|
438
|
+
// Set both the mtime and atime flags to match Info-ZIP, even though only the
|
|
439
|
+
// mtime field follows (the central-directory variant never carries atime).
|
|
440
|
+
buf.writeUInt8(EB_UT_FL_MTIME | EB_UT_FL_ATIME, 4);
|
|
441
|
+
buf.writeInt32LE(mtimeSeconds, 5);
|
|
442
|
+
return buf;
|
|
443
|
+
}
|
|
444
|
+
/** ZIP64 extended information extra field (0x0001) for a central directory record. */
|
|
445
|
+
function buildZip64ExtraField(entry) {
|
|
446
|
+
const buf = Buffer.allocUnsafe(ZIP64_EIEF_SIZE);
|
|
447
|
+
buf.writeUInt16LE(0x0001, 0);
|
|
448
|
+
buf.writeUInt16LE(ZIP64_EIEF_SIZE - 4, 2);
|
|
449
|
+
// Order must match the 0xffffffff sentinels above: uncompressed, compressed, offset.
|
|
450
|
+
writeUInt64LE(buf, entry.uncompressedSize, 4);
|
|
451
|
+
writeUInt64LE(buf, entry.compressedSize, 12);
|
|
452
|
+
writeUInt64LE(buf, entry.relativeOffsetOfLocalHeader, 20);
|
|
453
|
+
return buf;
|
|
454
|
+
}
|
|
455
|
+
function useZip64(entry) {
|
|
456
|
+
return (entry.forceZip64 ||
|
|
457
|
+
entry.uncompressedSize > 0xfffffffe ||
|
|
458
|
+
entry.compressedSize > 0xfffffffe ||
|
|
459
|
+
entry.relativeOffsetOfLocalHeader > 0xfffffffe);
|
|
460
|
+
}
|
|
461
|
+
function buildEndRecords(input) {
|
|
462
|
+
const { entryCount, centralDirectoryOffset, centralDirectorySize, comment, zip64EocdrOffset, forceZip64, } = input;
|
|
463
|
+
let needZip64 = forceZip64;
|
|
464
|
+
let normalEntryCount = entryCount;
|
|
465
|
+
if (forceZip64 || entryCount >= 0xffff) {
|
|
466
|
+
normalEntryCount = 0xffff;
|
|
467
|
+
needZip64 = true;
|
|
468
|
+
}
|
|
469
|
+
let normalCentralDirectorySize = centralDirectorySize;
|
|
470
|
+
if (forceZip64 || centralDirectorySize >= 0xffffffff) {
|
|
471
|
+
normalCentralDirectorySize = 0xffffffff;
|
|
472
|
+
needZip64 = true;
|
|
473
|
+
}
|
|
474
|
+
let normalCentralDirectoryOffset = centralDirectoryOffset;
|
|
475
|
+
if (forceZip64 || centralDirectoryOffset >= 0xffffffff) {
|
|
476
|
+
normalCentralDirectoryOffset = 0xffffffff;
|
|
477
|
+
needZip64 = true;
|
|
478
|
+
}
|
|
479
|
+
const eocdr = Buffer.allocUnsafe(EOCDR_SIZE + comment.length);
|
|
480
|
+
eocdr.writeUInt32LE(EOCDR_SIG, 0);
|
|
481
|
+
eocdr.writeUInt16LE(0, 4); // number of this disk
|
|
482
|
+
eocdr.writeUInt16LE(0, 6); // disk with the start of the central directory
|
|
483
|
+
eocdr.writeUInt16LE(normalEntryCount, 8); // entries on this disk
|
|
484
|
+
eocdr.writeUInt16LE(normalEntryCount, 10); // total entries
|
|
485
|
+
eocdr.writeUInt32LE(normalCentralDirectorySize, 12);
|
|
486
|
+
eocdr.writeUInt32LE(normalCentralDirectoryOffset, 16);
|
|
487
|
+
eocdr.writeUInt16LE(comment.length, 20);
|
|
488
|
+
comment.copy(eocdr, 22);
|
|
489
|
+
if (!needZip64)
|
|
490
|
+
return eocdr;
|
|
491
|
+
const zip64Eocdr = Buffer.allocUnsafe(ZIP64_EOCDR_SIZE);
|
|
492
|
+
zip64Eocdr.writeUInt32LE(ZIP64_EOCDR_SIG, 0);
|
|
493
|
+
// size of this record, excluding the first 12 bytes (signature + this field)
|
|
494
|
+
writeUInt64LE(zip64Eocdr, ZIP64_EOCDR_SIZE - 12, 4);
|
|
495
|
+
zip64Eocdr.writeUInt16LE(VERSION_MADE_BY, 12);
|
|
496
|
+
zip64Eocdr.writeUInt16LE(VERSION_NEEDED_ZIP64, 14);
|
|
497
|
+
zip64Eocdr.writeUInt32LE(0, 16); // number of this disk
|
|
498
|
+
zip64Eocdr.writeUInt32LE(0, 20); // disk with the start of the central directory
|
|
499
|
+
writeUInt64LE(zip64Eocdr, entryCount, 24); // entries on this disk
|
|
500
|
+
writeUInt64LE(zip64Eocdr, entryCount, 32); // total entries
|
|
501
|
+
writeUInt64LE(zip64Eocdr, centralDirectorySize, 40);
|
|
502
|
+
writeUInt64LE(zip64Eocdr, centralDirectoryOffset, 48);
|
|
503
|
+
const locator = Buffer.allocUnsafe(ZIP64_EOCDL_SIZE);
|
|
504
|
+
locator.writeUInt32LE(ZIP64_EOCDL_SIG, 0);
|
|
505
|
+
locator.writeUInt32LE(0, 4); // disk with the ZIP64 end-of-central-directory record
|
|
506
|
+
writeUInt64LE(locator, zip64EocdrOffset, 8);
|
|
507
|
+
locator.writeUInt32LE(1, 16); // total number of disks
|
|
508
|
+
return Buffer.concat([zip64Eocdr, locator, eocdr]);
|
|
509
|
+
}
|
|
510
|
+
function normalizeAndValidateEntryName(fileName, isDirectory) {
|
|
511
|
+
if (!fileName)
|
|
512
|
+
throw new Error('zip entry name must not be empty');
|
|
513
|
+
let name = normalizeZipEntryName(fileName);
|
|
514
|
+
if (isDirectory) {
|
|
515
|
+
if (!name.endsWith('/'))
|
|
516
|
+
name += '/';
|
|
517
|
+
}
|
|
518
|
+
else if (name.endsWith('/')) {
|
|
519
|
+
throw new Error(`file entry name must not end with "/": ${fileName}`);
|
|
520
|
+
}
|
|
521
|
+
assertSafeZipEntryName(name);
|
|
522
|
+
return name;
|
|
523
|
+
}
|
|
524
|
+
function clampInt32(n) {
|
|
525
|
+
if (n < -0x80000000)
|
|
526
|
+
return -0x80000000;
|
|
527
|
+
if (n > 0x7fffffff)
|
|
528
|
+
return 0x7fffffff;
|
|
529
|
+
return n;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Expand input paths (files and/or directories) into a flat list of files to add,
|
|
533
|
+
* resolving each entry's archive name relative to `baseDir` (or each input's parent).
|
|
534
|
+
*/
|
|
535
|
+
async function collectFiles(inputs, baseDir) {
|
|
536
|
+
const files = [];
|
|
537
|
+
for (const input of inputs) {
|
|
538
|
+
const stats = await fsp.stat(input);
|
|
539
|
+
const base = baseDir ? path.resolve(baseDir) : path.dirname(input);
|
|
540
|
+
if (stats.isDirectory()) {
|
|
541
|
+
// `glob` does the recursive walk and (with the default `onlyFiles`) drops
|
|
542
|
+
// directories; `**` is the only pattern and `input` is the cwd (never
|
|
543
|
+
// interpreted), so paths containing glob metacharacters stay safe. Sorted
|
|
544
|
+
// for deterministic archives.
|
|
545
|
+
const relPaths = (await glob('**', { cwd: input, dot: true })).sort(comparators.localeAsc);
|
|
546
|
+
for (const rel of relPaths) {
|
|
547
|
+
const absPath = path.join(input, rel);
|
|
548
|
+
files.push({ absPath, name: toEntryName(path.relative(base, absPath)) });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
files.push({ absPath: input, name: toEntryName(path.relative(base, input)) });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return files;
|
|
556
|
+
}
|
|
557
|
+
/** Convert an OS-native relative path into a forward-slash zip entry name. */
|
|
558
|
+
function toEntryName(relPath) {
|
|
559
|
+
return path.sep === '/' ? relPath : relPath.replaceAll(path.sep, '/');
|
|
560
|
+
}
|
|
561
|
+
const EMPTY = Buffer.alloc(0);
|
|
562
|
+
const LOCAL_FILE_HEADER_SIZE = 30;
|
|
563
|
+
const DATA_DESCRIPTOR_SIZE = 16;
|
|
564
|
+
const ZIP64_DATA_DESCRIPTOR_SIZE = 24;
|
|
565
|
+
const INFO_ZIP_TIMESTAMP_FIELD_SIZE = 9;
|
|
566
|
+
const ZIP64_EIEF_SIZE = 28;
|
|
567
|
+
// version made by: 3 (Unix) in the high byte, spec version 6.3 (63) in the low byte.
|
|
568
|
+
const VERSION_MADE_BY = (3 << 8) | 63;
|
|
569
|
+
const VERSION_NEEDED_UTF8 = 20;
|
|
570
|
+
const VERSION_NEEDED_ZIP64 = 45;
|
|
571
|
+
const FILE_NAME_IS_UTF8 = 1 << 11;
|
|
572
|
+
const UNKNOWN_CRC32_AND_FILE_SIZES = 1 << 3;
|
|
573
|
+
const EB_UT_FL_MTIME = 1 << 0;
|
|
574
|
+
const EB_UT_FL_ATIME = 1 << 1;
|
|
575
|
+
const DEFAULT_FILE_MODE = 0o100664;
|
|
576
|
+
const DEFAULT_DIR_MODE = 0o40775;
|
|
577
|
+
const DEFAULT_DEFLATE_LEVEL = 6;
|
|
578
|
+
// The 4-byte end-of-central-directory signature, as bytes, for comment validation.
|
|
579
|
+
const EOCDR_SIG_BYTES = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.111.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@standard-schema/spec": "^1",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@typescript/native-preview": "beta",
|
|
21
|
-
"@naturalcycles/dev-lib": "20.
|
|
21
|
+
"@naturalcycles/dev-lib": "20.49.0"
|
|
22
22
|
},
|
|
23
23
|
"exports": {
|
|
24
24
|
".": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"./stream/*.js": "./dist/stream/*.js",
|
|
39
39
|
"./yargs": "./dist/yargs/yargs.util.js",
|
|
40
40
|
"./ajv": "./dist/validation/ajv/index.js",
|
|
41
|
-
"./zip": "./dist/zip/
|
|
41
|
+
"./zip": "./dist/zip/index.js"
|
|
42
42
|
},
|
|
43
43
|
"bin": {
|
|
44
44
|
"kpy": "bin/kpy.js",
|
package/src/jwt/jwt.service.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { AnyObject, JWTString } from '@naturalcycles/js-lib/types'
|
|
|
5
5
|
import type { Algorithm, JwtHeader, SignOptions, VerifyOptions } from 'jsonwebtoken'
|
|
6
6
|
import jsonwebtoken from 'jsonwebtoken'
|
|
7
7
|
import type { AjvSchema, JSchema } from '../validation/ajv/jSchema.js'
|
|
8
|
+
|
|
8
9
|
export { jsonwebtoken }
|
|
9
10
|
export type { Algorithm, JwtHeader, SignOptions, VerifyOptions }
|
|
10
11
|
|