• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / loaders.gl / 20352515932

18 Dec 2025 09:56PM UTC coverage: 35.115% (-28.4%) from 63.485%
20352515932

push

github

web-flow
feat(loader-utils): Export is-type helpers (#3258)

1188 of 1998 branches covered (59.46%)

Branch coverage included in aggregate %.

147 of 211 new or added lines in 13 files covered. (69.67%)

30011 existing lines in 424 files now uncovered.

37457 of 108056 relevant lines covered (34.66%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.91
/modules/zip/src/parse-zip/cd-file-header.ts
1
// loaders.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
import {compareArrayBuffers, concatenateArrayBuffers} from '@loaders.gl/loader-utils';
1✔
6
import type {ReadableFile} from '@loaders.gl/loader-utils';
1✔
7
import {parseEoCDRecord} from './end-of-central-directory';
1✔
8
import {ZipSignature} from './search-from-the-end';
1✔
9
import {createZip64Info, setFieldToNumber} from './zip64-info-generation';
1✔
10
import {
1✔
11
  DataViewReadableFile,
1✔
12
  getReadableFileSize,
1✔
13
  readDataView,
1✔
14
  readRange
1✔
15
} from './readable-file-utils';
1✔
16

1✔
17
/**
1✔
18
 * zip central directory file header info
1✔
19
 * according to https://en.wikipedia.org/wiki/ZIP_(file_format)
1✔
20
 */
1✔
21
export type ZipCDFileHeader = {
1✔
22
  /** Compressed size */
1✔
23
  compressedSize: bigint;
1✔
24
  /** Uncompressed size */
1✔
25
  uncompressedSize: bigint;
1✔
26
  /** Extra field size */
1✔
27
  extraFieldLength: number;
1✔
28
  /** File name length */
1✔
29
  fileNameLength: number;
1✔
30
  /** File name */
1✔
31
  fileName: string;
1✔
32
  /** Extra field offset */
1✔
33
  extraOffset: bigint;
1✔
34
  /** Relative offset of local file header */
1✔
35
  localHeaderOffset: bigint;
1✔
36
};
1✔
37

1✔
38
/**
1✔
39
 * Data that might be in Zip64 notation inside extra data
1✔
40
 */
1✔
41
type Zip64Data = {
1✔
42
  /** Uncompressed size */
1✔
43
  uncompressedSize: bigint;
1✔
44
  /** Compressed size */
1✔
45
  compressedSize: bigint;
1✔
46
  /** Relative offset of local file header */
1✔
47
  localHeaderOffset: bigint;
1✔
48
  /** Start disk */
1✔
49
  startDisk: bigint;
1✔
50
};
1✔
51

1✔
52
// offsets accroding to https://en.wikipedia.org/wiki/ZIP_(file_format)
1✔
53
const CD_COMPRESSED_SIZE_OFFSET = 20;
1✔
54
const CD_UNCOMPRESSED_SIZE_OFFSET = 24;
1✔
55
const CD_FILE_NAME_LENGTH_OFFSET = 28;
1✔
56
const CD_EXTRA_FIELD_LENGTH_OFFSET = 30;
1✔
57
const CD_START_DISK_OFFSET = 32;
1✔
58
const CD_LOCAL_HEADER_OFFSET_OFFSET = 42;
1✔
59
const CD_FILE_NAME_OFFSET = 46n;
1✔
60

1✔
61
export const signature: ZipSignature = new Uint8Array([0x50, 0x4b, 0x01, 0x02]);
1✔
62

1✔
63
/**
1✔
64
 * Parses central directory file header of zip file
1✔
65
 * @param headerOffset - offset in the archive where header starts
1✔
66
 * @param buffer - buffer containing whole array
1✔
67
 * @returns Info from the header
1✔
68
 */
1✔
69
export const parseZipCDFileHeader = async (
1✔
UNCOV
70
  headerOffset: bigint,
×
UNCOV
71
  file: ReadableFile
×
UNCOV
72
): Promise<ZipCDFileHeader | null> => {
×
UNCOV
73
  const fileLength = await getReadableFileSize(file);
×
UNCOV
74
  if (headerOffset >= fileLength) {
×
UNCOV
75
    return null;
×
UNCOV
76
  }
×
UNCOV
77
  const mainHeader = await readDataView(file, headerOffset, headerOffset + CD_FILE_NAME_OFFSET);
×
UNCOV
78

×
UNCOV
79
  const magicBytes = mainHeader.buffer.slice(0, 4);
×
UNCOV
80
  if (!compareArrayBuffers(magicBytes, signature.buffer)) {
×
81
    return null;
×
82
  }
×
UNCOV
83

×
UNCOV
84
  const compressedSize = BigInt(mainHeader.getUint32(CD_COMPRESSED_SIZE_OFFSET, true));
×
UNCOV
85
  const uncompressedSize = BigInt(mainHeader.getUint32(CD_UNCOMPRESSED_SIZE_OFFSET, true));
×
UNCOV
86
  const extraFieldLength = mainHeader.getUint16(CD_EXTRA_FIELD_LENGTH_OFFSET, true);
×
UNCOV
87
  const startDisk = BigInt(mainHeader.getUint16(CD_START_DISK_OFFSET, true));
×
UNCOV
88
  const fileNameLength = mainHeader.getUint16(CD_FILE_NAME_LENGTH_OFFSET, true);
×
UNCOV
89

×
UNCOV
90
  const additionalHeader = await readRange(
×
UNCOV
91
    file,
×
UNCOV
92
    headerOffset + CD_FILE_NAME_OFFSET,
×
UNCOV
93
    headerOffset + CD_FILE_NAME_OFFSET + BigInt(fileNameLength + extraFieldLength)
×
UNCOV
94
  );
×
UNCOV
95

×
UNCOV
96
  const filenameBytes = additionalHeader.slice(0, fileNameLength);
×
UNCOV
97
  const fileName = new TextDecoder().decode(filenameBytes);
×
UNCOV
98

×
UNCOV
99
  const extraOffset = headerOffset + CD_FILE_NAME_OFFSET + BigInt(fileNameLength);
×
UNCOV
100
  const oldFormatOffset = mainHeader.getUint32(CD_LOCAL_HEADER_OFFSET_OFFSET, true);
×
UNCOV
101

×
UNCOV
102
  const localHeaderOffset = BigInt(oldFormatOffset);
×
UNCOV
103
  const extraField = new DataView(
×
UNCOV
104
    additionalHeader.slice(fileNameLength, additionalHeader.byteLength)
×
UNCOV
105
  );
×
UNCOV
106
  // looking for info that might be also be in zip64 extra field
×
UNCOV
107

×
UNCOV
108
  const zip64data: Zip64Data = {
×
UNCOV
109
    uncompressedSize,
×
UNCOV
110
    compressedSize,
×
UNCOV
111
    localHeaderOffset,
×
UNCOV
112
    startDisk
×
UNCOV
113
  };
×
UNCOV
114

×
UNCOV
115
  const res = findZip64DataInExtra(zip64data, extraField);
×
UNCOV
116

×
UNCOV
117
  return {
×
UNCOV
118
    ...zip64data,
×
UNCOV
119
    ...res,
×
UNCOV
120
    extraFieldLength,
×
UNCOV
121
    fileNameLength,
×
UNCOV
122
    fileName,
×
UNCOV
123
    extraOffset
×
UNCOV
124
  };
×
UNCOV
125
};
×
126

1✔
127
/**
1✔
128
 * Create iterator over files of zip archive
1✔
129
 * @param fileProvider - readable file that provides random access to the file
1✔
130
 */
1✔
UNCOV
131
export async function* makeZipCDHeaderIterator(
×
UNCOV
132
  fileProvider: ReadableFile
×
UNCOV
133
): AsyncIterable<ZipCDFileHeader> {
×
UNCOV
134
  const {cdStartOffset, cdByteSize} = await parseEoCDRecord(fileProvider);
×
UNCOV
135
  const centralDirectory = new DataViewReadableFile(
×
UNCOV
136
    new DataView(await readRange(fileProvider, cdStartOffset, cdStartOffset + cdByteSize))
×
UNCOV
137
  );
×
UNCOV
138
  let cdHeader = await parseZipCDFileHeader(0n, centralDirectory);
×
UNCOV
139
  while (cdHeader) {
×
UNCOV
140
    yield cdHeader;
×
UNCOV
141
    cdHeader = await parseZipCDFileHeader(
×
UNCOV
142
      cdHeader.extraOffset + BigInt(cdHeader.extraFieldLength),
×
UNCOV
143
      centralDirectory
×
UNCOV
144
    );
×
UNCOV
145
  }
×
UNCOV
146
}
×
147
/**
1✔
148
 * returns the number written in the provided bytes
1✔
149
 * @param bytes two bytes containing the number
1✔
150
 * @returns the number written in the provided bytes
1✔
151
 */
1✔
152
const getUint16 = (...bytes: [number, number]) => {
1✔
UNCOV
153
  return bytes[0] + bytes[1] * 16;
×
UNCOV
154
};
×
155

1✔
156
/**
1✔
157
 * reads all nesessary data from zip64 record in the extra data
1✔
158
 * @param zip64data values that might be in zip64 record
1✔
159
 * @param extraField full extra data
1✔
160
 * @returns data read from zip64
1✔
161
 */
1✔
162

1✔
163
const findZip64DataInExtra = (zip64data: Zip64Data, extraField: DataView): Partial<Zip64Data> => {
1✔
UNCOV
164
  const zip64dataList = findExpectedData(zip64data);
×
UNCOV
165

×
UNCOV
166
  const zip64DataRes: Partial<Zip64Data> = {};
×
UNCOV
167
  if (zip64dataList.length > 0) {
×
UNCOV
168
    // total length of data in zip64 notation in bytes
×
UNCOV
169
    const zip64chunkSize = zip64dataList.reduce((sum, curr) => sum + curr.length, 0);
×
UNCOV
170
    // we're looking for the zip64 nontation header (0x0001)
×
UNCOV
171
    // and a size field with a correct value next to it
×
UNCOV
172
    const offsetInExtraData = new Uint8Array(extraField.buffer).findIndex(
×
UNCOV
173
      (_val, i, arr) =>
×
UNCOV
174
        getUint16(arr[i], arr[i + 1]) === 0x0001 &&
×
UNCOV
175
        getUint16(arr[i + 2], arr[i + 3]) === zip64chunkSize
×
UNCOV
176
    );
×
UNCOV
177
    // then we read all the nesessary fields from the zip64 data
×
UNCOV
178
    let bytesRead = 0;
×
UNCOV
179
    for (const note of zip64dataList) {
×
UNCOV
180
      const offset = bytesRead;
×
UNCOV
181
      zip64DataRes[note.name] = extraField.getBigUint64(offsetInExtraData + 4 + offset, true);
×
UNCOV
182
      bytesRead = offset + note.length;
×
UNCOV
183
    }
×
UNCOV
184
  }
×
UNCOV
185

×
UNCOV
186
  return zip64DataRes;
×
UNCOV
187
};
×
188

1✔
189
/**
1✔
190
 * frind data that's expected to be in zip64
1✔
191
 * @param zip64data values that might be in zip64 record
1✔
192
 * @returns zip64 data description
1✔
193
 */
1✔
194

1✔
195
const findExpectedData = (zip64data: Zip64Data): {length: number; name: string}[] => {
1✔
UNCOV
196
  // We define fields that should be in zip64 data
×
UNCOV
197
  const zip64dataList: {length: number; name: string}[] = [];
×
UNCOV
198
  if (zip64data.uncompressedSize === BigInt(0xffffffff)) {
×
199
    zip64dataList.push({name: 'uncompressedSize', length: 8});
×
200
  }
×
UNCOV
201
  if (zip64data.compressedSize === BigInt(0xffffffff)) {
×
202
    zip64dataList.push({name: 'compressedSize', length: 8});
×
203
  }
×
UNCOV
204
  if (zip64data.localHeaderOffset === BigInt(0xffffffff)) {
×
UNCOV
205
    zip64dataList.push({name: 'localHeaderOffset', length: 8});
×
UNCOV
206
  }
×
UNCOV
207
  if (zip64data.startDisk === BigInt(0xffffffff)) {
×
208
    zip64dataList.push({name: 'startDisk', length: 4});
×
209
  }
×
UNCOV
210

×
UNCOV
211
  return zip64dataList;
×
UNCOV
212
};
×
213

1✔
214
/** info that can be placed into cd header */
1✔
215
type GenerateCDOptions = {
1✔
216
  /** CRC-32 of uncompressed data */
1✔
217
  crc32: number;
1✔
218
  /** File name */
1✔
219
  fileName: string;
1✔
220
  /** File size */
1✔
221
  length: number;
1✔
222
  /** Relative offset of local file header */
1✔
223
  offset: bigint;
1✔
224
};
1✔
225

1✔
226
/**
1✔
227
 * generates cd header for the file
1✔
228
 * @param options info that can be placed into cd header
1✔
229
 * @returns buffer with header
1✔
230
 */
1✔
231
export function generateCDHeader(options: GenerateCDOptions): ArrayBuffer {
1✔
UNCOV
232
  const optionsToUse = {
×
UNCOV
233
    ...options,
×
UNCOV
234
    fnlength: options.fileName.length,
×
UNCOV
235
    extraLength: 0
×
UNCOV
236
  };
×
UNCOV
237

×
UNCOV
238
  let zip64header: ArrayBuffer = new ArrayBuffer(0);
×
UNCOV
239

×
UNCOV
240
  const optionsToZip64: any = {};
×
UNCOV
241
  if (optionsToUse.offset >= 0xffffffff) {
×
UNCOV
242
    optionsToZip64.offset = optionsToUse.offset;
×
UNCOV
243
    optionsToUse.offset = BigInt(0xffffffff);
×
UNCOV
244
  }
×
UNCOV
245
  if (optionsToUse.length >= 0xffffffff) {
×
246
    optionsToZip64.size = optionsToUse.length;
×
247
    optionsToUse.length = 0xffffffff;
×
248
  }
×
UNCOV
249

×
UNCOV
250
  if (Object.keys(optionsToZip64).length) {
×
UNCOV
251
    zip64header = createZip64Info(optionsToZip64);
×
UNCOV
252
    optionsToUse.extraLength = zip64header.byteLength;
×
UNCOV
253
  }
×
UNCOV
254
  const header = new DataView(new ArrayBuffer(Number(CD_FILE_NAME_OFFSET)));
×
UNCOV
255

×
UNCOV
256
  for (const field of ZIP_HEADER_FIELDS) {
×
UNCOV
257
    setFieldToNumber(
×
UNCOV
258
      header,
×
UNCOV
259
      field.size,
×
UNCOV
260
      field.offset,
×
UNCOV
261
      optionsToUse[field.name ?? ''] ?? field.default ?? 0
×
UNCOV
262
    );
×
UNCOV
263
  }
×
UNCOV
264

×
UNCOV
265
  const encodedName = new TextEncoder().encode(optionsToUse.fileName);
×
UNCOV
266

×
UNCOV
267
  const resHeader = concatenateArrayBuffers(header.buffer, encodedName, zip64header);
×
UNCOV
268

×
UNCOV
269
  return resHeader;
×
UNCOV
270
}
×
271

1✔
272
/** Fields map */
1✔
273
const ZIP_HEADER_FIELDS = [
1✔
274
  // Central directory file header signature = 0x02014b50
1✔
275
  {
1✔
276
    offset: 0,
1✔
277
    size: 4,
1✔
278
    default: new DataView(signature.buffer).getUint32(0, true)
1✔
279
  },
1✔
280

1✔
281
  // Version made by
1✔
282
  {
1✔
283
    offset: 4,
1✔
284
    size: 2,
1✔
285
    default: 45
1✔
286
  },
1✔
287

1✔
288
  // Version needed to extract (minimum)
1✔
289
  {
1✔
290
    offset: 6,
1✔
291
    size: 2,
1✔
292
    default: 45
1✔
293
  },
1✔
294

1✔
295
  // General purpose bit flag
1✔
296
  {
1✔
297
    offset: 8,
1✔
298
    size: 2,
1✔
299
    default: 0
1✔
300
  },
1✔
301

1✔
302
  // Compression method
1✔
303
  {
1✔
304
    offset: 10,
1✔
305
    size: 2,
1✔
306
    default: 0
1✔
307
  },
1✔
308

1✔
309
  // File last modification time
1✔
310
  {
1✔
311
    offset: 12,
1✔
312
    size: 2,
1✔
313
    default: 0
1✔
314
  },
1✔
315

1✔
316
  // File last modification date
1✔
317
  {
1✔
318
    offset: 14,
1✔
319
    size: 2,
1✔
320
    default: 0
1✔
321
  },
1✔
322

1✔
323
  // CRC-32 of uncompressed data
1✔
324
  {
1✔
325
    offset: 16,
1✔
326
    size: 4,
1✔
327
    name: 'crc32'
1✔
328
  },
1✔
329

1✔
330
  // Compressed size (or 0xffffffff for ZIP64)
1✔
331
  {
1✔
332
    offset: 20,
1✔
333
    size: 4,
1✔
334
    name: 'length'
1✔
335
  },
1✔
336

1✔
337
  // Uncompressed size (or 0xffffffff for ZIP64)
1✔
338
  {
1✔
339
    offset: 24,
1✔
340
    size: 4,
1✔
341
    name: 'length'
1✔
342
  },
1✔
343

1✔
344
  // File name length (n)
1✔
345
  {
1✔
346
    offset: 28,
1✔
347
    size: 2,
1✔
348
    name: 'fnlength'
1✔
349
  },
1✔
350

1✔
351
  // Extra field length (m)
1✔
352
  {
1✔
353
    offset: 30,
1✔
354
    size: 2,
1✔
355
    default: 0,
1✔
356
    name: 'extraLength'
1✔
357
  },
1✔
358

1✔
359
  // File comment length (k)
1✔
360
  {
1✔
361
    offset: 32,
1✔
362
    size: 2,
1✔
363
    default: 0
1✔
364
  },
1✔
365

1✔
366
  // Disk number where file starts (or 0xffff for ZIP64)
1✔
367
  {
1✔
368
    offset: 34,
1✔
369
    size: 2,
1✔
370
    default: 0
1✔
371
  },
1✔
372

1✔
373
  // Internal file attributes
1✔
374
  {
1✔
375
    offset: 36,
1✔
376
    size: 2,
1✔
377
    default: 0
1✔
378
  },
1✔
379

1✔
380
  // External file attributes
1✔
381
  {
1✔
382
    offset: 38,
1✔
383
    size: 4,
1✔
384
    default: 0
1✔
385
  },
1✔
386

1✔
387
  // Relative offset of local file header
1✔
388
  {
1✔
389
    offset: 42,
1✔
390
    size: 4,
1✔
391
    name: 'offset'
1✔
392
  }
1✔
393
];
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc