file-type 21.3.3 → 21.3.4
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 +157 -50
- package/index.js +55 -18
- package/package.json +1 -1
- package/readme.md +1 -1
package/core.js
CHANGED
|
@@ -24,6 +24,7 @@ const maximumUnknownSizePayloadProbeSizeInBytes = maximumZipEntrySizeInBytes;
|
|
|
24
24
|
const maximumZipTextEntrySizeInBytes = maximumZipEntrySizeInBytes;
|
|
25
25
|
const maximumNestedGzipDetectionSizeInBytes = maximumUntrustedSkipSizeInBytes;
|
|
26
26
|
const maximumNestedGzipProbeDepth = 1;
|
|
27
|
+
const unknownSizeGzipProbeTimeoutInMilliseconds = 100;
|
|
27
28
|
const maximumId3HeaderSizeInBytes = maximumUntrustedSkipSizeInBytes;
|
|
28
29
|
const maximumEbmlDocumentTypeSizeInBytes = 64;
|
|
29
30
|
const maximumEbmlElementPayloadSizeInBytes = maximumUnknownSizePayloadProbeSizeInBytes;
|
|
@@ -47,6 +48,7 @@ const recoverableZipErrorMessagePrefixes = [
|
|
|
47
48
|
'Unsupported ZIP compression method:',
|
|
48
49
|
'ZIP entry compressed data exceeds ',
|
|
49
50
|
'ZIP entry decompressed data exceeds ',
|
|
51
|
+
'Expected data-descriptor-signature at position ',
|
|
50
52
|
];
|
|
51
53
|
const recoverableZipErrorCodes = new Set([
|
|
52
54
|
'Z_BUF_ERROR',
|
|
@@ -56,6 +58,27 @@ const recoverableZipErrorCodes = new Set([
|
|
|
56
58
|
|
|
57
59
|
class ParserHardLimitError extends Error {}
|
|
58
60
|
|
|
61
|
+
function patchWebByobTokenizerClose(tokenizer) {
|
|
62
|
+
const streamReader = tokenizer?.streamReader;
|
|
63
|
+
if (streamReader?.constructor?.name !== 'WebStreamByobReader') {
|
|
64
|
+
return tokenizer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const {reader} = streamReader;
|
|
68
|
+
const cancelAndRelease = async () => {
|
|
69
|
+
await reader.cancel();
|
|
70
|
+
reader.releaseLock();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
streamReader.close = cancelAndRelease;
|
|
74
|
+
streamReader.abort = async () => {
|
|
75
|
+
streamReader.interrupted = true;
|
|
76
|
+
await cancelAndRelease();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return tokenizer;
|
|
80
|
+
}
|
|
81
|
+
|
|
59
82
|
function getSafeBound(value, maximum, reason) {
|
|
60
83
|
if (
|
|
61
84
|
!Number.isFinite(value)
|
|
@@ -533,7 +556,8 @@ function _check(buffer, headers, options) {
|
|
|
533
556
|
}
|
|
534
557
|
|
|
535
558
|
export function normalizeSampleSize(sampleSize) {
|
|
536
|
-
//
|
|
559
|
+
// `sampleSize` is an explicit caller-controlled tuning knob, not untrusted file input.
|
|
560
|
+
// Preserve valid caller-requested probe depth here; applications must bound attacker-derived option values themselves.
|
|
537
561
|
if (!Number.isFinite(sampleSize)) {
|
|
538
562
|
return reasonableDetectionSizeInBytes;
|
|
539
563
|
}
|
|
@@ -541,6 +565,45 @@ export function normalizeSampleSize(sampleSize) {
|
|
|
541
565
|
return Math.max(1, Math.trunc(sampleSize));
|
|
542
566
|
}
|
|
543
567
|
|
|
568
|
+
function readByobReaderWithSignal(reader, buffer, signal) {
|
|
569
|
+
if (signal === undefined) {
|
|
570
|
+
return reader.read(buffer);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
signal.throwIfAborted();
|
|
574
|
+
|
|
575
|
+
return new Promise((resolve, reject) => {
|
|
576
|
+
const cleanup = () => {
|
|
577
|
+
signal.removeEventListener('abort', onAbort);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const onAbort = () => {
|
|
581
|
+
const abortReason = signal.reason;
|
|
582
|
+
cleanup();
|
|
583
|
+
|
|
584
|
+
(async () => {
|
|
585
|
+
try {
|
|
586
|
+
await reader.cancel(abortReason);
|
|
587
|
+
} catch {}
|
|
588
|
+
})();
|
|
589
|
+
|
|
590
|
+
reject(abortReason);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
signal.addEventListener('abort', onAbort, {once: true});
|
|
594
|
+
(async () => {
|
|
595
|
+
try {
|
|
596
|
+
const result = await reader.read(buffer);
|
|
597
|
+
cleanup();
|
|
598
|
+
resolve(result);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
cleanup();
|
|
601
|
+
reject(error);
|
|
602
|
+
}
|
|
603
|
+
})();
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
544
607
|
function normalizeMpegOffsetTolerance(mpegOffsetTolerance) {
|
|
545
608
|
// This value controls scan depth and therefore worst-case CPU work.
|
|
546
609
|
if (!Number.isFinite(mpegOffsetTolerance)) {
|
|
@@ -752,7 +815,11 @@ export class FileTypeParser {
|
|
|
752
815
|
};
|
|
753
816
|
}
|
|
754
817
|
|
|
755
|
-
|
|
818
|
+
createTokenizerFromWebStream(stream) {
|
|
819
|
+
return patchWebByobTokenizerClose(strtok3.fromWebStream(stream, this.getTokenizerOptions()));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async parseTokenizer(tokenizer, detectionReentryCount = 0) {
|
|
756
823
|
this.detectionReentryCount = detectionReentryCount;
|
|
757
824
|
const initialPosition = tokenizer.position;
|
|
758
825
|
// Iterate through all file-type detectors
|
|
@@ -782,6 +849,14 @@ export class FileTypeParser {
|
|
|
782
849
|
}
|
|
783
850
|
}
|
|
784
851
|
|
|
852
|
+
async fromTokenizer(tokenizer) {
|
|
853
|
+
try {
|
|
854
|
+
return await this.parseTokenizer(tokenizer);
|
|
855
|
+
} finally {
|
|
856
|
+
await tokenizer.close();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
785
860
|
async fromBuffer(input) {
|
|
786
861
|
if (!(input instanceof Uint8Array || input instanceof ArrayBuffer)) {
|
|
787
862
|
throw new TypeError(`Expected the \`input\` argument to be of type \`Uint8Array\` or \`ArrayBuffer\`, got \`${typeof input}\``);
|
|
@@ -797,21 +872,15 @@ export class FileTypeParser {
|
|
|
797
872
|
}
|
|
798
873
|
|
|
799
874
|
async fromBlob(blob) {
|
|
875
|
+
this.options.signal?.throwIfAborted();
|
|
800
876
|
const tokenizer = strtok3.fromBlob(blob, this.getTokenizerOptions());
|
|
801
|
-
|
|
802
|
-
return await this.fromTokenizer(tokenizer);
|
|
803
|
-
} finally {
|
|
804
|
-
await tokenizer.close();
|
|
805
|
-
}
|
|
877
|
+
return this.fromTokenizer(tokenizer);
|
|
806
878
|
}
|
|
807
879
|
|
|
808
880
|
async fromStream(stream) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
} finally {
|
|
813
|
-
await tokenizer.close();
|
|
814
|
-
}
|
|
881
|
+
this.options.signal?.throwIfAborted();
|
|
882
|
+
const tokenizer = this.createTokenizerFromWebStream(stream);
|
|
883
|
+
return this.fromTokenizer(tokenizer);
|
|
815
884
|
}
|
|
816
885
|
|
|
817
886
|
async toDetectionStream(stream, options) {
|
|
@@ -822,7 +891,7 @@ export class FileTypeParser {
|
|
|
822
891
|
const reader = stream.getReader({mode: 'byob'});
|
|
823
892
|
try {
|
|
824
893
|
// Read the first chunk from the stream
|
|
825
|
-
const {value: chunk, done} = await reader
|
|
894
|
+
const {value: chunk, done} = await readByobReaderWithSignal(reader, new Uint8Array(sampleSize), this.options.signal);
|
|
826
895
|
firstChunk = chunk;
|
|
827
896
|
if (!done && chunk) {
|
|
828
897
|
try {
|
|
@@ -859,6 +928,71 @@ export class FileTypeParser {
|
|
|
859
928
|
return newStream;
|
|
860
929
|
}
|
|
861
930
|
|
|
931
|
+
async detectGzip(tokenizer) {
|
|
932
|
+
if (this.gzipProbeDepth >= maximumNestedGzipProbeDepth) {
|
|
933
|
+
return {
|
|
934
|
+
ext: 'gz',
|
|
935
|
+
mime: 'application/gzip',
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const gzipHandler = new GzipHandler(tokenizer);
|
|
940
|
+
const limitedInflatedStream = createByteLimitedReadableStream(gzipHandler.inflate(), maximumNestedGzipDetectionSizeInBytes);
|
|
941
|
+
const hasUnknownSize = hasUnknownFileSize(tokenizer);
|
|
942
|
+
let timeout;
|
|
943
|
+
let probeSignal;
|
|
944
|
+
let probeParser;
|
|
945
|
+
let compressedFileType;
|
|
946
|
+
|
|
947
|
+
if (hasUnknownSize) {
|
|
948
|
+
const timeoutController = new AbortController();
|
|
949
|
+
timeout = setTimeout(() => {
|
|
950
|
+
timeoutController.abort(new DOMException(`Operation timed out after ${unknownSizeGzipProbeTimeoutInMilliseconds} ms`, 'TimeoutError'));
|
|
951
|
+
}, unknownSizeGzipProbeTimeoutInMilliseconds);
|
|
952
|
+
probeSignal = this.options.signal === undefined
|
|
953
|
+
? timeoutController.signal
|
|
954
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
955
|
+
: AbortSignal.any([this.options.signal, timeoutController.signal]);
|
|
956
|
+
probeParser = new FileTypeParser({
|
|
957
|
+
...this.options,
|
|
958
|
+
signal: probeSignal,
|
|
959
|
+
});
|
|
960
|
+
probeParser.gzipProbeDepth = this.gzipProbeDepth + 1;
|
|
961
|
+
} else {
|
|
962
|
+
this.gzipProbeDepth++;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
compressedFileType = await (probeParser ?? this).fromStream(limitedInflatedStream);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
if (
|
|
969
|
+
error?.name === 'AbortError'
|
|
970
|
+
&& probeSignal?.reason?.name !== 'TimeoutError'
|
|
971
|
+
) {
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Timeout, decompression, or inner-detection failures are expected for non-tar gzip files.
|
|
976
|
+
} finally {
|
|
977
|
+
clearTimeout(timeout);
|
|
978
|
+
if (!hasUnknownSize) {
|
|
979
|
+
this.gzipProbeDepth--;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (compressedFileType?.ext === 'tar') {
|
|
984
|
+
return {
|
|
985
|
+
ext: 'tar.gz',
|
|
986
|
+
mime: 'application/gzip',
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return {
|
|
991
|
+
ext: 'gz',
|
|
992
|
+
mime: 'application/gzip',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
862
996
|
check(header, options) {
|
|
863
997
|
return _check(this.buffer, header, options);
|
|
864
998
|
}
|
|
@@ -878,6 +1012,13 @@ export class FileTypeParser {
|
|
|
878
1012
|
|
|
879
1013
|
this.tokenizer = tokenizer;
|
|
880
1014
|
|
|
1015
|
+
if (hasUnknownFileSize(tokenizer)) {
|
|
1016
|
+
await tokenizer.peekBuffer(this.buffer, {length: 3, mayBeLess: true});
|
|
1017
|
+
if (this.check([0x1F, 0x8B, 0x8])) {
|
|
1018
|
+
return this.detectGzip(tokenizer);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
881
1022
|
await tokenizer.peekBuffer(this.buffer, {length: 32, mayBeLess: true});
|
|
882
1023
|
|
|
883
1024
|
// -- 2-byte signatures --
|
|
@@ -981,41 +1122,7 @@ export class FileTypeParser {
|
|
|
981
1122
|
}
|
|
982
1123
|
|
|
983
1124
|
if (this.check([0x1F, 0x8B, 0x8])) {
|
|
984
|
-
|
|
985
|
-
return {
|
|
986
|
-
ext: 'gz',
|
|
987
|
-
mime: 'application/gzip',
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const gzipHandler = new GzipHandler(tokenizer);
|
|
992
|
-
const limitedInflatedStream = createByteLimitedReadableStream(gzipHandler.inflate(), maximumNestedGzipDetectionSizeInBytes);
|
|
993
|
-
let compressedFileType;
|
|
994
|
-
try {
|
|
995
|
-
this.gzipProbeDepth++;
|
|
996
|
-
compressedFileType = await this.fromStream(limitedInflatedStream);
|
|
997
|
-
} catch (error) {
|
|
998
|
-
if (error?.name === 'AbortError') {
|
|
999
|
-
throw error;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// Decompression or inner-detection failures are expected for non-tar gzip files.
|
|
1003
|
-
} finally {
|
|
1004
|
-
this.gzipProbeDepth--;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// We only need enough inflated bytes to confidently decide whether this is tar.gz.
|
|
1008
|
-
if (compressedFileType?.ext === 'tar') {
|
|
1009
|
-
return {
|
|
1010
|
-
ext: 'tar.gz',
|
|
1011
|
-
mime: 'application/gzip',
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return {
|
|
1016
|
-
ext: 'gz',
|
|
1017
|
-
mime: 'application/gzip',
|
|
1018
|
-
};
|
|
1125
|
+
return this.detectGzip(tokenizer);
|
|
1019
1126
|
}
|
|
1020
1127
|
|
|
1021
1128
|
if (this.check([0x42, 0x5A, 0x68])) {
|
|
@@ -1076,7 +1183,7 @@ export class FileTypeParser {
|
|
|
1076
1183
|
}
|
|
1077
1184
|
|
|
1078
1185
|
this.detectionReentryCount++;
|
|
1079
|
-
return this.
|
|
1186
|
+
return this.parseTokenizer(tokenizer, this.detectionReentryCount); // Skip ID3 header, recursion
|
|
1080
1187
|
}
|
|
1081
1188
|
|
|
1082
1189
|
// Musepack, SV7
|
package/index.js
CHANGED
|
@@ -29,7 +29,8 @@ function isTokenizerStreamBoundsError(error) {
|
|
|
29
29
|
|
|
30
30
|
export class FileTypeParser extends DefaultFileTypeParser {
|
|
31
31
|
async fromStream(stream) {
|
|
32
|
-
|
|
32
|
+
this.options.signal?.throwIfAborted();
|
|
33
|
+
const tokenizer = await (stream instanceof WebReadableStream ? this.createTokenizerFromWebStream(stream) : strtok3.fromStream(stream, this.getTokenizerOptions()));
|
|
33
34
|
try {
|
|
34
35
|
return await super.fromTokenizer(tokenizer);
|
|
35
36
|
} catch (error) {
|
|
@@ -39,11 +40,18 @@ export class FileTypeParser extends DefaultFileTypeParser {
|
|
|
39
40
|
|
|
40
41
|
throw error;
|
|
41
42
|
} finally {
|
|
42
|
-
|
|
43
|
+
// TODO: Remove this when `strtok3.fromStream()` closes the underlying Readable instead of only aborting tokenizer reads.
|
|
44
|
+
if (
|
|
45
|
+
stream instanceof Readable
|
|
46
|
+
&& !stream.destroyed
|
|
47
|
+
) {
|
|
48
|
+
stream.destroy();
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
async fromFile(path) {
|
|
54
|
+
this.options.signal?.throwIfAborted();
|
|
47
55
|
// TODO: Remove this when `strtok3.fromFile()` safely rejects non-regular filesystem objects without a pathname race.
|
|
48
56
|
const fileHandle = await fs.open(path, fileSystemConstants.O_RDONLY | fileSystemConstants.O_NONBLOCK);
|
|
49
57
|
const fileStat = await fileHandle.stat();
|
|
@@ -59,11 +67,7 @@ export class FileTypeParser extends DefaultFileTypeParser {
|
|
|
59
67
|
size: fileStat.size,
|
|
60
68
|
},
|
|
61
69
|
});
|
|
62
|
-
|
|
63
|
-
return await super.fromTokenizer(tokenizer);
|
|
64
|
-
} finally {
|
|
65
|
-
await tokenizer.close();
|
|
66
|
-
}
|
|
70
|
+
return super.fromTokenizer(tokenizer);
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
async toDetectionStream(readableStream, options = {}) {
|
|
@@ -71,36 +75,69 @@ export class FileTypeParser extends DefaultFileTypeParser {
|
|
|
71
75
|
return super.toDetectionStream(readableStream, options);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
const sampleSize =
|
|
78
|
+
const {sampleSize = reasonableDetectionSizeInBytes} = options;
|
|
79
|
+
const {signal} = this.options;
|
|
80
|
+
const normalizedSampleSize = normalizeSampleSize(sampleSize);
|
|
81
|
+
|
|
82
|
+
signal?.throwIfAborted();
|
|
75
83
|
|
|
76
84
|
return new Promise((resolve, reject) => {
|
|
77
|
-
|
|
85
|
+
let isSettled = false;
|
|
86
|
+
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
readableStream.off('error', onError);
|
|
89
|
+
readableStream.off('readable', onReadable);
|
|
90
|
+
signal?.removeEventListener('abort', onAbort);
|
|
91
|
+
};
|
|
78
92
|
|
|
79
|
-
|
|
93
|
+
const settle = (callback, value) => {
|
|
94
|
+
if (isSettled) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
isSettled = true;
|
|
99
|
+
cleanup();
|
|
100
|
+
callback(value);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onError = error => {
|
|
104
|
+
settle(reject, error);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
if (!readableStream.destroyed) {
|
|
109
|
+
readableStream.destroy();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
settle(reject, signal.reason);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const onReadable = () => {
|
|
80
116
|
(async () => {
|
|
81
117
|
try {
|
|
82
|
-
// Set up output stream
|
|
83
118
|
const pass = new PassThrough();
|
|
84
119
|
const outputStream = pipeline ? pipeline(readableStream, pass, () => {}) : readableStream.pipe(pass);
|
|
85
|
-
|
|
86
|
-
// Read the input stream and detect the filetype
|
|
87
|
-
const chunk = readableStream.read(sampleSize) ?? readableStream.read() ?? new Uint8Array(0);
|
|
120
|
+
const chunk = readableStream.read(normalizedSampleSize) ?? readableStream.read() ?? new Uint8Array(0);
|
|
88
121
|
try {
|
|
89
122
|
pass.fileType = await this.fromBuffer(chunk);
|
|
90
123
|
} catch (error) {
|
|
91
124
|
if (error instanceof strtok3.EndOfStreamError) {
|
|
92
125
|
pass.fileType = undefined;
|
|
93
126
|
} else {
|
|
94
|
-
reject
|
|
127
|
+
settle(reject, error);
|
|
95
128
|
}
|
|
96
129
|
}
|
|
97
130
|
|
|
98
|
-
resolve
|
|
131
|
+
settle(resolve, outputStream);
|
|
99
132
|
} catch (error) {
|
|
100
|
-
reject
|
|
133
|
+
settle(reject, error);
|
|
101
134
|
}
|
|
102
135
|
})();
|
|
103
|
-
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
readableStream.on('error', onError);
|
|
139
|
+
readableStream.once('readable', onReadable);
|
|
140
|
+
signal?.addEventListener('abort', onAbort, {once: true});
|
|
104
141
|
});
|
|
105
142
|
}
|
|
106
143
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -441,7 +441,7 @@ import {FileTypeParser} from 'file-type';
|
|
|
441
441
|
|
|
442
442
|
const abortController = new AbortController()
|
|
443
443
|
|
|
444
|
-
const parser = new FileTypeParser({
|
|
444
|
+
const parser = new FileTypeParser({signal: abortController.signal});
|
|
445
445
|
|
|
446
446
|
const promise = parser.fromStream(blob.stream());
|
|
447
447
|
|