maplibre-tilepackage-protocol 1.2.1 → 1.2.2
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/CHANGELOG.md +13 -0
- package/dist/bundle.js +4314 -5342
- package/dist/bundle.js.map +1 -1
- package/dist/index.esm.js +487 -266
- package/dist/index.esm.js.map +1 -1
- package/package.json +42 -43
- package/rollup.config.mjs +0 -1
- package/src/default-decompress.js +4 -11
- package/src/get-header-and-filelist.js +5 -129
- package/src/get-json-from-file.js +1 -12
- package/src/index.js +75 -77
- package/src/maplibre-gl-js-protocol.js +0 -9
- package/src/source.js +5 -11
- package/src/zip/get-filelist.js +350 -0
- package/src/zip/get-json-from-file.js +47 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
|
|
2
|
+
const EOCD_SIGNATURE = 0x06054b50;
|
|
3
|
+
const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
|
|
4
|
+
const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
|
|
5
|
+
const MIN_EOCD_SIZE = 22;
|
|
6
|
+
const ZIP64_EOCD_SIZE = 56;
|
|
7
|
+
const MAX_TAIL_SCAN = 65536; // 64KB
|
|
8
|
+
const MAX_CENTRAL_DIR_SIZE = 500 * 1024 * 1024; // 500MB
|
|
9
|
+
const MAX_DIRECTORY_ENTRIES = 1000000;
|
|
10
|
+
const LOCAL_FILE_HEADER_BASE = 30;
|
|
11
|
+
|
|
12
|
+
function scanForSignature(view, signature, minSize) {
|
|
13
|
+
for (let i = view.byteLength - minSize; i >= 0; i--) {
|
|
14
|
+
if (view.getUint32(i, true) === signature) {
|
|
15
|
+
return i;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return -1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseZip64EndOfCentralDirectory(view, position) {
|
|
22
|
+
return {
|
|
23
|
+
entries: Number(view.getBigUint64(position + 32, true)),
|
|
24
|
+
size: Number(view.getBigUint64(position + 40, true)),
|
|
25
|
+
offset: Number(view.getBigUint64(position + 48, true)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseEndOfCentralDirectory(view, position) {
|
|
30
|
+
return {
|
|
31
|
+
entries: view.getUint16(position + 10, true),
|
|
32
|
+
size: view.getUint32(position + 12, true),
|
|
33
|
+
offset: view.getUint32(position + 16, true),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readLocalFileHeader(source, offset, decoder) {
|
|
38
|
+
const headerResp = await source.getBytes(offset, LOCAL_FILE_HEADER_BASE);
|
|
39
|
+
if (headerResp.data.byteLength < LOCAL_FILE_HEADER_BASE) {
|
|
40
|
+
throw new Error("Local file header truncated before 30 bytes");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const headerView = new DataView(headerResp.data, 0, LOCAL_FILE_HEADER_BASE);
|
|
44
|
+
const signature = headerView.getUint32(0, true);
|
|
45
|
+
if (signature !== LOCAL_FILE_HEADER_SIGNATURE) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Local file header signature mismatch (0x${signature.toString(16)})`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const filenameLength = headerView.getUint16(26, true);
|
|
52
|
+
const extraFieldLength = headerView.getUint16(28, true);
|
|
53
|
+
const trailingLength = filenameLength + extraFieldLength;
|
|
54
|
+
let filename = "";
|
|
55
|
+
|
|
56
|
+
if (filenameLength > 0) {
|
|
57
|
+
const trailingResp = await source.getBytes(
|
|
58
|
+
offset + LOCAL_FILE_HEADER_BASE,
|
|
59
|
+
trailingLength,
|
|
60
|
+
);
|
|
61
|
+
if (trailingResp.data.byteLength < trailingLength) {
|
|
62
|
+
throw new Error("Local header filename/extra data truncated");
|
|
63
|
+
}
|
|
64
|
+
const filenameView = new DataView(trailingResp.data, 0, filenameLength);
|
|
65
|
+
filename = decoder.decode(filenameView);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
filename,
|
|
70
|
+
filenameLength,
|
|
71
|
+
extraFieldLength,
|
|
72
|
+
dataOffset:
|
|
73
|
+
offset + LOCAL_FILE_HEADER_BASE + filenameLength + extraFieldLength,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function verifyLocalHeaderOffset({
|
|
78
|
+
source,
|
|
79
|
+
filename,
|
|
80
|
+
relativeOffset,
|
|
81
|
+
computedOffset,
|
|
82
|
+
decoder,
|
|
83
|
+
logger,
|
|
84
|
+
}) {
|
|
85
|
+
try {
|
|
86
|
+
const localHeader = await readLocalFileHeader(
|
|
87
|
+
source,
|
|
88
|
+
relativeOffset,
|
|
89
|
+
decoder,
|
|
90
|
+
);
|
|
91
|
+
if (localHeader.filename !== filename) {
|
|
92
|
+
logger(
|
|
93
|
+
`Local header filename mismatch for ${filename}: found ${localHeader.filename} at offset ${relativeOffset}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (localHeader.dataOffset !== computedOffset) {
|
|
97
|
+
logger(
|
|
98
|
+
`Data offset mismatch for ${filename}: calculated ${computedOffset}, actual ${localHeader.dataOffset}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return localHeader;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger(
|
|
104
|
+
`Failed to verify local header for ${filename} at ${relativeOffset}: ${error.message}`,
|
|
105
|
+
);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default async function getFilelistFromZip(source, options) {
|
|
111
|
+
const resolvedOptions = options || {};
|
|
112
|
+
const verifyOffsets =
|
|
113
|
+
typeof resolvedOptions.verifyOffsets === "boolean"
|
|
114
|
+
? resolvedOptions.verifyOffsets
|
|
115
|
+
: false;
|
|
116
|
+
const verificationLogger =
|
|
117
|
+
typeof resolvedOptions.verificationLogger === "function"
|
|
118
|
+
? resolvedOptions.verificationLogger
|
|
119
|
+
: console.warn;
|
|
120
|
+
const fileSize = await source.getSize();
|
|
121
|
+
if (fileSize < MIN_EOCD_SIZE) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"File too small to be a valid ZIP archive (minimum 22 bytes)",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const tailReadSize = Math.min(MAX_TAIL_SCAN, fileSize);
|
|
128
|
+
const tailOffset = fileSize - tailReadSize;
|
|
129
|
+
const tailResp = await source.getBytes(tailOffset, tailReadSize);
|
|
130
|
+
const tailView = new DataView(tailResp.data, 0, tailReadSize);
|
|
131
|
+
|
|
132
|
+
let entriesCentralDirectory;
|
|
133
|
+
let sizeCentralDirectory;
|
|
134
|
+
let offsetCentralDirectory;
|
|
135
|
+
|
|
136
|
+
const zip64Pos = scanForSignature(
|
|
137
|
+
tailView,
|
|
138
|
+
ZIP64_EOCD_SIGNATURE,
|
|
139
|
+
ZIP64_EOCD_SIZE,
|
|
140
|
+
);
|
|
141
|
+
if (zip64Pos !== -1) {
|
|
142
|
+
const zip64Data = parseZip64EndOfCentralDirectory(tailView, zip64Pos);
|
|
143
|
+
entriesCentralDirectory = zip64Data.entries;
|
|
144
|
+
sizeCentralDirectory = zip64Data.size;
|
|
145
|
+
offsetCentralDirectory = zip64Data.offset;
|
|
146
|
+
} else {
|
|
147
|
+
const eocdPos = scanForSignature(tailView, EOCD_SIGNATURE, MIN_EOCD_SIZE);
|
|
148
|
+
if (eocdPos === -1) {
|
|
149
|
+
throw new Error("End of Central Directory record not found in zip");
|
|
150
|
+
}
|
|
151
|
+
const eocdData = parseEndOfCentralDirectory(tailView, eocdPos);
|
|
152
|
+
entriesCentralDirectory = eocdData.entries;
|
|
153
|
+
sizeCentralDirectory = eocdData.size;
|
|
154
|
+
offsetCentralDirectory = eocdData.offset;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (sizeCentralDirectory > MAX_CENTRAL_DIR_SIZE) {
|
|
158
|
+
throw new Error("Central directory size exceeds maximum allowed (500MB)");
|
|
159
|
+
}
|
|
160
|
+
if (entriesCentralDirectory > MAX_DIRECTORY_ENTRIES) {
|
|
161
|
+
throw new Error("Too many entries in central directory (max 1000000)");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (offsetCentralDirectory < 0) {
|
|
165
|
+
throw new Error("Central directory offset is negative, file is corrupted");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const availableBytes = fileSize - offsetCentralDirectory;
|
|
169
|
+
if (availableBytes < 0) {
|
|
170
|
+
throw new Error("Central directory offset is beyond file size");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let safeCentralDirectorySize = sizeCentralDirectory;
|
|
174
|
+
if (safeCentralDirectorySize > availableBytes) {
|
|
175
|
+
console.warn(
|
|
176
|
+
"Central directory size extends past end of file, clamping to available bytes",
|
|
177
|
+
);
|
|
178
|
+
safeCentralDirectorySize = availableBytes;
|
|
179
|
+
}
|
|
180
|
+
if (safeCentralDirectorySize <= 0) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"Central directory size is zero or negative after validation",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const centralDirectory = await source.getBytes(
|
|
187
|
+
offsetCentralDirectory,
|
|
188
|
+
safeCentralDirectorySize,
|
|
189
|
+
);
|
|
190
|
+
const v = new DataView(centralDirectory.data, 0, safeCentralDirectorySize);
|
|
191
|
+
|
|
192
|
+
if (v.getUint32(0, true) !== CENTRAL_DIRECTORY_SIGNATURE) {
|
|
193
|
+
throw new Error("Wrong magic number for Central Directory archive");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const zipFiles = {};
|
|
197
|
+
let entryStart = 0;
|
|
198
|
+
const decoder = new TextDecoder("utf-8");
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < entriesCentralDirectory; i++) {
|
|
201
|
+
if (entryStart + 46 > safeCentralDirectorySize) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const generalPurposeFlag = v.getUint16(entryStart + 8, true);
|
|
206
|
+
const compressionMethod = v.getUint16(entryStart + 10, true);
|
|
207
|
+
if (compressionMethod !== 0 && compressionMethod !== 8) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"Unsupported compression method: " +
|
|
210
|
+
compressionMethod +
|
|
211
|
+
" only STORE(0) and DEFLATE(8) are supported",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
let sizeFile = v.getUint32(entryStart + 20, true);
|
|
215
|
+
const uncompressedSize = v.getUint32(entryStart + 24, true);
|
|
216
|
+
const sizeFileName = v.getUint16(entryStart + 28, true);
|
|
217
|
+
const sizeExtraField = v.getUint16(entryStart + 30, true);
|
|
218
|
+
const sizeComment = v.getUint16(entryStart + 32, true);
|
|
219
|
+
let relativeOffset = v.getUint32(entryStart + 42, true);
|
|
220
|
+
|
|
221
|
+
const headerLength = 46 + sizeFileName + sizeExtraField + sizeComment;
|
|
222
|
+
if (entryStart + headerLength > safeCentralDirectorySize) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const filenameView = new DataView(
|
|
227
|
+
centralDirectory.data,
|
|
228
|
+
entryStart + 46,
|
|
229
|
+
sizeFileName,
|
|
230
|
+
);
|
|
231
|
+
let filename;
|
|
232
|
+
try {
|
|
233
|
+
filename = decoder.decode(filenameView);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
"Failed to decode filename in ZIP entry: " + error.message,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const zip64SizeOverflow = sizeFile === 0xffffffff;
|
|
241
|
+
const zip64UncompressedOverflow = uncompressedSize === 0xffffffff;
|
|
242
|
+
const zip64OffsetOverflow = relativeOffset === 0xffffffff;
|
|
243
|
+
const hasZip64Marker =
|
|
244
|
+
zip64SizeOverflow || zip64UncompressedOverflow || zip64OffsetOverflow;
|
|
245
|
+
let localExtraFieldZip64Length = 0;
|
|
246
|
+
|
|
247
|
+
if (hasZip64Marker && sizeExtraField > 0) {
|
|
248
|
+
const extraStart = entryStart + 46 + sizeFileName;
|
|
249
|
+
const extraEnd = extraStart + sizeExtraField;
|
|
250
|
+
if (extraEnd <= safeCentralDirectorySize) {
|
|
251
|
+
const extraView = new DataView(
|
|
252
|
+
centralDirectory.data,
|
|
253
|
+
extraStart,
|
|
254
|
+
sizeExtraField,
|
|
255
|
+
);
|
|
256
|
+
let extraOffset = 0;
|
|
257
|
+
let iterations = 100;
|
|
258
|
+
while (extraOffset + 4 <= sizeExtraField && iterations > 0) {
|
|
259
|
+
iterations--;
|
|
260
|
+
const headerId = extraView.getUint16(extraOffset, true);
|
|
261
|
+
const dataSize = extraView.getUint16(extraOffset + 2, true);
|
|
262
|
+
|
|
263
|
+
if (headerId === 0x0001) {
|
|
264
|
+
// APPNOTE.TXT (4.5.3) – local header ZIP64 field only stores
|
|
265
|
+
// the compressed/uncompressed sizes when a 64-bit value is needed.
|
|
266
|
+
const dataStart = extraOffset + 4;
|
|
267
|
+
const dataEnd = dataStart + dataSize;
|
|
268
|
+
let cursor = dataStart;
|
|
269
|
+
let localZip64DataSize = 0;
|
|
270
|
+
|
|
271
|
+
if (zip64UncompressedOverflow) {
|
|
272
|
+
if (cursor + 8 > dataEnd) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"ZIP64 extra field truncated while reading uncompressed size",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
extraView.getBigUint64(cursor, true);
|
|
278
|
+
cursor += 8;
|
|
279
|
+
localZip64DataSize += 8;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (zip64SizeOverflow) {
|
|
283
|
+
if (cursor + 8 > dataEnd) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
"ZIP64 extra field truncated while reading compressed size",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
sizeFile = Number(extraView.getBigUint64(cursor, true));
|
|
289
|
+
cursor += 8;
|
|
290
|
+
localZip64DataSize += 8;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (zip64OffsetOverflow) {
|
|
294
|
+
if (cursor + 8 > dataEnd) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
"ZIP64 extra field truncated while reading relative offset",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
relativeOffset = Number(extraView.getBigUint64(cursor, true));
|
|
300
|
+
cursor += 8;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (localZip64DataSize > 0) {
|
|
304
|
+
localExtraFieldZip64Length = 4 + localZip64DataSize;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (dataSize === 0) {
|
|
310
|
+
extraOffset += 4;
|
|
311
|
+
} else {
|
|
312
|
+
extraOffset += 4 + dataSize;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
let absoluteOffset =
|
|
318
|
+
relativeOffset + 30 + sizeFileName + localExtraFieldZip64Length;
|
|
319
|
+
|
|
320
|
+
if (verifyOffsets) {
|
|
321
|
+
const verification = await verifyLocalHeaderOffset({
|
|
322
|
+
source,
|
|
323
|
+
filename,
|
|
324
|
+
relativeOffset,
|
|
325
|
+
computedOffset: absoluteOffset,
|
|
326
|
+
decoder,
|
|
327
|
+
logger: verificationLogger,
|
|
328
|
+
});
|
|
329
|
+
if (verification && typeof verification.dataOffset === "number") {
|
|
330
|
+
absoluteOffset = verification.dataOffset;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
zipFiles[filename] = {
|
|
334
|
+
filename,
|
|
335
|
+
size: sizeFile,
|
|
336
|
+
generalPurposeFlag,
|
|
337
|
+
compressionMethod,
|
|
338
|
+
uncompressedSize,
|
|
339
|
+
relativeOffset,
|
|
340
|
+
absoluteOffset,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
entryStart += headerLength;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
files: zipFiles,
|
|
348
|
+
etag: tailResp.etag,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export default async function getJsonFromZip(file, zipFiles, source) {
|
|
2
|
+
//console.log("getJsonFromZip:", file);
|
|
3
|
+
const decoder = new TextDecoder("utf-8");
|
|
4
|
+
if (zipFiles[file]) {
|
|
5
|
+
const fileOffset = zipFiles[file].absoluteOffset;
|
|
6
|
+
const fileJSON = await source.getBytes(fileOffset, zipFiles[file].size);
|
|
7
|
+
let text;
|
|
8
|
+
try {
|
|
9
|
+
text = decoder.decode(fileJSON.data);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Failed to decode JSON bytes from " + file + ": " + error.message,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const trimmed = text.trim();
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
throw new Error("JSON file " + file + " is empty or whitespace only");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const firstChar = trimmed[0];
|
|
22
|
+
const lastChar = trimmed[trimmed.length - 1];
|
|
23
|
+
const looksLikeJsonObject = firstChar === "{" && lastChar === "}";
|
|
24
|
+
const looksLikeJsonArray = firstChar === "[" && lastChar === "]";
|
|
25
|
+
if (!looksLikeJsonObject && !looksLikeJsonArray) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"File " +
|
|
28
|
+
file +
|
|
29
|
+
" does not appear to contain JSON (starts with '" +
|
|
30
|
+
firstChar +
|
|
31
|
+
"' and ends with '" +
|
|
32
|
+
lastChar +
|
|
33
|
+
"')",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(trimmed);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Failed to parse JSON from " + file + ": " + error.message,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
console.warn(`File ${file} not found in Zip file`);
|
|
46
|
+
return {};
|
|
47
|
+
}
|