mime-bytes 0.29.2 → 0.31.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/esm/index.js +2 -0
- package/esm/type-agreement.js +153 -0
- package/index.d.ts +1 -0
- package/index.js +6 -1
- package/package.json +2 -2
- package/type-agreement.d.ts +72 -0
- package/type-agreement.js +158 -0
package/esm/index.js
CHANGED
|
@@ -5,6 +5,8 @@ export { defaultDetector, detectFromBuffer, detectFromExtension, detectFromStrea
|
|
|
5
5
|
export { CONTENT_TYPE_MAPPINGS, detectCharset, FILE_TYPES, getContentTypeByExtension, getFileTypeByExtension, getFileTypeByMagicBytes, getFileTypesByCategory } from './file-types-registry';
|
|
6
6
|
// Export peek stream functionality
|
|
7
7
|
export { BufferPeekStream, peek } from './peak';
|
|
8
|
+
// Export declared-type / extension / magic-byte agreement checking
|
|
9
|
+
export { checkTypeAgreement, mimeTypeForFilename, mimeTypesAgree } from './type-agreement';
|
|
8
10
|
// Export utility functions
|
|
9
11
|
export * from './utils/extensions';
|
|
10
12
|
export * from './utils/magic-bytes';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agreement between the three things that claim to say what a file is.
|
|
3
|
+
*
|
|
4
|
+
* An upload arrives with up to three independent statements about its type:
|
|
5
|
+
*
|
|
6
|
+
* * the **declared** MIME type — what the client says it is;
|
|
7
|
+
* * the **extension** on the filename — what the client says it is again, in a
|
|
8
|
+
* form the operating system and the browser act on;
|
|
9
|
+
* * the **magic bytes** — what it actually is.
|
|
10
|
+
*
|
|
11
|
+
* Only the third is evidence. The other two are the attack surface: a browser
|
|
12
|
+
* that fetches `avatar.jpg` and receives HTML will happily run the script in it,
|
|
13
|
+
* which is why `html-as-jpg.jpg` is a fixture in this repo rather than a
|
|
14
|
+
* curiosity. So the rule is agreement, not precedence: when two statements
|
|
15
|
+
* describe different kinds of file, the upload is rejected instead of being
|
|
16
|
+
* silently relabelled.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately conservative about what counts as disagreement, because a false
|
|
19
|
+
* rejection is a broken upload:
|
|
20
|
+
*
|
|
21
|
+
* * a statement that is absent or `application/octet-stream` says nothing, and
|
|
22
|
+
* cannot contradict anything;
|
|
23
|
+
* * text formats are indistinguishable by leading bytes — a `.csv`, a `.json`
|
|
24
|
+
* and an `.svg` are all "some text" — so text is compared as one family;
|
|
25
|
+
* * container formats really are the thing they contain: a `.docx` *is* a ZIP,
|
|
26
|
+
* an `.m4a` *is* an MP4 box stream. Those pairs are declared equivalent.
|
|
27
|
+
*/
|
|
28
|
+
import { getContentTypeByExtension } from './file-types-registry';
|
|
29
|
+
import { getExtension } from './utils/extensions';
|
|
30
|
+
import { normalizeMimeType, resolveMimeAlias } from './utils/mime-types';
|
|
31
|
+
/** Says nothing about the content, so it can never contradict anything. */
|
|
32
|
+
const UNKNOWN_MIME = 'application/octet-stream';
|
|
33
|
+
/**
|
|
34
|
+
* Types whose bytes are text, and are therefore mutually indistinguishable to a
|
|
35
|
+
* magic-byte sniff: it can tell text from binary, not JSON from CSV.
|
|
36
|
+
*/
|
|
37
|
+
const TEXT_LIKE = [
|
|
38
|
+
'text/',
|
|
39
|
+
'application/json',
|
|
40
|
+
'application/ld+json',
|
|
41
|
+
'application/xml',
|
|
42
|
+
'application/xhtml+xml',
|
|
43
|
+
'application/javascript',
|
|
44
|
+
'application/ecmascript',
|
|
45
|
+
'application/typescript',
|
|
46
|
+
'application/x-httpd-php',
|
|
47
|
+
'application/x-sh',
|
|
48
|
+
'application/x-csh',
|
|
49
|
+
'application/x-python',
|
|
50
|
+
'application/x-ruby',
|
|
51
|
+
'application/x-perl',
|
|
52
|
+
'application/x-yaml',
|
|
53
|
+
'application/yaml',
|
|
54
|
+
'application/toml',
|
|
55
|
+
'application/sql',
|
|
56
|
+
'image/svg+xml',
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Formats that are physically another format: their leading bytes are the
|
|
60
|
+
* container's, so a sniff naming the container is not a contradiction.
|
|
61
|
+
*
|
|
62
|
+
* Keyed by the container the bytes look like; the values are the specific types
|
|
63
|
+
* that legitimately sit inside it.
|
|
64
|
+
*/
|
|
65
|
+
const CONTAINER_FAMILIES = {
|
|
66
|
+
'application/zip': [
|
|
67
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
68
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
69
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
70
|
+
'application/vnd.oasis.opendocument.text',
|
|
71
|
+
'application/vnd.oasis.opendocument.spreadsheet',
|
|
72
|
+
'application/vnd.oasis.opendocument.presentation',
|
|
73
|
+
'application/epub+zip',
|
|
74
|
+
'application/java-archive',
|
|
75
|
+
'application/vnd.android.package-archive',
|
|
76
|
+
'application/x-xpinstall',
|
|
77
|
+
],
|
|
78
|
+
'video/mp4': ['audio/mp4', 'audio/x-m4a', 'video/quicktime', 'video/x-m4v'],
|
|
79
|
+
'audio/mp4': ['video/mp4', 'audio/x-m4a', 'video/x-m4v'],
|
|
80
|
+
'video/quicktime': ['video/mp4'],
|
|
81
|
+
'application/x-riff': ['audio/wav', 'audio/x-wav', 'video/avi', 'video/x-msvideo', 'image/webp'],
|
|
82
|
+
'application/gzip': ['application/tar', 'application/x-gtar'],
|
|
83
|
+
};
|
|
84
|
+
function isTextLike(mime) {
|
|
85
|
+
return TEXT_LIKE.some((prefix) => mime === prefix || mime.startsWith(prefix));
|
|
86
|
+
}
|
|
87
|
+
/** A statement that carries no information about the content. */
|
|
88
|
+
function isSilent(mime) {
|
|
89
|
+
return !mime || mime === UNKNOWN_MIME;
|
|
90
|
+
}
|
|
91
|
+
function canonical(mime) {
|
|
92
|
+
if (!mime)
|
|
93
|
+
return null;
|
|
94
|
+
const normalized = resolveMimeAlias(normalizeMimeType(mime));
|
|
95
|
+
return normalized || null;
|
|
96
|
+
}
|
|
97
|
+
function inSameContainerFamily(a, b) {
|
|
98
|
+
return (CONTAINER_FAMILIES[a]?.includes(b) ?? false) || (CONTAINER_FAMILIES[b]?.includes(a) ?? false);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Whether two type statements describe the same kind of file.
|
|
102
|
+
*
|
|
103
|
+
* Silence agrees with everything; text agrees with text; a container agrees with
|
|
104
|
+
* what it contains. Everything else must match exactly.
|
|
105
|
+
*/
|
|
106
|
+
export function mimeTypesAgree(a, b) {
|
|
107
|
+
const left = canonical(a);
|
|
108
|
+
const right = canonical(b);
|
|
109
|
+
if (isSilent(left) || isSilent(right))
|
|
110
|
+
return true;
|
|
111
|
+
if (left === right)
|
|
112
|
+
return true;
|
|
113
|
+
if (isTextLike(left) && isTextLike(right))
|
|
114
|
+
return true;
|
|
115
|
+
return inSameContainerFamily(left, right);
|
|
116
|
+
}
|
|
117
|
+
/** The MIME type a filename's extension claims, or null when it claims nothing. */
|
|
118
|
+
export function mimeTypeForFilename(filename) {
|
|
119
|
+
if (!filename)
|
|
120
|
+
return null;
|
|
121
|
+
const extension = getExtension(filename);
|
|
122
|
+
if (!extension)
|
|
123
|
+
return null;
|
|
124
|
+
return canonical(getContentTypeByExtension(extension));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check the declared type, the filename's extension and the detected bytes
|
|
128
|
+
* against each other.
|
|
129
|
+
*
|
|
130
|
+
* Returns the first disagreement found, bytes-first: when bytes are available
|
|
131
|
+
* they are the evidence, so "the extension lies about the bytes" is the more
|
|
132
|
+
* useful thing to report than "the two client claims differ".
|
|
133
|
+
*/
|
|
134
|
+
export function checkTypeAgreement(input) {
|
|
135
|
+
const { filename } = input;
|
|
136
|
+
const declaredMime = canonical(input.declaredMime);
|
|
137
|
+
const detectedMime = canonical(input.detectedMime);
|
|
138
|
+
const extensionMime = mimeTypeForFilename(filename);
|
|
139
|
+
const violation = (code, message) => ({
|
|
140
|
+
ok: false,
|
|
141
|
+
violation: { code, message, filename, extensionMime, declaredMime, detectedMime },
|
|
142
|
+
});
|
|
143
|
+
if (!mimeTypesAgree(extensionMime, detectedMime)) {
|
|
144
|
+
return violation('EXTENSION_BYTES_MISMATCH', `file "${filename}" is named as ${extensionMime} but its bytes are ${detectedMime}`);
|
|
145
|
+
}
|
|
146
|
+
if (!mimeTypesAgree(declaredMime, detectedMime)) {
|
|
147
|
+
return violation('DECLARED_BYTES_MISMATCH', `upload declares ${declaredMime} but its bytes are ${detectedMime}`);
|
|
148
|
+
}
|
|
149
|
+
if (!mimeTypesAgree(declaredMime, extensionMime)) {
|
|
150
|
+
return violation('DECLARED_EXTENSION_MISMATCH', `upload declares ${declaredMime} but is named "${filename}", which is ${extensionMime}`);
|
|
151
|
+
}
|
|
152
|
+
return { ok: true };
|
|
153
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { defaultDetector, detectFromBuffer, detectFromExtension, detectFromStream, FileTypeDetector, FileTypeDetectorOptions } from './file-type-detector';
|
|
2
2
|
export { CONTENT_TYPE_MAPPINGS, ContentTypeMapping, detectCharset, DetectionResult, FILE_TYPES, FileTypeDefinition, getContentTypeByExtension, getFileTypeByExtension, getFileTypeByMagicBytes, getFileTypesByCategory } from './file-types-registry';
|
|
3
3
|
export { BufferPeekStream, peek, PeekCallback, PeekPromise, PeekStreamOptions } from './peak';
|
|
4
|
+
export { checkTypeAgreement, mimeTypeForFilename, mimeTypesAgree, TypeAgreementInput, TypeAgreementResult, TypeAgreementViolation } from './type-agreement';
|
|
4
5
|
export * from './utils/extensions';
|
|
5
6
|
export * from './utils/magic-bytes';
|
|
6
7
|
export * from './utils/mime-types';
|
package/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.peek = exports.BufferPeekStream = exports.getFileTypesByCategory = exports.getFileTypeByMagicBytes = exports.getFileTypeByExtension = exports.getContentTypeByExtension = exports.FILE_TYPES = exports.detectCharset = exports.CONTENT_TYPE_MAPPINGS = exports.FileTypeDetector = exports.detectFromStream = exports.detectFromExtension = exports.detectFromBuffer = exports.defaultDetector = void 0;
|
|
18
|
+
exports.mimeTypesAgree = exports.mimeTypeForFilename = exports.checkTypeAgreement = exports.peek = exports.BufferPeekStream = exports.getFileTypesByCategory = exports.getFileTypeByMagicBytes = exports.getFileTypeByExtension = exports.getContentTypeByExtension = exports.FILE_TYPES = exports.detectCharset = exports.CONTENT_TYPE_MAPPINGS = exports.FileTypeDetector = exports.detectFromStream = exports.detectFromExtension = exports.detectFromBuffer = exports.defaultDetector = void 0;
|
|
19
19
|
// Export the main detector class and convenience functions
|
|
20
20
|
var file_type_detector_1 = require("./file-type-detector");
|
|
21
21
|
Object.defineProperty(exports, "defaultDetector", { enumerable: true, get: function () { return file_type_detector_1.defaultDetector; } });
|
|
@@ -36,6 +36,11 @@ Object.defineProperty(exports, "getFileTypesByCategory", { enumerable: true, get
|
|
|
36
36
|
var peak_1 = require("./peak");
|
|
37
37
|
Object.defineProperty(exports, "BufferPeekStream", { enumerable: true, get: function () { return peak_1.BufferPeekStream; } });
|
|
38
38
|
Object.defineProperty(exports, "peek", { enumerable: true, get: function () { return peak_1.peek; } });
|
|
39
|
+
// Export declared-type / extension / magic-byte agreement checking
|
|
40
|
+
var type_agreement_1 = require("./type-agreement");
|
|
41
|
+
Object.defineProperty(exports, "checkTypeAgreement", { enumerable: true, get: function () { return type_agreement_1.checkTypeAgreement; } });
|
|
42
|
+
Object.defineProperty(exports, "mimeTypeForFilename", { enumerable: true, get: function () { return type_agreement_1.mimeTypeForFilename; } });
|
|
43
|
+
Object.defineProperty(exports, "mimeTypesAgree", { enumerable: true, get: function () { return type_agreement_1.mimeTypesAgree; } });
|
|
39
44
|
// Export utility functions
|
|
40
45
|
__exportStar(require("./utils/extensions"), exports);
|
|
41
46
|
__exportStar(require("./utils/magic-bytes"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mime-bytes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Lightning-fast file type detection using magic bytes (file signatures) with a focus on stream processing and minimal memory usage",
|
|
6
6
|
"main": "index.js",
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"glob": "^13.0.6",
|
|
34
34
|
"makage": "^0.3.0"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "d0d796b7eb0d16c8c19405a49c0ca62ce3df117b"
|
|
37
37
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agreement between the three things that claim to say what a file is.
|
|
3
|
+
*
|
|
4
|
+
* An upload arrives with up to three independent statements about its type:
|
|
5
|
+
*
|
|
6
|
+
* * the **declared** MIME type — what the client says it is;
|
|
7
|
+
* * the **extension** on the filename — what the client says it is again, in a
|
|
8
|
+
* form the operating system and the browser act on;
|
|
9
|
+
* * the **magic bytes** — what it actually is.
|
|
10
|
+
*
|
|
11
|
+
* Only the third is evidence. The other two are the attack surface: a browser
|
|
12
|
+
* that fetches `avatar.jpg` and receives HTML will happily run the script in it,
|
|
13
|
+
* which is why `html-as-jpg.jpg` is a fixture in this repo rather than a
|
|
14
|
+
* curiosity. So the rule is agreement, not precedence: when two statements
|
|
15
|
+
* describe different kinds of file, the upload is rejected instead of being
|
|
16
|
+
* silently relabelled.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately conservative about what counts as disagreement, because a false
|
|
19
|
+
* rejection is a broken upload:
|
|
20
|
+
*
|
|
21
|
+
* * a statement that is absent or `application/octet-stream` says nothing, and
|
|
22
|
+
* cannot contradict anything;
|
|
23
|
+
* * text formats are indistinguishable by leading bytes — a `.csv`, a `.json`
|
|
24
|
+
* and an `.svg` are all "some text" — so text is compared as one family;
|
|
25
|
+
* * container formats really are the thing they contain: a `.docx` *is* a ZIP,
|
|
26
|
+
* an `.m4a` *is* an MP4 box stream. Those pairs are declared equivalent.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Whether two type statements describe the same kind of file.
|
|
30
|
+
*
|
|
31
|
+
* Silence agrees with everything; text agrees with text; a container agrees with
|
|
32
|
+
* what it contains. Everything else must match exactly.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mimeTypesAgree(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
35
|
+
/** The MIME type a filename's extension claims, or null when it claims nothing. */
|
|
36
|
+
export declare function mimeTypeForFilename(filename: string | null | undefined): string | null;
|
|
37
|
+
export interface TypeAgreementInput {
|
|
38
|
+
/** The uploaded filename, if any. Its extension is one of the claims. */
|
|
39
|
+
filename?: string | null;
|
|
40
|
+
/** The MIME type the client declared for the upload. */
|
|
41
|
+
declaredMime?: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* The MIME type detected from the file's leading bytes. Omitted when the bytes
|
|
44
|
+
* have not been seen — a presigned upload before its confirm read — in which
|
|
45
|
+
* case only the two client-supplied claims are compared with each other.
|
|
46
|
+
*/
|
|
47
|
+
detectedMime?: string | null;
|
|
48
|
+
}
|
|
49
|
+
export type TypeAgreementViolation = {
|
|
50
|
+
code: 'EXTENSION_BYTES_MISMATCH' | 'DECLARED_BYTES_MISMATCH' | 'DECLARED_EXTENSION_MISMATCH';
|
|
51
|
+
message: string;
|
|
52
|
+
filename?: string | null;
|
|
53
|
+
extensionMime?: string | null;
|
|
54
|
+
declaredMime?: string | null;
|
|
55
|
+
detectedMime?: string | null;
|
|
56
|
+
};
|
|
57
|
+
export type TypeAgreementResult = {
|
|
58
|
+
ok: true;
|
|
59
|
+
violation?: undefined;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
violation: TypeAgreementViolation;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Check the declared type, the filename's extension and the detected bytes
|
|
66
|
+
* against each other.
|
|
67
|
+
*
|
|
68
|
+
* Returns the first disagreement found, bytes-first: when bytes are available
|
|
69
|
+
* they are the evidence, so "the extension lies about the bytes" is the more
|
|
70
|
+
* useful thing to report than "the two client claims differ".
|
|
71
|
+
*/
|
|
72
|
+
export declare function checkTypeAgreement(input: TypeAgreementInput): TypeAgreementResult;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agreement between the three things that claim to say what a file is.
|
|
4
|
+
*
|
|
5
|
+
* An upload arrives with up to three independent statements about its type:
|
|
6
|
+
*
|
|
7
|
+
* * the **declared** MIME type — what the client says it is;
|
|
8
|
+
* * the **extension** on the filename — what the client says it is again, in a
|
|
9
|
+
* form the operating system and the browser act on;
|
|
10
|
+
* * the **magic bytes** — what it actually is.
|
|
11
|
+
*
|
|
12
|
+
* Only the third is evidence. The other two are the attack surface: a browser
|
|
13
|
+
* that fetches `avatar.jpg` and receives HTML will happily run the script in it,
|
|
14
|
+
* which is why `html-as-jpg.jpg` is a fixture in this repo rather than a
|
|
15
|
+
* curiosity. So the rule is agreement, not precedence: when two statements
|
|
16
|
+
* describe different kinds of file, the upload is rejected instead of being
|
|
17
|
+
* silently relabelled.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately conservative about what counts as disagreement, because a false
|
|
20
|
+
* rejection is a broken upload:
|
|
21
|
+
*
|
|
22
|
+
* * a statement that is absent or `application/octet-stream` says nothing, and
|
|
23
|
+
* cannot contradict anything;
|
|
24
|
+
* * text formats are indistinguishable by leading bytes — a `.csv`, a `.json`
|
|
25
|
+
* and an `.svg` are all "some text" — so text is compared as one family;
|
|
26
|
+
* * container formats really are the thing they contain: a `.docx` *is* a ZIP,
|
|
27
|
+
* an `.m4a` *is* an MP4 box stream. Those pairs are declared equivalent.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.mimeTypesAgree = mimeTypesAgree;
|
|
31
|
+
exports.mimeTypeForFilename = mimeTypeForFilename;
|
|
32
|
+
exports.checkTypeAgreement = checkTypeAgreement;
|
|
33
|
+
const file_types_registry_1 = require("./file-types-registry");
|
|
34
|
+
const extensions_1 = require("./utils/extensions");
|
|
35
|
+
const mime_types_1 = require("./utils/mime-types");
|
|
36
|
+
/** Says nothing about the content, so it can never contradict anything. */
|
|
37
|
+
const UNKNOWN_MIME = 'application/octet-stream';
|
|
38
|
+
/**
|
|
39
|
+
* Types whose bytes are text, and are therefore mutually indistinguishable to a
|
|
40
|
+
* magic-byte sniff: it can tell text from binary, not JSON from CSV.
|
|
41
|
+
*/
|
|
42
|
+
const TEXT_LIKE = [
|
|
43
|
+
'text/',
|
|
44
|
+
'application/json',
|
|
45
|
+
'application/ld+json',
|
|
46
|
+
'application/xml',
|
|
47
|
+
'application/xhtml+xml',
|
|
48
|
+
'application/javascript',
|
|
49
|
+
'application/ecmascript',
|
|
50
|
+
'application/typescript',
|
|
51
|
+
'application/x-httpd-php',
|
|
52
|
+
'application/x-sh',
|
|
53
|
+
'application/x-csh',
|
|
54
|
+
'application/x-python',
|
|
55
|
+
'application/x-ruby',
|
|
56
|
+
'application/x-perl',
|
|
57
|
+
'application/x-yaml',
|
|
58
|
+
'application/yaml',
|
|
59
|
+
'application/toml',
|
|
60
|
+
'application/sql',
|
|
61
|
+
'image/svg+xml',
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* Formats that are physically another format: their leading bytes are the
|
|
65
|
+
* container's, so a sniff naming the container is not a contradiction.
|
|
66
|
+
*
|
|
67
|
+
* Keyed by the container the bytes look like; the values are the specific types
|
|
68
|
+
* that legitimately sit inside it.
|
|
69
|
+
*/
|
|
70
|
+
const CONTAINER_FAMILIES = {
|
|
71
|
+
'application/zip': [
|
|
72
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
73
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
74
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
75
|
+
'application/vnd.oasis.opendocument.text',
|
|
76
|
+
'application/vnd.oasis.opendocument.spreadsheet',
|
|
77
|
+
'application/vnd.oasis.opendocument.presentation',
|
|
78
|
+
'application/epub+zip',
|
|
79
|
+
'application/java-archive',
|
|
80
|
+
'application/vnd.android.package-archive',
|
|
81
|
+
'application/x-xpinstall',
|
|
82
|
+
],
|
|
83
|
+
'video/mp4': ['audio/mp4', 'audio/x-m4a', 'video/quicktime', 'video/x-m4v'],
|
|
84
|
+
'audio/mp4': ['video/mp4', 'audio/x-m4a', 'video/x-m4v'],
|
|
85
|
+
'video/quicktime': ['video/mp4'],
|
|
86
|
+
'application/x-riff': ['audio/wav', 'audio/x-wav', 'video/avi', 'video/x-msvideo', 'image/webp'],
|
|
87
|
+
'application/gzip': ['application/tar', 'application/x-gtar'],
|
|
88
|
+
};
|
|
89
|
+
function isTextLike(mime) {
|
|
90
|
+
return TEXT_LIKE.some((prefix) => mime === prefix || mime.startsWith(prefix));
|
|
91
|
+
}
|
|
92
|
+
/** A statement that carries no information about the content. */
|
|
93
|
+
function isSilent(mime) {
|
|
94
|
+
return !mime || mime === UNKNOWN_MIME;
|
|
95
|
+
}
|
|
96
|
+
function canonical(mime) {
|
|
97
|
+
if (!mime)
|
|
98
|
+
return null;
|
|
99
|
+
const normalized = (0, mime_types_1.resolveMimeAlias)((0, mime_types_1.normalizeMimeType)(mime));
|
|
100
|
+
return normalized || null;
|
|
101
|
+
}
|
|
102
|
+
function inSameContainerFamily(a, b) {
|
|
103
|
+
return (CONTAINER_FAMILIES[a]?.includes(b) ?? false) || (CONTAINER_FAMILIES[b]?.includes(a) ?? false);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether two type statements describe the same kind of file.
|
|
107
|
+
*
|
|
108
|
+
* Silence agrees with everything; text agrees with text; a container agrees with
|
|
109
|
+
* what it contains. Everything else must match exactly.
|
|
110
|
+
*/
|
|
111
|
+
function mimeTypesAgree(a, b) {
|
|
112
|
+
const left = canonical(a);
|
|
113
|
+
const right = canonical(b);
|
|
114
|
+
if (isSilent(left) || isSilent(right))
|
|
115
|
+
return true;
|
|
116
|
+
if (left === right)
|
|
117
|
+
return true;
|
|
118
|
+
if (isTextLike(left) && isTextLike(right))
|
|
119
|
+
return true;
|
|
120
|
+
return inSameContainerFamily(left, right);
|
|
121
|
+
}
|
|
122
|
+
/** The MIME type a filename's extension claims, or null when it claims nothing. */
|
|
123
|
+
function mimeTypeForFilename(filename) {
|
|
124
|
+
if (!filename)
|
|
125
|
+
return null;
|
|
126
|
+
const extension = (0, extensions_1.getExtension)(filename);
|
|
127
|
+
if (!extension)
|
|
128
|
+
return null;
|
|
129
|
+
return canonical((0, file_types_registry_1.getContentTypeByExtension)(extension));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check the declared type, the filename's extension and the detected bytes
|
|
133
|
+
* against each other.
|
|
134
|
+
*
|
|
135
|
+
* Returns the first disagreement found, bytes-first: when bytes are available
|
|
136
|
+
* they are the evidence, so "the extension lies about the bytes" is the more
|
|
137
|
+
* useful thing to report than "the two client claims differ".
|
|
138
|
+
*/
|
|
139
|
+
function checkTypeAgreement(input) {
|
|
140
|
+
const { filename } = input;
|
|
141
|
+
const declaredMime = canonical(input.declaredMime);
|
|
142
|
+
const detectedMime = canonical(input.detectedMime);
|
|
143
|
+
const extensionMime = mimeTypeForFilename(filename);
|
|
144
|
+
const violation = (code, message) => ({
|
|
145
|
+
ok: false,
|
|
146
|
+
violation: { code, message, filename, extensionMime, declaredMime, detectedMime },
|
|
147
|
+
});
|
|
148
|
+
if (!mimeTypesAgree(extensionMime, detectedMime)) {
|
|
149
|
+
return violation('EXTENSION_BYTES_MISMATCH', `file "${filename}" is named as ${extensionMime} but its bytes are ${detectedMime}`);
|
|
150
|
+
}
|
|
151
|
+
if (!mimeTypesAgree(declaredMime, detectedMime)) {
|
|
152
|
+
return violation('DECLARED_BYTES_MISMATCH', `upload declares ${declaredMime} but its bytes are ${detectedMime}`);
|
|
153
|
+
}
|
|
154
|
+
if (!mimeTypesAgree(declaredMime, extensionMime)) {
|
|
155
|
+
return violation('DECLARED_EXTENSION_MISMATCH', `upload declares ${declaredMime} but is named "${filename}", which is ${extensionMime}`);
|
|
156
|
+
}
|
|
157
|
+
return { ok: true };
|
|
158
|
+
}
|