@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.
@@ -414,7 +414,7 @@ export class JSchema<OUT, Opt>
414
414
  * The usage of this function is discouraged as it defeats the purpose of having type-safe validation.
415
415
  */
416
416
  castAs<T>(): JSchema<T, Opt> {
417
- return this as unknown as JSchema<T, Opt>
417
+ return this as any as JSchema<T, Opt>
418
418
  }
419
419
 
420
420
  /**
@@ -529,7 +529,7 @@ export class JSchema<OUT, Opt>
529
529
  const clone = this.cloneAndUpdateSchema({
530
530
  postValidation: fn,
531
531
  })
532
- return clone as unknown as JSchema<OUT2, Opt>
532
+ return clone as any as JSchema<OUT2, Opt>
533
533
  }
534
534
 
535
535
  /**
@@ -557,7 +557,7 @@ export class JBuilder<OUT, Opt> extends JSchema<OUT, Opt> {
557
557
  * The usage of this function is discouraged as it defeats the purpose of having type-safe validation.
558
558
  */
559
559
  override castAs<T>(): JBuilder<T, Opt> {
560
- return this as unknown as JBuilder<T, Opt>
560
+ return this as any as JBuilder<T, Opt>
561
561
  }
562
562
 
563
563
  $schema($schema: string): this {
@@ -683,7 +683,7 @@ export class JBuilder<OUT, Opt> extends JSchema<OUT, Opt> {
683
683
  const { customValidations = [] } = this.schema
684
684
  return this.cloneAndUpdateSchema({
685
685
  customValidations: [...customValidations, validator],
686
- }) as unknown as JBuilder<OUT2, Opt>
686
+ }) as any as JBuilder<OUT2, Opt>
687
687
  }
688
688
 
