@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.
@@ -0,0 +1,3 @@
1
+ export * from './zip2.js';
2
+ export * from './zipReader.js';
3
+ export * from './zipWriter.js';
@@ -0,0 +1,3 @@
1
+ export * from './zip2.js';
2
+ export * from './zipReader.js';
3
+ export * from './zipWriter.js';
@@ -1,5 +1,8 @@
1
1
  import type { ZlibOptions, ZstdOptions } from 'node:zlib';
2
2
  import type { Integer } from '@naturalcycles/js-lib/types';
3
+ import type { ZipEntry } from './zipReader.js';
4
+ import { ZipWriter } from './zipWriter.js';
5
+ import type { ZipFileEntry, ZipPathsOptions } from './zipWriter.js';
3
6
  declare class Zip2 {
4
7
  decompressZstdOrInflateToString(buf: Buffer): Promise<string>;
5
8
  decompressZstdOrInflateToStringSync(buf: Buffer): string;
@@ -54,6 +57,40 @@ declare class Zip2 {
54
57
  zstdDecompressSync(input: Buffer, options?: ZstdOptions): Buffer<ArrayBuffer>;
55
58
  isZstdBuffer(input: Buffer): boolean;
56
59
  isGzipBuffer(input: Buffer): boolean;
60
+ /**
61
+ * Open a zip file and extract all of its entries into `destDir`, streaming each
62
+ * file to disk. Directories are created as needed.
63
+ *
64
+ * Path-traversal attempts (absolute paths, `..` segments, or paths escaping
65
+ * `destDir`) are rejected.
66
+ *
67
+ * Returns the list of extracted entries.
68
+ */
69
+ extractZipFileToDirectory(zipFilePath: string, destDir: string): Promise<ZipEntry[]>;
70
+ /**
71
+ * Create a zip archive on disk from a list of files and/or directories.
72
+ */
73
+ zipPaths(paths: string[], outputZipFilePath: string, opt?: ZipPathsOptions): Promise<void>;
74
+ /**
75
+ * Create a zip archive on disk, returning a {@link ZipWriter} that streams to it.
76
+ *
77
+ * Add entries with {@link ZipWriter.addFile}/{@link ZipWriter.addBuffer}/etc.,
78
+ * then call {@link ZipWriter.finalize} (or use `await using`):
79
+ *
80
+ * ```ts
81
+ * const zip = createZip('archive.zip')
82
+ * await zip.addFile('./photo.jpg')
83
+ * await zip.addBuffer(Buffer.from('{"a":1}'), 'data.json')
84
+ * await zip.finalize()
85
+ * ```
86
+ */
87
+ createZip(filePath: string): ZipWriter;
88
+ /**
89
+ * Build a zip archive entirely in memory and return it as a Buffer.
90
+ *
91
+ * For large archives or streamed inputs prefer {@link createZip} / {@link ZipWriter}.
92
+ */
93
+ createZipBuffer(entries: ZipFileEntry[]): Promise<Buffer>;
57
94
  }
58
95
  export declare const zip2: Zip2;
59
96
  export {};
package/dist/zip/zip2.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { createWriteStream } from 'node:fs';
1
2
  import { promisify } from 'node:util';
2
3
  import zlib from 'node:zlib';
4
+ import { openZip } from './zipReader.js';
5
+ import { createZipBuffer, zipPaths, ZipWriter } from './zipWriter.js';
3
6
  const deflateAsync = promisify(zlib.deflate.bind(zlib));
4
7
  const inflateAsync = promisify(zlib.inflate.bind(zlib));
5
8
  const gzipAsync = promisify(zlib.gzip.bind(zlib));
@@ -124,6 +127,54 @@ class Zip2 {
124
127
  isGzipBuffer(input) {
125
128
  return input[0] === 0x1f && input[1] === 0x8b;
126
129
  }
130
+ /**
131
+ * Open a zip file and extract all of its entries into `destDir`, streaming each
132
+ * file to disk. Directories are created as needed.
133
+ *
134
+ * Path-traversal attempts (absolute paths, `..` segments, or paths escaping
135
+ * `destDir`) are rejected.
136
+ *
137
+ * Returns the list of extracted entries.
138
+ */
139
+ async extractZipFileToDirectory(zipFilePath, destDir) {
140
+ const zip = await openZip(zipFilePath);
141
+ try {
142
+ return await zip.extractAll(destDir);
143
+ }
144
+ finally {
145
+ await zip.close();
146
+ }
147
+ }
148
+ /**
149
+ * Create a zip archive on disk from a list of files and/or directories.
150
+ */
151
+ async zipPaths(paths, outputZipFilePath, opt) {
152
+ return await zipPaths(paths, outputZipFilePath, opt);
153
+ }
154
+ /**
155
+ * Create a zip archive on disk, returning a {@link ZipWriter} that streams to it.
156
+ *
157
+ * Add entries with {@link ZipWriter.addFile}/{@link ZipWriter.addBuffer}/etc.,
158
+ * then call {@link ZipWriter.finalize} (or use `await using`):
159
+ *
160
+ * ```ts
161
+ * const zip = createZip('archive.zip')
162
+ * await zip.addFile('./photo.jpg')
163
+ * await zip.addBuffer(Buffer.from('{"a":1}'), 'data.json')
164
+ * await zip.finalize()
165
+ * ```
166
+ */
167
+ createZip(filePath) {
168
+ return new ZipWriter(createWriteStream(filePath));
169
+ }
170
+ /**
171
+ * Build a zip archive entirely in memory and return it as a Buffer.
172
+ *
173
+ * For large archives or streamed inputs prefer {@link createZip} / {@link ZipWriter}.
174
+ */
175
+ async createZipBuffer(entries) {
176
+ return await createZipBuffer(entries);
177
+ }
127
178
  }
128
179
  export const zip2 = new Zip2();
129
180
  const ZSTD_MAGIC_NUMBER = 0xfd2fb528;
@@ -0,0 +1,41 @@
1
+ import type { UnixTimestamp } from '@naturalcycles/js-lib/types';
2
+ export declare const STORED = 0;
3
+ export declare const DEFLATE = 8;
4
+ export declare const LOCAL_FILE_HEADER_SIG = 67324752;
5
+ export declare const DATA_DESCRIPTOR_SIG = 134695760;
6
+ export declare const CDFH_SIG = 33639248;
7
+ export declare const CDFH_SIZE = 46;
8
+ export declare const EOCDR_SIG = 101010256;
9
+ export declare const EOCDR_SIZE = 22;
10
+ export declare const ZIP64_EOCDL_SIG = 117853008;
11
+ export declare const ZIP64_EOCDL_SIZE = 20;
12
+ export declare const ZIP64_EOCDR_SIG = 101075792;
13
+ export declare const ZIP64_EOCDR_SIZE = 56;
14
+ /** The .zip comment and per-entry name/comment length fields are all 16-bit. */
15
+ export declare const MAX_COMMENT_SIZE = 65535;
16
+ /** Read a 64-bit little-endian unsigned integer, rejecting values above `Number.MAX_SAFE_INTEGER`. */
17
+ export declare function readUInt64LE(buf: Buffer, offset: number): number;
18
+ /** Write a 64-bit little-endian unsigned integer. */
19
+ export declare function writeUInt64LE(buf: Buffer, value: number, offset: number): void;
20
+ /**
21
+ * Decode a packed DOS date + time pair into a {@link UnixTimestamp} (seconds).
22
+ * The DOS fields are local-time, so they are interpreted in the local timezone.
23
+ * Used when no Info-ZIP extended timestamp is present.
24
+ */
25
+ export declare function dosDateTimeToUnix(date: number, time: number): UnixTimestamp;
26
+ /**
27
+ * Encode a {@link UnixTimestamp} (seconds) into the packed DOS date + time pair stored
28
+ * in local file and central directory headers. The DOS fields are local-time, so the
29
+ * timestamp is rendered in the local timezone. Out-of-range dates are clamped to 1980-2107.
30
+ */
31
+ export declare function unixToDosDateTime(ts: UnixTimestamp): {
32
+ date: number;
33
+ time: number;
34
+ };
35
+ /** Normalize Windows-style separators, like yauzl in non-strict mode. */
36
+ export declare function normalizeZipEntryName(name: string): string;
37
+ /**
38
+ * Reject entry names that would escape the extraction directory: absolute paths
39
+ * (drive letters or a leading `/`) and any `..` path segment.
40
+ */
41
+ export declare function assertSafeZipEntryName(name: string): void;
@@ -0,0 +1,88 @@
1
+ /*
2
+
3
+ Shared low-level helpers and constants for the zip reader and writer.
4
+
5
+ These are the parts of the ZIP file format that both reading (`zipReader.ts`) and
6
+ writing (`zipWriter.ts`) need in common: record signatures and fixed sizes, 64-bit
7
+ integer read/write helpers, DOS date/time conversion (both directions) and
8
+ entry-name normalization/validation.
9
+
10
+ */
11
+ // oxlint-disable no-bitwise -- the ZIP format packs DOS date/time into bit fields
12
+ // Compression methods.
13
+ export const STORED = 0;
14
+ export const DEFLATE = 8;
15
+ // Record signatures (little-endian uint32) and fixed sizes (in bytes).
16
+ export const LOCAL_FILE_HEADER_SIG = 0x04034b50;
17
+ export const DATA_DESCRIPTOR_SIG = 0x08074b50;
18
+ export const CDFH_SIG = 0x02014b50;
19
+ export const CDFH_SIZE = 46;
20
+ export const EOCDR_SIG = 0x06054b50;
21
+ export const EOCDR_SIZE = 22;
22
+ export const ZIP64_EOCDL_SIG = 0x07064b50;
23
+ export const ZIP64_EOCDL_SIZE = 20;
24
+ export const ZIP64_EOCDR_SIG = 0x06064b50;
25
+ export const ZIP64_EOCDR_SIZE = 56;
26
+ /** The .zip comment and per-entry name/comment length fields are all 16-bit. */
27
+ export const MAX_COMMENT_SIZE = 0xffff;
28
+ const MAX_SAFE_INTEGER_BIG = BigInt(Number.MAX_SAFE_INTEGER);
29
+ /** Read a 64-bit little-endian unsigned integer, rejecting values above `Number.MAX_SAFE_INTEGER`. */
30
+ export function readUInt64LE(buf, offset) {
31
+ const value = buf.readBigUInt64LE(offset);
32
+ if (value > MAX_SAFE_INTEGER_BIG) {
33
+ throw new Error('zip file too large: 64-bit values above Number.MAX_SAFE_INTEGER are not supported');
34
+ }
35
+ return Number(value);
36
+ }
37
+ /** Write a 64-bit little-endian unsigned integer. */
38
+ export function writeUInt64LE(buf, value, offset) {
39
+ buf.writeBigUInt64LE(BigInt(value), offset);
40
+ }
41
+ /**
42
+ * Decode a packed DOS date + time pair into a {@link UnixTimestamp} (seconds).
43
+ * The DOS fields are local-time, so they are interpreted in the local timezone.
44
+ * Used when no Info-ZIP extended timestamp is present.
45
+ */
46
+ export function dosDateTimeToUnix(date, time) {
47
+ const day = date & 0x1f; // 1-31
48
+ const month = ((date >> 5) & 0x0f) - 1; // 1-12 -> 0-11
49
+ const year = ((date >> 9) & 0x7f) + 1980; // 0-127 -> 1980-2107
50
+ const second = (time & 0x1f) * 2; // 0-29 -> 0-58
51
+ const minute = (time >> 5) & 0x3f; // 0-59
52
+ const hour = (time >> 11) & 0x1f; // 0-23
53
+ return Math.floor(new Date(year, month, day, hour, minute, second).getTime() / 1000);
54
+ }
55
+ const MIN_DOS_DATE = new Date(1980, 0, 1);
56
+ const MAX_DOS_DATE = new Date(2107, 11, 31, 23, 59, 58);
57
+ /**
58
+ * Encode a {@link UnixTimestamp} (seconds) into the packed DOS date + time pair stored
59
+ * in local file and central directory headers. The DOS fields are local-time, so the
60
+ * timestamp is rendered in the local timezone. Out-of-range dates are clamped to 1980-2107.
61
+ */
62
+ export function unixToDosDateTime(ts) {
63
+ const jsDate = new Date(ts * 1000);
64
+ const d = jsDate < MIN_DOS_DATE ? MIN_DOS_DATE : jsDate > MAX_DOS_DATE ? MAX_DOS_DATE : jsDate;
65
+ const date = (d.getDate() & 0x1f) | // 1-31
66
+ (((d.getMonth() + 1) & 0x0f) << 5) | // 1-12
67
+ (((d.getFullYear() - 1980) & 0x7f) << 9); // 1980-2107
68
+ const time = Math.floor(d.getSeconds() / 2) | // 0-29
69
+ ((d.getMinutes() & 0x3f) << 5) | // 0-59
70
+ ((d.getHours() & 0x1f) << 11); // 0-23
71
+ return { date, time };
72
+ }
73
+ /** Normalize Windows-style separators, like yauzl in non-strict mode. */
74
+ export function normalizeZipEntryName(name) {
75
+ return name.replaceAll('\\', '/');
76
+ }
77
+ /**
78
+ * Reject entry names that would escape the extraction directory: absolute paths
79
+ * (drive letters or a leading `/`) and any `..` path segment.
80
+ */
81
+ export function assertSafeZipEntryName(name) {
82
+ if (/^[a-z]:/i.test(name) || name.startsWith('/')) {
83
+ throw new Error(`absolute path in zip entry: ${name}`);
84
+ }
85
+ if (name.split('/').includes('..')) {
86
+ throw new Error(`invalid relative path in zip entry: ${name}`);
87
+ }
88
+ }
@@ -0,0 +1,142 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { UnixTimestamp } from '@naturalcycles/js-lib/types';
3
+ /**
4
+ * Open a zip archive from a file on disk.
5
+ *
6
+ * Reads and parses the central directory; the returned {@link ZipReader} exposes
7
+ * the list of entries and lets you read their contents.
8
+ *
9
+ * Remember to call {@link ZipReader.close} when done, or use {@link extractZip}.
10
+ */
11
+ export declare function openZip(filePath: string): Promise<ZipReader>;
12
+ /**
13
+ * Open a zip archive from an in-memory Buffer.
14
+ */
15
+ export declare function openZipBuffer(buffer: Buffer): Promise<ZipReader>;
16
+ /**
17
+ * Reads entries from an open zip archive.
18
+ *
19
+ * Create one via {@link openZip} or {@link openZipBuffer}.
20
+ *
21
+ * Implements `AsyncDisposable`, so it can be used with `await using` to close
22
+ * automatically on scope exit:
23
+ *
24
+ * ```ts
25
+ * await using zip = await openZip('archive.zip')
26
+ * const buf = await zip.readEntry(zip.entries[0])
27
+ * // zip.close() is called automatically here
28
+ * ```
29
+ */
30
+ export declare class ZipReader implements AsyncDisposable {
31
+ private source;
32
+ /**
33
+ * All entries (files and directories) found in the archive,
34
+ * in central-directory order.
35
+ */
36
+ readonly entries: ZipEntry[];
37
+ /**
38
+ * Archive-level comment (empty string if none).
39
+ */
40
+ readonly comment: string;
41
+ constructor(source: ZipSource,
42
+ /**
43
+ * All entries (files and directories) found in the archive,
44
+ * in central-directory order.
45
+ */
46
+ entries: ZipEntry[],
47
+ /**
48
+ * Archive-level comment (empty string if none).
49
+ */
50
+ comment: string);
51
+ /**
52
+ * Read and fully decompress an entry into a Buffer.
53
+ *
54
+ * Validates the uncompressed size and CRC-32 checksum.
55
+ * For large entries prefer {@link openReadStream}.
56
+ */
57
+ readEntry(entry: ZipEntry): Promise<Buffer>;
58
+ /**
59
+ * Open a Readable stream of an entry's decompressed contents.
60
+ *
61
+ * Useful for piping large entries to disk without buffering them in memory.
62
+ * Unlike {@link readEntry}, this does not verify the CRC-32 checksum.
63
+ */
64
+ openReadStream(entry: ZipEntry): Promise<Readable>;
65
+ /**
66
+ * Extract all entries into `destDir`, streaming each file to disk.
67
+ * See {@link extractZip}.
68
+ */
69
+ extractAll(destDir: string): Promise<ZipEntry[]>;
70
+ /**
71
+ * Close the underlying file handle. No-op for buffer-backed archives.
72
+ * Safe to call multiple times.
73
+ */
74
+ close(): Promise<void>;
75
+ /**
76
+ * Called by `await using`; closes the archive. See {@link close}.
77
+ */
78
+ [Symbol.asyncDispose](): Promise<void>;
79
+ private assertReadable;
80
+ private findFileDataStart;
81
+ }
82
+ /**
83
+ * Random-access byte source backing a {@link ZipReader}.
84
+ */
85
+ interface ZipSource {
86
+ readonly size: number;
87
+ /** Read exactly `length` bytes starting at `position`; throws on EOF. */
88
+ read: (position: number, length: number) => Promise<Buffer>;
89
+ /** Stream raw bytes in the `[start, end)` range. */
90
+ createReadStream: (start: number, end: number) => Readable;
91
+ /** Release any held resources. Idempotent. */
92
+ close: () => Promise<void>;
93
+ }
94
+ /**
95
+ * A single entry (file or directory) inside a zip archive,
96
+ * as parsed from its central directory record.
97
+ */
98
+ export interface ZipEntry {
99
+ /**
100
+ * Entry name, using `/` as the path separator.
101
+ * Directory entries end with a trailing `/`.
102
+ */
103
+ fileName: string;
104
+ /**
105
+ * Uncompressed size, in bytes.
106
+ */
107
+ uncompressedSize: number;
108
+ /**
109
+ * Compressed size, in bytes.
110
+ */
111
+ compressedSize: number;
112
+ /**
113
+ * Compression method: `0` = stored (no compression), `8` = deflate.
114
+ * Other methods cannot be read.
115
+ */
116
+ compressionMethod: number;
117
+ /**
118
+ * Expected CRC-32 checksum of the uncompressed data.
119
+ */
120
+ crc32: number;
121
+ /**
122
+ * Last modification time of the entry, as a Unix timestamp in seconds.
123
+ */
124
+ lastModified: UnixTimestamp;
125
+ /**
126
+ * True if the entry is a directory (its `fileName` ends with `/`).
127
+ */
128
+ isDirectory: boolean;
129
+ /**
130
+ * True if the entry is encrypted. Encrypted entries cannot be read.
131
+ */
132
+ isEncrypted: boolean;
133
+ /**
134
+ * Optional per-entry comment (empty string if none).
135
+ */
136
+ comment: string;
137
+ /** Bit flags from the central directory record. */
138
+ generalPurposeBitFlag: number;
139
+ /** Byte offset of the entry's local file header. */
140
+ relativeOffsetOfLocalHeader: number;
141
+ }
142
+ export {};