file-type 16.5.2 → 17.0.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/browser.d.ts +22 -28
- package/browser.js +21 -19
- package/core.d.ts +362 -356
- package/core.js +1214 -1155
- package/index.d.ts +4 -17
- package/index.js +5 -24
- package/package.json +28 -16
- package/readme.md +113 -97
- package/supported.js +275 -278
- package/util.js +10 -12
package/core.js
CHANGED
|
@@ -1,43 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {Buffer} from 'node:buffer';
|
|
2
|
+
import * as Token from 'token-types';
|
|
3
|
+
import * as strtok3 from 'strtok3/core';
|
|
4
|
+
import {
|
|
5
5
|
stringToBytes,
|
|
6
6
|
tarHeaderChecksumMatches,
|
|
7
|
-
uint32SyncSafeToken
|
|
8
|
-
}
|
|
9
|
-
|
|
7
|
+
uint32SyncSafeToken,
|
|
8
|
+
} from './util.js';
|
|
9
|
+
import {extensions, mimeTypes} from './supported.js';
|
|
10
10
|
|
|
11
|
-
const minimumBytes = 4100; // A fair amount of file-types are detectable within this range
|
|
11
|
+
const minimumBytes = 4100; // A fair amount of file-types are detectable within this range.
|
|
12
12
|
|
|
13
|
-
async function
|
|
13
|
+
export async function fileTypeFromStream(stream) {
|
|
14
14
|
const tokenizer = await strtok3.fromStream(stream);
|
|
15
15
|
try {
|
|
16
|
-
return await
|
|
16
|
+
return await fileTypeFromTokenizer(tokenizer);
|
|
17
17
|
} finally {
|
|
18
18
|
await tokenizer.close();
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async function
|
|
23
|
-
if (!(input instanceof Uint8Array || input instanceof ArrayBuffer
|
|
22
|
+
export async function fileTypeFromBuffer(input) {
|
|
23
|
+
if (!(input instanceof Uint8Array || input instanceof ArrayBuffer)) {
|
|
24
24
|
throw new TypeError(`Expected the \`input\` argument to be of type \`Uint8Array\` or \`Buffer\` or \`ArrayBuffer\`, got \`${typeof input}\``);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const buffer = input instanceof
|
|
27
|
+
const buffer = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
28
28
|
|
|
29
29
|
if (!(buffer && buffer.length > 1)) {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
return fromTokenizer(tokenizer);
|
|
33
|
+
return fileTypeFromTokenizer(strtok3.fromBuffer(buffer));
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
function _check(buffer, headers, options) {
|
|
38
37
|
options = {
|
|
39
38
|
offset: 0,
|
|
40
|
-
...options
|
|
39
|
+
...options,
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
for (const [index, header] of headers.entries()) {
|
|
@@ -55,9 +54,9 @@ function _check(buffer, headers, options) {
|
|
|
55
54
|
return true;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
async function
|
|
57
|
+
export async function fileTypeFromTokenizer(tokenizer) {
|
|
59
58
|
try {
|
|
60
|
-
return
|
|
59
|
+
return new FileTypeParser().parse(tokenizer);
|
|
61
60
|
} catch (error) {
|
|
62
61
|
if (!(error instanceof strtok3.EndOfStreamError)) {
|
|
63
62
|
throw error;
|
|
@@ -65,1401 +64,1461 @@ async function fromTokenizer(tokenizer) {
|
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const check = (header, options) => _check(buffer, header, options);
|
|
72
|
-
const checkString = (header, options) => check(stringToBytes(header), options);
|
|
73
|
-
|
|
74
|
-
// Keep reading until EOF if the file size is unknown.
|
|
75
|
-
if (!tokenizer.fileInfo.size) {
|
|
76
|
-
tokenizer.fileInfo.size = Number.MAX_SAFE_INTEGER;
|
|
67
|
+
class FileTypeParser {
|
|
68
|
+
check(header, options) {
|
|
69
|
+
return _check(this.buffer, header, options);
|
|
77
70
|
}
|
|
78
71
|
|
|
79
|
-
|
|
72
|
+
checkString(header, options) {
|
|
73
|
+
return this.check(stringToBytes(header), options);
|
|
74
|
+
}
|
|
80
75
|
|
|
81
|
-
|
|
76
|
+
async parse(tokenizer) {
|
|
77
|
+
this.buffer = Buffer.alloc(minimumBytes);
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
};
|
|
88
|
-
}
|
|
79
|
+
// Keep reading until EOF if the file size is unknown.
|
|
80
|
+
if (tokenizer.fileInfo.size === undefined) {
|
|
81
|
+
tokenizer.fileInfo.size = Number.MAX_SAFE_INTEGER;
|
|
82
|
+
}
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
95
|
-
}
|
|
84
|
+
// Keep reading until EOF if the file size is unknown.
|
|
85
|
+
if (tokenizer.fileInfo.size === undefined) {
|
|
86
|
+
tokenizer.fileInfo.size = Number.MAX_SAFE_INTEGER;
|
|
87
|
+
}
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
ext: 'dmg',
|
|
100
|
-
mime: 'application/x-apple-diskimage'
|
|
101
|
-
};
|
|
102
|
-
}
|
|
89
|
+
this.tokenizer = tokenizer;
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
ext: 'exe',
|
|
107
|
-
mime: 'application/x-msdownload'
|
|
108
|
-
};
|
|
109
|
-
}
|
|
91
|
+
await tokenizer.peekBuffer(this.buffer, {length: 12, mayBeLess: true});
|
|
110
92
|
|
|
111
|
-
|
|
112
|
-
await tokenizer.peekBuffer(buffer, {length: 24, mayBeLess: true});
|
|
93
|
+
// -- 2-byte signatures --
|
|
113
94
|
|
|
114
|
-
if (
|
|
115
|
-
checkString(' EPSF-', {offset: 14})) {
|
|
95
|
+
if (this.check([0x42, 0x4D])) {
|
|
116
96
|
return {
|
|
117
|
-
ext: '
|
|
118
|
-
mime: '
|
|
97
|
+
ext: 'bmp',
|
|
98
|
+
mime: 'image/bmp',
|
|
119
99
|
};
|
|
120
100
|
}
|
|
121
101
|
|
|
122
|
-
|
|
123
|
-
ext: 'ps',
|
|
124
|
-
mime: 'application/postscript'
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
check([0x1F, 0xA0]) ||
|
|
130
|
-
check([0x1F, 0x9D])
|
|
131
|
-
) {
|
|
132
|
-
return {
|
|
133
|
-
ext: 'Z',
|
|
134
|
-
mime: 'application/x-compress'
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// -- 3-byte signatures --
|
|
139
|
-
|
|
140
|
-
if (check([0xFF, 0xD8, 0xFF])) {
|
|
141
|
-
return {
|
|
142
|
-
ext: 'jpg',
|
|
143
|
-
mime: 'image/jpeg'
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (check([0x49, 0x49, 0xBC])) {
|
|
148
|
-
return {
|
|
149
|
-
ext: 'jxr',
|
|
150
|
-
mime: 'image/vnd.ms-photo'
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (check([0x1F, 0x8B, 0x8])) {
|
|
155
|
-
return {
|
|
156
|
-
ext: 'gz',
|
|
157
|
-
mime: 'application/gzip'
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (check([0x42, 0x5A, 0x68])) {
|
|
162
|
-
return {
|
|
163
|
-
ext: 'bz2',
|
|
164
|
-
mime: 'application/x-bzip2'
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (checkString('ID3')) {
|
|
169
|
-
await tokenizer.ignore(6); // Skip ID3 header until the header size
|
|
170
|
-
const id3HeaderLen = await tokenizer.readToken(uint32SyncSafeToken);
|
|
171
|
-
if (tokenizer.position + id3HeaderLen > tokenizer.fileInfo.size) {
|
|
172
|
-
// Guess file type based on ID3 header for backward compatibility
|
|
102
|
+
if (this.check([0x0B, 0x77])) {
|
|
173
103
|
return {
|
|
174
|
-
ext: '
|
|
175
|
-
mime: 'audio/
|
|
104
|
+
ext: 'ac3',
|
|
105
|
+
mime: 'audio/vnd.dolby.dd-raw',
|
|
176
106
|
};
|
|
177
107
|
}
|
|
178
108
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
109
|
+
if (this.check([0x78, 0x01])) {
|
|
110
|
+
return {
|
|
111
|
+
ext: 'dmg',
|
|
112
|
+
mime: 'application/x-apple-diskimage',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
182
115
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
}
|
|
116
|
+
if (this.check([0x4D, 0x5A])) {
|
|
117
|
+
return {
|
|
118
|
+
ext: 'exe',
|
|
119
|
+
mime: 'application/x-msdownload',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
190
122
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
check([0x57, 0x53], {offset: 1})
|
|
194
|
-
) {
|
|
195
|
-
return {
|
|
196
|
-
ext: 'swf',
|
|
197
|
-
mime: 'application/x-shockwave-flash'
|
|
198
|
-
};
|
|
199
|
-
}
|
|
123
|
+
if (this.check([0x25, 0x21])) {
|
|
124
|
+
await tokenizer.peekBuffer(this.buffer, {length: 24, mayBeLess: true});
|
|
200
125
|
|
|
201
|
-
|
|
126
|
+
if (
|
|
127
|
+
this.checkString('PS-Adobe-', {offset: 2})
|
|
128
|
+
&& this.checkString(' EPSF-', {offset: 14})
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
ext: 'eps',
|
|
132
|
+
mime: 'application/eps',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
202
135
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
}
|
|
136
|
+
return {
|
|
137
|
+
ext: 'ps',
|
|
138
|
+
mime: 'application/postscript',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
209
141
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
142
|
+
if (
|
|
143
|
+
this.check([0x1F, 0xA0])
|
|
144
|
+
|| this.check([0x1F, 0x9D])
|
|
145
|
+
) {
|
|
146
|
+
return {
|
|
147
|
+
ext: 'Z',
|
|
148
|
+
mime: 'application/x-compress',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
216
151
|
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
ext: 'psd',
|
|
220
|
-
mime: 'image/vnd.adobe.photoshop'
|
|
221
|
-
};
|
|
222
|
-
}
|
|
152
|
+
// -- 3-byte signatures --
|
|
223
153
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
154
|
+
if (this.check([0xFF, 0xD8, 0xFF])) {
|
|
155
|
+
return {
|
|
156
|
+
ext: 'jpg',
|
|
157
|
+
mime: 'image/jpeg',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
230
160
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
}
|
|
161
|
+
if (this.check([0x49, 0x49, 0xBC])) {
|
|
162
|
+
return {
|
|
163
|
+
ext: 'jxr',
|
|
164
|
+
mime: 'image/vnd.ms-photo',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
238
167
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
168
|
+
if (this.check([0x1F, 0x8B, 0x8])) {
|
|
169
|
+
return {
|
|
170
|
+
ext: 'gz',
|
|
171
|
+
mime: 'application/gzip',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
245
174
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
175
|
+
if (this.check([0x42, 0x5A, 0x68])) {
|
|
176
|
+
return {
|
|
177
|
+
ext: 'bz2',
|
|
178
|
+
mime: 'application/x-bzip2',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
252
181
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const zipHeader = {
|
|
262
|
-
compressedSize: buffer.readUInt32LE(18),
|
|
263
|
-
uncompressedSize: buffer.readUInt32LE(22),
|
|
264
|
-
filenameLength: buffer.readUInt16LE(26),
|
|
265
|
-
extraFieldLength: buffer.readUInt16LE(28)
|
|
182
|
+
if (this.checkString('ID3')) {
|
|
183
|
+
await tokenizer.ignore(6); // Skip ID3 header until the header size
|
|
184
|
+
const id3HeaderLength = await tokenizer.readToken(uint32SyncSafeToken);
|
|
185
|
+
if (tokenizer.position + id3HeaderLength > tokenizer.fileInfo.size) {
|
|
186
|
+
// Guess file type based on ID3 header for backward compatibility
|
|
187
|
+
return {
|
|
188
|
+
ext: 'mp3',
|
|
189
|
+
mime: 'audio/mpeg',
|
|
266
190
|
};
|
|
191
|
+
}
|
|
267
192
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// Assumes signed `.xpi` from addons.mozilla.org
|
|
272
|
-
if (zipHeader.filename === 'META-INF/mozilla.rsa') {
|
|
273
|
-
return {
|
|
274
|
-
ext: 'xpi',
|
|
275
|
-
mime: 'application/x-xpinstall'
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (zipHeader.filename.endsWith('.rels') || zipHeader.filename.endsWith('.xml')) {
|
|
280
|
-
const type = zipHeader.filename.split('/')[0];
|
|
281
|
-
switch (type) {
|
|
282
|
-
case '_rels':
|
|
283
|
-
break;
|
|
284
|
-
case 'word':
|
|
285
|
-
return {
|
|
286
|
-
ext: 'docx',
|
|
287
|
-
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
288
|
-
};
|
|
289
|
-
case 'ppt':
|
|
290
|
-
return {
|
|
291
|
-
ext: 'pptx',
|
|
292
|
-
mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
293
|
-
};
|
|
294
|
-
case 'xl':
|
|
295
|
-
return {
|
|
296
|
-
ext: 'xlsx',
|
|
297
|
-
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
298
|
-
};
|
|
299
|
-
default:
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (zipHeader.filename.startsWith('xl/')) {
|
|
305
|
-
return {
|
|
306
|
-
ext: 'xlsx',
|
|
307
|
-
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (zipHeader.filename.startsWith('3D/') && zipHeader.filename.endsWith('.model')) {
|
|
312
|
-
return {
|
|
313
|
-
ext: '3mf',
|
|
314
|
-
mime: 'model/3mf'
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// The docx, xlsx and pptx file types extend the Office Open XML file format:
|
|
319
|
-
// https://en.wikipedia.org/wiki/Office_Open_XML_file_formats
|
|
320
|
-
// We look for:
|
|
321
|
-
// - one entry named '[Content_Types].xml' or '_rels/.rels',
|
|
322
|
-
// - one entry indicating specific type of file.
|
|
323
|
-
// MS Office, OpenOffice and LibreOffice may put the parts in different order, so the check should not rely on it.
|
|
324
|
-
if (zipHeader.filename === 'mimetype' && zipHeader.compressedSize === zipHeader.uncompressedSize) {
|
|
325
|
-
const mimeType = await tokenizer.readToken(new Token.StringType(zipHeader.compressedSize, 'utf-8'));
|
|
326
|
-
|
|
327
|
-
switch (mimeType) {
|
|
328
|
-
case 'application/epub+zip':
|
|
329
|
-
return {
|
|
330
|
-
ext: 'epub',
|
|
331
|
-
mime: 'application/epub+zip'
|
|
332
|
-
};
|
|
333
|
-
case 'application/vnd.oasis.opendocument.text':
|
|
334
|
-
return {
|
|
335
|
-
ext: 'odt',
|
|
336
|
-
mime: 'application/vnd.oasis.opendocument.text'
|
|
337
|
-
};
|
|
338
|
-
case 'application/vnd.oasis.opendocument.spreadsheet':
|
|
339
|
-
return {
|
|
340
|
-
ext: 'ods',
|
|
341
|
-
mime: 'application/vnd.oasis.opendocument.spreadsheet'
|
|
342
|
-
};
|
|
343
|
-
case 'application/vnd.oasis.opendocument.presentation':
|
|
344
|
-
return {
|
|
345
|
-
ext: 'odp',
|
|
346
|
-
mime: 'application/vnd.oasis.opendocument.presentation'
|
|
347
|
-
};
|
|
348
|
-
default:
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Try to find next header manually when current one is corrupted
|
|
353
|
-
if (zipHeader.compressedSize === 0) {
|
|
354
|
-
let nextHeaderIndex = -1;
|
|
193
|
+
await tokenizer.ignore(id3HeaderLength);
|
|
194
|
+
return fileTypeFromTokenizer(tokenizer); // Skip ID3 header, recursion
|
|
195
|
+
}
|
|
355
196
|
|
|
356
|
-
|
|
357
|
-
|
|
197
|
+
// Musepack, SV7
|
|
198
|
+
if (this.checkString('MP+')) {
|
|
199
|
+
return {
|
|
200
|
+
ext: 'mpc',
|
|
201
|
+
mime: 'audio/x-musepack',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
358
204
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
} catch (error) {
|
|
368
|
-
if (!(error instanceof strtok3.EndOfStreamError)) {
|
|
369
|
-
throw error;
|
|
370
|
-
}
|
|
205
|
+
if (
|
|
206
|
+
(this.buffer[0] === 0x43 || this.buffer[0] === 0x46)
|
|
207
|
+
&& this.check([0x57, 0x53], {offset: 1})
|
|
208
|
+
) {
|
|
209
|
+
return {
|
|
210
|
+
ext: 'swf',
|
|
211
|
+
mime: 'application/x-shockwave-flash',
|
|
212
|
+
};
|
|
371
213
|
}
|
|
372
214
|
|
|
373
|
-
|
|
374
|
-
ext: 'zip',
|
|
375
|
-
mime: 'application/zip'
|
|
376
|
-
};
|
|
377
|
-
}
|
|
215
|
+
// -- 4-byte signatures --
|
|
378
216
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
217
|
+
if (this.check([0x47, 0x49, 0x46])) {
|
|
218
|
+
return {
|
|
219
|
+
ext: 'gif',
|
|
220
|
+
mime: 'image/gif',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
384
223
|
|
|
385
|
-
|
|
386
|
-
if (_check(type, [0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64])) {
|
|
224
|
+
if (this.checkString('FLIF')) {
|
|
387
225
|
return {
|
|
388
|
-
ext: '
|
|
389
|
-
mime: '
|
|
226
|
+
ext: 'flif',
|
|
227
|
+
mime: 'image/flif',
|
|
390
228
|
};
|
|
391
229
|
}
|
|
392
230
|
|
|
393
|
-
|
|
394
|
-
if (_check(type, [0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61])) {
|
|
231
|
+
if (this.checkString('8BPS')) {
|
|
395
232
|
return {
|
|
396
|
-
ext: '
|
|
397
|
-
mime: '
|
|
233
|
+
ext: 'psd',
|
|
234
|
+
mime: 'image/vnd.adobe.photoshop',
|
|
398
235
|
};
|
|
399
236
|
}
|
|
400
237
|
|
|
401
|
-
|
|
402
|
-
if (_check(type, [0x01, 0x76, 0x69, 0x64, 0x65, 0x6F, 0x00])) {
|
|
238
|
+
if (this.checkString('WEBP', {offset: 8})) {
|
|
403
239
|
return {
|
|
404
|
-
ext: '
|
|
405
|
-
mime: '
|
|
240
|
+
ext: 'webp',
|
|
241
|
+
mime: 'image/webp',
|
|
406
242
|
};
|
|
407
243
|
}
|
|
408
244
|
|
|
409
|
-
//
|
|
410
|
-
if (
|
|
245
|
+
// Musepack, SV8
|
|
246
|
+
if (this.checkString('MPCK')) {
|
|
411
247
|
return {
|
|
412
|
-
ext: '
|
|
413
|
-
mime: 'audio/
|
|
248
|
+
ext: 'mpc',
|
|
249
|
+
mime: 'audio/x-musepack',
|
|
414
250
|
};
|
|
415
251
|
}
|
|
416
252
|
|
|
417
|
-
|
|
418
|
-
if (_check(type, [0x53, 0x70, 0x65, 0x65, 0x78, 0x20, 0x20])) {
|
|
253
|
+
if (this.checkString('FORM')) {
|
|
419
254
|
return {
|
|
420
|
-
ext: '
|
|
421
|
-
mime: 'audio/
|
|
255
|
+
ext: 'aif',
|
|
256
|
+
mime: 'audio/aiff',
|
|
422
257
|
};
|
|
423
258
|
}
|
|
424
259
|
|
|
425
|
-
|
|
426
|
-
if (_check(type, [0x01, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73])) {
|
|
260
|
+
if (this.checkString('icns', {offset: 0})) {
|
|
427
261
|
return {
|
|
428
|
-
ext: '
|
|
429
|
-
mime: '
|
|
262
|
+
ext: 'icns',
|
|
263
|
+
mime: 'image/icns',
|
|
430
264
|
};
|
|
431
265
|
}
|
|
432
266
|
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
267
|
+
// Zip-based file formats
|
|
268
|
+
// Need to be before the `zip` check
|
|
269
|
+
if (this.check([0x50, 0x4B, 0x3, 0x4])) { // Local file header signature
|
|
270
|
+
try {
|
|
271
|
+
while (tokenizer.position + 30 < tokenizer.fileInfo.size) {
|
|
272
|
+
await tokenizer.readBuffer(this.buffer, {length: 30});
|
|
273
|
+
|
|
274
|
+
// https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
|
|
275
|
+
const zipHeader = {
|
|
276
|
+
compressedSize: this.buffer.readUInt32LE(18),
|
|
277
|
+
uncompressedSize: this.buffer.readUInt32LE(22),
|
|
278
|
+
filenameLength: this.buffer.readUInt16LE(26),
|
|
279
|
+
extraFieldLength: this.buffer.readUInt16LE(28),
|
|
280
|
+
};
|
|
439
281
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
(buffer[2] === 0x3 || buffer[2] === 0x5 || buffer[2] === 0x7) &&
|
|
443
|
-
(buffer[3] === 0x4 || buffer[3] === 0x6 || buffer[3] === 0x8)
|
|
444
|
-
) {
|
|
445
|
-
return {
|
|
446
|
-
ext: 'zip',
|
|
447
|
-
mime: 'application/zip'
|
|
448
|
-
};
|
|
449
|
-
}
|
|
282
|
+
zipHeader.filename = await tokenizer.readToken(new Token.StringType(zipHeader.filenameLength, 'utf-8'));
|
|
283
|
+
await tokenizer.ignore(zipHeader.extraFieldLength);
|
|
450
284
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (
|
|
458
|
-
checkString('ftyp', {offset: 4}) &&
|
|
459
|
-
(buffer[8] & 0x60) !== 0x00 // Brand major, first character ASCII?
|
|
460
|
-
) {
|
|
461
|
-
// They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect.
|
|
462
|
-
// For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension.
|
|
463
|
-
const brandMajor = buffer.toString('binary', 8, 12).replace('\0', ' ').trim();
|
|
464
|
-
switch (brandMajor) {
|
|
465
|
-
case 'avif':
|
|
466
|
-
return {ext: 'avif', mime: 'image/avif'};
|
|
467
|
-
case 'mif1':
|
|
468
|
-
return {ext: 'heic', mime: 'image/heif'};
|
|
469
|
-
case 'msf1':
|
|
470
|
-
return {ext: 'heic', mime: 'image/heif-sequence'};
|
|
471
|
-
case 'heic':
|
|
472
|
-
case 'heix':
|
|
473
|
-
return {ext: 'heic', mime: 'image/heic'};
|
|
474
|
-
case 'hevc':
|
|
475
|
-
case 'hevx':
|
|
476
|
-
return {ext: 'heic', mime: 'image/heic-sequence'};
|
|
477
|
-
case 'qt':
|
|
478
|
-
return {ext: 'mov', mime: 'video/quicktime'};
|
|
479
|
-
case 'M4V':
|
|
480
|
-
case 'M4VH':
|
|
481
|
-
case 'M4VP':
|
|
482
|
-
return {ext: 'm4v', mime: 'video/x-m4v'};
|
|
483
|
-
case 'M4P':
|
|
484
|
-
return {ext: 'm4p', mime: 'video/mp4'};
|
|
485
|
-
case 'M4B':
|
|
486
|
-
return {ext: 'm4b', mime: 'audio/mp4'};
|
|
487
|
-
case 'M4A':
|
|
488
|
-
return {ext: 'm4a', mime: 'audio/x-m4a'};
|
|
489
|
-
case 'F4V':
|
|
490
|
-
return {ext: 'f4v', mime: 'video/mp4'};
|
|
491
|
-
case 'F4P':
|
|
492
|
-
return {ext: 'f4p', mime: 'video/mp4'};
|
|
493
|
-
case 'F4A':
|
|
494
|
-
return {ext: 'f4a', mime: 'audio/mp4'};
|
|
495
|
-
case 'F4B':
|
|
496
|
-
return {ext: 'f4b', mime: 'audio/mp4'};
|
|
497
|
-
case 'crx':
|
|
498
|
-
return {ext: 'cr3', mime: 'image/x-canon-cr3'};
|
|
499
|
-
default:
|
|
500
|
-
if (brandMajor.startsWith('3g')) {
|
|
501
|
-
if (brandMajor.startsWith('3g2')) {
|
|
502
|
-
return {ext: '3g2', mime: 'video/3gpp2'};
|
|
285
|
+
// Assumes signed `.xpi` from addons.mozilla.org
|
|
286
|
+
if (zipHeader.filename === 'META-INF/mozilla.rsa') {
|
|
287
|
+
return {
|
|
288
|
+
ext: 'xpi',
|
|
289
|
+
mime: 'application/x-xpinstall',
|
|
290
|
+
};
|
|
503
291
|
}
|
|
504
292
|
|
|
505
|
-
|
|
506
|
-
|
|
293
|
+
if (zipHeader.filename.endsWith('.rels') || zipHeader.filename.endsWith('.xml')) {
|
|
294
|
+
const type = zipHeader.filename.split('/')[0];
|
|
295
|
+
switch (type) {
|
|
296
|
+
case '_rels':
|
|
297
|
+
break;
|
|
298
|
+
case 'word':
|
|
299
|
+
return {
|
|
300
|
+
ext: 'docx',
|
|
301
|
+
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
302
|
+
};
|
|
303
|
+
case 'ppt':
|
|
304
|
+
return {
|
|
305
|
+
ext: 'pptx',
|
|
306
|
+
mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
307
|
+
};
|
|
308
|
+
case 'xl':
|
|
309
|
+
return {
|
|
310
|
+
ext: 'xlsx',
|
|
311
|
+
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
312
|
+
};
|
|
313
|
+
default:
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
507
317
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
318
|
+
if (zipHeader.filename.startsWith('xl/')) {
|
|
319
|
+
return {
|
|
320
|
+
ext: 'xlsx',
|
|
321
|
+
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
511
324
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
325
|
+
if (zipHeader.filename.startsWith('3D/') && zipHeader.filename.endsWith('.model')) {
|
|
326
|
+
return {
|
|
327
|
+
ext: '3mf',
|
|
328
|
+
mime: 'model/3mf',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
518
331
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
332
|
+
// The docx, xlsx and pptx file types extend the Office Open XML file format:
|
|
333
|
+
// https://en.wikipedia.org/wiki/Office_Open_XML_file_formats
|
|
334
|
+
// We look for:
|
|
335
|
+
// - one entry named '[Content_Types].xml' or '_rels/.rels',
|
|
336
|
+
// - one entry indicating specific type of file.
|
|
337
|
+
// MS Office, OpenOffice and LibreOffice may put the parts in different order, so the check should not rely on it.
|
|
338
|
+
if (zipHeader.filename === 'mimetype' && zipHeader.compressedSize === zipHeader.uncompressedSize) {
|
|
339
|
+
const mimeType = await tokenizer.readToken(new Token.StringType(zipHeader.compressedSize, 'utf-8'));
|
|
340
|
+
|
|
341
|
+
switch (mimeType) {
|
|
342
|
+
case 'application/epub+zip':
|
|
343
|
+
return {
|
|
344
|
+
ext: 'epub',
|
|
345
|
+
mime: 'application/epub+zip',
|
|
346
|
+
};
|
|
347
|
+
case 'application/vnd.oasis.opendocument.text':
|
|
348
|
+
return {
|
|
349
|
+
ext: 'odt',
|
|
350
|
+
mime: 'application/vnd.oasis.opendocument.text',
|
|
351
|
+
};
|
|
352
|
+
case 'application/vnd.oasis.opendocument.spreadsheet':
|
|
353
|
+
return {
|
|
354
|
+
ext: 'ods',
|
|
355
|
+
mime: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
356
|
+
};
|
|
357
|
+
case 'application/vnd.oasis.opendocument.presentation':
|
|
358
|
+
return {
|
|
359
|
+
ext: 'odp',
|
|
360
|
+
mime: 'application/vnd.oasis.opendocument.presentation',
|
|
361
|
+
};
|
|
362
|
+
default:
|
|
363
|
+
}
|
|
364
|
+
}
|
|
531
365
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
check([0x00, 0x01, 0x00, 0x00], {offset: 4}) ||
|
|
536
|
-
checkString('OTTO', {offset: 4})
|
|
537
|
-
)
|
|
538
|
-
) {
|
|
539
|
-
return {
|
|
540
|
-
ext: 'woff2',
|
|
541
|
-
mime: 'font/woff2'
|
|
542
|
-
};
|
|
543
|
-
}
|
|
366
|
+
// Try to find next header manually when current one is corrupted
|
|
367
|
+
if (zipHeader.compressedSize === 0) {
|
|
368
|
+
let nextHeaderIndex = -1;
|
|
544
369
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
ext: 'pcap',
|
|
548
|
-
mime: 'application/vnd.tcpdump.pcap'
|
|
549
|
-
};
|
|
550
|
-
}
|
|
370
|
+
while (nextHeaderIndex < 0 && (tokenizer.position < tokenizer.fileInfo.size)) {
|
|
371
|
+
await tokenizer.peekBuffer(this.buffer, {mayBeLess: true});
|
|
551
372
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
373
|
+
nextHeaderIndex = this.buffer.indexOf('504B0304', 0, 'hex');
|
|
374
|
+
// Move position to the next header if found, skip the whole buffer otherwise
|
|
375
|
+
await tokenizer.ignore(nextHeaderIndex >= 0 ? nextHeaderIndex : this.buffer.length);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
await tokenizer.ignore(zipHeader.compressedSize);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (!(error instanceof strtok3.EndOfStreamError)) {
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
559
386
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
}
|
|
387
|
+
return {
|
|
388
|
+
ext: 'zip',
|
|
389
|
+
mime: 'application/zip',
|
|
390
|
+
};
|
|
391
|
+
}
|
|
566
392
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
393
|
+
if (this.checkString('OggS')) {
|
|
394
|
+
// This is an OGG container
|
|
395
|
+
await tokenizer.ignore(28);
|
|
396
|
+
const type = Buffer.alloc(8);
|
|
397
|
+
await tokenizer.readBuffer(type);
|
|
573
398
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
399
|
+
// Needs to be before `ogg` check
|
|
400
|
+
if (_check(type, [0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64])) {
|
|
401
|
+
return {
|
|
402
|
+
ext: 'opus',
|
|
403
|
+
mime: 'audio/opus',
|
|
404
|
+
};
|
|
405
|
+
}
|
|
580
406
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
407
|
+
// If ' theora' in header.
|
|
408
|
+
if (_check(type, [0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61])) {
|
|
409
|
+
return {
|
|
410
|
+
ext: 'ogv',
|
|
411
|
+
mime: 'video/ogg',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
587
414
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
415
|
+
// If '\x01video' in header.
|
|
416
|
+
if (_check(type, [0x01, 0x76, 0x69, 0x64, 0x65, 0x6F, 0x00])) {
|
|
417
|
+
return {
|
|
418
|
+
ext: 'ogm',
|
|
419
|
+
mime: 'video/ogg',
|
|
420
|
+
};
|
|
421
|
+
}
|
|
593
422
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
423
|
+
// If ' FLAC' in header https://xiph.org/flac/faq.html
|
|
424
|
+
if (_check(type, [0x7F, 0x46, 0x4C, 0x41, 0x43])) {
|
|
425
|
+
return {
|
|
426
|
+
ext: 'oga',
|
|
427
|
+
mime: 'audio/ogg',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
601
430
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
431
|
+
// 'Speex ' in header https://en.wikipedia.org/wiki/Speex
|
|
432
|
+
if (_check(type, [0x53, 0x70, 0x65, 0x65, 0x78, 0x20, 0x20])) {
|
|
433
|
+
return {
|
|
434
|
+
ext: 'spx',
|
|
435
|
+
mime: 'audio/ogg',
|
|
436
|
+
};
|
|
437
|
+
}
|
|
608
438
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
439
|
+
// If '\x01vorbis' in header
|
|
440
|
+
if (_check(type, [0x01, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73])) {
|
|
441
|
+
return {
|
|
442
|
+
ext: 'ogg',
|
|
443
|
+
mime: 'audio/ogg',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
615
446
|
|
|
616
|
-
|
|
617
|
-
if (check([0x49, 0x49, 0x2A, 0x0])) {
|
|
618
|
-
if (checkString('CR', {offset: 8})) {
|
|
447
|
+
// Default OGG container https://www.iana.org/assignments/media-types/application/ogg
|
|
619
448
|
return {
|
|
620
|
-
ext: '
|
|
621
|
-
mime: '
|
|
449
|
+
ext: 'ogx',
|
|
450
|
+
mime: 'application/ogg',
|
|
622
451
|
};
|
|
623
452
|
}
|
|
624
453
|
|
|
625
|
-
if (
|
|
454
|
+
if (
|
|
455
|
+
this.check([0x50, 0x4B])
|
|
456
|
+
&& (this.buffer[2] === 0x3 || this.buffer[2] === 0x5 || this.buffer[2] === 0x7)
|
|
457
|
+
&& (this.buffer[3] === 0x4 || this.buffer[3] === 0x6 || this.buffer[3] === 0x8)
|
|
458
|
+
) {
|
|
626
459
|
return {
|
|
627
|
-
ext: '
|
|
628
|
-
mime: '
|
|
460
|
+
ext: 'zip',
|
|
461
|
+
mime: 'application/zip',
|
|
629
462
|
};
|
|
630
463
|
}
|
|
631
464
|
|
|
465
|
+
//
|
|
466
|
+
|
|
467
|
+
// File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format)
|
|
468
|
+
// It's not required to be first, but it's recommended to be. Almost all ISO base media files start with `ftyp` box.
|
|
469
|
+
// `ftyp` box must contain a brand major identifier, which must consist of ISO 8859-1 printable characters.
|
|
470
|
+
// Here we check for 8859-1 printable characters (for simplicity, it's a mask which also catches one non-printable character).
|
|
632
471
|
if (
|
|
633
|
-
|
|
634
|
-
(
|
|
635
|
-
check([0x27, 0x00, 0xFE, 0x00], {offset: 8}))
|
|
472
|
+
this.checkString('ftyp', {offset: 4})
|
|
473
|
+
&& (this.buffer[8] & 0x60) !== 0x00 // Brand major, first character ASCII?
|
|
636
474
|
) {
|
|
475
|
+
// They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect.
|
|
476
|
+
// For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension.
|
|
477
|
+
const brandMajor = this.buffer.toString('binary', 8, 12).replace('\0', ' ').trim();
|
|
478
|
+
switch (brandMajor) {
|
|
479
|
+
case 'avif':
|
|
480
|
+
return {ext: 'avif', mime: 'image/avif'};
|
|
481
|
+
case 'mif1':
|
|
482
|
+
return {ext: 'heic', mime: 'image/heif'};
|
|
483
|
+
case 'msf1':
|
|
484
|
+
return {ext: 'heic', mime: 'image/heif-sequence'};
|
|
485
|
+
case 'heic':
|
|
486
|
+
case 'heix':
|
|
487
|
+
return {ext: 'heic', mime: 'image/heic'};
|
|
488
|
+
case 'hevc':
|
|
489
|
+
case 'hevx':
|
|
490
|
+
return {ext: 'heic', mime: 'image/heic-sequence'};
|
|
491
|
+
case 'qt':
|
|
492
|
+
return {ext: 'mov', mime: 'video/quicktime'};
|
|
493
|
+
case 'M4V':
|
|
494
|
+
case 'M4VH':
|
|
495
|
+
case 'M4VP':
|
|
496
|
+
return {ext: 'm4v', mime: 'video/x-m4v'};
|
|
497
|
+
case 'M4P':
|
|
498
|
+
return {ext: 'm4p', mime: 'video/mp4'};
|
|
499
|
+
case 'M4B':
|
|
500
|
+
return {ext: 'm4b', mime: 'audio/mp4'};
|
|
501
|
+
case 'M4A':
|
|
502
|
+
return {ext: 'm4a', mime: 'audio/x-m4a'};
|
|
503
|
+
case 'F4V':
|
|
504
|
+
return {ext: 'f4v', mime: 'video/mp4'};
|
|
505
|
+
case 'F4P':
|
|
506
|
+
return {ext: 'f4p', mime: 'video/mp4'};
|
|
507
|
+
case 'F4A':
|
|
508
|
+
return {ext: 'f4a', mime: 'audio/mp4'};
|
|
509
|
+
case 'F4B':
|
|
510
|
+
return {ext: 'f4b', mime: 'audio/mp4'};
|
|
511
|
+
case 'crx':
|
|
512
|
+
return {ext: 'cr3', mime: 'image/x-canon-cr3'};
|
|
513
|
+
default:
|
|
514
|
+
if (brandMajor.startsWith('3g')) {
|
|
515
|
+
if (brandMajor.startsWith('3g2')) {
|
|
516
|
+
return {ext: '3g2', mime: 'video/3gpp2'};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {ext: '3gp', mime: 'video/3gpp'};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {ext: 'mp4', mime: 'video/mp4'};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (this.checkString('MThd')) {
|
|
637
527
|
return {
|
|
638
|
-
ext: '
|
|
639
|
-
mime: '
|
|
528
|
+
ext: 'mid',
|
|
529
|
+
mime: 'audio/midi',
|
|
640
530
|
};
|
|
641
531
|
}
|
|
642
532
|
|
|
643
|
-
buffer = Buffer.alloc(24);
|
|
644
|
-
await tokenizer.peekBuffer(buffer);
|
|
645
533
|
if (
|
|
646
|
-
(
|
|
647
|
-
|
|
648
|
-
|
|
534
|
+
this.checkString('wOFF')
|
|
535
|
+
&& (
|
|
536
|
+
this.check([0x00, 0x01, 0x00, 0x00], {offset: 4})
|
|
537
|
+
|| this.checkString('OTTO', {offset: 4})
|
|
538
|
+
)
|
|
649
539
|
) {
|
|
650
540
|
return {
|
|
651
|
-
ext: '
|
|
652
|
-
mime: '
|
|
541
|
+
ext: 'woff',
|
|
542
|
+
mime: 'font/woff',
|
|
653
543
|
};
|
|
654
544
|
}
|
|
655
545
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (checkString('MAC ')) {
|
|
671
|
-
return {
|
|
672
|
-
ext: 'ape',
|
|
673
|
-
mime: 'audio/ape'
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// https://github.com/threatstack/libmagic/blob/master/magic/Magdir/matroska
|
|
678
|
-
if (check([0x1A, 0x45, 0xDF, 0xA3])) { // Root element: EBML
|
|
679
|
-
async function readField() {
|
|
680
|
-
const msb = await tokenizer.peekNumber(Token.UINT8);
|
|
681
|
-
let mask = 0x80;
|
|
682
|
-
let ic = 0; // 0 = A, 1 = B, 2 = C, 3 = D
|
|
546
|
+
if (
|
|
547
|
+
this.checkString('wOF2')
|
|
548
|
+
&& (
|
|
549
|
+
this.check([0x00, 0x01, 0x00, 0x00], {offset: 4})
|
|
550
|
+
|| this.checkString('OTTO', {offset: 4})
|
|
551
|
+
)
|
|
552
|
+
) {
|
|
553
|
+
return {
|
|
554
|
+
ext: 'woff2',
|
|
555
|
+
mime: 'font/woff2',
|
|
556
|
+
};
|
|
557
|
+
}
|
|
683
558
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
559
|
+
if (this.check([0xD4, 0xC3, 0xB2, 0xA1]) || this.check([0xA1, 0xB2, 0xC3, 0xD4])) {
|
|
560
|
+
return {
|
|
561
|
+
ext: 'pcap',
|
|
562
|
+
mime: 'application/vnd.tcpdump.pcap',
|
|
563
|
+
};
|
|
564
|
+
}
|
|
688
565
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return
|
|
566
|
+
// Sony DSD Stream File (DSF)
|
|
567
|
+
if (this.checkString('DSD ')) {
|
|
568
|
+
return {
|
|
569
|
+
ext: 'dsf',
|
|
570
|
+
mime: 'audio/x-dsf', // Non-standard
|
|
571
|
+
};
|
|
692
572
|
}
|
|
693
573
|
|
|
694
|
-
|
|
695
|
-
const id = await readField();
|
|
696
|
-
const lenField = await readField();
|
|
697
|
-
lenField[0] ^= 0x80 >> (lenField.length - 1);
|
|
698
|
-
const nrLen = Math.min(6, lenField.length); // JavaScript can max read 6 bytes integer
|
|
574
|
+
if (this.checkString('LZIP')) {
|
|
699
575
|
return {
|
|
700
|
-
|
|
701
|
-
|
|
576
|
+
ext: 'lz',
|
|
577
|
+
mime: 'application/x-lzip',
|
|
702
578
|
};
|
|
703
579
|
}
|
|
704
580
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
581
|
+
if (this.checkString('fLaC')) {
|
|
582
|
+
return {
|
|
583
|
+
ext: 'flac',
|
|
584
|
+
mime: 'audio/x-flac',
|
|
585
|
+
};
|
|
586
|
+
}
|
|
711
587
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
588
|
+
if (this.check([0x42, 0x50, 0x47, 0xFB])) {
|
|
589
|
+
return {
|
|
590
|
+
ext: 'bpg',
|
|
591
|
+
mime: 'image/bpg',
|
|
592
|
+
};
|
|
715
593
|
}
|
|
716
594
|
|
|
717
|
-
|
|
718
|
-
|
|
595
|
+
if (this.checkString('wvpk')) {
|
|
596
|
+
return {
|
|
597
|
+
ext: 'wv',
|
|
598
|
+
mime: 'audio/wavpack',
|
|
599
|
+
};
|
|
600
|
+
}
|
|
719
601
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
};
|
|
602
|
+
if (this.checkString('%PDF')) {
|
|
603
|
+
await tokenizer.ignore(1350);
|
|
604
|
+
const maxBufferSize = 10 * 1024 * 1024;
|
|
605
|
+
const buffer = Buffer.alloc(Math.min(maxBufferSize, tokenizer.fileInfo.size));
|
|
606
|
+
await tokenizer.readBuffer(buffer, {mayBeLess: true});
|
|
726
607
|
|
|
727
|
-
|
|
608
|
+
// Check if this is an Adobe Illustrator file
|
|
609
|
+
if (buffer.includes(Buffer.from('AIPrivateData'))) {
|
|
728
610
|
return {
|
|
729
|
-
ext: '
|
|
730
|
-
mime: '
|
|
611
|
+
ext: 'ai',
|
|
612
|
+
mime: 'application/postscript',
|
|
731
613
|
};
|
|
614
|
+
}
|
|
732
615
|
|
|
733
|
-
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// RIFF file format which might be AVI, WAV, QCP, etc
|
|
739
|
-
if (check([0x52, 0x49, 0x46, 0x46])) {
|
|
740
|
-
if (check([0x41, 0x56, 0x49], {offset: 8})) {
|
|
616
|
+
// Assume this is just a normal PDF
|
|
741
617
|
return {
|
|
742
|
-
ext: '
|
|
743
|
-
mime: '
|
|
618
|
+
ext: 'pdf',
|
|
619
|
+
mime: 'application/pdf',
|
|
744
620
|
};
|
|
745
621
|
}
|
|
746
622
|
|
|
747
|
-
if (check([
|
|
623
|
+
if (this.check([0x00, 0x61, 0x73, 0x6D])) {
|
|
748
624
|
return {
|
|
749
|
-
ext: '
|
|
750
|
-
mime: '
|
|
625
|
+
ext: 'wasm',
|
|
626
|
+
mime: 'application/wasm',
|
|
751
627
|
};
|
|
752
628
|
}
|
|
753
629
|
|
|
754
|
-
//
|
|
755
|
-
if (check([
|
|
630
|
+
// TIFF, little-endian type
|
|
631
|
+
if (this.check([0x49, 0x49])) {
|
|
632
|
+
const fileType = await this.readTiffHeader(false);
|
|
633
|
+
if (fileType) {
|
|
634
|
+
return fileType;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// TIFF, big-endian type
|
|
639
|
+
if (this.check([0x4D, 0x4D])) {
|
|
640
|
+
const fileType = await this.readTiffHeader(true);
|
|
641
|
+
if (fileType) {
|
|
642
|
+
return fileType;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (this.checkString('MAC ')) {
|
|
756
647
|
return {
|
|
757
|
-
ext: '
|
|
758
|
-
mime: 'audio/
|
|
648
|
+
ext: 'ape',
|
|
649
|
+
mime: 'audio/ape',
|
|
759
650
|
};
|
|
760
651
|
}
|
|
761
|
-
}
|
|
762
652
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
653
|
+
// https://github.com/threatstack/libmagic/blob/master/magic/Magdir/matroska
|
|
654
|
+
if (this.check([0x1A, 0x45, 0xDF, 0xA3])) { // Root element: EBML
|
|
655
|
+
async function readField() {
|
|
656
|
+
const msb = await tokenizer.peekNumber(Token.UINT8);
|
|
657
|
+
let mask = 0x80;
|
|
658
|
+
let ic = 0; // 0 = A, 1 = B, 2 = C, 3
|
|
659
|
+
// = D
|
|
660
|
+
|
|
661
|
+
while ((msb & mask) === 0) {
|
|
662
|
+
++ic;
|
|
663
|
+
mask >>= 1;
|
|
664
|
+
}
|
|
769
665
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
};
|
|
775
|
-
}
|
|
666
|
+
const id = Buffer.alloc(ic + 1);
|
|
667
|
+
await tokenizer.readBuffer(id);
|
|
668
|
+
return id;
|
|
669
|
+
}
|
|
776
670
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
671
|
+
async function readElement() {
|
|
672
|
+
const id = await readField();
|
|
673
|
+
const lengthField = await readField();
|
|
674
|
+
lengthField[0] ^= 0x80 >> (lengthField.length - 1);
|
|
675
|
+
const nrLength = Math.min(6, lengthField.length); // JavaScript can max read 6 bytes integer
|
|
676
|
+
return {
|
|
677
|
+
id: id.readUIntBE(0, id.length),
|
|
678
|
+
len: lengthField.readUIntBE(lengthField.length - nrLength, nrLength),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
783
681
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
};
|
|
792
|
-
}
|
|
682
|
+
async function readChildren(level, children) {
|
|
683
|
+
while (children > 0) {
|
|
684
|
+
const element = await readElement();
|
|
685
|
+
if (element.id === 0x42_82) {
|
|
686
|
+
const rawValue = await tokenizer.readToken(new Token.StringType(element.len, 'utf-8'));
|
|
687
|
+
return rawValue.replace(/\00.*$/g, ''); // Return DocType
|
|
688
|
+
}
|
|
793
689
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
};
|
|
799
|
-
}
|
|
690
|
+
await tokenizer.ignore(element.len); // ignore payload
|
|
691
|
+
--children;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
800
694
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
ext: 'eps',
|
|
804
|
-
mime: 'application/eps'
|
|
805
|
-
};
|
|
806
|
-
}
|
|
695
|
+
const re = await readElement();
|
|
696
|
+
const docType = await readChildren(1, re.len);
|
|
807
697
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
698
|
+
switch (docType) {
|
|
699
|
+
case 'webm':
|
|
700
|
+
return {
|
|
701
|
+
ext: 'webm',
|
|
702
|
+
mime: 'video/webm',
|
|
703
|
+
};
|
|
814
704
|
|
|
815
|
-
|
|
705
|
+
case 'matroska':
|
|
706
|
+
return {
|
|
707
|
+
ext: 'mkv',
|
|
708
|
+
mime: 'video/x-matroska',
|
|
709
|
+
};
|
|
816
710
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
};
|
|
822
|
-
}
|
|
711
|
+
default:
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
823
715
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
716
|
+
// RIFF file format which might be AVI, WAV, QCP, etc
|
|
717
|
+
if (this.check([0x52, 0x49, 0x46, 0x46])) {
|
|
718
|
+
if (this.check([0x41, 0x56, 0x49], {offset: 8})) {
|
|
719
|
+
return {
|
|
720
|
+
ext: 'avi',
|
|
721
|
+
mime: 'video/vnd.avi',
|
|
722
|
+
};
|
|
723
|
+
}
|
|
830
724
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
725
|
+
if (this.check([0x57, 0x41, 0x56, 0x45], {offset: 8})) {
|
|
726
|
+
return {
|
|
727
|
+
ext: 'wav',
|
|
728
|
+
mime: 'audio/vnd.wave',
|
|
729
|
+
};
|
|
730
|
+
}
|
|
837
731
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
732
|
+
// QLCM, QCP file
|
|
733
|
+
if (this.check([0x51, 0x4C, 0x43, 0x4D], {offset: 8})) {
|
|
734
|
+
return {
|
|
735
|
+
ext: 'qcp',
|
|
736
|
+
mime: 'audio/qcelp',
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
844
740
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
741
|
+
if (this.checkString('SQLi')) {
|
|
742
|
+
return {
|
|
743
|
+
ext: 'sqlite',
|
|
744
|
+
mime: 'application/x-sqlite3',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
851
747
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
checkString('-lh5-', {offset: 2}) ||
|
|
859
|
-
checkString('-lh6-', {offset: 2}) ||
|
|
860
|
-
checkString('-lh7-', {offset: 2}) ||
|
|
861
|
-
checkString('-lzs-', {offset: 2}) ||
|
|
862
|
-
checkString('-lz4-', {offset: 2}) ||
|
|
863
|
-
checkString('-lz5-', {offset: 2}) ||
|
|
864
|
-
checkString('-lhd-', {offset: 2})
|
|
865
|
-
) {
|
|
866
|
-
return {
|
|
867
|
-
ext: 'lzh',
|
|
868
|
-
mime: 'application/x-lzh-compressed'
|
|
869
|
-
};
|
|
870
|
-
}
|
|
748
|
+
if (this.check([0x4E, 0x45, 0x53, 0x1A])) {
|
|
749
|
+
return {
|
|
750
|
+
ext: 'nes',
|
|
751
|
+
mime: 'application/x-nintendo-nes-rom',
|
|
752
|
+
};
|
|
753
|
+
}
|
|
871
754
|
|
|
872
|
-
|
|
873
|
-
if (check([0x00, 0x00, 0x01, 0xBA])) {
|
|
874
|
-
// MPEG-PS, MPEG-1 Part 1
|
|
875
|
-
if (check([0x21], {offset: 4, mask: [0xF1]})) {
|
|
755
|
+
if (this.checkString('Cr24')) {
|
|
876
756
|
return {
|
|
877
|
-
ext: '
|
|
878
|
-
mime: '
|
|
757
|
+
ext: 'crx',
|
|
758
|
+
mime: 'application/x-google-chrome-extension',
|
|
879
759
|
};
|
|
880
760
|
}
|
|
881
761
|
|
|
882
|
-
|
|
883
|
-
|
|
762
|
+
if (
|
|
763
|
+
this.checkString('MSCF')
|
|
764
|
+
|| this.checkString('ISc(')
|
|
765
|
+
) {
|
|
884
766
|
return {
|
|
885
|
-
ext: '
|
|
886
|
-
mime: '
|
|
767
|
+
ext: 'cab',
|
|
768
|
+
mime: 'application/vnd.ms-cab-compressed',
|
|
887
769
|
};
|
|
888
770
|
}
|
|
889
|
-
}
|
|
890
771
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
772
|
+
if (this.check([0xED, 0xAB, 0xEE, 0xDB])) {
|
|
773
|
+
return {
|
|
774
|
+
ext: 'rpm',
|
|
775
|
+
mime: 'application/x-rpm',
|
|
776
|
+
};
|
|
777
|
+
}
|
|
897
778
|
|
|
898
|
-
|
|
779
|
+
if (this.check([0xC5, 0xD0, 0xD3, 0xC6])) {
|
|
780
|
+
return {
|
|
781
|
+
ext: 'eps',
|
|
782
|
+
mime: 'application/eps',
|
|
783
|
+
};
|
|
784
|
+
}
|
|
899
785
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
786
|
+
if (this.check([0x28, 0xB5, 0x2F, 0xFD])) {
|
|
787
|
+
return {
|
|
788
|
+
ext: 'zst',
|
|
789
|
+
mime: 'application/zstd',
|
|
790
|
+
};
|
|
791
|
+
}
|
|
906
792
|
|
|
907
|
-
|
|
908
|
-
return {
|
|
909
|
-
ext: 'xml',
|
|
910
|
-
mime: 'application/xml'
|
|
911
|
-
};
|
|
912
|
-
}
|
|
793
|
+
// -- 5-byte signatures --
|
|
913
794
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
795
|
+
if (this.check([0x4F, 0x54, 0x54, 0x4F, 0x00])) {
|
|
796
|
+
return {
|
|
797
|
+
ext: 'otf',
|
|
798
|
+
mime: 'font/otf',
|
|
799
|
+
};
|
|
800
|
+
}
|
|
920
801
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
mime: 'application/x-rar-compressed'
|
|
928
|
-
};
|
|
929
|
-
}
|
|
802
|
+
if (this.checkString('#!AMR')) {
|
|
803
|
+
return {
|
|
804
|
+
ext: 'amr',
|
|
805
|
+
mime: 'audio/amr',
|
|
806
|
+
};
|
|
807
|
+
}
|
|
930
808
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
809
|
+
if (this.checkString('{\\rtf')) {
|
|
810
|
+
return {
|
|
811
|
+
ext: 'rtf',
|
|
812
|
+
mime: 'application/rtf',
|
|
813
|
+
};
|
|
814
|
+
}
|
|
937
815
|
|
|
938
|
-
|
|
816
|
+
if (this.check([0x46, 0x4C, 0x56, 0x01])) {
|
|
817
|
+
return {
|
|
818
|
+
ext: 'flv',
|
|
819
|
+
mime: 'video/x-flv',
|
|
820
|
+
};
|
|
821
|
+
}
|
|
939
822
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
823
|
+
if (this.checkString('IMPM')) {
|
|
824
|
+
return {
|
|
825
|
+
ext: 'it',
|
|
826
|
+
mime: 'audio/x-it',
|
|
827
|
+
};
|
|
828
|
+
}
|
|
946
829
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
830
|
+
if (
|
|
831
|
+
this.checkString('-lh0-', {offset: 2})
|
|
832
|
+
|| this.checkString('-lh1-', {offset: 2})
|
|
833
|
+
|| this.checkString('-lh2-', {offset: 2})
|
|
834
|
+
|| this.checkString('-lh3-', {offset: 2})
|
|
835
|
+
|| this.checkString('-lh4-', {offset: 2})
|
|
836
|
+
|| this.checkString('-lh5-', {offset: 2})
|
|
837
|
+
|| this.checkString('-lh6-', {offset: 2})
|
|
838
|
+
|| this.checkString('-lh7-', {offset: 2})
|
|
839
|
+
|| this.checkString('-lzs-', {offset: 2})
|
|
840
|
+
|| this.checkString('-lz4-', {offset: 2})
|
|
841
|
+
|| this.checkString('-lz5-', {offset: 2})
|
|
842
|
+
|| this.checkString('-lhd-', {offset: 2})
|
|
843
|
+
) {
|
|
951
844
|
return {
|
|
952
|
-
ext: '
|
|
953
|
-
mime: 'application/x-
|
|
845
|
+
ext: 'lzh',
|
|
846
|
+
mime: 'application/x-lzh-compressed',
|
|
954
847
|
};
|
|
955
848
|
}
|
|
956
849
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
850
|
+
// MPEG program stream (PS or MPEG-PS)
|
|
851
|
+
if (this.check([0x00, 0x00, 0x01, 0xBA])) {
|
|
852
|
+
// MPEG-PS, MPEG-1 Part 1
|
|
853
|
+
if (this.check([0x21], {offset: 4, mask: [0xF1]})) {
|
|
854
|
+
return {
|
|
855
|
+
ext: 'mpg', // May also be .ps, .mpeg
|
|
856
|
+
mime: 'video/MP1S',
|
|
857
|
+
};
|
|
858
|
+
}
|
|
962
859
|
|
|
963
|
-
|
|
860
|
+
// MPEG-PS, MPEG-2 Part 1
|
|
861
|
+
if (this.check([0x44], {offset: 4, mask: [0xC4]})) {
|
|
862
|
+
return {
|
|
863
|
+
ext: 'mpg', // May also be .mpg, .m2p, .vob or .sub
|
|
864
|
+
mime: 'video/MP2P',
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
}
|
|
964
868
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
869
|
+
if (this.checkString('ITSF')) {
|
|
870
|
+
return {
|
|
871
|
+
ext: 'chm',
|
|
872
|
+
mime: 'application/vnd.ms-htmlhelp',
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// -- 6-byte signatures --
|
|
877
|
+
|
|
878
|
+
if (this.check([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])) {
|
|
879
|
+
return {
|
|
880
|
+
ext: 'xz',
|
|
881
|
+
mime: 'application/x-xz',
|
|
882
|
+
};
|
|
883
|
+
}
|
|
969
884
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
885
|
+
if (this.checkString('<?xml ')) {
|
|
886
|
+
return {
|
|
887
|
+
ext: 'xml',
|
|
888
|
+
mime: 'application/xml',
|
|
889
|
+
};
|
|
890
|
+
}
|
|
973
891
|
|
|
974
|
-
|
|
892
|
+
if (this.check([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])) {
|
|
893
|
+
return {
|
|
894
|
+
ext: '7z',
|
|
895
|
+
mime: 'application/x-7z-compressed',
|
|
896
|
+
};
|
|
897
|
+
}
|
|
975
898
|
|
|
976
|
-
|
|
899
|
+
if (
|
|
900
|
+
this.check([0x52, 0x61, 0x72, 0x21, 0x1A, 0x7])
|
|
901
|
+
&& (this.buffer[6] === 0x0 || this.buffer[6] === 0x1)
|
|
902
|
+
) {
|
|
977
903
|
return {
|
|
978
|
-
|
|
979
|
-
|
|
904
|
+
ext: 'rar',
|
|
905
|
+
mime: 'application/x-rar-compressed',
|
|
980
906
|
};
|
|
981
907
|
}
|
|
982
908
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
909
|
+
if (this.checkString('solid ')) {
|
|
910
|
+
return {
|
|
911
|
+
ext: 'stl',
|
|
912
|
+
mime: 'model/stl',
|
|
913
|
+
};
|
|
914
|
+
}
|
|
988
915
|
|
|
989
|
-
|
|
990
|
-
case 'IDAT':
|
|
991
|
-
return {
|
|
992
|
-
ext: 'png',
|
|
993
|
-
mime: 'image/png'
|
|
994
|
-
};
|
|
995
|
-
case 'acTL':
|
|
996
|
-
return {
|
|
997
|
-
ext: 'apng',
|
|
998
|
-
mime: 'image/apng'
|
|
999
|
-
};
|
|
1000
|
-
default:
|
|
1001
|
-
await tokenizer.ignore(chunk.length + 4); // Ignore chunk-data + CRC
|
|
1002
|
-
}
|
|
1003
|
-
} while (tokenizer.position + 8 < tokenizer.fileInfo.size);
|
|
916
|
+
// -- 7-byte signatures --
|
|
1004
917
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
918
|
+
if (this.checkString('BLENDER')) {
|
|
919
|
+
return {
|
|
920
|
+
ext: 'blend',
|
|
921
|
+
mime: 'application/x-blender',
|
|
922
|
+
};
|
|
923
|
+
}
|
|
1010
924
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
925
|
+
if (this.checkString('!<arch>')) {
|
|
926
|
+
await tokenizer.ignore(8);
|
|
927
|
+
const string = await tokenizer.readToken(new Token.StringType(13, 'ascii'));
|
|
928
|
+
if (string === 'debian-binary') {
|
|
929
|
+
return {
|
|
930
|
+
ext: 'deb',
|
|
931
|
+
mime: 'application/x-deb',
|
|
932
|
+
};
|
|
933
|
+
}
|
|
1017
934
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
935
|
+
return {
|
|
936
|
+
ext: 'ar',
|
|
937
|
+
mime: 'application/x-unix-archive',
|
|
938
|
+
};
|
|
939
|
+
}
|
|
1024
940
|
|
|
1025
|
-
|
|
1026
|
-
if (
|
|
1027
|
-
check([0x66, 0x72, 0x65, 0x65], {offset: 4}) || // `free`
|
|
1028
|
-
check([0x6D, 0x64, 0x61, 0x74], {offset: 4}) || // `mdat` MJPEG
|
|
1029
|
-
check([0x6D, 0x6F, 0x6F, 0x76], {offset: 4}) || // `moov`
|
|
1030
|
-
check([0x77, 0x69, 0x64, 0x65], {offset: 4}) // `wide`
|
|
1031
|
-
) {
|
|
1032
|
-
return {
|
|
1033
|
-
ext: 'mov',
|
|
1034
|
-
mime: 'video/quicktime'
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
941
|
+
// -- 8-byte signatures --
|
|
1037
942
|
|
|
1038
|
-
|
|
943
|
+
if (this.check([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) {
|
|
944
|
+
// APNG format (https://wiki.mozilla.org/APNG_Specification)
|
|
945
|
+
// 1. Find the first IDAT (image data) chunk (49 44 41 54)
|
|
946
|
+
// 2. Check if there is an "acTL" chunk before the IDAT one (61 63 54 4C)
|
|
1039
947
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
mime: 'image/x-olympus-orf'
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
948
|
+
// Offset calculated as follows:
|
|
949
|
+
// - 8 bytes: PNG signature
|
|
950
|
+
// - 4 (length) + 4 (chunk type) + 13 (chunk data) + 4 (CRC): IHDR chunk
|
|
1046
951
|
|
|
1047
|
-
|
|
1048
|
-
return {
|
|
1049
|
-
ext: 'xcf',
|
|
1050
|
-
mime: 'image/x-xcf'
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
952
|
+
await tokenizer.ignore(8); // ignore PNG signature
|
|
1053
953
|
|
|
1054
|
-
|
|
954
|
+
async function readChunkHeader() {
|
|
955
|
+
return {
|
|
956
|
+
length: await tokenizer.readToken(Token.INT32_BE),
|
|
957
|
+
type: await tokenizer.readToken(new Token.StringType(4, 'binary')),
|
|
958
|
+
};
|
|
959
|
+
}
|
|
1055
960
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
961
|
+
do {
|
|
962
|
+
const chunk = await readChunkHeader();
|
|
963
|
+
if (chunk.length < 0) {
|
|
964
|
+
return; // Invalid chunk length
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
switch (chunk.type) {
|
|
968
|
+
case 'IDAT':
|
|
969
|
+
return {
|
|
970
|
+
ext: 'png',
|
|
971
|
+
mime: 'image/png',
|
|
972
|
+
};
|
|
973
|
+
case 'acTL':
|
|
974
|
+
return {
|
|
975
|
+
ext: 'apng',
|
|
976
|
+
mime: 'image/apng',
|
|
977
|
+
};
|
|
978
|
+
default:
|
|
979
|
+
await tokenizer.ignore(chunk.length + 4); // Ignore chunk-data + CRC
|
|
980
|
+
}
|
|
981
|
+
} while (tokenizer.position + 8 < tokenizer.fileInfo.size);
|
|
1062
982
|
|
|
1063
|
-
// ASF_Header_Object first 80 bytes
|
|
1064
|
-
if (check([0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9])) {
|
|
1065
|
-
async function readHeader() {
|
|
1066
|
-
const guid = Buffer.alloc(16);
|
|
1067
|
-
await tokenizer.readBuffer(guid);
|
|
1068
983
|
return {
|
|
1069
|
-
|
|
1070
|
-
|
|
984
|
+
ext: 'png',
|
|
985
|
+
mime: 'image/png',
|
|
1071
986
|
};
|
|
1072
987
|
}
|
|
1073
988
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
// Sync on Stream-Properties-Object (B7DC0791-A9B7-11CF-8EE6-00C00C205365)
|
|
1081
|
-
const typeId = Buffer.alloc(16);
|
|
1082
|
-
payload -= await tokenizer.readBuffer(typeId);
|
|
1083
|
-
|
|
1084
|
-
if (_check(typeId, [0x40, 0x9E, 0x69, 0xF8, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B])) {
|
|
1085
|
-
// Found audio:
|
|
1086
|
-
return {
|
|
1087
|
-
ext: 'asf',
|
|
1088
|
-
mime: 'audio/x-ms-asf'
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
989
|
+
if (this.check([0x41, 0x52, 0x52, 0x4F, 0x57, 0x31, 0x00, 0x00])) {
|
|
990
|
+
return {
|
|
991
|
+
ext: 'arrow',
|
|
992
|
+
mime: 'application/x-apache-arrow',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
1091
995
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
996
|
+
if (this.check([0x67, 0x6C, 0x54, 0x46, 0x02, 0x00, 0x00, 0x00])) {
|
|
997
|
+
return {
|
|
998
|
+
ext: 'glb',
|
|
999
|
+
mime: 'model/gltf-binary',
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1099
1002
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1003
|
+
// `mov` format variants
|
|
1004
|
+
if (
|
|
1005
|
+
this.check([0x66, 0x72, 0x65, 0x65], {offset: 4}) // `free`
|
|
1006
|
+
|| this.check([0x6D, 0x64, 0x61, 0x74], {offset: 4}) // `mdat` MJPEG
|
|
1007
|
+
|| this.check([0x6D, 0x6F, 0x6F, 0x76], {offset: 4}) // `moov`
|
|
1008
|
+
|| this.check([0x77, 0x69, 0x64, 0x65], {offset: 4}) // `wide`
|
|
1009
|
+
) {
|
|
1010
|
+
return {
|
|
1011
|
+
ext: 'mov',
|
|
1012
|
+
mime: 'video/quicktime',
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1102
1015
|
|
|
1103
|
-
|
|
1016
|
+
if (this.check([0xEF, 0xBB, 0xBF]) && this.checkString('<?xml', {offset: 3})) { // UTF-8-BOM
|
|
1017
|
+
return {
|
|
1018
|
+
ext: 'xml',
|
|
1019
|
+
mime: 'application/xml',
|
|
1020
|
+
};
|
|
1104
1021
|
}
|
|
1105
1022
|
|
|
1106
|
-
//
|
|
1107
|
-
return {
|
|
1108
|
-
ext: 'asf',
|
|
1109
|
-
mime: 'application/vnd.ms-asf'
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1023
|
+
// -- 9-byte signatures --
|
|
1112
1024
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1025
|
+
if (this.check([0x49, 0x49, 0x52, 0x4F, 0x08, 0x00, 0x00, 0x00, 0x18])) {
|
|
1026
|
+
return {
|
|
1027
|
+
ext: 'orf',
|
|
1028
|
+
mime: 'image/x-olympus-orf',
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1119
1031
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1032
|
+
if (this.checkString('gimp xcf ')) {
|
|
1033
|
+
return {
|
|
1034
|
+
ext: 'xcf',
|
|
1035
|
+
mime: 'image/x-xcf',
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1126
1038
|
|
|
1127
|
-
|
|
1128
|
-
return {
|
|
1129
|
-
ext: 'shp',
|
|
1130
|
-
mime: 'application/x-esri-shape'
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1039
|
+
// -- 12-byte signatures --
|
|
1133
1040
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1041
|
+
if (this.check([0x49, 0x49, 0x55, 0x00, 0x18, 0x00, 0x00, 0x00, 0x88, 0xE7, 0x74, 0xD8])) {
|
|
1042
|
+
return {
|
|
1043
|
+
ext: 'rw2',
|
|
1044
|
+
mime: 'image/x-panasonic-rw2',
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1136
1047
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
ext: 'jp2',
|
|
1143
|
-
mime: 'image/jp2'
|
|
1144
|
-
};
|
|
1145
|
-
case 'jpx ':
|
|
1048
|
+
// ASF_Header_Object first 80 bytes
|
|
1049
|
+
if (this.check([0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9])) {
|
|
1050
|
+
async function readHeader() {
|
|
1051
|
+
const guid = Buffer.alloc(16);
|
|
1052
|
+
await tokenizer.readBuffer(guid);
|
|
1146
1053
|
return {
|
|
1147
|
-
|
|
1148
|
-
|
|
1054
|
+
id: guid,
|
|
1055
|
+
size: Number(await tokenizer.readToken(Token.UINT64_LE)),
|
|
1149
1056
|
};
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
await tokenizer.ignore(30);
|
|
1060
|
+
// Search for header should be in first 1KB of file.
|
|
1061
|
+
while (tokenizer.position + 24 < tokenizer.fileInfo.size) {
|
|
1062
|
+
const header = await readHeader();
|
|
1063
|
+
let payload = header.size - 24;
|
|
1064
|
+
if (_check(header.id, [0x91, 0x07, 0xDC, 0xB7, 0xB7, 0xA9, 0xCF, 0x11, 0x8E, 0xE6, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65])) {
|
|
1065
|
+
// Sync on Stream-Properties-Object (B7DC0791-A9B7-11CF-8EE6-00C00C205365)
|
|
1066
|
+
const typeId = Buffer.alloc(16);
|
|
1067
|
+
payload -= await tokenizer.readBuffer(typeId);
|
|
1068
|
+
|
|
1069
|
+
if (_check(typeId, [0x40, 0x9E, 0x69, 0xF8, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B])) {
|
|
1070
|
+
// Found audio:
|
|
1071
|
+
return {
|
|
1072
|
+
ext: 'asf',
|
|
1073
|
+
mime: 'audio/x-ms-asf',
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (_check(typeId, [0xC0, 0xEF, 0x19, 0xBC, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B])) {
|
|
1078
|
+
// Found video:
|
|
1079
|
+
return {
|
|
1080
|
+
ext: 'asf',
|
|
1081
|
+
mime: 'video/x-ms-asf',
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
await tokenizer.ignore(payload);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Default to ASF generic extension
|
|
1092
|
+
return {
|
|
1093
|
+
ext: 'asf',
|
|
1094
|
+
mime: 'application/vnd.ms-asf',
|
|
1095
|
+
};
|
|
1162
1096
|
}
|
|
1163
|
-
}
|
|
1164
1097
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
mime: 'image/jxl'
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1098
|
+
if (this.check([0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A])) {
|
|
1099
|
+
return {
|
|
1100
|
+
ext: 'ktx',
|
|
1101
|
+
mime: 'image/ktx',
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1174
1104
|
|
|
1175
|
-
|
|
1105
|
+
if ((this.check([0x7E, 0x10, 0x04]) || this.check([0x7E, 0x18, 0x04])) && this.check([0x30, 0x4D, 0x49, 0x45], {offset: 4})) {
|
|
1106
|
+
return {
|
|
1107
|
+
ext: 'mie',
|
|
1108
|
+
mime: 'application/x-mie',
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1176
1111
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
mime: 'video/mpeg'
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1112
|
+
if (this.check([0x27, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], {offset: 2})) {
|
|
1113
|
+
return {
|
|
1114
|
+
ext: 'shp',
|
|
1115
|
+
mime: 'application/x-esri-shape',
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1186
1118
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
ext: 'ttf',
|
|
1190
|
-
mime: 'font/ttf'
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1119
|
+
if (this.check([0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A])) {
|
|
1120
|
+
// JPEG-2000 family
|
|
1193
1121
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1122
|
+
await tokenizer.ignore(20);
|
|
1123
|
+
const type = await tokenizer.readToken(new Token.StringType(4, 'ascii'));
|
|
1124
|
+
switch (type) {
|
|
1125
|
+
case 'jp2 ':
|
|
1126
|
+
return {
|
|
1127
|
+
ext: 'jp2',
|
|
1128
|
+
mime: 'image/jp2',
|
|
1129
|
+
};
|
|
1130
|
+
case 'jpx ':
|
|
1131
|
+
return {
|
|
1132
|
+
ext: 'jpx',
|
|
1133
|
+
mime: 'image/jpx',
|
|
1134
|
+
};
|
|
1135
|
+
case 'jpm ':
|
|
1136
|
+
return {
|
|
1137
|
+
ext: 'jpm',
|
|
1138
|
+
mime: 'image/jpm',
|
|
1139
|
+
};
|
|
1140
|
+
case 'mjp2':
|
|
1141
|
+
return {
|
|
1142
|
+
ext: 'mj2',
|
|
1143
|
+
mime: 'image/mj2',
|
|
1144
|
+
};
|
|
1145
|
+
default:
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1200
1149
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1150
|
+
if (
|
|
1151
|
+
this.check([0xFF, 0x0A])
|
|
1152
|
+
|| this.check([0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A])
|
|
1153
|
+
) {
|
|
1154
|
+
return {
|
|
1155
|
+
ext: 'jxl',
|
|
1156
|
+
mime: 'image/jxl',
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1207
1159
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1160
|
+
if (
|
|
1161
|
+
this.check([0xFE, 0xFF, 0, 60, 0, 63, 0, 120, 0, 109, 0, 108]) // UTF-16-BOM-LE
|
|
1162
|
+
|| this.check([0xFF, 0xFE, 60, 0, 63, 0, 120, 0, 109, 0, 108, 0]) // UTF-16-BOM-LE
|
|
1163
|
+
) {
|
|
1164
|
+
return {
|
|
1165
|
+
ext: 'xml',
|
|
1166
|
+
mime: 'application/xml',
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1215
1169
|
|
|
1216
|
-
|
|
1217
|
-
await tokenizer.peekBuffer(buffer, {length: Math.min(256, tokenizer.fileInfo.size), mayBeLess: true});
|
|
1170
|
+
// -- Unsafe signatures --
|
|
1218
1171
|
|
|
1219
|
-
|
|
1172
|
+
if (
|
|
1173
|
+
this.check([0x0, 0x0, 0x1, 0xBA])
|
|
1174
|
+
|| this.check([0x0, 0x0, 0x1, 0xB3])
|
|
1175
|
+
) {
|
|
1176
|
+
return {
|
|
1177
|
+
ext: 'mpg',
|
|
1178
|
+
mime: 'video/mpeg',
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1220
1181
|
|
|
1221
|
-
|
|
1222
|
-
if (checkString('VCARD', {offset: 6})) {
|
|
1182
|
+
if (this.check([0x00, 0x01, 0x00, 0x00, 0x00])) {
|
|
1223
1183
|
return {
|
|
1224
|
-
ext: '
|
|
1225
|
-
mime: '
|
|
1184
|
+
ext: 'ttf',
|
|
1185
|
+
mime: 'font/ttf',
|
|
1226
1186
|
};
|
|
1227
1187
|
}
|
|
1228
1188
|
|
|
1229
|
-
if (
|
|
1189
|
+
if (this.check([0x00, 0x00, 0x01, 0x00])) {
|
|
1230
1190
|
return {
|
|
1231
|
-
ext: '
|
|
1232
|
-
mime: '
|
|
1191
|
+
ext: 'ico',
|
|
1192
|
+
mime: 'image/x-icon',
|
|
1233
1193
|
};
|
|
1234
1194
|
}
|
|
1235
|
-
}
|
|
1236
1195
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1196
|
+
if (this.check([0x00, 0x00, 0x02, 0x00])) {
|
|
1197
|
+
return {
|
|
1198
|
+
ext: 'cur',
|
|
1199
|
+
mime: 'image/x-icon',
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1244
1202
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1203
|
+
if (this.check([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])) {
|
|
1204
|
+
// Detected Microsoft Compound File Binary File (MS-CFB) Format.
|
|
1205
|
+
return {
|
|
1206
|
+
ext: 'cfb',
|
|
1207
|
+
mime: 'application/x-cfb',
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1251
1210
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
ext: 'voc',
|
|
1255
|
-
mime: 'audio/x-voc'
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1211
|
+
// Increase sample size from 12 to 256.
|
|
1212
|
+
await tokenizer.peekBuffer(this.buffer, {length: Math.min(256, tokenizer.fileInfo.size), mayBeLess: true});
|
|
1258
1213
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
return {
|
|
1268
|
-
ext: 'asar',
|
|
1269
|
-
mime: 'application/x-asar'
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
} catch (_) {
|
|
1214
|
+
// -- 15-byte signatures --
|
|
1215
|
+
|
|
1216
|
+
if (this.checkString('BEGIN:')) {
|
|
1217
|
+
if (this.checkString('VCARD', {offset: 6})) {
|
|
1218
|
+
return {
|
|
1219
|
+
ext: 'vcf',
|
|
1220
|
+
mime: 'text/vcard',
|
|
1221
|
+
};
|
|
1273
1222
|
}
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
1223
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1224
|
+
if (this.checkString('VCALENDAR', {offset: 6})) {
|
|
1225
|
+
return {
|
|
1226
|
+
ext: 'ics',
|
|
1227
|
+
mime: 'text/calendar',
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1283
1231
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1232
|
+
// `raf` is here just to keep all the raw image detectors together.
|
|
1233
|
+
if (this.checkString('FUJIFILMCCD-RAW')) {
|
|
1234
|
+
return {
|
|
1235
|
+
ext: 'raf',
|
|
1236
|
+
mime: 'image/x-fujifilm-raf',
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1290
1239
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1240
|
+
if (this.checkString('Extended Module:')) {
|
|
1241
|
+
return {
|
|
1242
|
+
ext: 'xm',
|
|
1243
|
+
mime: 'audio/x-xm',
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1297
1246
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1247
|
+
if (this.checkString('Creative Voice File')) {
|
|
1248
|
+
return {
|
|
1249
|
+
ext: 'voc',
|
|
1250
|
+
mime: 'audio/x-voc',
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1304
1253
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1254
|
+
if (this.check([0x04, 0x00, 0x00, 0x00]) && this.buffer.length >= 16) { // Rough & quick check Pickle/ASAR
|
|
1255
|
+
const jsonSize = this.buffer.readUInt32LE(12);
|
|
1256
|
+
if (jsonSize > 12 && this.buffer.length >= jsonSize + 16) {
|
|
1257
|
+
try {
|
|
1258
|
+
const header = this.buffer.slice(16, jsonSize + 16).toString();
|
|
1259
|
+
const json = JSON.parse(header);
|
|
1260
|
+
// Check if Pickle is ASAR
|
|
1261
|
+
if (json.files) { // Final check, assuring Pickle/ASAR format
|
|
1262
|
+
return {
|
|
1263
|
+
ext: 'asar',
|
|
1264
|
+
mime: 'application/x-asar',
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
} catch {}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1311
1270
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1271
|
+
if (this.check([0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02])) {
|
|
1272
|
+
return {
|
|
1273
|
+
ext: 'mxf',
|
|
1274
|
+
mime: 'application/mxf',
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1318
1277
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1278
|
+
if (this.checkString('SCRM', {offset: 44})) {
|
|
1279
|
+
return {
|
|
1280
|
+
ext: 's3m',
|
|
1281
|
+
mime: 'audio/x-s3m',
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1325
1284
|
|
|
1326
|
-
|
|
1327
|
-
check([
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
) {
|
|
1334
|
-
return {
|
|
1335
|
-
ext: 'eot',
|
|
1336
|
-
mime: 'application/vnd.ms-fontobject'
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1285
|
+
// Raw MPEG-2 transport stream (188-byte packets)
|
|
1286
|
+
if (this.check([0x47]) && this.check([0x47], {offset: 188})) {
|
|
1287
|
+
return {
|
|
1288
|
+
ext: 'mts',
|
|
1289
|
+
mime: 'video/mp2t',
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1339
1292
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1293
|
+
// Blu-ray Disc Audio-Video (BDAV) MPEG-2 transport stream has 4-byte TP_extra_header before each 188-byte packet
|
|
1294
|
+
if (this.check([0x47], {offset: 4}) && this.check([0x47], {offset: 196})) {
|
|
1295
|
+
return {
|
|
1296
|
+
ext: 'mts',
|
|
1297
|
+
mime: 'video/mp2t',
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1346
1300
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1301
|
+
if (this.check([0x42, 0x4F, 0x4F, 0x4B, 0x4D, 0x4F, 0x42, 0x49], {offset: 60})) {
|
|
1302
|
+
return {
|
|
1303
|
+
ext: 'mobi',
|
|
1304
|
+
mime: 'application/x-mobipocket-ebook',
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1349
1307
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1308
|
+
if (this.check([0x44, 0x49, 0x43, 0x4D], {offset: 128})) {
|
|
1309
|
+
return {
|
|
1310
|
+
ext: 'dcm',
|
|
1311
|
+
mime: 'application/dicom',
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1357
1314
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1315
|
+
if (this.check([0x4C, 0x00, 0x00, 0x00, 0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46])) {
|
|
1316
|
+
return {
|
|
1317
|
+
ext: 'lnk',
|
|
1318
|
+
mime: 'application/x.ms.shortcut', // Invented by us
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1364
1321
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1322
|
+
if (this.check([0x62, 0x6F, 0x6F, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x6D, 0x61, 0x72, 0x6B, 0x00, 0x00, 0x00, 0x00])) {
|
|
1323
|
+
return {
|
|
1324
|
+
ext: 'alias',
|
|
1325
|
+
mime: 'application/x.apple.alias', // Invented by us
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1371
1328
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1329
|
+
if (
|
|
1330
|
+
this.check([0x4C, 0x50], {offset: 34})
|
|
1331
|
+
&& (
|
|
1332
|
+
this.check([0x00, 0x00, 0x01], {offset: 8})
|
|
1333
|
+
|| this.check([0x01, 0x00, 0x02], {offset: 8})
|
|
1334
|
+
|| this.check([0x02, 0x00, 0x02], {offset: 8})
|
|
1335
|
+
)
|
|
1336
|
+
) {
|
|
1337
|
+
return {
|
|
1338
|
+
ext: 'eot',
|
|
1339
|
+
mime: 'application/vnd.ms-fontobject',
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1382
1342
|
|
|
1383
|
-
|
|
1343
|
+
if (this.check([0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5, 0xBD, 0x31, 0xEF, 0xE7, 0xFE, 0x74, 0xB7, 0x1D])) {
|
|
1384
1344
|
return {
|
|
1385
|
-
ext: '
|
|
1386
|
-
mime: '
|
|
1345
|
+
ext: 'indd',
|
|
1346
|
+
mime: 'application/x-indesign',
|
|
1387
1347
|
};
|
|
1388
1348
|
}
|
|
1389
1349
|
|
|
1390
|
-
//
|
|
1391
|
-
|
|
1392
|
-
|
|
1350
|
+
// Increase sample size from 256 to 512
|
|
1351
|
+
await tokenizer.peekBuffer(this.buffer, {length: Math.min(512, tokenizer.fileInfo.size), mayBeLess: true});
|
|
1352
|
+
|
|
1353
|
+
// Requires a buffer size of 512 bytes
|
|
1354
|
+
if (tarHeaderChecksumMatches(this.buffer)) {
|
|
1393
1355
|
return {
|
|
1394
|
-
ext: '
|
|
1395
|
-
mime: '
|
|
1356
|
+
ext: 'tar',
|
|
1357
|
+
mime: 'application/x-tar',
|
|
1396
1358
|
};
|
|
1397
1359
|
}
|
|
1398
1360
|
|
|
1399
|
-
|
|
1400
|
-
if (check([0x04], {offset: 1, mask: [0x06]})) {
|
|
1361
|
+
if (this.check([0xFF, 0xFE, 0xFF, 0x0E, 0x53, 0x00, 0x6B, 0x00, 0x65, 0x00, 0x74, 0x00, 0x63, 0x00, 0x68, 0x00, 0x55, 0x00, 0x70, 0x00, 0x20, 0x00, 0x4D, 0x00, 0x6F, 0x00, 0x64, 0x00, 0x65, 0x00, 0x6C, 0x00])) {
|
|
1401
1362
|
return {
|
|
1402
|
-
ext: '
|
|
1403
|
-
mime: '
|
|
1363
|
+
ext: 'skp',
|
|
1364
|
+
mime: 'application/vnd.sketchup.skp',
|
|
1404
1365
|
};
|
|
1405
1366
|
}
|
|
1406
1367
|
|
|
1407
|
-
|
|
1408
|
-
if (check([0x06], {offset: 1, mask: [0x06]})) {
|
|
1368
|
+
if (this.checkString('-----BEGIN PGP MESSAGE-----')) {
|
|
1409
1369
|
return {
|
|
1410
|
-
ext: '
|
|
1411
|
-
mime: '
|
|
1370
|
+
ext: 'pgp',
|
|
1371
|
+
mime: 'application/pgp-encrypted',
|
|
1412
1372
|
};
|
|
1413
1373
|
}
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
1374
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1375
|
+
// Check MPEG 1 or 2 Layer 3 header, or 'layer 0' for ADTS (MPEG sync-word 0xFFE)
|
|
1376
|
+
if (this.buffer.length >= 2 && this.check([0xFF, 0xE0], {offset: 0, mask: [0xFF, 0xE0]})) {
|
|
1377
|
+
if (this.check([0x10], {offset: 1, mask: [0x16]})) {
|
|
1378
|
+
// Check for (ADTS) MPEG-2
|
|
1379
|
+
if (this.check([0x08], {offset: 1, mask: [0x08]})) {
|
|
1380
|
+
return {
|
|
1381
|
+
ext: 'aac',
|
|
1382
|
+
mime: 'audio/aac',
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1420
1385
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1386
|
+
// Must be (ADTS) MPEG-4
|
|
1387
|
+
return {
|
|
1388
|
+
ext: 'aac',
|
|
1389
|
+
mime: 'audio/aac',
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// MPEG 1 or 2 Layer 3 header
|
|
1394
|
+
// Check for MPEG layer 3
|
|
1395
|
+
if (this.check([0x02], {offset: 1, mask: [0x06]})) {
|
|
1396
|
+
return {
|
|
1397
|
+
ext: 'mp3',
|
|
1398
|
+
mime: 'audio/mpeg',
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Check for MPEG layer 2
|
|
1403
|
+
if (this.check([0x04], {offset: 1, mask: [0x06]})) {
|
|
1404
|
+
return {
|
|
1405
|
+
ext: 'mp2',
|
|
1406
|
+
mime: 'audio/mpeg',
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Check for MPEG layer 1
|
|
1411
|
+
if (this.check([0x06], {offset: 1, mask: [0x06]})) {
|
|
1412
|
+
return {
|
|
1413
|
+
ext: 'mp1',
|
|
1414
|
+
mime: 'audio/mpeg',
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1431
1417
|
}
|
|
1418
|
+
}
|
|
1432
1419
|
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1420
|
+
async readTiffTag(bigEndian) {
|
|
1421
|
+
const tagId = await this.tokenizer.readToken(bigEndian ? Token.UINT16_BE : Token.UINT16_LE);
|
|
1422
|
+
this.tokenizer.ignore(10);
|
|
1423
|
+
switch (tagId) {
|
|
1424
|
+
case 50_341:
|
|
1425
|
+
return {
|
|
1426
|
+
ext: 'arw',
|
|
1427
|
+
mime: 'image/x-sony-arw',
|
|
1428
|
+
};
|
|
1429
|
+
case 50_706:
|
|
1430
|
+
return {
|
|
1431
|
+
ext: 'dng',
|
|
1432
|
+
mime: 'image/x-adobe-dng',
|
|
1433
|
+
};
|
|
1434
|
+
default:
|
|
1440
1435
|
}
|
|
1436
|
+
}
|
|
1441
1437
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
stream
|
|
1451
|
-
};
|
|
1452
|
-
|
|
1453
|
-
Object.defineProperty(fileType, 'extensions', {
|
|
1454
|
-
get() {
|
|
1455
|
-
return new Set(supported.extensions);
|
|
1438
|
+
async readTiffIFD(bigEndian) {
|
|
1439
|
+
const numberOfTags = await this.tokenizer.readToken(bigEndian ? Token.UINT16_BE : Token.UINT16_LE);
|
|
1440
|
+
for (let n = 0; n < numberOfTags; ++n) {
|
|
1441
|
+
const fileType = await this.readTiffTag(bigEndian);
|
|
1442
|
+
if (fileType) {
|
|
1443
|
+
return fileType;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1456
1446
|
}
|
|
1457
|
-
});
|
|
1458
1447
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1448
|
+
async readTiffHeader(bigEndian) {
|
|
1449
|
+
const version = (bigEndian ? Token.UINT16_BE : Token.UINT16_LE).get(this.buffer, 2);
|
|
1450
|
+
const ifdOffset = (bigEndian ? Token.UINT32_BE : Token.UINT32_LE).get(this.buffer, 4);
|
|
1451
|
+
|
|
1452
|
+
if (version === 42) {
|
|
1453
|
+
// TIFF file header
|
|
1454
|
+
if (ifdOffset >= 6) {
|
|
1455
|
+
if (this.checkString('CR', {offset: 8})) {
|
|
1456
|
+
return {
|
|
1457
|
+
ext: 'cr2',
|
|
1458
|
+
mime: 'image/x-canon-cr2',
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (ifdOffset >= 8 && (this.check([0x1C, 0x00, 0xFE, 0x00], {offset: 8}) || this.check([0x1F, 0x00, 0x0B, 0x00], {offset: 8}))) {
|
|
1463
|
+
return {
|
|
1464
|
+
ext: 'nef',
|
|
1465
|
+
mime: 'image/x-nikon-nef',
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
await this.tokenizer.ignore(ifdOffset);
|
|
1471
|
+
const fileType = await this.readTiffIFD(false);
|
|
1472
|
+
return fileType ? fileType : {
|
|
1473
|
+
ext: 'tif',
|
|
1474
|
+
mime: 'image/tiff',
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (version === 43) { // Big TIFF file header
|
|
1479
|
+
return {
|
|
1480
|
+
ext: 'tif',
|
|
1481
|
+
mime: 'image/tiff',
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1462
1484
|
}
|
|
1463
|
-
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
export async function fileTypeStream(readableStream, {sampleSize = minimumBytes} = {}) {
|
|
1488
|
+
// eslint-disable-next-line node/no-unsupported-features/es-syntax
|
|
1489
|
+
const {default: stream} = await import('node:stream');
|
|
1490
|
+
|
|
1491
|
+
return new Promise((resolve, reject) => {
|
|
1492
|
+
readableStream.on('error', reject);
|
|
1493
|
+
|
|
1494
|
+
readableStream.once('readable', () => {
|
|
1495
|
+
(async () => {
|
|
1496
|
+
try {
|
|
1497
|
+
// Set up output stream
|
|
1498
|
+
const pass = new stream.PassThrough();
|
|
1499
|
+
const outputStream = stream.pipeline ? stream.pipeline(readableStream, pass, () => {}) : readableStream.pipe(pass);
|
|
1500
|
+
|
|
1501
|
+
// Read the input stream and detect the filetype
|
|
1502
|
+
const chunk = readableStream.read(sampleSize) || readableStream.read() || Buffer.alloc(0);
|
|
1503
|
+
try {
|
|
1504
|
+
const fileType = await fileTypeFromBuffer(chunk);
|
|
1505
|
+
pass.fileType = fileType;
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
if (error instanceof strtok3.EndOfStreamError) {
|
|
1508
|
+
pass.fileType = undefined;
|
|
1509
|
+
} else {
|
|
1510
|
+
reject(error);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
resolve(outputStream);
|
|
1515
|
+
} catch (error) {
|
|
1516
|
+
reject(error);
|
|
1517
|
+
}
|
|
1518
|
+
})();
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1464
1522
|
|
|
1465
|
-
|
|
1523
|
+
export const supportedExtensions = new Set(extensions);
|
|
1524
|
+
export const supportedMimeTypes = new Set(mimeTypes);
|