689
689
  /**
@@ -700,7 +700,7 @@ export class JBuilder<OUT, Opt> extends JSchema<OUT, Opt> {
700
700
  const { customConversions = [] } = this.schema
701
701
  return this.cloneAndUpdateSchema({
702
702
  customConversions: [...customConversions, converter],
703
- }) as unknown as JBuilder<OUT2, Opt>
703
+ }) as any as JBuilder<OUT2, Opt>
704
704
  }
705
705
  }
706
706
 
@@ -783,7 +783,7 @@ export class JString<
783
783
  }
784
784
 
785
785
  branded<B extends string>(): JString<B, Opt> {
786
- return this as unknown as JString<B, Opt>
786
+ return this as any as JString<B, Opt>
787
787
  }
788
788
 
789
789
  /**
@@ -934,7 +934,7 @@ export class JNumber<
934
934
  }
935
935
 
936
936
  branded<B extends number>(): JNumber<B, Opt> {
937
- return this as unknown as JNumber<B, Opt>
937
+ return this as any as JNumber<B, Opt>
938
938
  }
939
939
 
940
940
  multipleOf(multipleOf: number): this {
@@ -1131,7 +1131,7 @@ export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends
1131
1131
  const clone = this.clone()
1132
1132
  mergeJsonSchemaObjects(clone.schema as any, other.schema as any)
1133
1133
  _objectAssign(clone.schema, { hasIsOfTypeCheck: false })
1134
- return clone as unknown as JObject<OUT & OUT2, false>
1134
+ return clone as JObject<OUT & OUT2, false>
1135
1135
  }
1136
1136
 
1137
1137
  /**
@@ -1229,7 +1229,7 @@ export class JObjectInfer<
1229
1229
  // the new schema loses that quality.
1230
1230
  _objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false })
1231
1231
 
1232
- return newBuilder as unknown as JObjectInfer<
1232
+ return newBuilder as any as JObjectInfer<
1233
1233
  {
1234
1234
  [K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
1235
1235
  ? NEW_PROPS[K]
@@ -1321,7 +1321,7 @@ export class JEnum<
1321
1321
  }
1322
1322
 
1323
1323
  branded<B extends OUT>(): JEnum<B, Opt> {
1324
- return this as unknown as JEnum<B, Opt>
1324
+ return this as any as JEnum<B, Opt>
1325
1325
  }
1326
1326
  }
1327
1327
 
@@ -0,0 +1,3 @@
1
+ export * from './zip2.js'
2
+ export * from './zipReader.js'
3
+ export * from './zipWriter.js'
package/src/zip/zip2.ts CHANGED
@@ -1,7 +1,12 @@
1
+ import { createWriteStream } from 'node:fs'
1
2
  import { promisify } from 'node:util'
2
3
  import type { ZlibOptions, ZstdOptions } from 'node:zlib'
3
4
  import zlib from 'node:zlib'
4
5
  import type { Integer } from '@naturalcycles/js-lib/types'
6
+ import { openZip } from './zipReader.js'
7
+ import type { ZipEntry } from './zipReader.js'
8
+ import { createZipBuffer, zipPaths, ZipWriter } from './zipWriter.js'
9
+ import type { ZipFileEntry, ZipPathsOptions } from './zipWriter.js'
5
10
 
6
11
  const deflateAsync = promisify(zlib.deflate.bind(zlib))
7
12
  const inflateAsync = promisify(zlib.inflate.bind(zlib))
@@ -158,6 +163,57 @@ class Zip2 {
158
163
  isGzipBuffer(input: Buffer): boolean {
159
164
  return input[0] === 0x1f && input[1] === 0x8b
160
165
  }
166
+
167
+ /**
168
+ * Open a zip file and extract all of its entries into `destDir`, streaming each
169
+ * file to disk. Directories are created as needed.
170
+ *
171
+ * Path-traversal attempts (absolute paths, `..` segments, or paths escaping
172
+ * `destDir`) are rejected.
173
+ *
174
+ * Returns the list of extracted entries.
175
+ */
176
+ async extractZipFileToDirectory(zipFilePath: string, destDir: string): Promise<ZipEntry[]> {
177
+ const zip = await openZip(zipFilePath)
178
+ try {
179
+ return await zip.extractAll(destDir)
180
+ } finally {
181
+ await zip.close()
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Create a zip archive on disk from a list of files and/or directories.
187
+ */
188
+ async zipPaths(paths: string[], outputZipFilePath: string, opt?: ZipPathsOptions): Promise<void> {
189
+ return await zipPaths(paths, outputZipFilePath, opt)
190
+ }
191
+
192
+ /**
193
+ * Create a zip archive on disk, returning a {@link ZipWriter} that streams to it.
194
+ *
195
+ * Add entries with {@link ZipWriter.addFile}/{@link ZipWriter.addBuffer}/etc.,
196
+ * then call {@link ZipWriter.finalize} (or use `await using`):
197
+ *
198
+ * ```ts
199
+ * const zip = createZip('archive.zip')
200
+ * await zip.addFile('./photo.jpg')
201
+ * await zip.addBuffer(Buffer.from('{"a":1}'), 'data.json')
202
+ * await zip.finalize()
203
+ * ```
204
+ */
205
+ createZip(filePath: string): ZipWriter {
206
+ return new ZipWriter(createWriteStream(filePath))
207
+ }
208
+
209
+ /**
210
+ * Build a zip archive entirely in memory and return it as a Buffer.
211
+ *
212
+ * For large archives or streamed inputs prefer {@link createZip} / {@link ZipWriter}.
213
+ */
214
+ async createZipBuffer(entries: ZipFileEntry[]): Promise<Buffer> {
215
+ return await createZipBuffer(entries)
216
+ }
161
217
  }
162
218
 
163
219
  export const zip2 = new Zip2()
@@ -0,0 +1,111 @@
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
+
12
+ import type { UnixTimestamp } from '@naturalcycles/js-lib/types'
13
+
14
+ // oxlint-disable no-bitwise -- the ZIP format packs DOS date/time into bit fields
15
+
16
+ // Compression methods.
17
+ export const STORED = 0
18
+ export const DEFLATE = 8
19
+
20
+ // Record signatures (little-endian uint32) and fixed sizes (in bytes).
21
+ export const LOCAL_FILE_HEADER_SIG = 0x04034b50
22
+ export const DATA_DESCRIPTOR_SIG = 0x08074b50
23
+ export const CDFH_SIG = 0x02014b50
24
+ export const CDFH_SIZE = 46
25
+ export const EOCDR_SIG = 0x06054b50
26
+ export const EOCDR_SIZE = 22
27
+ export const ZIP64_EOCDL_SIG = 0x07064b50
28
+ export const ZIP64_EOCDL_SIZE = 20
29
+ export const ZIP64_EOCDR_SIG = 0x06064b50
30
+ export const ZIP64_EOCDR_SIZE = 56
31
+
32
+ /** The .zip comment and per-entry name/comment length fields are all 16-bit. */
33
+ export const MAX_COMMENT_SIZE = 0xffff
34
+
35
+ const MAX_SAFE_INTEGER_BIG = BigInt(Number.MAX_SAFE_INTEGER)
36
+
37
+ /** Read a 64-bit little-endian unsigned integer, rejecting values above `Number.MAX_SAFE_INTEGER`. */
38
+ export function readUInt64LE(buf: Buffer, offset: number): number {
39
+ const value = buf.readBigUInt64LE(offset)
40
+ if (value > MAX_SAFE_INTEGER_BIG) {
41
+ throw new Error(
42
+ 'zip file too large: 64-bit values above Number.MAX_SAFE_INTEGER are not supported',
43
+ )
44
+ }
45
+ return Number(value)
46
+ }
47
+
48
+ /** Write a 64-bit little-endian unsigned integer. */
49
+ export function writeUInt64LE(buf: Buffer, value: number, offset: number): void {
50
+ buf.writeBigUInt64LE(BigInt(value), offset)
51
+ }
52
+
53
+ /**
54
+ * Decode a packed DOS date + time pair into a {@link UnixTimestamp} (seconds).
55
+ * The DOS fields are local-time, so they are interpreted in the local timezone.
56
+ * Used when no Info-ZIP extended timestamp is present.
57
+ */
58
+ export function dosDateTimeToUnix(date: number, time: number): UnixTimestamp {
59
+ const day = date & 0x1f // 1-31
60
+ const month = ((date >> 5) & 0x0f) - 1 // 1-12 -> 0-11
61
+ const year = ((date >> 9) & 0x7f) + 1980 // 0-127 -> 1980-2107
62
+ const second = (time & 0x1f) * 2 // 0-29 -> 0-58
63
+ const minute = (time >> 5) & 0x3f // 0-59
64
+ const hour = (time >> 11) & 0x1f // 0-23
65
+ return Math.floor(
66
+ new Date(year, month, day, hour, minute, second).getTime() / 1000,
67
+ ) as UnixTimestamp
68
+ }
69
+
70
+ const MIN_DOS_DATE = new Date(1980, 0, 1)
71
+ const MAX_DOS_DATE = new Date(2107, 11, 31, 23, 59, 58)
72
+
73
+ /**
74
+ * Encode a {@link UnixTimestamp} (seconds) into the packed DOS date + time pair stored
75
+ * in local file and central directory headers. The DOS fields are local-time, so the
76
+ * timestamp is rendered in the local timezone. Out-of-range dates are clamped to 1980-2107.
77
+ */
78
+ export function unixToDosDateTime(ts: UnixTimestamp): { date: number; time: number } {
79
+ const jsDate = new Date(ts * 1000)
80
+ const d = jsDate < MIN_DOS_DATE ? MIN_DOS_DATE : jsDate > MAX_DOS_DATE ? MAX_DOS_DATE : jsDate
81
+
82
+ const date =
83
+ (d.getDate() & 0x1f) | // 1-31
84
+ (((d.getMonth() + 1) & 0x0f) << 5) | // 1-12
85
+ (((d.getFullYear() - 1980) & 0x7f) << 9) // 1980-2107
86
+
87
+ const time =
88
+ Math.floor(d.getSeconds() / 2) | // 0-29
89
+ ((d.getMinutes() & 0x3f) << 5) | // 0-59
90
+ ((d.getHours() & 0x1f) << 11) // 0-23
91
+
92
+ return { date, time }
93
+ }
94
+
95
+ /** Normalize Windows-style separators, like yauzl in non-strict mode. */
96
+ export function normalizeZipEntryName(name: string): string {
97
+ return name.replaceAll('\\', '/')
98
+ }
99
+
100
+ /**
101
+ * Reject entry names that would escape the extraction directory: absolute paths
102
+ * (drive letters or a leading `/`) and any `..` path segment.
103
+ */
104
+ export function assertSafeZipEntryName(name: string): void {
105
+ if (/^[a-z]:/i.test(name) || name.startsWith('/')) {
106
+ throw new Error(`absolute path in zip entry: ${name}`)
107
+ }
108
+ if (name.split('/').includes('..')) {
109
+ throw new Error(`invalid relative path in zip entry: ${name}`)
110
+ }
111
+ }