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/core.js CHANGED
@@ -1,43 +1,42 @@
1
- 'use strict';
2
- const Token = require('token-types');
3
- const strtok3 = require('strtok3/lib/core');
4
- const {
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
- } = require('./util');
9
- const supported = require('./supported');
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 fromStream(stream) {
13
+ export async function fileTypeFromStream(stream) {
14
14
  const tokenizer = await strtok3.fromStream(stream);
15
15
  try {
16
- return await fromTokenizer(tokenizer);
16
+ return await fileTypeFromTokenizer(tokenizer);
17
17
  } finally {
18
18
  await tokenizer.close();
19
19
  }
20
20
  }
21
21
 
22
- async function fromBuffer(input) {
23
- if (!(input instanceof Uint8Array || input instanceof ArrayBuffer || Buffer.isBuffer(input))) {
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 Buffer ? input : Buffer.from(input);
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
- const tokenizer = strtok3.fromBuffer(buffer);
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 fromTokenizer(tokenizer) {
57
+ export async function fileTypeFromTokenizer(tokenizer) {
59
58
  try {
60
- return _fromTokenizer(tokenizer);
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
- async function _fromTokenizer(tokenizer) {
69
- let buffer = Buffer.alloc(minimumBytes);
70
- const bytesRead = 12;
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
- await tokenizer.peekBuffer(buffer, {length: bytesRead, mayBeLess: true});
72
+ checkString(header, options) {
73
+ return this.check(stringToBytes(header), options);
74
+ }
80
75
 
81
- // -- 2-byte signatures --
76
+ async parse(tokenizer) {
77
+ this.buffer = Buffer.alloc(minimumBytes);
82
78
 
83
- if (check([0x42, 0x4D])) {
84
- return {
85
- ext: 'bmp',
86
- mime: 'image/bmp'
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
- if (check([0x0B, 0x77])) {
91
- return {
92
- ext: 'ac3',
93
- mime: 'audio/vnd.dolby.dd-raw'
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
- if (check([0x78, 0x01])) {
98
- return {
99
- ext: 'dmg',
100
- mime: 'application/x-apple-diskimage'
101
- };
102
- }
89
+ this.tokenizer = tokenizer;
103
90
 
104
- if (check([0x4D, 0x5A])) {
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
- if (check([0x25, 0x21])) {
112
- await tokenizer.peekBuffer(buffer, {length: 24, mayBeLess: true});
93
+ // -- 2-byte signatures --
113
94
 
114
- if (checkString('PS-Adobe-', {offset: 2}) &&
115
- checkString(' EPSF-', {offset: 14})) {
95
+ if (this.check([0x42, 0x4D])) {
116
96
  return {
117
- ext: 'eps',
118
- mime: 'application/eps'
97
+ ext: 'bmp',
98
+ mime: 'image/bmp',
119
99
  };
120
100
  }
121
101
 
122
- return {
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: 'mp3',
175
- mime: 'audio/mpeg'
104
+ ext: 'ac3',
105
+ mime: 'audio/vnd.dolby.dd-raw',
176
106
  };
177
107
  }
178
108
 
179
- await tokenizer.ignore(id3HeaderLen);
180
- return fromTokenizer(tokenizer); // Skip ID3 header, recursion
181
- }
109
+ if (this.check([0x78, 0x01])) {
110
+ return {
111
+ ext: 'dmg',
112
+ mime: 'application/x-apple-diskimage',
113
+ };
114
+ }
182
115
 
183
- // Musepack, SV7
184
- if (checkString('MP+')) {
185
- return {
186
- ext: 'mpc',
187
- mime: 'audio/x-musepack'
188
- };
189
- }
116
+ if (this.check([0x4D, 0x5A])) {
117
+ return {
118
+ ext: 'exe',
119
+ mime: 'application/x-msdownload',
120
+ };
121
+ }
190
122
 
191
- if (
192
- (buffer[0] === 0x43 || buffer[0] === 0x46) &&
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
- // -- 4-byte signatures --
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
- if (check([0x47, 0x49, 0x46])) {
204
- return {
205
- ext: 'gif',
206
- mime: 'image/gif'
207
- };
208
- }
136
+ return {
137
+ ext: 'ps',
138
+ mime: 'application/postscript',
139
+ };
140
+ }
209
141
 
210
- if (checkString('FLIF')) {
211
- return {
212
- ext: 'flif',
213
- mime: 'image/flif'
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
- if (checkString('8BPS')) {
218
- return {
219
- ext: 'psd',
220
- mime: 'image/vnd.adobe.photoshop'
221
- };
222
- }
152
+ // -- 3-byte signatures --
223
153
 
224
- if (checkString('WEBP', {offset: 8})) {
225
- return {
226
- ext: 'webp',
227
- mime: 'image/webp'
228
- };
229
- }
154
+ if (this.check([0xFF, 0xD8, 0xFF])) {
155
+ return {
156
+ ext: 'jpg',
157
+ mime: 'image/jpeg',
158
+ };
159
+ }
230
160
 
231
- // Musepack, SV8
232
- if (checkString('MPCK')) {
233
- return {
234
- ext: 'mpc',
235
- mime: 'audio/x-musepack'
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
- if (checkString('FORM')) {
240
- return {
241
- ext: 'aif',
242
- mime: 'audio/aiff'
243
- };
244
- }
168
+ if (this.check([0x1F, 0x8B, 0x8])) {
169
+ return {
170
+ ext: 'gz',
171
+ mime: 'application/gzip',
172
+ };
173
+ }
245
174
 
246
- if (checkString('icns', {offset: 0})) {
247
- return {
248
- ext: 'icns',
249
- mime: 'image/icns'
250
- };
251
- }
175
+ if (this.check([0x42, 0x5A, 0x68])) {
176
+ return {
177
+ ext: 'bz2',
178
+ mime: 'application/x-bzip2',
179
+ };
180
+ }
252
181
 
253
- // Zip-based file formats
254
- // Need to be before the `zip` check
255
- if (check([0x50, 0x4B, 0x3, 0x4])) { // Local file header signature
256
- try {
257
- while (tokenizer.position + 30 < tokenizer.fileInfo.size) {
258
- await tokenizer.readBuffer(buffer, {length: 30});
259
-
260
- // https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
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
- zipHeader.filename = await tokenizer.readToken(new Token.StringType(zipHeader.filenameLength, 'utf-8'));
269
- await tokenizer.ignore(zipHeader.extraFieldLength);
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
- while (nextHeaderIndex < 0 && (tokenizer.position < tokenizer.fileInfo.size)) {
357
- await tokenizer.peekBuffer(buffer, {mayBeLess: true});
197
+ // Musepack, SV7
198
+ if (this.checkString('MP+')) {
199
+ return {
200
+ ext: 'mpc',
201
+ mime: 'audio/x-musepack',
202
+ };
203
+ }
358
204
 
359
- nextHeaderIndex = buffer.indexOf('504B0304', 0, 'hex');
360
- // Move position to the next header if found, skip the whole buffer otherwise
361
- await tokenizer.ignore(nextHeaderIndex >= 0 ? nextHeaderIndex : buffer.length);
362
- }
363
- } else {
364
- await tokenizer.ignore(zipHeader.compressedSize);
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
- return {
374
- ext: 'zip',
375
- mime: 'application/zip'
376
- };
377
- }
215
+ // -- 4-byte signatures --
378
216
 
379
- if (checkString('OggS')) {
380
- // This is an OGG container
381
- await tokenizer.ignore(28);
382
- const type = Buffer.alloc(8);
383
- await tokenizer.readBuffer(type);
217
+ if (this.check([0x47, 0x49, 0x46])) {
218
+ return {
219
+ ext: 'gif',
220
+ mime: 'image/gif',
221
+ };
222
+ }
384
223
 
385
- // Needs to be before `ogg` check
386
- if (_check(type, [0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64])) {
224
+ if (this.checkString('FLIF')) {
387
225
  return {
388
- ext: 'opus',
389
- mime: 'audio/opus'
226
+ ext: 'flif',
227
+ mime: 'image/flif',
390
228
  };
391
229
  }
392
230
 
393
- // If ' theora' in header.
394
- if (_check(type, [0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61])) {
231
+ if (this.checkString('8BPS')) {
395
232
  return {
396
- ext: 'ogv',
397
- mime: 'video/ogg'
233
+ ext: 'psd',
234
+ mime: 'image/vnd.adobe.photoshop',
398
235
  };
399
236
  }
400
237
 
401
- // If '\x01video' in header.
402
- if (_check(type, [0x01, 0x76, 0x69, 0x64, 0x65, 0x6F, 0x00])) {
238
+ if (this.checkString('WEBP', {offset: 8})) {
403
239
  return {
404
- ext: 'ogm',
405
- mime: 'video/ogg'
240
+ ext: 'webp',
241
+ mime: 'image/webp',
406
242
  };
407
243
  }
408
244
 
409
- // If ' FLAC' in header https://xiph.org/flac/faq.html
410
- if (_check(type, [0x7F, 0x46, 0x4C, 0x41, 0x43])) {
245
+ // Musepack, SV8
246
+ if (this.checkString('MPCK')) {
411
247
  return {
412
- ext: 'oga',
413
- mime: 'audio/ogg'
248
+ ext: 'mpc',
249
+ mime: 'audio/x-musepack',
414
250
  };
415
251
  }
416
252
 
417
- // 'Speex ' in header https://en.wikipedia.org/wiki/Speex
418
- if (_check(type, [0x53, 0x70, 0x65, 0x65, 0x78, 0x20, 0x20])) {
253
+ if (this.checkString('FORM')) {
419
254
  return {
420
- ext: 'spx',
421
- mime: 'audio/ogg'
255
+ ext: 'aif',
256
+ mime: 'audio/aiff',
422
257
  };
423
258
  }
424
259
 
425
- // If '\x01vorbis' in header
426
- if (_check(type, [0x01, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73])) {
260
+ if (this.checkString('icns', {offset: 0})) {
427
261
  return {
428
- ext: 'ogg',
429
- mime: 'audio/ogg'
262
+ ext: 'icns',
263
+ mime: 'image/icns',
430
264
  };
431
265
  }
432
266
 
433
- // Default OGG container https://www.iana.org/assignments/media-types/application/ogg
434
- return {
435
- ext: 'ogx',
436
- mime: 'application/ogg'
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
- if (
441
- check([0x50, 0x4B]) &&
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
- // File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format)
454
- // It's not required to be first, but it's recommended to be. Almost all ISO base media files start with `ftyp` box.
455
- // `ftyp` box must contain a brand major identifier, which must consist of ISO 8859-1 printable characters.
456
- // Here we check for 8859-1 printable characters (for simplicity, it's a mask which also catches one non-printable character).
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
- return {ext: '3gp', mime: 'video/3gpp'};
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
- return {ext: 'mp4', mime: 'video/mp4'};
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
- if (checkString('MThd')) {
513
- return {
514
- ext: 'mid',
515
- mime: 'audio/midi'
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
- if (
520
- checkString('wOFF') &&
521
- (
522
- check([0x00, 0x01, 0x00, 0x00], {offset: 4}) ||
523
- checkString('OTTO', {offset: 4})
524
- )
525
- ) {
526
- return {
527
- ext: 'woff',
528
- mime: 'font/woff'
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
- if (
533
- checkString('wOF2') &&
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
- if (check([0xD4, 0xC3, 0xB2, 0xA1]) || check([0xA1, 0xB2, 0xC3, 0xD4])) {
546
- return {
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
- // Sony DSD Stream File (DSF)
553
- if (checkString('DSD ')) {
554
- return {
555
- ext: 'dsf',
556
- mime: 'audio/x-dsf' // Non-standard
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
- if (checkString('LZIP')) {
561
- return {
562
- ext: 'lz',
563
- mime: 'application/x-lzip'
564
- };
565
- }
387
+ return {
388
+ ext: 'zip',
389
+ mime: 'application/zip',
390
+ };
391
+ }
566
392
 
567
- if (checkString('fLaC')) {
568
- return {
569
- ext: 'flac',
570
- mime: 'audio/x-flac'
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
- if (check([0x42, 0x50, 0x47, 0xFB])) {
575
- return {
576
- ext: 'bpg',
577
- mime: 'image/bpg'
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
- if (checkString('wvpk')) {
582
- return {
583
- ext: 'wv',
584
- mime: 'audio/wavpack'
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
- if (checkString('%PDF')) {
589
- await tokenizer.ignore(1350);
590
- const maxBufferSize = 10 * 1024 * 1024;
591
- const buffer = Buffer.alloc(Math.min(maxBufferSize, tokenizer.fileInfo.size));
592
- await tokenizer.readBuffer(buffer, {mayBeLess: true});
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
- // Check if this is an Adobe Illustrator file
595
- if (buffer.includes(Buffer.from('AIPrivateData'))) {
596
- return {
597
- ext: 'ai',
598
- mime: 'application/postscript'
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
- // Assume this is just a normal PDF
603
- return {
604
- ext: 'pdf',
605
- mime: 'application/pdf'
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
- if (check([0x00, 0x61, 0x73, 0x6D])) {
610
- return {
611
- ext: 'wasm',
612
- mime: 'application/wasm'
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
- // TIFF, little-endian type
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: 'cr2',
621
- mime: 'image/x-canon-cr2'
449
+ ext: 'ogx',
450
+ mime: 'application/ogg',
622
451
  };
623
452
  }
624
453
 
625
- if (check([0x1C, 0x00, 0xFE, 0x00], {offset: 8}) || check([0x1F, 0x00, 0x0B, 0x00], {offset: 8})) {
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: 'nef',
628
- mime: 'image/x-nikon-nef'
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
- check([0x08, 0x00, 0x00, 0x00], {offset: 4}) &&
634
- (check([0x2D, 0x00, 0xFE, 0x00], {offset: 8}) ||
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: 'dng',
639
- mime: 'image/x-adobe-dng'
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
- (check([0x10, 0xFB, 0x86, 0x01], {offset: 4}) || check([0x08, 0x00, 0x00, 0x00], {offset: 4})) &&
647
- // This pattern differentiates ARW from other TIFF-ish file types:
648
- check([0x00, 0xFE, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x01], {offset: 9})
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: 'arw',
652
- mime: 'image/x-sony-arw'
541
+ ext: 'woff',
542
+ mime: 'font/woff',
653
543
  };
654
544
  }
655
545
 
656
- return {
657
- ext: 'tif',
658
- mime: 'image/tiff'
659
- };
660
- }
661
-
662
- // TIFF, big-endian type
663
- if (check([0x4D, 0x4D, 0x0, 0x2A])) {
664
- return {
665
- ext: 'tif',
666
- mime: 'image/tiff'
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
- while ((msb & mask) === 0) {
685
- ++ic;
686
- mask >>= 1;
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
- const id = Buffer.alloc(ic + 1);
690
- await tokenizer.readBuffer(id);
691
- return id;
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
- async function readElement() {
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
- id: id.readUIntBE(0, id.length),
701
- len: lenField.readUIntBE(lenField.length - nrLen, nrLen)
576
+ ext: 'lz',
577
+ mime: 'application/x-lzip',
702
578
  };
703
579
  }
704
580
 
705
- async function readChildren(level, children) {
706
- while (children > 0) {
707
- const e = await readElement();
708
- if (e.id === 0x4282) {
709
- return tokenizer.readToken(new Token.StringType(e.len, 'utf-8')); // Return DocType
710
- }
581
+ if (this.checkString('fLaC')) {
582
+ return {
583
+ ext: 'flac',
584
+ mime: 'audio/x-flac',
585
+ };
586
+ }
711
587
 
712
- await tokenizer.ignore(e.len); // ignore payload
713
- --children;
714
- }
588
+ if (this.check([0x42, 0x50, 0x47, 0xFB])) {
589
+ return {
590
+ ext: 'bpg',
591
+ mime: 'image/bpg',
592
+ };
715
593
  }
716
594
 
717
- const re = await readElement();
718
- const docType = await readChildren(1, re.len);
595
+ if (this.checkString('wvpk')) {
596
+ return {
597
+ ext: 'wv',
598
+ mime: 'audio/wavpack',
599
+ };
600
+ }
719
601
 
720
- switch (docType) {
721
- case 'webm':
722
- return {
723
- ext: 'webm',
724
- mime: 'video/webm'
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
- case 'matroska':
608
+ // Check if this is an Adobe Illustrator file
609
+ if (buffer.includes(Buffer.from('AIPrivateData'))) {
728
610
  return {
729
- ext: 'mkv',
730
- mime: 'video/x-matroska'
611
+ ext: 'ai',
612
+ mime: 'application/postscript',
731
613
  };
614
+ }
732
615
 
733
- default:
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: 'avi',
743
- mime: 'video/vnd.avi'
618
+ ext: 'pdf',
619
+ mime: 'application/pdf',
744
620
  };
745
621
  }
746
622
 
747
- if (check([0x57, 0x41, 0x56, 0x45], {offset: 8})) {
623
+ if (this.check([0x00, 0x61, 0x73, 0x6D])) {
748
624
  return {
749
- ext: 'wav',
750
- mime: 'audio/vnd.wave'
625
+ ext: 'wasm',
626
+ mime: 'application/wasm',
751
627
  };
752
628
  }
753
629
 
754
- // QLCM, QCP file
755
- if (check([0x51, 0x4C, 0x43, 0x4D], {offset: 8})) {
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: 'qcp',
758
- mime: 'audio/qcelp'
648
+ ext: 'ape',
649
+ mime: 'audio/ape',
759
650
  };
760
651
  }
761
- }
762
652
 
763
- if (checkString('SQLi')) {
764
- return {
765
- ext: 'sqlite',
766
- mime: 'application/x-sqlite3'
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
- if (check([0x4E, 0x45, 0x53, 0x1A])) {
771
- return {
772
- ext: 'nes',
773
- mime: 'application/x-nintendo-nes-rom'
774
- };
775
- }
666
+ const id = Buffer.alloc(ic + 1);
667
+ await tokenizer.readBuffer(id);
668
+ return id;
669
+ }
776
670
 
777
- if (checkString('Cr24')) {
778
- return {
779
- ext: 'crx',
780
- mime: 'application/x-google-chrome-extension'
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
- if (
785
- checkString('MSCF') ||
786
- checkString('ISc(')
787
- ) {
788
- return {
789
- ext: 'cab',
790
- mime: 'application/vnd.ms-cab-compressed'
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
- if (check([0xED, 0xAB, 0xEE, 0xDB])) {
795
- return {
796
- ext: 'rpm',
797
- mime: 'application/x-rpm'
798
- };
799
- }
690
+ await tokenizer.ignore(element.len); // ignore payload
691
+ --children;
692
+ }
693
+ }
800
694
 
801
- if (check([0xC5, 0xD0, 0xD3, 0xC6])) {
802
- return {
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
- if (check([0x28, 0xB5, 0x2F, 0xFD])) {
809
- return {
810
- ext: 'zst',
811
- mime: 'application/zstd'
812
- };
813
- }
698
+ switch (docType) {
699
+ case 'webm':
700
+ return {
701
+ ext: 'webm',
702
+ mime: 'video/webm',
703
+ };
814
704
 
815
- // -- 5-byte signatures --
705
+ case 'matroska':
706
+ return {
707
+ ext: 'mkv',
708
+ mime: 'video/x-matroska',
709
+ };
816
710
 
817
- if (check([0x4F, 0x54, 0x54, 0x4F, 0x00])) {
818
- return {
819
- ext: 'otf',
820
- mime: 'font/otf'
821
- };
822
- }
711
+ default:
712
+ return;
713
+ }
714
+ }
823
715
 
824
- if (checkString('#!AMR')) {
825
- return {
826
- ext: 'amr',
827
- mime: 'audio/amr'
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
- if (checkString('{\\rtf')) {
832
- return {
833
- ext: 'rtf',
834
- mime: 'application/rtf'
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
- if (check([0x46, 0x4C, 0x56, 0x01])) {
839
- return {
840
- ext: 'flv',
841
- mime: 'video/x-flv'
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
- if (checkString('IMPM')) {
846
- return {
847
- ext: 'it',
848
- mime: 'audio/x-it'
849
- };
850
- }
741
+ if (this.checkString('SQLi')) {
742
+ return {
743
+ ext: 'sqlite',
744
+ mime: 'application/x-sqlite3',
745
+ };
746
+ }
851
747
 
852
- if (
853
- checkString('-lh0-', {offset: 2}) ||
854
- checkString('-lh1-', {offset: 2}) ||
855
- checkString('-lh2-', {offset: 2}) ||
856
- checkString('-lh3-', {offset: 2}) ||
857
- checkString('-lh4-', {offset: 2}) ||
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
- // MPEG program stream (PS or MPEG-PS)
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: 'mpg', // May also be .ps, .mpeg
878
- mime: 'video/MP1S'
757
+ ext: 'crx',
758
+ mime: 'application/x-google-chrome-extension',
879
759
  };
880
760
  }
881
761
 
882
- // MPEG-PS, MPEG-2 Part 1
883
- if (check([0x44], {offset: 4, mask: [0xC4]})) {
762
+ if (
763
+ this.checkString('MSCF')
764
+ || this.checkString('ISc(')
765
+ ) {
884
766
  return {
885
- ext: 'mpg', // May also be .mpg, .m2p, .vob or .sub
886
- mime: 'video/MP2P'
767
+ ext: 'cab',
768
+ mime: 'application/vnd.ms-cab-compressed',
887
769
  };
888
770
  }
889
- }
890
771
 
891
- if (checkString('ITSF')) {
892
- return {
893
- ext: 'chm',
894
- mime: 'application/vnd.ms-htmlhelp'
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
- // -- 6-byte signatures --
779
+ if (this.check([0xC5, 0xD0, 0xD3, 0xC6])) {
780
+ return {
781
+ ext: 'eps',
782
+ mime: 'application/eps',
783
+ };
784
+ }
899
785
 
900
- if (check([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])) {
901
- return {
902
- ext: 'xz',
903
- mime: 'application/x-xz'
904
- };
905
- }
786
+ if (this.check([0x28, 0xB5, 0x2F, 0xFD])) {
787
+ return {
788
+ ext: 'zst',
789
+ mime: 'application/zstd',
790
+ };
791
+ }
906
792
 
907
- if (checkString('<?xml ')) {
908
- return {
909
- ext: 'xml',
910
- mime: 'application/xml'
911
- };
912
- }
793
+ // -- 5-byte signatures --
913
794
 
914
- if (check([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])) {
915
- return {
916
- ext: '7z',
917
- mime: 'application/x-7z-compressed'
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
- if (
922
- check([0x52, 0x61, 0x72, 0x21, 0x1A, 0x7]) &&
923
- (buffer[6] === 0x0 || buffer[6] === 0x1)
924
- ) {
925
- return {
926
- ext: 'rar',
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
- if (checkString('solid ')) {
932
- return {
933
- ext: 'stl',
934
- mime: 'model/stl'
935
- };
936
- }
809
+ if (this.checkString('{\\rtf')) {
810
+ return {
811
+ ext: 'rtf',
812
+ mime: 'application/rtf',
813
+ };
814
+ }
937
815
 
938
- // -- 7-byte signatures --
816
+ if (this.check([0x46, 0x4C, 0x56, 0x01])) {
817
+ return {
818
+ ext: 'flv',
819
+ mime: 'video/x-flv',
820
+ };
821
+ }
939
822
 
940
- if (checkString('BLENDER')) {
941
- return {
942
- ext: 'blend',
943
- mime: 'application/x-blender'
944
- };
945
- }
823
+ if (this.checkString('IMPM')) {
824
+ return {
825
+ ext: 'it',
826
+ mime: 'audio/x-it',
827
+ };
828
+ }
946
829
 
947
- if (checkString('!<arch>')) {
948
- await tokenizer.ignore(8);
949
- const str = await tokenizer.readToken(new Token.StringType(13, 'ascii'));
950
- if (str === 'debian-binary') {
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: 'deb',
953
- mime: 'application/x-deb'
845
+ ext: 'lzh',
846
+ mime: 'application/x-lzh-compressed',
954
847
  };
955
848
  }
956
849
 
957
- return {
958
- ext: 'ar',
959
- mime: 'application/x-unix-archive'
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
- // -- 8-byte signatures --
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
- if (check([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) {
966
- // APNG format (https://wiki.mozilla.org/APNG_Specification)
967
- // 1. Find the first IDAT (image data) chunk (49 44 41 54)
968
- // 2. Check if there is an "acTL" chunk before the IDAT one (61 63 54 4C)
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
- // Offset calculated as follows:
971
- // - 8 bytes: PNG signature
972
- // - 4 (length) + 4 (chunk type) + 13 (chunk data) + 4 (CRC): IHDR chunk
885
+ if (this.checkString('<?xml ')) {
886
+ return {
887
+ ext: 'xml',
888
+ mime: 'application/xml',
889
+ };
890
+ }
973
891
 
974
- await tokenizer.ignore(8); // ignore PNG signature
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
- async function readChunkHeader() {
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
- length: await tokenizer.readToken(Token.INT32_BE),
979
- type: await tokenizer.readToken(new Token.StringType(4, 'binary'))
904
+ ext: 'rar',
905
+ mime: 'application/x-rar-compressed',
980
906
  };
981
907
  }
982
908
 
983
- do {
984
- const chunk = await readChunkHeader();
985
- if (chunk.length < 0) {
986
- return; // Invalid chunk length
987
- }
909
+ if (this.checkString('solid ')) {
910
+ return {
911
+ ext: 'stl',
912
+ mime: 'model/stl',
913
+ };
914
+ }
988
915
 
989
- switch (chunk.type) {
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
- return {
1006
- ext: 'png',
1007
- mime: 'image/png'
1008
- };
1009
- }
918
+ if (this.checkString('BLENDER')) {
919
+ return {
920
+ ext: 'blend',
921
+ mime: 'application/x-blender',
922
+ };
923
+ }
1010
924
 
1011
- if (check([0x41, 0x52, 0x52, 0x4F, 0x57, 0x31, 0x00, 0x00])) {
1012
- return {
1013
- ext: 'arrow',
1014
- mime: 'application/x-apache-arrow'
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
- if (check([0x67, 0x6C, 0x54, 0x46, 0x02, 0x00, 0x00, 0x00])) {
1019
- return {
1020
- ext: 'glb',
1021
- mime: 'model/gltf-binary'
1022
- };
1023
- }
935
+ return {
936
+ ext: 'ar',
937
+ mime: 'application/x-unix-archive',
938
+ };
939
+ }
1024
940
 
1025
- // `mov` format variants
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
- // -- 9-byte signatures --
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
- if (check([0x49, 0x49, 0x52, 0x4F, 0x08, 0x00, 0x00, 0x00, 0x18])) {
1041
- return {
1042
- ext: 'orf',
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
- if (checkString('gimp xcf ')) {
1048
- return {
1049
- ext: 'xcf',
1050
- mime: 'image/x-xcf'
1051
- };
1052
- }
952
+ await tokenizer.ignore(8); // ignore PNG signature
1053
953
 
1054
- // -- 12-byte signatures --
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
- if (check([0x49, 0x49, 0x55, 0x00, 0x18, 0x00, 0x00, 0x00, 0x88, 0xE7, 0x74, 0xD8])) {
1057
- return {
1058
- ext: 'rw2',
1059
- mime: 'image/x-panasonic-rw2'
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
- id: guid,
1070
- size: Number(await tokenizer.readToken(Token.UINT64_LE))
984
+ ext: 'png',
985
+ mime: 'image/png',
1071
986
  };
1072
987
  }
1073
988
 
1074
- await tokenizer.ignore(30);
1075
- // Search for header should be in first 1KB of file.
1076
- while (tokenizer.position + 24 < tokenizer.fileInfo.size) {
1077
- const header = await readHeader();
1078
- let payload = header.size - 24;
1079
- if (_check(header.id, [0x91, 0x07, 0xDC, 0xB7, 0xB7, 0xA9, 0xCF, 0x11, 0x8E, 0xE6, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65])) {
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
- if (_check(typeId, [0xC0, 0xEF, 0x19, 0xBC, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B])) {
1093
- // Found video:
1094
- return {
1095
- ext: 'asf',
1096
- mime: 'video/x-ms-asf'
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
- break;
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
- await tokenizer.ignore(payload);
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
- // Default to ASF generic extension
1107
- return {
1108
- ext: 'asf',
1109
- mime: 'application/vnd.ms-asf'
1110
- };
1111
- }
1023
+ // -- 9-byte signatures --
1112
1024
 
1113
- if (check([0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A])) {
1114
- return {
1115
- ext: 'ktx',
1116
- mime: 'image/ktx'
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
- if ((check([0x7E, 0x10, 0x04]) || check([0x7E, 0x18, 0x04])) && check([0x30, 0x4D, 0x49, 0x45], {offset: 4})) {
1121
- return {
1122
- ext: 'mie',
1123
- mime: 'application/x-mie'
1124
- };
1125
- }
1032
+ if (this.checkString('gimp xcf ')) {
1033
+ return {
1034
+ ext: 'xcf',
1035
+ mime: 'image/x-xcf',
1036
+ };
1037
+ }
1126
1038
 
1127
- if (check([0x27, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], {offset: 2})) {
1128
- return {
1129
- ext: 'shp',
1130
- mime: 'application/x-esri-shape'
1131
- };
1132
- }
1039
+ // -- 12-byte signatures --
1133
1040
 
1134
- if (check([0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A])) {
1135
- // JPEG-2000 family
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
- await tokenizer.ignore(20);
1138
- const type = await tokenizer.readToken(new Token.StringType(4, 'ascii'));
1139
- switch (type) {
1140
- case 'jp2 ':
1141
- return {
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
- ext: 'jpx',
1148
- mime: 'image/jpx'
1054
+ id: guid,
1055
+ size: Number(await tokenizer.readToken(Token.UINT64_LE)),
1149
1056
  };
1150
- case 'jpm ':
1151
- return {
1152
- ext: 'jpm',
1153
- mime: 'image/jpm'
1154
- };
1155
- case 'mjp2':
1156
- return {
1157
- ext: 'mj2',
1158
- mime: 'image/mj2'
1159
- };
1160
- default:
1161
- return;
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
- if (
1166
- check([0xFF, 0x0A]) ||
1167
- check([0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A])
1168
- ) {
1169
- return {
1170
- ext: 'jxl',
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
- // -- Unsafe signatures --
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
- if (
1178
- check([0x0, 0x0, 0x1, 0xBA]) ||
1179
- check([0x0, 0x0, 0x1, 0xB3])
1180
- ) {
1181
- return {
1182
- ext: 'mpg',
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
- if (check([0x00, 0x01, 0x00, 0x00, 0x00])) {
1188
- return {
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
- if (check([0x00, 0x00, 0x01, 0x00])) {
1195
- return {
1196
- ext: 'ico',
1197
- mime: 'image/x-icon'
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
- if (check([0x00, 0x00, 0x02, 0x00])) {
1202
- return {
1203
- ext: 'cur',
1204
- mime: 'image/x-icon'
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
- if (check([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])) {
1209
- // Detected Microsoft Compound File Binary File (MS-CFB) Format.
1210
- return {
1211
- ext: 'cfb',
1212
- mime: 'application/x-cfb'
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
- // Increase sample size from 12 to 256.
1217
- await tokenizer.peekBuffer(buffer, {length: Math.min(256, tokenizer.fileInfo.size), mayBeLess: true});
1170
+ // -- Unsafe signatures --
1218
1171
 
1219
- // -- 15-byte signatures --
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
- if (checkString('BEGIN:')) {
1222
- if (checkString('VCARD', {offset: 6})) {
1182
+ if (this.check([0x00, 0x01, 0x00, 0x00, 0x00])) {
1223
1183
  return {
1224
- ext: 'vcf',
1225
- mime: 'text/vcard'
1184
+ ext: 'ttf',
1185
+ mime: 'font/ttf',
1226
1186
  };
1227
1187
  }
1228
1188
 
1229
- if (checkString('VCALENDAR', {offset: 6})) {
1189
+ if (this.check([0x00, 0x00, 0x01, 0x00])) {
1230
1190
  return {
1231
- ext: 'ics',
1232
- mime: 'text/calendar'
1191
+ ext: 'ico',
1192
+ mime: 'image/x-icon',
1233
1193
  };
1234
1194
  }
1235
- }
1236
1195
 
1237
- // `raf` is here just to keep all the raw image detectors together.
1238
- if (checkString('FUJIFILMCCD-RAW')) {
1239
- return {
1240
- ext: 'raf',
1241
- mime: 'image/x-fujifilm-raf'
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
- if (checkString('Extended Module:')) {
1246
- return {
1247
- ext: 'xm',
1248
- mime: 'audio/x-xm'
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
- if (checkString('Creative Voice File')) {
1253
- return {
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
- if (check([0x04, 0x00, 0x00, 0x00]) && buffer.length >= 16) { // Rough & quick check Pickle/ASAR
1260
- const jsonSize = buffer.readUInt32LE(12);
1261
- if (jsonSize > 12 && buffer.length >= jsonSize + 16) {
1262
- try {
1263
- const header = buffer.slice(16, jsonSize + 16).toString();
1264
- const json = JSON.parse(header);
1265
- // Check if Pickle is ASAR
1266
- if (json.files) { // Final check, assuring Pickle/ASAR format
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
- if (check([0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02])) {
1278
- return {
1279
- ext: 'mxf',
1280
- mime: 'application/mxf'
1281
- };
1282
- }
1224
+ if (this.checkString('VCALENDAR', {offset: 6})) {
1225
+ return {
1226
+ ext: 'ics',
1227
+ mime: 'text/calendar',
1228
+ };
1229
+ }
1230
+ }
1283
1231
 
1284
- if (checkString('SCRM', {offset: 44})) {
1285
- return {
1286
- ext: 's3m',
1287
- mime: 'audio/x-s3m'
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
- if (check([0x47], {offset: 4}) && (check([0x47], {offset: 192}) || check([0x47], {offset: 196}))) {
1292
- return {
1293
- ext: 'mts',
1294
- mime: 'video/mp2t'
1295
- };
1296
- }
1240
+ if (this.checkString('Extended Module:')) {
1241
+ return {
1242
+ ext: 'xm',
1243
+ mime: 'audio/x-xm',
1244
+ };
1245
+ }
1297
1246
 
1298
- if (check([0x42, 0x4F, 0x4F, 0x4B, 0x4D, 0x4F, 0x42, 0x49], {offset: 60})) {
1299
- return {
1300
- ext: 'mobi',
1301
- mime: 'application/x-mobipocket-ebook'
1302
- };
1303
- }
1247
+ if (this.checkString('Creative Voice File')) {
1248
+ return {
1249
+ ext: 'voc',
1250
+ mime: 'audio/x-voc',
1251
+ };
1252
+ }
1304
1253
 
1305
- if (check([0x44, 0x49, 0x43, 0x4D], {offset: 128})) {
1306
- return {
1307
- ext: 'dcm',
1308
- mime: 'application/dicom'
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
- if (check([0x4C, 0x00, 0x00, 0x00, 0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46])) {
1313
- return {
1314
- ext: 'lnk',
1315
- mime: 'application/x.ms.shortcut' // Invented by us
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
- if (check([0x62, 0x6F, 0x6F, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x6D, 0x61, 0x72, 0x6B, 0x00, 0x00, 0x00, 0x00])) {
1320
- return {
1321
- ext: 'alias',
1322
- mime: 'application/x.apple.alias' // Invented by us
1323
- };
1324
- }
1278
+ if (this.checkString('SCRM', {offset: 44})) {
1279
+ return {
1280
+ ext: 's3m',
1281
+ mime: 'audio/x-s3m',
1282
+ };
1283
+ }
1325
1284
 
1326
- if (
1327
- check([0x4C, 0x50], {offset: 34}) &&
1328
- (
1329
- check([0x00, 0x00, 0x01], {offset: 8}) ||
1330
- check([0x01, 0x00, 0x02], {offset: 8}) ||
1331
- check([0x02, 0x00, 0x02], {offset: 8})
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
- if (check([0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5, 0xBD, 0x31, 0xEF, 0xE7, 0xFE, 0x74, 0xB7, 0x1D])) {
1341
- return {
1342
- ext: 'indd',
1343
- mime: 'application/x-indesign'
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
- // Increase sample size from 256 to 512
1348
- await tokenizer.peekBuffer(buffer, {length: Math.min(512, tokenizer.fileInfo.size), mayBeLess: true});
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
- // Requires a buffer size of 512 bytes
1351
- if (tarHeaderChecksumMatches(buffer)) {
1352
- return {
1353
- ext: 'tar',
1354
- mime: 'application/x-tar'
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
- if (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])) {
1359
- return {
1360
- ext: 'skp',
1361
- mime: 'application/vnd.sketchup.skp'
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
- if (checkString('-----BEGIN PGP MESSAGE-----')) {
1366
- return {
1367
- ext: 'pgp',
1368
- mime: 'application/pgp-encrypted'
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
- // Check MPEG 1 or 2 Layer 3 header, or 'layer 0' for ADTS (MPEG sync-word 0xFFE)
1373
- if (buffer.length >= 2 && check([0xFF, 0xE0], {offset: 0, mask: [0xFF, 0xE0]})) {
1374
- if (check([0x10], {offset: 1, mask: [0x16]})) {
1375
- // Check for (ADTS) MPEG-2
1376
- if (check([0x08], {offset: 1, mask: [0x08]})) {
1377
- return {
1378
- ext: 'aac',
1379
- mime: 'audio/aac'
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
- // Must be (ADTS) MPEG-4
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: 'aac',
1386
- mime: 'audio/aac'
1345
+ ext: 'indd',
1346
+ mime: 'application/x-indesign',
1387
1347
  };
1388
1348
  }
1389
1349
 
1390
- // MPEG 1 or 2 Layer 3 header
1391
- // Check for MPEG layer 3
1392
- if (check([0x02], {offset: 1, mask: [0x06]})) {
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: 'mp3',
1395
- mime: 'audio/mpeg'
1356
+ ext: 'tar',
1357
+ mime: 'application/x-tar',
1396
1358
  };
1397
1359
  }
1398
1360
 
1399
- // Check for MPEG layer 2
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: 'mp2',
1403
- mime: 'audio/mpeg'
1363
+ ext: 'skp',
1364
+ mime: 'application/vnd.sketchup.skp',
1404
1365
  };
1405
1366
  }
1406
1367
 
1407
- // Check for MPEG layer 1
1408
- if (check([0x06], {offset: 1, mask: [0x06]})) {
1368
+ if (this.checkString('-----BEGIN PGP MESSAGE-----')) {
1409
1369
  return {
1410
- ext: 'mp1',
1411
- mime: 'audio/mpeg'
1370
+ ext: 'pgp',
1371
+ mime: 'application/pgp-encrypted',
1412
1372
  };
1413
1373
  }
1414
- }
1415
- }
1416
1374
 
1417
- const stream = readableStream => new Promise((resolve, reject) => {
1418
- // Using `eval` to work around issues when bundling with Webpack
1419
- const stream = eval('require')('stream'); // eslint-disable-line no-eval
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
- readableStream.on('error', reject);
1422
- readableStream.once('readable', async () => {
1423
- // Set up output stream
1424
- const pass = new stream.PassThrough();
1425
- let outputStream;
1426
- if (stream.pipeline) {
1427
- outputStream = stream.pipeline(readableStream, pass, () => {
1428
- });
1429
- } else {
1430
- outputStream = readableStream.pipe(pass);
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
- // Read the input stream and detect the filetype
1434
- const chunk = readableStream.read(minimumBytes) || readableStream.read() || Buffer.alloc(0);
1435
- try {
1436
- const fileType = await fromBuffer(chunk);
1437
- pass.fileType = fileType;
1438
- } catch (error) {
1439
- reject(error);
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
- resolve(outputStream);
1443
- });
1444
- });
1445
-
1446
- const fileType = {
1447
- fromStream,
1448
- fromTokenizer,
1449
- fromBuffer,
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
- Object.defineProperty(fileType, 'mimeTypes', {
1460
- get() {
1461
- return new Set(supported.mimeTypes);
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
- module.exports = fileType;
1523
+ export const supportedExtensions = new Set(extensions);
1524
+ export const supportedMimeTypes = new Set(mimeTypes);