file-type 16.5.3 → 17.1.0

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