file-type 19.6.0 → 20.0.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.d.ts +36 -335
- package/core.js +309 -235
- package/index.d.ts +9 -3
- package/index.js +12 -6
- package/package.json +27 -9
- package/readme.md +73 -29
- package/supported.js +34 -0
package/core.js
CHANGED
|
@@ -4,7 +4,8 @@ Primary entry point, Node.js specific entry point is index.js
|
|
|
4
4
|
|
|
5
5
|
import * as Token from 'token-types';
|
|
6
6
|
import * as strtok3 from 'strtok3/core';
|
|
7
|
-
import {
|
|
7
|
+
import {ZipHandler} from '@tokenizer/inflate';
|
|
8
|
+
import {includes, getUintBE} from 'uint8array-extras';
|
|
8
9
|
import {
|
|
9
10
|
stringToBytes,
|
|
10
11
|
tarHeaderChecksumMatches,
|
|
@@ -26,6 +27,127 @@ export async function fileTypeFromBlob(blob) {
|
|
|
26
27
|
return new FileTypeParser().fromBlob(blob);
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function getFileTypeFromMimeType(mimeType) {
|
|
31
|
+
switch (mimeType) {
|
|
32
|
+
case 'application/epub+zip':
|
|
33
|
+
return {
|
|
34
|
+
ext: 'epub',
|
|
35
|
+
mime: 'application/epub+zip',
|
|
36
|
+
};
|
|
37
|
+
case 'application/vnd.oasis.opendocument.text':
|
|
38
|
+
return {
|
|
39
|
+
ext: 'odt',
|
|
40
|
+
mime: 'application/vnd.oasis.opendocument.text',
|
|
41
|
+
};
|
|
42
|
+
case 'application/vnd.oasis.opendocument.text-template':
|
|
43
|
+
return {
|
|
44
|
+
ext: 'ott',
|
|
45
|
+
mime: 'application/vnd.oasis.opendocument.text-template',
|
|
46
|
+
};
|
|
47
|
+
case 'application/vnd.oasis.opendocument.spreadsheet':
|
|
48
|
+
return {
|
|
49
|
+
ext: 'ods',
|
|
50
|
+
mime: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
51
|
+
};
|
|
52
|
+
case 'application/vnd.oasis.opendocument.spreadsheet-template':
|
|
53
|
+
return {
|
|
54
|
+
ext: 'ots',
|
|
55
|
+
mime: 'application/vnd.oasis.opendocument.spreadsheet-template',
|
|
56
|
+
};
|
|
57
|
+
case 'application/vnd.oasis.opendocument.presentation':
|
|
58
|
+
return {
|
|
59
|
+
ext: 'odp',
|
|
60
|
+
mime: 'application/vnd.oasis.opendocument.presentation',
|
|
61
|
+
};
|
|
62
|
+
case 'application/vnd.oasis.opendocument.presentation-template':
|
|
63
|
+
return {
|
|
64
|
+
ext: 'otp',
|
|
65
|
+
mime: 'application/vnd.oasis.opendocument.presentation-template',
|
|
66
|
+
};
|
|
67
|
+
case 'application/vnd.oasis.opendocument.graphics':
|
|
68
|
+
return {
|
|
69
|
+
ext: 'odg',
|
|
70
|
+
mime: 'application/vnd.oasis.opendocument.graphics',
|
|
71
|
+
};
|
|
72
|
+
case 'application/vnd.oasis.opendocument.graphics-template':
|
|
73
|
+
return {
|
|
74
|
+
ext: 'otg',
|
|
75
|
+
mime: 'application/vnd.oasis.opendocument.graphics-template',
|
|
76
|
+
};
|
|
77
|
+
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
|
|
78
|
+
return {
|
|
79
|
+
ext: 'xlsx',
|
|
80
|
+
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
81
|
+
};
|
|
82
|
+
case 'application/vnd.ms-excel.sheet.macroEnabled':
|
|
83
|
+
return {
|
|
84
|
+
ext: 'xlsm',
|
|
85
|
+
mime: 'application/vnd.ms-excel.sheet.macroEnabled.12',
|
|
86
|
+
};
|
|
87
|
+
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.template':
|
|
88
|
+
return {
|
|
89
|
+
ext: 'xltx',
|
|
90
|
+
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
|
91
|
+
};
|
|
92
|
+
case 'application/vnd.ms-excel.template.macroEnabled':
|
|
93
|
+
return {
|
|
94
|
+
ext: 'xltm',
|
|
95
|
+
mime: 'application/vnd.ms-excel.template.macroenabled.12',
|
|
96
|
+
};
|
|
97
|
+
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
|
98
|
+
return {
|
|
99
|
+
ext: 'docx',
|
|
100
|
+
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
101
|
+
};
|
|
102
|
+
case 'application/vnd.ms-word.document.macroEnabled':
|
|
103
|
+
return {
|
|
104
|
+
ext: 'docm',
|
|
105
|
+
mime: 'application/vnd.ms-word.document.macroEnabled.12',
|
|
106
|
+
};
|
|
107
|
+
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.template':
|
|
108
|
+
return {
|
|
109
|
+
ext: 'dotx',
|
|
110
|
+
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
|
111
|
+
};
|
|
112
|
+
case 'application/vnd.ms-word.template.macroEnabledTemplate':
|
|
113
|
+
return {
|
|
114
|
+
ext: 'dotm',
|
|
115
|
+
mime: 'application/vnd.ms-word.template.macroEnabled.12',
|
|
116
|
+
};
|
|
117
|
+
case 'application/vnd.openxmlformats-officedocument.presentationml.template':
|
|
118
|
+
return {
|
|
119
|
+
ext: 'potx',
|
|
120
|
+
mime: 'application/vnd.openxmlformats-officedocument.presentationml.template',
|
|
121
|
+
};
|
|
122
|
+
case 'application/vnd.ms-powerpoint.template.macroEnabled':
|
|
123
|
+
return {
|
|
124
|
+
ext: 'potm',
|
|
125
|
+
mime: 'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
|
126
|
+
};
|
|
127
|
+
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
|
|
128
|
+
return {
|
|
129
|
+
ext: 'pptx',
|
|
130
|
+
mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
131
|
+
};
|
|
132
|
+
case 'application/vnd.ms-powerpoint.presentation.macroEnabled':
|
|
133
|
+
return {
|
|
134
|
+
ext: 'pptm',
|
|
135
|
+
mime: 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
|
136
|
+
};
|
|
137
|
+
case 'application/vnd.ms-visio.drawing':
|
|
138
|
+
return {
|
|
139
|
+
ext: 'vsdx',
|
|
140
|
+
mime: 'application/vnd.visio',
|
|
141
|
+
};
|
|
142
|
+
case 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml':
|
|
143
|
+
return {
|
|
144
|
+
ext: '3mf',
|
|
145
|
+
mime: 'model/3mf',
|
|
146
|
+
};
|
|
147
|
+
default:
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
29
151
|
function _check(buffer, headers, options) {
|
|
30
152
|
options = {
|
|
31
153
|
offset: 0,
|
|
@@ -57,20 +179,20 @@ export async function fileTypeStream(webStream, options) {
|
|
|
57
179
|
|
|
58
180
|
export class FileTypeParser {
|
|
59
181
|
constructor(options) {
|
|
60
|
-
this.detectors = options?.customDetectors
|
|
182
|
+
this.detectors = [...(options?.customDetectors ?? []),
|
|
183
|
+
{id: 'core', detect: this.detectConfident},
|
|
184
|
+
{id: 'core.imprecise', detect: this.detectImprecise}];
|
|
61
185
|
this.tokenizerOptions = {
|
|
62
186
|
abortSignal: options?.signal,
|
|
63
187
|
};
|
|
64
|
-
this.fromTokenizer = this.fromTokenizer.bind(this);
|
|
65
|
-
this.fromBuffer = this.fromBuffer.bind(this);
|
|
66
|
-
this.parse = this.parse.bind(this);
|
|
67
188
|
}
|
|
68
189
|
|
|
69
190
|
async fromTokenizer(tokenizer) {
|
|
70
191
|
const initialPosition = tokenizer.position;
|
|
71
192
|
|
|
72
|
-
|
|
73
|
-
|
|
193
|
+
// Iterate through all file-type detectors
|
|
194
|
+
for (const detector of this.detectors) {
|
|
195
|
+
const fileType = await detector.detect(tokenizer);
|
|
74
196
|
if (fileType) {
|
|
75
197
|
return fileType;
|
|
76
198
|
}
|
|
@@ -79,8 +201,6 @@ export class FileTypeParser {
|
|
|
79
201
|
return undefined; // Cannot proceed scanning of the tokenizer is at an arbitrary position
|
|
80
202
|
}
|
|
81
203
|
}
|
|
82
|
-
|
|
83
|
-
return this.parse(tokenizer);
|
|
84
204
|
}
|
|
85
205
|
|
|
86
206
|
async fromBuffer(input) {
|
|
@@ -163,7 +283,8 @@ export class FileTypeParser {
|
|
|
163
283
|
return this.check(stringToBytes(header), options);
|
|
164
284
|
}
|
|
165
285
|
|
|
166
|
-
|
|
286
|
+
// Detections with a high degree of certainty in identifying the correct file type
|
|
287
|
+
detectConfident = async tokenizer => {
|
|
167
288
|
this.buffer = new Uint8Array(reasonableDetectionSizeInBytes);
|
|
168
289
|
|
|
169
290
|
// Keep reading until EOF if the file size is unknown.
|
|
@@ -253,7 +374,7 @@ export class FileTypeParser {
|
|
|
253
374
|
if (this.check([0xEF, 0xBB, 0xBF])) { // UTF-8-BOM
|
|
254
375
|
// Strip off UTF-8-BOM
|
|
255
376
|
this.tokenizer.ignore(3);
|
|
256
|
-
return this.
|
|
377
|
+
return this.detectConfident(tokenizer);
|
|
257
378
|
}
|
|
258
379
|
|
|
259
380
|
if (this.check([0x47, 0x49, 0x46])) {
|
|
@@ -387,140 +508,69 @@ export class FileTypeParser {
|
|
|
387
508
|
// Zip-based file formats
|
|
388
509
|
// Need to be before the `zip` check
|
|
389
510
|
if (this.check([0x50, 0x4B, 0x3, 0x4])) { // Local file header signature
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
compressedSize: view.getUint32(18, true),
|
|
399
|
-
uncompressedSize: view.getUint32(22, true),
|
|
400
|
-
filenameLength: view.getUint16(26, true),
|
|
401
|
-
extraFieldLength: view.getUint16(28, true),
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
zipHeader.filename = await tokenizer.readToken(new Token.StringType(zipHeader.filenameLength, 'utf-8'));
|
|
405
|
-
await tokenizer.ignore(zipHeader.extraFieldLength);
|
|
406
|
-
|
|
407
|
-
if (/classes\d*\.dex/.test(zipHeader.filename)) {
|
|
511
|
+
let fileType;
|
|
512
|
+
await new ZipHandler(tokenizer).unzip(zipHeader => {
|
|
513
|
+
switch (zipHeader.filename) {
|
|
514
|
+
case 'META-INF/mozilla.rsa':
|
|
515
|
+
fileType = {
|
|
516
|
+
ext: 'xpi',
|
|
517
|
+
mime: 'application/x-xpinstall',
|
|
518
|
+
};
|
|
408
519
|
return {
|
|
409
|
-
|
|
410
|
-
|
|
520
|
+
stop: true,
|
|
521
|
+
};
|
|
522
|
+
case 'META-INF/MANIFEST.MF':
|
|
523
|
+
fileType = {
|
|
524
|
+
ext: 'jar',
|
|
525
|
+
mime: 'application/java-archive',
|
|
411
526
|
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Assumes signed `.xpi` from addons.mozilla.org
|
|
415
|
-
if (zipHeader.filename === 'META-INF/mozilla.rsa') {
|
|
416
527
|
return {
|
|
417
|
-
|
|
418
|
-
mime: 'application/x-xpinstall',
|
|
528
|
+
stop: true,
|
|
419
529
|
};
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (zipHeader.filename.endsWith('.rels') || zipHeader.filename.endsWith('.xml')) {
|
|
423
|
-
const type = zipHeader.filename.split('/')[0];
|
|
424
|
-
switch (type) {
|
|
425
|
-
case '_rels':
|
|
426
|
-
break;
|
|
427
|
-
case 'word':
|
|
428
|
-
return {
|
|
429
|
-
ext: 'docx',
|
|
430
|
-
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
431
|
-
};
|
|
432
|
-
case 'ppt':
|
|
433
|
-
return {
|
|
434
|
-
ext: 'pptx',
|
|
435
|
-
mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
436
|
-
};
|
|
437
|
-
case 'xl':
|
|
438
|
-
return {
|
|
439
|
-
ext: 'xlsx',
|
|
440
|
-
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
441
|
-
};
|
|
442
|
-
case 'visio':
|
|
443
|
-
return {
|
|
444
|
-
ext: 'vsdx',
|
|
445
|
-
mime: 'application/vnd.visio',
|
|
446
|
-
};
|
|
447
|
-
default:
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (zipHeader.filename.startsWith('xl/')) {
|
|
530
|
+
case 'mimetype':
|
|
453
531
|
return {
|
|
454
|
-
|
|
455
|
-
|
|
532
|
+
async handler(fileData) {
|
|
533
|
+
// Use TextDecoder to decode the UTF-8 encoded data
|
|
534
|
+
const mimeType = new TextDecoder('utf-8').decode(fileData).trim();
|
|
535
|
+
fileType = getFileTypeFromMimeType(mimeType);
|
|
536
|
+
},
|
|
537
|
+
stop: true,
|
|
456
538
|
};
|
|
457
|
-
}
|
|
458
539
|
|
|
459
|
-
|
|
540
|
+
case '[Content_Types].xml':
|
|
460
541
|
return {
|
|
461
|
-
|
|
462
|
-
|
|
542
|
+
async handler(fileData) {
|
|
543
|
+
// Use TextDecoder to decode the UTF-8 encoded data
|
|
544
|
+
let xmlContent = new TextDecoder('utf-8').decode(fileData);
|
|
545
|
+
const endPos = xmlContent.indexOf('.main+xml"');
|
|
546
|
+
if (endPos === -1) {
|
|
547
|
+
const mimeType = 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml';
|
|
548
|
+
if (xmlContent.includes(`ContentType="${mimeType}"`)) {
|
|
549
|
+
fileType = getFileTypeFromMimeType(mimeType);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
xmlContent = xmlContent.slice(0, Math.max(0, endPos));
|
|
553
|
+
const firstPos = xmlContent.lastIndexOf('"');
|
|
554
|
+
const mimeType = xmlContent.slice(Math.max(0, firstPos + 1));
|
|
555
|
+
fileType = getFileTypeFromMimeType(mimeType);
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
stop: true,
|
|
463
559
|
};
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// MS Office, OpenOffice and LibreOffice may put the parts in different order, so the check should not rely on it.
|
|
472
|
-
if (zipHeader.filename === 'mimetype' && zipHeader.compressedSize === zipHeader.uncompressedSize) {
|
|
473
|
-
let mimeType = await tokenizer.readToken(new Token.StringType(zipHeader.compressedSize, 'utf-8'));
|
|
474
|
-
mimeType = mimeType.trim();
|
|
475
|
-
|
|
476
|
-
switch (mimeType) {
|
|
477
|
-
case 'application/epub+zip':
|
|
478
|
-
return {
|
|
479
|
-
ext: 'epub',
|
|
480
|
-
mime: 'application/epub+zip',
|
|
481
|
-
};
|
|
482
|
-
case 'application/vnd.oasis.opendocument.text':
|
|
483
|
-
return {
|
|
484
|
-
ext: 'odt',
|
|
485
|
-
mime: 'application/vnd.oasis.opendocument.text',
|
|
486
|
-
};
|
|
487
|
-
case 'application/vnd.oasis.opendocument.spreadsheet':
|
|
488
|
-
return {
|
|
489
|
-
ext: 'ods',
|
|
490
|
-
mime: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
491
|
-
};
|
|
492
|
-
case 'application/vnd.oasis.opendocument.presentation':
|
|
493
|
-
return {
|
|
494
|
-
ext: 'odp',
|
|
495
|
-
mime: 'application/vnd.oasis.opendocument.presentation',
|
|
496
|
-
};
|
|
497
|
-
default:
|
|
560
|
+
default:
|
|
561
|
+
if (/classes\d*\.dex/.test(zipHeader.filename)) {
|
|
562
|
+
fileType = {
|
|
563
|
+
ext: 'apk',
|
|
564
|
+
mime: 'application/vnd.android.package-archive',
|
|
565
|
+
};
|
|
566
|
+
return {stop: true};
|
|
498
567
|
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Try to find next header manually when current one is corrupted
|
|
502
|
-
if (zipHeader.compressedSize === 0) {
|
|
503
|
-
let nextHeaderIndex = -1;
|
|
504
|
-
|
|
505
|
-
while (nextHeaderIndex < 0 && (tokenizer.position < tokenizer.fileInfo.size)) {
|
|
506
|
-
await tokenizer.peekBuffer(this.buffer, {mayBeLess: true});
|
|
507
568
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
// Move position to the next header if found, skip the whole buffer otherwise
|
|
511
|
-
await tokenizer.ignore(nextHeaderIndex >= 0 ? nextHeaderIndex : this.buffer.length);
|
|
512
|
-
}
|
|
513
|
-
} else {
|
|
514
|
-
await tokenizer.ignore(zipHeader.compressedSize);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
} catch (error) {
|
|
518
|
-
if (!(error instanceof strtok3.EndOfStreamError)) {
|
|
519
|
-
throw error;
|
|
569
|
+
return {};
|
|
520
570
|
}
|
|
521
|
-
}
|
|
571
|
+
});
|
|
522
572
|
|
|
523
|
-
return {
|
|
573
|
+
return fileType ?? {
|
|
524
574
|
ext: 'zip',
|
|
525
575
|
mime: 'application/zip',
|
|
526
576
|
};
|
|
@@ -598,68 +648,6 @@ export class FileTypeParser {
|
|
|
598
648
|
};
|
|
599
649
|
}
|
|
600
650
|
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
// File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format)
|
|
604
|
-
// It's not required to be first, but it's recommended to be. Almost all ISO base media files start with `ftyp` box.
|
|
605
|
-
// `ftyp` box must contain a brand major identifier, which must consist of ISO 8859-1 printable characters.
|
|
606
|
-
// Here we check for 8859-1 printable characters (for simplicity, it's a mask which also catches one non-printable character).
|
|
607
|
-
if (
|
|
608
|
-
this.checkString('ftyp', {offset: 4})
|
|
609
|
-
&& (this.buffer[8] & 0x60) !== 0x00 // Brand major, first character ASCII?
|
|
610
|
-
) {
|
|
611
|
-
// They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect.
|
|
612
|
-
// For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension.
|
|
613
|
-
const brandMajor = new Token.StringType(4, 'latin1').get(this.buffer, 8).replace('\0', ' ').trim();
|
|
614
|
-
switch (brandMajor) {
|
|
615
|
-
case 'avif':
|
|
616
|
-
case 'avis':
|
|
617
|
-
return {ext: 'avif', mime: 'image/avif'};
|
|
618
|
-
case 'mif1':
|
|
619
|
-
return {ext: 'heic', mime: 'image/heif'};
|
|
620
|
-
case 'msf1':
|
|
621
|
-
return {ext: 'heic', mime: 'image/heif-sequence'};
|
|
622
|
-
case 'heic':
|
|
623
|
-
case 'heix':
|
|
624
|
-
return {ext: 'heic', mime: 'image/heic'};
|
|
625
|
-
case 'hevc':
|
|
626
|
-
case 'hevx':
|
|
627
|
-
return {ext: 'heic', mime: 'image/heic-sequence'};
|
|
628
|
-
case 'qt':
|
|
629
|
-
return {ext: 'mov', mime: 'video/quicktime'};
|
|
630
|
-
case 'M4V':
|
|
631
|
-
case 'M4VH':
|
|
632
|
-
case 'M4VP':
|
|
633
|
-
return {ext: 'm4v', mime: 'video/x-m4v'};
|
|
634
|
-
case 'M4P':
|
|
635
|
-
return {ext: 'm4p', mime: 'video/mp4'};
|
|
636
|
-
case 'M4B':
|
|
637
|
-
return {ext: 'm4b', mime: 'audio/mp4'};
|
|
638
|
-
case 'M4A':
|
|
639
|
-
return {ext: 'm4a', mime: 'audio/x-m4a'};
|
|
640
|
-
case 'F4V':
|
|
641
|
-
return {ext: 'f4v', mime: 'video/mp4'};
|
|
642
|
-
case 'F4P':
|
|
643
|
-
return {ext: 'f4p', mime: 'video/mp4'};
|
|
644
|
-
case 'F4A':
|
|
645
|
-
return {ext: 'f4a', mime: 'audio/mp4'};
|
|
646
|
-
case 'F4B':
|
|
647
|
-
return {ext: 'f4b', mime: 'audio/mp4'};
|
|
648
|
-
case 'crx':
|
|
649
|
-
return {ext: 'cr3', mime: 'image/x-canon-cr3'};
|
|
650
|
-
default:
|
|
651
|
-
if (brandMajor.startsWith('3g')) {
|
|
652
|
-
if (brandMajor.startsWith('3g2')) {
|
|
653
|
-
return {ext: '3g2', mime: 'video/3gpp2'};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
return {ext: '3gp', mime: 'video/3gpp'};
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return {ext: 'mp4', mime: 'video/mp4'};
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
651
|
if (this.checkString('MThd')) {
|
|
664
652
|
return {
|
|
665
653
|
ext: 'mid',
|
|
@@ -841,9 +829,9 @@ export class FileTypeParser {
|
|
|
841
829
|
}
|
|
842
830
|
|
|
843
831
|
const re = await readElement();
|
|
844
|
-
const
|
|
832
|
+
const documentType = await readChildren(re.len);
|
|
845
833
|
|
|
846
|
-
switch (
|
|
834
|
+
switch (documentType) {
|
|
847
835
|
case 'webm':
|
|
848
836
|
return {
|
|
849
837
|
ext: 'webm',
|
|
@@ -966,6 +954,13 @@ export class FileTypeParser {
|
|
|
966
954
|
};
|
|
967
955
|
}
|
|
968
956
|
|
|
957
|
+
if (this.check([0x04, 0x22, 0x4D, 0x18])) {
|
|
958
|
+
return {
|
|
959
|
+
ext: 'lz4',
|
|
960
|
+
mime: 'application/x-lz4', // Invented by us
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
969
964
|
// -- 5-byte signatures --
|
|
970
965
|
|
|
971
966
|
if (this.check([0x4F, 0x54, 0x54, 0x4F, 0x00])) {
|
|
@@ -1056,6 +1051,13 @@ export class FileTypeParser {
|
|
|
1056
1051
|
};
|
|
1057
1052
|
}
|
|
1058
1053
|
|
|
1054
|
+
if (this.checkString('DRACO')) {
|
|
1055
|
+
return {
|
|
1056
|
+
ext: 'drc',
|
|
1057
|
+
mime: 'application/vnd.google.draco', // Invented by us
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1059
1061
|
// -- 6-byte signatures --
|
|
1060
1062
|
|
|
1061
1063
|
if (this.check([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])) {
|
|
@@ -1241,6 +1243,66 @@ export class FileTypeParser {
|
|
|
1241
1243
|
};
|
|
1242
1244
|
}
|
|
1243
1245
|
|
|
1246
|
+
// File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format)
|
|
1247
|
+
// It's not required to be first, but it's recommended to be. Almost all ISO base media files start with `ftyp` box.
|
|
1248
|
+
// `ftyp` box must contain a brand major identifier, which must consist of ISO 8859-1 printable characters.
|
|
1249
|
+
// Here we check for 8859-1 printable characters (for simplicity, it's a mask which also catches one non-printable character).
|
|
1250
|
+
if (
|
|
1251
|
+
this.checkString('ftyp', {offset: 4})
|
|
1252
|
+
&& (this.buffer[8] & 0x60) !== 0x00 // Brand major, first character ASCII?
|
|
1253
|
+
) {
|
|
1254
|
+
// They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect.
|
|
1255
|
+
// For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension.
|
|
1256
|
+
const brandMajor = new Token.StringType(4, 'latin1').get(this.buffer, 8).replace('\0', ' ').trim();
|
|
1257
|
+
switch (brandMajor) {
|
|
1258
|
+
case 'avif':
|
|
1259
|
+
case 'avis':
|
|
1260
|
+
return {ext: 'avif', mime: 'image/avif'};
|
|
1261
|
+
case 'mif1':
|
|
1262
|
+
return {ext: 'heic', mime: 'image/heif'};
|
|
1263
|
+
case 'msf1':
|
|
1264
|
+
return {ext: 'heic', mime: 'image/heif-sequence'};
|
|
1265
|
+
case 'heic':
|
|
1266
|
+
case 'heix':
|
|
1267
|
+
return {ext: 'heic', mime: 'image/heic'};
|
|
1268
|
+
case 'hevc':
|
|
1269
|
+
case 'hevx':
|
|
1270
|
+
return {ext: 'heic', mime: 'image/heic-sequence'};
|
|
1271
|
+
case 'qt':
|
|
1272
|
+
return {ext: 'mov', mime: 'video/quicktime'};
|
|
1273
|
+
case 'M4V':
|
|
1274
|
+
case 'M4VH':
|
|
1275
|
+
case 'M4VP':
|
|
1276
|
+
return {ext: 'm4v', mime: 'video/x-m4v'};
|
|
1277
|
+
case 'M4P':
|
|
1278
|
+
return {ext: 'm4p', mime: 'video/mp4'};
|
|
1279
|
+
case 'M4B':
|
|
1280
|
+
return {ext: 'm4b', mime: 'audio/mp4'};
|
|
1281
|
+
case 'M4A':
|
|
1282
|
+
return {ext: 'm4a', mime: 'audio/x-m4a'};
|
|
1283
|
+
case 'F4V':
|
|
1284
|
+
return {ext: 'f4v', mime: 'video/mp4'};
|
|
1285
|
+
case 'F4P':
|
|
1286
|
+
return {ext: 'f4p', mime: 'video/mp4'};
|
|
1287
|
+
case 'F4A':
|
|
1288
|
+
return {ext: 'f4a', mime: 'audio/mp4'};
|
|
1289
|
+
case 'F4B':
|
|
1290
|
+
return {ext: 'f4b', mime: 'audio/mp4'};
|
|
1291
|
+
case 'crx':
|
|
1292
|
+
return {ext: 'cr3', mime: 'image/x-canon-cr3'};
|
|
1293
|
+
default:
|
|
1294
|
+
if (brandMajor.startsWith('3g')) {
|
|
1295
|
+
if (brandMajor.startsWith('3g2')) {
|
|
1296
|
+
return {ext: '3g2', mime: 'video/3gpp2'};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return {ext: '3gp', mime: 'video/3gpp'};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return {ext: 'mp4', mime: 'video/mp4'};
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1244
1306
|
// -- 12-byte signatures --
|
|
1245
1307
|
|
|
1246
1308
|
if (this.check([0x49, 0x49, 0x55, 0x00, 0x18, 0x00, 0x00, 0x00, 0x88, 0xE7, 0x74, 0xD8])) {
|
|
@@ -1380,39 +1442,6 @@ export class FileTypeParser {
|
|
|
1380
1442
|
return undefined; // Some unknown text based format
|
|
1381
1443
|
}
|
|
1382
1444
|
|
|
1383
|
-
// -- Unsafe signatures --
|
|
1384
|
-
|
|
1385
|
-
if (
|
|
1386
|
-
this.check([0x0, 0x0, 0x1, 0xBA])
|
|
1387
|
-
|| this.check([0x0, 0x0, 0x1, 0xB3])
|
|
1388
|
-
) {
|
|
1389
|
-
return {
|
|
1390
|
-
ext: 'mpg',
|
|
1391
|
-
mime: 'video/mpeg',
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
if (this.check([0x00, 0x01, 0x00, 0x00, 0x00])) {
|
|
1396
|
-
return {
|
|
1397
|
-
ext: 'ttf',
|
|
1398
|
-
mime: 'font/ttf',
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
if (this.check([0x00, 0x00, 0x01, 0x00])) {
|
|
1403
|
-
return {
|
|
1404
|
-
ext: 'ico',
|
|
1405
|
-
mime: 'image/x-icon',
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
if (this.check([0x00, 0x00, 0x02, 0x00])) {
|
|
1410
|
-
return {
|
|
1411
|
-
ext: 'cur',
|
|
1412
|
-
mime: 'image/x-icon',
|
|
1413
|
-
};
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
1445
|
if (this.check([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])) {
|
|
1417
1446
|
// Detected Microsoft Compound File Binary File (MS-CFB) Format.
|
|
1418
1447
|
return {
|
|
@@ -1618,6 +1647,44 @@ export class FileTypeParser {
|
|
|
1618
1647
|
mime: 'application/pgp-encrypted',
|
|
1619
1648
|
};
|
|
1620
1649
|
}
|
|
1650
|
+
};
|
|
1651
|
+
// Detections with limited supporting data, resulting in a higher likelihood of false positives
|
|
1652
|
+
detectImprecise = async tokenizer => {
|
|
1653
|
+
this.buffer = new Uint8Array(reasonableDetectionSizeInBytes);
|
|
1654
|
+
|
|
1655
|
+
// Read initial sample size of 8 bytes
|
|
1656
|
+
await tokenizer.peekBuffer(this.buffer, {length: Math.min(8, tokenizer.fileInfo.size), mayBeLess: true});
|
|
1657
|
+
|
|
1658
|
+
if (
|
|
1659
|
+
this.check([0x0, 0x0, 0x1, 0xBA])
|
|
1660
|
+
|| this.check([0x0, 0x0, 0x1, 0xB3])
|
|
1661
|
+
) {
|
|
1662
|
+
return {
|
|
1663
|
+
ext: 'mpg',
|
|
1664
|
+
mime: 'video/mpeg',
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (this.check([0x00, 0x01, 0x00, 0x00, 0x00])) {
|
|
1669
|
+
return {
|
|
1670
|
+
ext: 'ttf',
|
|
1671
|
+
mime: 'font/ttf',
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (this.check([0x00, 0x00, 0x01, 0x00])) {
|
|
1676
|
+
return {
|
|
1677
|
+
ext: 'ico',
|
|
1678
|
+
mime: 'image/x-icon',
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
if (this.check([0x00, 0x00, 0x02, 0x00])) {
|
|
1683
|
+
return {
|
|
1684
|
+
ext: 'cur',
|
|
1685
|
+
mime: 'image/x-icon',
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1621
1688
|
|
|
1622
1689
|
// Check MPEG 1 or 2 Layer 3 header, or 'layer 0' for ADTS (MPEG sync-word 0xFFE)
|
|
1623
1690
|
if (this.buffer.length >= 2 && this.check([0xFF, 0xE0], {offset: 0, mask: [0xFF, 0xE0]})) {
|
|
@@ -1662,7 +1729,7 @@ export class FileTypeParser {
|
|
|
1662
1729
|
};
|
|
1663
1730
|
}
|
|
1664
1731
|
}
|
|
1665
|
-
}
|
|
1732
|
+
};
|
|
1666
1733
|
|
|
1667
1734
|
async readTiffTag(bigEndian) {
|
|
1668
1735
|
const tagId = await this.tokenizer.readToken(bigEndian ? Token.UINT16_BE : Token.UINT16_LE);
|
|
@@ -1706,11 +1773,18 @@ export class FileTypeParser {
|
|
|
1706
1773
|
};
|
|
1707
1774
|
}
|
|
1708
1775
|
|
|
1709
|
-
if (ifdOffset >= 8
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1776
|
+
if (ifdOffset >= 8) {
|
|
1777
|
+
const someId1 = (bigEndian ? Token.UINT16_BE : Token.UINT16_LE).get(this.buffer, 8);
|
|
1778
|
+
const someId2 = (bigEndian ? Token.UINT16_BE : Token.UINT16_LE).get(this.buffer, 10);
|
|
1779
|
+
|
|
1780
|
+
if (
|
|
1781
|
+
(someId1 === 0x1C && someId2 === 0xFE)
|
|
1782
|
+
|| (someId1 === 0x1F && someId2 === 0x0B)) {
|
|
1783
|
+
return {
|
|
1784
|
+
ext: 'nef',
|
|
1785
|
+
mime: 'image/x-nikon-nef',
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1714
1788
|
}
|
|
1715
1789
|
}
|
|
1716
1790
|
|
package/index.d.ts
CHANGED
|
@@ -4,8 +4,14 @@ Typings for Node.js specific entry point.
|
|
|
4
4
|
|
|
5
5
|
import type {Readable as NodeReadableStream} from 'node:stream';
|
|
6
6
|
import type {AnyWebByteStream} from 'strtok3';
|
|
7
|
-
import
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
type FileTypeResult,
|
|
9
|
+
type StreamOptions,
|
|
10
|
+
type AnyWebReadableStream,
|
|
11
|
+
type Detector,
|
|
12
|
+
type AnyWebReadableByteStreamWithFileType,
|
|
13
|
+
FileTypeParser as DefaultFileTypeParser,
|
|
14
|
+
} from './core.js';
|
|
9
15
|
|
|
10
16
|
export type ReadableStreamWithFileType = NodeReadableStream & {
|
|
11
17
|
readonly fileType?: FileTypeResult;
|
|
@@ -14,7 +20,7 @@ export type ReadableStreamWithFileType = NodeReadableStream & {
|
|
|
14
20
|
/**
|
|
15
21
|
Extending `FileTypeParser` with Node.js engine specific functions.
|
|
16
22
|
*/
|
|
17
|
-
export declare class
|
|
23
|
+
export declare class FileTypeParser extends DefaultFileTypeParser {
|
|
18
24
|
/**
|
|
19
25
|
@param stream - Node.js `stream.Readable` or web `ReadableStream`.
|
|
20
26
|
*/
|