file-type 21.3.4 → 22.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.
@@ -0,0 +1,123 @@
1
+ import * as Token from 'token-types';
2
+ import * as strtok3 from 'strtok3/core';
3
+ import {
4
+ ParserHardLimitError,
5
+ safeIgnore,
6
+ hasUnknownFileSize,
7
+ hasExceededUnknownSizeScanBudget,
8
+ } from '../parser.js';
9
+
10
+ const maximumPngChunkCount = 512;
11
+ const maximumPngStreamScanBudgetInBytes = 16 * 1024 * 1024;
12
+ const maximumPngChunkSizeInBytes = 1024 * 1024;
13
+
14
+ function isPngAncillaryChunk(type) {
15
+ return (type.codePointAt(0) & 0x20) !== 0;
16
+ }
17
+
18
+ export async function detectPng(tokenizer) {
19
+ const pngFileType = {
20
+ ext: 'png',
21
+ mime: 'image/png',
22
+ };
23
+
24
+ const apngFileType = {
25
+ ext: 'apng',
26
+ mime: 'image/apng',
27
+ };
28
+
29
+ // APNG format (https://wiki.mozilla.org/APNG_Specification)
30
+ // 1. Find the first IDAT (image data) chunk (49 44 41 54)
31
+ // 2. Check if there is an "acTL" chunk before the IDAT one (61 63 54 4C)
32
+
33
+ // Offset calculated as follows:
34
+ // - 8 bytes: PNG signature
35
+ // - 4 (length) + 4 (chunk type) + 13 (chunk data) + 4 (CRC): IHDR chunk
36
+
37
+ await tokenizer.ignore(8); // ignore PNG signature
38
+
39
+ async function readChunkHeader() {
40
+ return {
41
+ length: await tokenizer.readToken(Token.INT32_BE),
42
+ type: await tokenizer.readToken(new Token.StringType(4, 'latin1')),
43
+ };
44
+ }
45
+
46
+ const isUnknownPngStream = hasUnknownFileSize(tokenizer);
47
+ const pngScanStart = tokenizer.position;
48
+ let pngChunkCount = 0;
49
+ let hasSeenImageHeader = false;
50
+ do {
51
+ pngChunkCount++;
52
+ if (pngChunkCount > maximumPngChunkCount) {
53
+ break;
54
+ }
55
+
56
+ if (hasExceededUnknownSizeScanBudget(tokenizer, pngScanStart, maximumPngStreamScanBudgetInBytes)) {
57
+ break;
58
+ }
59
+
60
+ const previousPosition = tokenizer.position;
61
+ const chunk = await readChunkHeader();
62
+ if (chunk.length < 0) {
63
+ return; // Invalid chunk length
64
+ }
65
+
66
+ if (chunk.type === 'IHDR') {
67
+ // PNG requires the first real image header to be a 13-byte IHDR chunk.
68
+ if (chunk.length !== 13) {
69
+ return;
70
+ }
71
+
72
+ hasSeenImageHeader = true;
73
+ }
74
+
75
+ switch (chunk.type) {
76
+ case 'IDAT':
77
+ return pngFileType;
78
+ case 'acTL':
79
+ return apngFileType;
80
+ default:
81
+ if (
82
+ !hasSeenImageHeader
83
+ && chunk.type !== 'CgBI'
84
+ ) {
85
+ return;
86
+ }
87
+
88
+ if (
89
+ isUnknownPngStream
90
+ && chunk.length > maximumPngChunkSizeInBytes
91
+ ) {
92
+ // Avoid huge attacker-controlled skips when probing unknown-size streams.
93
+ return hasSeenImageHeader && isPngAncillaryChunk(chunk.type) ? pngFileType : undefined;
94
+ }
95
+
96
+ try {
97
+ await safeIgnore(tokenizer, chunk.length + 4, {
98
+ maximumLength: isUnknownPngStream ? maximumPngChunkSizeInBytes + 4 : tokenizer.fileInfo.size,
99
+ reason: 'PNG chunk payload',
100
+ }); // Ignore chunk-data + CRC
101
+ } catch (error) {
102
+ if (
103
+ !isUnknownPngStream
104
+ && (
105
+ error instanceof ParserHardLimitError
106
+ || error instanceof strtok3.EndOfStreamError
107
+ )
108
+ ) {
109
+ return pngFileType;
110
+ }
111
+
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ // Safeguard against malformed files: bail if the position did not advance.
117
+ if (tokenizer.position <= previousPosition) {
118
+ break;
119
+ }
120
+ } while (tokenizer.position + 8 < tokenizer.fileInfo.size);
121
+
122
+ return pngFileType;
123
+ }