@ryuzaki13/react-foundation-lib 2.18.0 → 2.19.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/dist/file/checkFileSignature.d.ts +8 -0
- package/dist/file/checkFileSignature.d.ts.map +1 -0
- package/dist/file/fileSignatureConstants.d.ts +20 -0
- package/dist/file/fileSignatureConstants.d.ts.map +1 -0
- package/dist/file/fileSignatureTypes.d.ts +15 -0
- package/dist/file/fileSignatureTypes.d.ts.map +1 -0
- package/dist/file/index.d.ts +2 -0
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +237 -68
- package/dist/file/index.js.map +1 -1
- package/dist/file/signature/checkArchiveSignature.d.ts +3 -0
- package/dist/file/signature/checkArchiveSignature.d.ts.map +1 -0
- package/dist/file/signature/checkPdfSignature.d.ts +8 -0
- package/dist/file/signature/checkPdfSignature.d.ts.map +1 -0
- package/dist/file/signature/hasSignatureBytes.d.ts +5 -0
- package/dist/file/signature/hasSignatureBytes.d.ts.map +1 -0
- package/dist/media/index.js +2 -2
- package/dist/media/index.js.map +1 -1
- package/package.json +1 -1
- package/src/file/README.mdx +63 -6
- package/src/media/README.mdx +5 -5
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FileSignatureCheckOptions, type FileSignatureCheckResult } from "./fileSignatureTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Частичная pure-проверка fixed headers: нет FileReader, Node API, декодирования,
|
|
4
|
+
* распаковки или чтения всего файла. Любой match относится только к header.
|
|
5
|
+
* Общий ZIP/OLE marker не подтверждает Office/ODF/XFL; allowlist принадлежит caller.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkFileSignature({ extension, bytes }: FileSignatureCheckOptions): FileSignatureCheckResult;
|
|
8
|
+
//# sourceMappingURL=checkFileSignature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkFileSignature.d.ts","sourceRoot":"","sources":["../../src/file/checkFileSignature.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,yBAAyB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAmErG;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,yBAAyB,GAAG,wBAAwB,CAuB5G"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Приватный словарь результатов; значения сохраняют публичный string union. */
|
|
2
|
+
export declare const FILE_SIGNATURE_RESULT: {
|
|
3
|
+
readonly MATCH: "match";
|
|
4
|
+
readonly MISMATCH: "mismatch";
|
|
5
|
+
readonly UNSUPPORTED: "unsupported";
|
|
6
|
+
};
|
|
7
|
+
/** Только dispatch поддержанных проверок; это не предметный allowlist caller. */
|
|
8
|
+
export declare const FILE_SIGNATURE_EXTENSION: {
|
|
9
|
+
readonly PNG: "png";
|
|
10
|
+
readonly JPG: "jpg";
|
|
11
|
+
readonly JPEG: "jpeg";
|
|
12
|
+
readonly BMP: "bmp";
|
|
13
|
+
readonly PSD: "psd";
|
|
14
|
+
readonly RTF: "rtf";
|
|
15
|
+
readonly RAR: "rar";
|
|
16
|
+
readonly ZIP: "zip";
|
|
17
|
+
readonly SEVEN_ZIP: "7z";
|
|
18
|
+
readonly PDF: "pdf";
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=fileSignatureConstants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSignatureConstants.d.ts","sourceRoot":"","sources":["../../src/file/fileSignatureConstants.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,eAAO,MAAM,qBAAqB;;;;CAIxB,CAAC;AAEX,iFAAiF;AACjF,eAAO,MAAM,wBAAwB;;;;;;;;;;;CAW3B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FILE_SIGNATURE_EXTENSION, type FILE_SIGNATURE_RESULT } from "./fileSignatureConstants";
|
|
2
|
+
/** Bytes принадлежат caller; расширение передаётся без точки и имени файла. */
|
|
3
|
+
export type FileSignatureCheckOptions = Readonly<{
|
|
4
|
+
extension: string;
|
|
5
|
+
bytes: Uint8Array;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* match подтверждает только поддержанный header, не целостность или безопасность
|
|
9
|
+
* файла. unsupported означает отсутствие надёжного решения этой проверкой;
|
|
10
|
+
* допустимость такого файла определяет вызывающая policy, а не foundation.
|
|
11
|
+
*/
|
|
12
|
+
export type FileSignatureCheckResult = (typeof FILE_SIGNATURE_RESULT)[keyof typeof FILE_SIGNATURE_RESULT];
|
|
13
|
+
/** Внутренний archive dispatch; public index не публикует этот тип. */
|
|
14
|
+
export type ArchiveFileSignatureExtension = typeof FILE_SIGNATURE_EXTENSION.RAR | typeof FILE_SIGNATURE_EXTENSION.ZIP | typeof FILE_SIGNATURE_EXTENSION.SEVEN_ZIP;
|
|
15
|
+
//# sourceMappingURL=fileSignatureTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSignatureTypes.d.ts","sourceRoot":"","sources":["../../src/file/fileSignatureTypes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,wBAAwB,EAAE,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAErG,+EAA+E;AAC/E,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CAClB,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,OAAO,qBAAqB,CAAC,CAAC;AAE1G,uEAAuE;AACvE,MAAM,MAAM,6BAA6B,GACxC,OAAO,wBAAwB,CAAC,GAAG,GAAG,OAAO,wBAAwB,CAAC,GAAG,GAAG,OAAO,wBAAwB,CAAC,SAAS,CAAC"}
|
package/dist/file/index.d.ts
CHANGED
package/dist/file/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,KAAK,yBAAyB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACrG,cAAc,sBAAsB,CAAC;AACrC,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
|
package/dist/file/index.js
CHANGED
|
@@ -1,50 +1,219 @@
|
|
|
1
|
+
//#region src/file/fileSignatureConstants.ts
|
|
2
|
+
var e = {
|
|
3
|
+
MATCH: "match",
|
|
4
|
+
MISMATCH: "mismatch",
|
|
5
|
+
UNSUPPORTED: "unsupported"
|
|
6
|
+
}, t = {
|
|
7
|
+
PNG: "png",
|
|
8
|
+
JPG: "jpg",
|
|
9
|
+
JPEG: "jpeg",
|
|
10
|
+
BMP: "bmp",
|
|
11
|
+
PSD: "psd",
|
|
12
|
+
RTF: "rtf",
|
|
13
|
+
RAR: "rar",
|
|
14
|
+
ZIP: "zip",
|
|
15
|
+
SEVEN_ZIP: "7z",
|
|
16
|
+
PDF: "pdf"
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/file/signature/hasSignatureBytes.ts
|
|
20
|
+
function n(e, t, n = 0) {
|
|
21
|
+
return n + t.length > e.length ? !1 : t.every((t, r) => e[n + r] === t);
|
|
22
|
+
}
|
|
23
|
+
function r(e, t) {
|
|
24
|
+
return e.length < t.length && e.every((e, n) => e === t[n]);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/file/signature/checkArchiveSignature.ts
|
|
28
|
+
var i = [
|
|
29
|
+
82,
|
|
30
|
+
97,
|
|
31
|
+
114,
|
|
32
|
+
33
|
|
33
|
+
], a = [26, 7], o = 0, s = 1, c = 0, l = [80, 75], u = [3, 4], d = [5, 6], f = 30, ee = 22, te = [55, 122], p = [
|
|
34
|
+
55,
|
|
35
|
+
122,
|
|
36
|
+
188,
|
|
37
|
+
175,
|
|
38
|
+
39,
|
|
39
|
+
28
|
|
40
|
+
], ne = 2, re = 0, m = 32;
|
|
41
|
+
function ie(t) {
|
|
42
|
+
if (r(t, i)) return e.MISMATCH;
|
|
43
|
+
if (!n(t, i)) return e.UNSUPPORTED;
|
|
44
|
+
let l = i.length + a.length;
|
|
45
|
+
return t.length <= l || !n(t, a, i.length) ? e.MISMATCH : t[l] === o ? e.MATCH : t[l] === s ? t[l + 1] === c ? e.MATCH : e.MISMATCH : e.UNSUPPORTED;
|
|
46
|
+
}
|
|
47
|
+
function ae(t) {
|
|
48
|
+
if (r(t, l)) return e.MISMATCH;
|
|
49
|
+
if (!n(t, l)) return e.UNSUPPORTED;
|
|
50
|
+
let i = l.length;
|
|
51
|
+
return t.length <= i ? e.MISMATCH : t[i] === u[0] ? t.length >= f && n(t, u, i) ? e.MATCH : e.MISMATCH : t[i] === d[0] ? t.length >= ee && n(t, d, i) ? e.MATCH : e.MISMATCH : e.UNSUPPORTED;
|
|
52
|
+
}
|
|
53
|
+
function h(t) {
|
|
54
|
+
return r(t, p) ? e.MISMATCH : n(t, p) ? t.length < p.length + ne ? e.MISMATCH : t[p.length] === re ? t.length >= m ? e.MATCH : e.MISMATCH : e.UNSUPPORTED : n(t, te) ? e.MISMATCH : e.UNSUPPORTED;
|
|
55
|
+
}
|
|
56
|
+
function oe(e, n) {
|
|
57
|
+
return e === t.RAR ? ie(n) : e === t.ZIP ? ae(n) : h(n);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/file/signature/checkPdfSignature.ts
|
|
61
|
+
var g = [
|
|
62
|
+
37,
|
|
63
|
+
80,
|
|
64
|
+
68,
|
|
65
|
+
70
|
|
66
|
+
], _ = 1024, v = 8, y = 4, b = 45, x = 5, S = 6, se = 46, ce = 7, C = 48, w = 57, le = 49, ue = 55, de = 50, T = 48;
|
|
67
|
+
function E(t) {
|
|
68
|
+
if (r(t, g)) return e.MISMATCH;
|
|
69
|
+
let i = Math.min(t.length, _), a = !1, o = !1;
|
|
70
|
+
for (let r = 0; r + g.length <= i; r += 1) {
|
|
71
|
+
if (!n(t, g, r)) continue;
|
|
72
|
+
if (r + v > i) {
|
|
73
|
+
r + v > t.length ? a = !0 : o = !0;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let s = t[r + x], c = t[r + ce];
|
|
77
|
+
if (t[r + y] !== b || t[r + S] !== se || s < C || s > w || c < C || c > w) {
|
|
78
|
+
a = !0;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (s === le && c <= ue || s === de && c === T) return e.MATCH;
|
|
82
|
+
o = !0;
|
|
83
|
+
}
|
|
84
|
+
return o ? e.UNSUPPORTED : a ? e.MISMATCH : e.UNSUPPORTED;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/file/checkFileSignature.ts
|
|
88
|
+
var D = [
|
|
89
|
+
137,
|
|
90
|
+
80,
|
|
91
|
+
78,
|
|
92
|
+
71,
|
|
93
|
+
13,
|
|
94
|
+
10,
|
|
95
|
+
26,
|
|
96
|
+
10
|
|
97
|
+
], O = 255, k = 216, A = 32, j = [66, 77], M = 14, N = 6, P = [
|
|
98
|
+
0,
|
|
99
|
+
0,
|
|
100
|
+
0,
|
|
101
|
+
0
|
|
102
|
+
], F = [
|
|
103
|
+
56,
|
|
104
|
+
66,
|
|
105
|
+
80,
|
|
106
|
+
83
|
|
107
|
+
], I = [0, 1], L = [
|
|
108
|
+
0,
|
|
109
|
+
0,
|
|
110
|
+
0,
|
|
111
|
+
0,
|
|
112
|
+
0,
|
|
113
|
+
0
|
|
114
|
+
], R = [
|
|
115
|
+
123,
|
|
116
|
+
92,
|
|
117
|
+
114,
|
|
118
|
+
116,
|
|
119
|
+
102
|
|
120
|
+
], z = [
|
|
121
|
+
123,
|
|
122
|
+
92,
|
|
123
|
+
114,
|
|
124
|
+
116,
|
|
125
|
+
102,
|
|
126
|
+
49
|
|
127
|
+
], B = {
|
|
128
|
+
digit: [48, 57],
|
|
129
|
+
uppercase: [65, 90],
|
|
130
|
+
lowercase: [97, 122]
|
|
131
|
+
};
|
|
132
|
+
function V(t) {
|
|
133
|
+
if (t[0] !== O) return e.MISMATCH;
|
|
134
|
+
let n = 1;
|
|
135
|
+
for (; n < Math.min(t.length, A) && t[n] === O;) n += 1;
|
|
136
|
+
return n === A && t.length > n ? e.UNSUPPORTED : t[n] === k ? e.MATCH : e.MISMATCH;
|
|
137
|
+
}
|
|
138
|
+
function H(t) {
|
|
139
|
+
return r(t, j) ? e.MISMATCH : n(t, j) ? t.length < M ? e.MISMATCH : n(t, P, N) ? e.MATCH : e.MISMATCH : e.UNSUPPORTED;
|
|
140
|
+
}
|
|
141
|
+
function fe(t) {
|
|
142
|
+
if (!n(t, F)) return e.MISMATCH;
|
|
143
|
+
let r = F.length + I.length;
|
|
144
|
+
return t.length < r ? e.MISMATCH : n(t, I, F.length) ? n(t, L, r) ? e.MATCH : e.MISMATCH : e.UNSUPPORTED;
|
|
145
|
+
}
|
|
146
|
+
function pe(t) {
|
|
147
|
+
if (r(t, z)) return e.MISMATCH;
|
|
148
|
+
if (!n(t, R)) return e.UNSUPPORTED;
|
|
149
|
+
if (!n(t, z) || t.length <= z.length) return e.MISMATCH;
|
|
150
|
+
let i = t[z.length];
|
|
151
|
+
return i >= B.digit[0] && i <= B.digit[1] || i >= B.uppercase[0] && i <= B.uppercase[1] || i >= B.lowercase[0] && i <= B.lowercase[1] ? e.MISMATCH : e.MATCH;
|
|
152
|
+
}
|
|
153
|
+
function me({ extension: r, bytes: i }) {
|
|
154
|
+
let a = r.trim().toLowerCase();
|
|
155
|
+
switch (a) {
|
|
156
|
+
case t.PNG: return n(i, D) ? e.MATCH : e.MISMATCH;
|
|
157
|
+
case t.JPG:
|
|
158
|
+
case t.JPEG: return V(i);
|
|
159
|
+
case t.BMP: return H(i);
|
|
160
|
+
case t.PSD: return fe(i);
|
|
161
|
+
case t.RTF: return pe(i);
|
|
162
|
+
case t.RAR:
|
|
163
|
+
case t.ZIP:
|
|
164
|
+
case t.SEVEN_ZIP: return oe(a, i);
|
|
165
|
+
case t.PDF: return E(i);
|
|
166
|
+
default: return e.UNSUPPORTED;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
1
170
|
//#region src/file/contentDisposition.ts
|
|
2
|
-
function
|
|
171
|
+
function U(e, t = "file") {
|
|
3
172
|
let n = e.replace(/[\r\n]/g, " ").replace(/"/g, "'").trim();
|
|
4
173
|
return n.length > 0 ? n : t;
|
|
5
174
|
}
|
|
6
|
-
function
|
|
175
|
+
function W(e) {
|
|
7
176
|
return encodeURIComponent(e).replace(/['()]/g, (e) => `%${e.charCodeAt(0).toString(16).toUpperCase()}`).replace(/\*/g, "%2A");
|
|
8
177
|
}
|
|
9
|
-
function
|
|
178
|
+
function G(e, t = "file.bin") {
|
|
10
179
|
let n = e.replace(/[\r\n]/g, " ").replace(/["\\]/g, "_").replace(/[^\x20-\x7E]/g, "_").replace(/\s+/g, " ").trim();
|
|
11
180
|
return n.length > 0 ? n : t;
|
|
12
181
|
}
|
|
13
|
-
function
|
|
14
|
-
let
|
|
15
|
-
return `${
|
|
182
|
+
function he({ disposition: e, filename: t, asciiFallback: n = "file.bin" }) {
|
|
183
|
+
let r = U(t, n);
|
|
184
|
+
return `${e}; filename="${G(r, n)}"; filename*=UTF-8''${W(r)}`;
|
|
16
185
|
}
|
|
17
186
|
//#endregion
|
|
18
187
|
//#region src/file/read-file/types.ts
|
|
19
|
-
var
|
|
188
|
+
var K = class extends Error {
|
|
20
189
|
code;
|
|
21
190
|
cause;
|
|
22
191
|
constructor(e, t, n) {
|
|
23
192
|
super(t ?? e), this.name = "ReadFileError", this.code = e, this.cause = n;
|
|
24
193
|
}
|
|
25
|
-
},
|
|
26
|
-
function
|
|
194
|
+
}, q = "[a-z0-9!#$&^_.+-]+", ge = RegExp(`^${q}\\/${q}$`, "i"), _e = RegExp(`^${q}\\/(?:${q}|\\*)$`, "i"), ve = /^\.[^\s,]+$/;
|
|
195
|
+
function ye(e) {
|
|
27
196
|
if (e) {
|
|
28
|
-
for (let [t, n] of e.entries()) if (!
|
|
197
|
+
for (let [t, n] of e.entries()) if (!ge.test(n)) throw Error(`DropZone: некорректный allowedMime[${t}]="${n}". Ожидается полный MIME-тип вида "application/pdf" без wildcard и параметров.`);
|
|
29
198
|
}
|
|
30
199
|
}
|
|
31
|
-
function
|
|
200
|
+
function be(e) {
|
|
32
201
|
if (e) {
|
|
33
|
-
for (let [t, n] of e.entries()) if (!(
|
|
202
|
+
for (let [t, n] of e.entries()) if (!(ve.test(n) || _e.test(n))) throw Error(`DropZone: некорректный accept[${t}]="${n}". Ожидается расширение вида ".pdf" или MIME-тип вида "application/pdf" / "image/*".`);
|
|
34
203
|
}
|
|
35
204
|
}
|
|
36
|
-
function
|
|
37
|
-
if (e?.aborted) throw new
|
|
205
|
+
function J(e) {
|
|
206
|
+
if (e?.aborted) throw new K("READ_ABORTED", "Прервано по сигналу");
|
|
38
207
|
}
|
|
39
208
|
//#endregion
|
|
40
209
|
//#region src/file/read-file/readFile.ts
|
|
41
|
-
function
|
|
210
|
+
function Y(e) {
|
|
42
211
|
return e?.trim().toLowerCase() ?? "";
|
|
43
212
|
}
|
|
44
|
-
function
|
|
45
|
-
return
|
|
213
|
+
function xe(e) {
|
|
214
|
+
return Y(e.split(".").pop());
|
|
46
215
|
}
|
|
47
|
-
function
|
|
216
|
+
function X(e) {
|
|
48
217
|
return {
|
|
49
218
|
mime: e.type,
|
|
50
219
|
size: e.size,
|
|
@@ -52,7 +221,7 @@ function m(e) {
|
|
|
52
221
|
lastModified: e.lastModified
|
|
53
222
|
};
|
|
54
223
|
}
|
|
55
|
-
function
|
|
224
|
+
function Z(e, t) {
|
|
56
225
|
if (!e) return;
|
|
57
226
|
let n = () => {
|
|
58
227
|
try {
|
|
@@ -61,81 +230,81 @@ function h(e, t) {
|
|
|
61
230
|
};
|
|
62
231
|
return e.addEventListener("abort", n, { once: !0 }), () => e.removeEventListener("abort", n);
|
|
63
232
|
}
|
|
64
|
-
function
|
|
233
|
+
function Se(e, t) {
|
|
65
234
|
if (t.allowedMime && t.allowedMime.length > 0) {
|
|
66
|
-
let n = new Set(t.allowedMime.map((e) =>
|
|
67
|
-
if (!n.has(r)) throw new
|
|
235
|
+
let n = new Set(t.allowedMime.map((e) => Y(e))), r = Y(e.type);
|
|
236
|
+
if (!n.has(r)) throw new K("MIME_NOT_ALLOWED", `Неподдерживаемый MIME тип: ${e.type}`);
|
|
68
237
|
} else if (t.accept && t.accept.length > 0) {
|
|
69
|
-
let n =
|
|
70
|
-
if (!n) throw new
|
|
71
|
-
if (!t.accept.map((e) =>
|
|
238
|
+
let n = xe(e.name) || e.type.split("/").pop();
|
|
239
|
+
if (!n) throw new K("MIME_NOT_ALLOWED", "Неопределенный MIME тип");
|
|
240
|
+
if (!t.accept.map((e) => Y(e)).some((e) => e.includes(n))) throw new K("MIME_NOT_ALLOWED", `Неподдерживаемый MIME тип: ${n}`);
|
|
72
241
|
}
|
|
73
|
-
if (typeof t.maxBytes == "number" && e.size > t.maxBytes) throw new
|
|
242
|
+
if (typeof t.maxBytes == "number" && e.size > t.maxBytes) throw new K("FILE_TOO_LARGE", `Файл слишком большой: ${e.size} > ${t.maxBytes}`);
|
|
74
243
|
}
|
|
75
|
-
function
|
|
244
|
+
function Ce(e, t) {
|
|
76
245
|
return new Promise((n, r) => {
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
let e =
|
|
246
|
+
let i = new FileReader(), a = Z(t.signal, i);
|
|
247
|
+
i.onerror = () => {
|
|
248
|
+
a?.(), r(new K("READ_FAILED", "Не удалось прочитать файл как DataURL", i.error));
|
|
249
|
+
}, i.onabort = () => {
|
|
250
|
+
a?.(), r(new K("READ_ABORTED", "Чтение файла прервано"));
|
|
251
|
+
}, i.onload = () => {
|
|
252
|
+
a?.();
|
|
253
|
+
let e = i.result;
|
|
85
254
|
if (typeof e != "string") {
|
|
86
|
-
r(new
|
|
255
|
+
r(new K("READ_FAILED", "Неожиданный тип результата FileReader"));
|
|
87
256
|
return;
|
|
88
257
|
}
|
|
89
258
|
n(e);
|
|
90
259
|
};
|
|
91
260
|
try {
|
|
92
|
-
|
|
261
|
+
i.readAsDataURL(e);
|
|
93
262
|
} catch (e) {
|
|
94
|
-
|
|
263
|
+
a?.(), r(new K("READ_FAILED", "При чтении DataURL функция FileReader выдала ошибку", e));
|
|
95
264
|
}
|
|
96
265
|
});
|
|
97
266
|
}
|
|
98
|
-
function
|
|
267
|
+
function we(e, t) {
|
|
99
268
|
return new Promise((n, r) => {
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
let e =
|
|
269
|
+
let i = new FileReader(), a = Z(t.signal, i);
|
|
270
|
+
i.onerror = () => {
|
|
271
|
+
a?.(), r(new K("READ_FAILED", "Не удалось прочитать файл как ArrayBuffer.", i.error));
|
|
272
|
+
}, i.onabort = () => {
|
|
273
|
+
a?.(), r(new K("READ_ABORTED", "Чтение файла прервано"));
|
|
274
|
+
}, i.onload = () => {
|
|
275
|
+
a?.();
|
|
276
|
+
let e = i.result;
|
|
108
277
|
if (!(e instanceof ArrayBuffer)) {
|
|
109
|
-
r(new
|
|
278
|
+
r(new K("READ_FAILED", "Неожиданный тип результата FileReader"));
|
|
110
279
|
return;
|
|
111
280
|
}
|
|
112
281
|
n(e);
|
|
113
282
|
};
|
|
114
283
|
try {
|
|
115
|
-
|
|
284
|
+
i.readAsArrayBuffer(e);
|
|
116
285
|
} catch (e) {
|
|
117
|
-
|
|
286
|
+
a?.(), r(new K("READ_FAILED", "При чтении ArrayBuffer возникла ошибка FileReader", e));
|
|
118
287
|
}
|
|
119
288
|
});
|
|
120
289
|
}
|
|
121
|
-
async function
|
|
122
|
-
|
|
123
|
-
let n = t.mode ?? "data-url", r =
|
|
290
|
+
async function Q(e, t = {}) {
|
|
291
|
+
J(t.signal), Se(e, t);
|
|
292
|
+
let n = t.mode ?? "data-url", r = X(e);
|
|
124
293
|
return n === "array-buffer" ? {
|
|
125
294
|
mode: "array-buffer",
|
|
126
295
|
meta: r,
|
|
127
|
-
buffer: await
|
|
296
|
+
buffer: await we(e, t),
|
|
128
297
|
file: e
|
|
129
298
|
} : {
|
|
130
299
|
mode: "data-url",
|
|
131
300
|
meta: r,
|
|
132
|
-
dataUrl: await
|
|
301
|
+
dataUrl: await Ce(e, t),
|
|
133
302
|
file: e
|
|
134
303
|
};
|
|
135
304
|
}
|
|
136
305
|
//#endregion
|
|
137
306
|
//#region src/file/image/types.ts
|
|
138
|
-
var
|
|
307
|
+
var $ = class extends Error {
|
|
139
308
|
code;
|
|
140
309
|
cause;
|
|
141
310
|
constructor(e, t, n) {
|
|
@@ -144,11 +313,11 @@ var b = class extends Error {
|
|
|
144
313
|
};
|
|
145
314
|
//#endregion
|
|
146
315
|
//#region src/file/image/readImageFile.ts
|
|
147
|
-
function
|
|
316
|
+
function Te(e) {
|
|
148
317
|
return e.startsWith("image/");
|
|
149
318
|
}
|
|
150
|
-
async function
|
|
151
|
-
|
|
319
|
+
async function Ee(e, t) {
|
|
320
|
+
J(t);
|
|
152
321
|
let n = new Image();
|
|
153
322
|
return n.decoding = "async", n.src = e, await new Promise((e, t) => {
|
|
154
323
|
let r = () => {
|
|
@@ -159,23 +328,23 @@ async function S(e, t) {
|
|
|
159
328
|
}, n.onerror = (e) => {
|
|
160
329
|
r(), t(e);
|
|
161
330
|
};
|
|
162
|
-
}),
|
|
331
|
+
}), J(t), {
|
|
163
332
|
width: n.naturalWidth,
|
|
164
333
|
height: n.naturalHeight
|
|
165
334
|
};
|
|
166
335
|
}
|
|
167
|
-
async function
|
|
168
|
-
if (!
|
|
336
|
+
async function De(e, t = {}) {
|
|
337
|
+
if (!Te(e.type)) throw new $("NOT_AN_IMAGE", `Файл не является изображением. mime=${e.type || "unknown"}`);
|
|
169
338
|
let n = {
|
|
170
339
|
allowedMime: t.allowedMime,
|
|
171
340
|
maxBytes: t.maxBytes,
|
|
172
341
|
signal: t.signal
|
|
173
342
|
};
|
|
174
|
-
if (t.mode === "array-buffer") return
|
|
343
|
+
if (t.mode === "array-buffer") return Q(e, {
|
|
175
344
|
...n,
|
|
176
345
|
mode: "array-buffer"
|
|
177
346
|
});
|
|
178
|
-
let r = await
|
|
347
|
+
let r = await Q(e, n), i = await Ee(r.dataUrl, t.signal);
|
|
179
348
|
return {
|
|
180
349
|
...r,
|
|
181
350
|
dimensions: i
|
|
@@ -183,7 +352,7 @@ async function C(e, t = {}) {
|
|
|
183
352
|
}
|
|
184
353
|
//#endregion
|
|
185
354
|
//#region src/file/mimeType.ts
|
|
186
|
-
var
|
|
355
|
+
var Oe = {
|
|
187
356
|
pdf: "application/pdf",
|
|
188
357
|
doc: "application/msword",
|
|
189
358
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
@@ -202,10 +371,10 @@ var w = {
|
|
|
202
371
|
gif: "image/gif",
|
|
203
372
|
webp: "image/webp"
|
|
204
373
|
};
|
|
205
|
-
function
|
|
206
|
-
return
|
|
374
|
+
function ke(e) {
|
|
375
|
+
return Oe[e.trim().toLowerCase()] ?? "application/octet-stream";
|
|
207
376
|
}
|
|
208
377
|
//#endregion
|
|
209
|
-
export {
|
|
378
|
+
export { K as ReadFileError, $ as ReadImageError, J as assertNotAborted, be as assertValidAccept, ye as assertValidAllowedMime, Z as attachAbort, X as buildMeta, me as checkFileSignature, G as createAsciiFilenameFallback, he as createContentDispositionHeader, W as encodeFilenameRFC5987, Q as readFile, De as readImageFile, ke as resolveMimeTypeByExtension, U as sanitizeFileName };
|
|
210
379
|
|
|
211
380
|
//# sourceMappingURL=index.js.map
|
package/dist/file/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/file/contentDisposition.ts","../../src/file/read-file/types.ts","../../src/file/read-file/assert.ts","../../src/file/read-file/readFile.ts","../../src/file/image/types.ts","../../src/file/image/readImageFile.ts","../../src/file/mimeType.ts"],"sourcesContent":["export type ContentDispositionKind = \"inline\" | \"attachment\";\n\nexport type ContentDispositionOptions = {\n\treadonly disposition: ContentDispositionKind;\n\treadonly filename: string;\n\treadonly asciiFallback?: string;\n};\n\nexport function sanitizeFileName(value: string, fallback = \"file\"): string {\n\tconst normalized = value\n\t\t.replace(/[\\r\\n]/g, \" \")\n\t\t.replace(/\"/g, \"'\")\n\t\t.trim();\n\n\treturn normalized.length > 0 ? normalized : fallback;\n}\n\nexport function encodeFilenameRFC5987(value: string): string {\n\treturn encodeURIComponent(value)\n\t\t.replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)\n\t\t.replace(/\\*/g, \"%2A\");\n}\n\nexport function createAsciiFilenameFallback(value: string, fallback = \"file.bin\"): string {\n\tconst asciiValue = value\n\t\t.replace(/[\\r\\n]/g, \" \")\n\t\t.replace(/[\"\\\\]/g, \"_\")\n\t\t.replace(/[^\\x20-\\x7E]/g, \"_\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\n\treturn asciiValue.length > 0 ? asciiValue : fallback;\n}\n\nexport function createContentDispositionHeader({ disposition, filename, asciiFallback = \"file.bin\" }: ContentDispositionOptions): string {\n\tconst sanitizedFilename = sanitizeFileName(filename, asciiFallback);\n\tconst asciiFilename = createAsciiFilenameFallback(sanitizedFilename, asciiFallback);\n\tconst encodedFilename = encodeFilenameRFC5987(sanitizedFilename);\n\n\treturn `${disposition}; filename=\"${asciiFilename}\"; filename*=UTF-8''${encodedFilename}`;\n}\n","export type ReadMode = \"data-url\" | \"array-buffer\";\n\nexport type ReadFileErrorCode = \"NO_FILE\" | \"MIME_NOT_ALLOWED\" | \"FILE_TOO_LARGE\" | \"READ_ABORTED\" | \"READ_FAILED\";\n\nexport class ReadFileError extends Error {\n\tpublic readonly code: ReadFileErrorCode;\n\tpublic readonly cause?: unknown;\n\n\tconstructor(code: ReadFileErrorCode, message?: string, cause?: unknown) {\n\t\tsuper(message ?? code);\n\t\tthis.name = \"ReadFileError\";\n\t\tthis.code = code;\n\t\tthis.cause = cause;\n\t}\n}\n\nexport type FileMeta = {\n\tmime: string;\n\tsize: number;\n\tname: string;\n\tlastModified: number;\n};\n\nexport type ReadFileOptionsBase = {\n\taccept?: readonly string[];\n\t/** Ограничение по MIME (если не указано — любые файлы) */\n\tallowedMime?: readonly string[];\n\t/** Ограничение по размеру файла (байты) */\n\tmaxBytes?: number;\n\t/** AbortController для отмены */\n\tsignal?: AbortSignal;\n};\n\nexport type ReadFileAsDataUrlOptions = ReadFileOptionsBase & {\n\tmode?: \"data-url\";\n};\n\nexport type ReadFileAsArrayBufferOptions = ReadFileOptionsBase & {\n\tmode: \"array-buffer\";\n};\n\nexport type ReadFileOptions = ReadFileAsDataUrlOptions | ReadFileAsArrayBufferOptions;\n\nexport type ReadFileAsDataUrlResult = {\n\tmode: \"data-url\";\n\tmeta: FileMeta;\n\tdataUrl: string;\n\tfile: File;\n};\n\nexport type ReadFileAsArrayBufferResult = {\n\tmode: \"array-buffer\";\n\tmeta: FileMeta;\n\tbuffer: ArrayBuffer;\n\tfile: File;\n};\n\nexport type ReadFileResult = ReadFileAsDataUrlResult | ReadFileAsArrayBufferResult;\n","import { ReadFileError } from \"./types\";\n\nconst MIME_PART_PATTERN = \"[a-z0-9!#$&^_.+-]+\";\nconst STRICT_MIME_PATTERN = new RegExp(`^${MIME_PART_PATTERN}\\\\/${MIME_PART_PATTERN}$`, \"i\");\nconst ACCEPT_MIME_PATTERN = new RegExp(`^${MIME_PART_PATTERN}\\\\/(?:${MIME_PART_PATTERN}|\\\\*)$`, \"i\");\nconst ACCEPT_EXTENSION_PATTERN = /^\\.[^\\s,]+$/;\n\nexport function assertValidAllowedMime(allowedMime?: readonly string[]): void {\n\tif (!allowedMime) return;\n\n\tfor (const [index, mime] of allowedMime.entries()) {\n\t\tif (!STRICT_MIME_PATTERN.test(mime)) {\n\t\t\tthrow new Error(\n\t\t\t\t`DropZone: некорректный allowedMime[${index}]=\"${mime}\". Ожидается полный MIME-тип вида \"application/pdf\" без wildcard и параметров.`\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function assertValidAccept(accept?: readonly string[]): void {\n\tif (!accept) return;\n\n\tfor (const [index, specifier] of accept.entries()) {\n\t\tconst isValidSpecifier = ACCEPT_EXTENSION_PATTERN.test(specifier) || ACCEPT_MIME_PATTERN.test(specifier);\n\n\t\tif (!isValidSpecifier) {\n\t\t\tthrow new Error(\n\t\t\t\t`DropZone: некорректный accept[${index}]=\"${specifier}\". Ожидается расширение вида \".pdf\" или MIME-тип вида \"application/pdf\" / \"image/*\".`\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function assertNotAborted(signal?: AbortSignal): void {\n\tif (signal?.aborted) {\n\t\tthrow new ReadFileError(\"READ_ABORTED\", \"Прервано по сигналу\");\n\t}\n}\n","import { assertNotAborted } from \"./assert\";\nimport { FileMeta, ReadFileError, ReadFileOptions, ReadFileResult, ReadMode } from \"./types\";\n\nfunction normalizeMime(mime?: string): string {\n\treturn mime?.trim().toLowerCase() ?? \"\";\n}\n\nfunction getFileExtension(fileName: string): string {\n\tconst extension = fileName.split(\".\").pop();\n\treturn normalizeMime(extension);\n}\n\nexport function buildMeta(file: File): FileMeta {\n\treturn {\n\t\tmime: file.type,\n\t\tsize: file.size,\n\t\tname: file.name,\n\t\tlastModified: file.lastModified\n\t};\n}\n\nexport function attachAbort(signal: AbortSignal | undefined, reader: FileReader): (() => void) | undefined {\n\tif (!signal) return undefined;\n\n\tconst onAbort = () => {\n\t\ttry {\n\t\t\treader.abort();\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t};\n\n\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\treturn () => signal.removeEventListener(\"abort\", onAbort);\n}\n\nfunction validateFile(file: File, opts: ReadFileOptions): void {\n\tif (opts.allowedMime && opts.allowedMime.length > 0) {\n\t\tconst allowed = new Set(opts.allowedMime.map((mime) => normalizeMime(mime)));\n\t\tconst fileMime = normalizeMime(file.type);\n\t\tif (!allowed.has(fileMime)) {\n\t\t\tthrow new ReadFileError(\"MIME_NOT_ALLOWED\", `Неподдерживаемый MIME тип: ${file.type}`);\n\t\t}\n\t} else if (opts.accept && opts.accept.length > 0) {\n\t\t// Берём расширение файла, если его нет, пытаемся вытащить из MIME\n\t\tconst fileMime = getFileExtension(file.name) || file.type.split(\"/\").pop();\n\t\tif (!fileMime) {\n\t\t\t// NOTE: Если неизвестный mime и нет расширения? Пока запретить такой сценарий\n\t\t\tthrow new ReadFileError(\"MIME_NOT_ALLOWED\", `Неопределенный MIME тип`);\n\t\t}\n\n\t\tconst allowed = opts.accept.map((mime) => normalizeMime(mime));\n\t\tconst isAllowed = allowed.some((mime) => mime.includes(fileMime));\n\n\t\tif (!isAllowed) {\n\t\t\tthrow new ReadFileError(\"MIME_NOT_ALLOWED\", `Неподдерживаемый MIME тип: ${fileMime}`);\n\t\t}\n\t}\n\n\tif (typeof opts.maxBytes === \"number\" && file.size > opts.maxBytes) {\n\t\tthrow new ReadFileError(\"FILE_TOO_LARGE\", `Файл слишком большой: ${file.size} > ${opts.maxBytes}`);\n\t}\n}\n\nfunction readAsDataUrl(file: File, opts: ReadFileOptions): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\n\t\tconst detachAbort = attachAbort(opts.signal, reader);\n\n\t\treader.onerror = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Не удалось прочитать файл как DataURL\", reader.error));\n\t\t};\n\n\t\treader.onabort = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_ABORTED\", \"Чтение файла прервано\"));\n\t\t};\n\n\t\treader.onload = () => {\n\t\t\tdetachAbort?.();\n\t\t\tconst result = reader.result;\n\t\t\tif (typeof result !== \"string\") {\n\t\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Неожиданный тип результата FileReader\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(result);\n\t\t};\n\n\t\ttry {\n\t\t\treader.readAsDataURL(file);\n\t\t} catch (cause) {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"При чтении DataURL функция FileReader выдала ошибку\", cause));\n\t\t}\n\t});\n}\n\nfunction readAsArrayBuffer(file: File, opts: ReadFileOptions): Promise<ArrayBuffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\n\t\tconst detachAbort = attachAbort(opts.signal, reader);\n\n\t\treader.onerror = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Не удалось прочитать файл как ArrayBuffer.\", reader.error));\n\t\t};\n\n\t\treader.onabort = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_ABORTED\", \"Чтение файла прервано\"));\n\t\t};\n\n\t\treader.onload = () => {\n\t\t\tdetachAbort?.();\n\t\t\tconst result = reader.result;\n\t\t\tif (!(result instanceof ArrayBuffer)) {\n\t\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Неожиданный тип результата FileReader\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(result);\n\t\t};\n\n\t\ttry {\n\t\t\treader.readAsArrayBuffer(file);\n\t\t} catch (cause) {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"При чтении ArrayBuffer возникла ошибка FileReader\", cause));\n\t\t}\n\t});\n}\n\n// Перегрузки для строгой типизации результата\n\nexport async function readFile(\n\tfile: File,\n\topts?: Omit<ReadFileOptions, \"mode\"> & { mode?: \"data-url\" }\n): Promise<Extract<ReadFileResult, { mode: \"data-url\" }>>;\nexport async function readFile(\n\tfile: File,\n\topts: Omit<ReadFileOptions, \"mode\"> & { mode: \"array-buffer\" }\n): Promise<Extract<ReadFileResult, { mode: \"array-buffer\" }>>;\n\nexport async function readFile(file: File, opts: ReadFileOptions = {}): Promise<ReadFileResult> {\n\tassertNotAborted(opts.signal);\n\tvalidateFile(file, opts);\n\n\tconst mode: ReadMode = opts.mode ?? \"data-url\";\n\tconst meta = buildMeta(file);\n\n\tif (mode === \"array-buffer\") {\n\t\tconst buffer = await readAsArrayBuffer(file, opts);\n\t\treturn { mode: \"array-buffer\", meta, buffer, file };\n\t}\n\n\tconst dataUrl = await readAsDataUrl(file, opts);\n\treturn { mode: \"data-url\", meta, dataUrl, file };\n}\n","import type { ReadFileAsArrayBufferResult, ReadFileAsDataUrlResult, ReadFileErrorCode, ReadFileOptionsBase } from \"../read-file\";\n\nexport type ImageMime = \"image/png\" | \"image/jpeg\" | \"image/webp\" | \"image/gif\" | \"image/svg+xml\" | \"image/avif\";\n\nexport type ReadImageErrorCode = ReadFileErrorCode | \"NOT_AN_IMAGE\" | \"IMAGE_DECODE_FAILED\";\n\nexport class ReadImageError extends Error {\n\tpublic readonly code: ReadImageErrorCode;\n\tpublic readonly cause?: unknown;\n\n\tconstructor(code: ReadImageErrorCode, message?: string, cause?: unknown) {\n\t\tsuper(message ?? code);\n\t\tthis.name = \"ReadImageError\";\n\t\tthis.code = code;\n\t\tthis.cause = cause;\n\t}\n}\n\nexport type ReadImageOptionsBase = Omit<ReadFileOptionsBase, \"allowedMime\"> & {\n\t/** Ограничение по MIME (если не указано — любые image/*) */\n\tallowedMime?: readonly ImageMime[];\n};\n\nexport type ReadImageAsDataUrlOptions = ReadImageOptionsBase & {\n\tmode?: \"data-url\";\n};\n\nexport type ReadImageAsArrayBufferOptions = ReadImageOptionsBase & {\n\tmode: \"array-buffer\";\n};\n\nexport type ReadImageOptions = ReadImageAsDataUrlOptions | ReadImageAsArrayBufferOptions;\n\nexport type ImageDimensions = {\n\twidth: number;\n\theight: number;\n};\n\nexport type ReadImageAsDataUrlResult = ReadFileAsDataUrlResult & {\n\t/** безопасно вытащенные размеры (через decode) */\n\tdimensions?: ImageDimensions;\n};\n\nexport type ReadImageAsArrayBufferResult = ReadFileAsArrayBufferResult;\n\nexport type ReadImageResult = ReadImageAsDataUrlResult | ReadImageAsArrayBufferResult;\n","import { assertNotAborted, readFile } from \"../read-file\";\n\nimport { ReadImageError } from \"./types\";\n\nimport type { ImageDimensions, ReadImageAsArrayBufferResult, ReadImageAsDataUrlResult, ReadImageOptions } from \"./types\";\n\nfunction isImageMime(mime: string): boolean {\n\treturn mime.startsWith(\"image/\");\n}\n\nasync function decodeImageDimensions(dataUrl: string, signal?: AbortSignal): Promise<ImageDimensions> {\n\tassertNotAborted(signal);\n\n\tconst img = new Image();\n\timg.decoding = \"async\";\n\timg.src = dataUrl;\n\n\t// decode() поддерживается в современных браузерах; fallback на onload\n\t// NOTE: большого размера png падают...\n\t// if (typeof img.decode === \"function\") {\n\t// \ttry {\n\t// \t\tawait img.decode();\n\t// \t\tassertNotAborted(signal);\n\t// \t\treturn { width: img.naturalWidth, height: img.naturalHeight };\n\t// \t} catch (cause) {\n\t// \t\tthrow new ReadImageError(\"IMAGE_DECODE_FAILED\", \"Не удалось декодировать изображение\", cause);\n\t// \t}\n\t// }\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst cleanup = () => {\n\t\t\timg.onload = null;\n\t\t\timg.onerror = null;\n\t\t};\n\n\t\timg.onload = () => {\n\t\t\tcleanup();\n\t\t\tresolve();\n\t\t};\n\t\timg.onerror = (ev) => {\n\t\t\tcleanup();\n\t\t\treject(ev);\n\t\t};\n\t});\n\n\tassertNotAborted(signal);\n\treturn { width: img.naturalWidth, height: img.naturalHeight };\n}\n\n// Перегрузки для строгой типизации результата\n\nexport async function readImageFile(\n\tfile: File,\n\topts?: Omit<ReadImageOptions, \"mode\"> & { mode?: \"data-url\" }\n): Promise<ReadImageAsDataUrlResult>;\nexport async function readImageFile(\n\tfile: File,\n\topts: Omit<ReadImageOptions, \"mode\"> & { mode: \"array-buffer\" }\n): Promise<ReadImageAsArrayBufferResult>;\n\nexport async function readImageFile(\n\tfile: File,\n\topts: ReadImageOptions = {}\n): Promise<ReadImageAsDataUrlResult | ReadImageAsArrayBufferResult> {\n\tif (!isImageMime(file.type)) {\n\t\tthrow new ReadImageError(\"NOT_AN_IMAGE\", `Файл не является изображением. mime=${file.type || \"unknown\"}`);\n\t}\n\n\tconst baseOpts = { allowedMime: opts.allowedMime, maxBytes: opts.maxBytes, signal: opts.signal };\n\n\tif (opts.mode === \"array-buffer\") {\n\t\treturn readFile(file, { ...baseOpts, mode: \"array-buffer\" });\n\t}\n\n\tconst result = await readFile(file, baseOpts);\n\tconst dimensions = await decodeImageDimensions(result.dataUrl, opts.signal);\n\n\treturn { ...result, dimensions };\n}\n","const EXTENSION_CONTENT_TYPE: Readonly<Record<string, string>> = {\n\tpdf: \"application/pdf\",\n\tdoc: \"application/msword\",\n\tdocx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n\txls: \"application/vnd.ms-excel\",\n\txlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\tppt: \"application/vnd.ms-powerpoint\",\n\tpptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n\ttxt: \"text/plain; charset=UTF-8\",\n\trtf: \"application/rtf\",\n\todt: \"application/vnd.oasis.opendocument.text\",\n\tzip: \"application/zip\",\n\trar: \"application/vnd.rar\",\n\tjpg: \"image/jpeg\",\n\tjpeg: \"image/jpeg\",\n\tpng: \"image/png\",\n\tgif: \"image/gif\",\n\twebp: \"image/webp\"\n};\n\nexport function resolveMimeTypeByExtension(extension: string): string {\n\tconst normalizedExtension = extension.trim().toLowerCase();\n\n\treturn EXTENSION_CONTENT_TYPE[normalizedExtension] ?? \"application/octet-stream\";\n}\n"],"mappings":";AAQA,SAAgB,EAAiB,GAAe,IAAW,QAAgB;CAC1E,IAAM,IAAa,EACjB,QAAQ,WAAW,GAAG,CAAC,CACvB,QAAQ,MAAM,GAAG,CAAC,CAClB,KAAK;CAEP,OAAO,EAAW,SAAS,IAAI,IAAa;AAC7C;AAEA,SAAgB,EAAsB,GAAuB;CAC5D,OAAO,mBAAmB,CAAK,CAAC,CAC9B,QAAQ,WAAW,MAAS,IAAI,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,YAAY,GAAG,CAAC,CAChF,QAAQ,OAAO,KAAK;AACvB;AAEA,SAAgB,EAA4B,GAAe,IAAW,YAAoB;CACzF,IAAM,IAAa,EACjB,QAAQ,WAAW,GAAG,CAAC,CACvB,QAAQ,UAAU,GAAG,CAAC,CACtB,QAAQ,iBAAiB,GAAG,CAAC,CAC7B,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;CAEP,OAAO,EAAW,SAAS,IAAI,IAAa;AAC7C;AAEA,SAAgB,EAA+B,EAAE,gBAAa,aAAU,mBAAgB,cAAiD;CACxI,IAAM,IAAoB,EAAiB,GAAU,CAAa;CAIlE,OAAO,GAAG,EAAY,cAHA,EAA4B,GAAmB,CAGjC,EAAc,sBAF1B,EAAsB,CAE0B;AACzE;;;ACpCA,IAAa,IAAb,cAAmC,MAAM;CACxC;CACA;CAEA,YAAY,GAAyB,GAAkB,GAAiB;EAIvE,AAHA,MAAM,KAAW,CAAI,GACrB,KAAK,OAAO,iBACZ,KAAK,OAAO,GACZ,KAAK,QAAQ;CACd;AACD,GCZM,IAAoB,sBACpB,IAA0B,OAAO,IAAI,EAAkB,KAAK,EAAkB,IAAI,GAAG,GACrF,IAA0B,OAAO,IAAI,EAAkB,QAAQ,EAAkB,SAAS,GAAG,GAC7F,IAA2B;AAEjC,SAAgB,EAAuB,GAAuC;CACxE,OAEL;OAAK,IAAM,CAAC,GAAO,MAAS,EAAY,QAAQ,GAC/C,IAAI,CAAC,EAAoB,KAAK,CAAI,GACjC,MAAU,MACT,sCAAsC,EAAM,KAAK,EAAK,+EACvD;CAAA;AAGH;AAEA,SAAgB,EAAkB,GAAkC;CAC9D,OAEL;OAAK,IAAM,CAAC,GAAO,MAAc,EAAO,QAAQ,GAG/C,IAAI,EAFqB,EAAyB,KAAK,CAAS,KAAK,EAAoB,KAAK,CAAS,IAGtG,MAAU,MACT,iCAAiC,EAAM,KAAK,EAAU,qFACvD;CAAA;AAGH;AAEA,SAAgB,EAAiB,GAA4B;CAC5D,IAAI,GAAQ,SACX,MAAM,IAAI,EAAc,gBAAgB,qBAAqB;AAE/D;;;AClCA,SAAS,EAAc,GAAuB;CAC7C,OAAO,GAAM,KAAK,CAAC,CAAC,YAAY,KAAK;AACtC;AAEA,SAAS,EAAiB,GAA0B;CAEnD,OAAO,EADW,EAAS,MAAM,GAAG,CAAC,CAAC,IACjB,CAAS;AAC/B;AAEA,SAAgB,EAAU,GAAsB;CAC/C,OAAO;EACN,MAAM,EAAK;EACX,MAAM,EAAK;EACX,MAAM,EAAK;EACX,cAAc,EAAK;CACpB;AACD;AAEA,SAAgB,EAAY,GAAiC,GAA8C;CAC1G,IAAI,CAAC,GAAQ;CAEb,IAAM,UAAgB;EACrB,IAAI;GACH,EAAO,MAAM;EACd,QAAQ,CAER;CACD;CAGA,OADA,EAAO,iBAAiB,SAAS,GAAS,EAAE,MAAM,GAAK,CAAC,SAC3C,EAAO,oBAAoB,SAAS,CAAO;AACzD;AAEA,SAAS,EAAa,GAAY,GAA6B;CAC9D,IAAI,EAAK,eAAe,EAAK,YAAY,SAAS,GAAG;EACpD,IAAM,IAAU,IAAI,IAAI,EAAK,YAAY,KAAK,MAAS,EAAc,CAAI,CAAC,CAAC,GACrE,IAAW,EAAc,EAAK,IAAI;EACxC,IAAI,CAAC,EAAQ,IAAI,CAAQ,GACxB,MAAM,IAAI,EAAc,oBAAoB,8BAA8B,EAAK,MAAM;CAEvF,OAAO,IAAI,EAAK,UAAU,EAAK,OAAO,SAAS,GAAG;EAEjD,IAAM,IAAW,EAAiB,EAAK,IAAI,KAAK,EAAK,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI;EACzE,IAAI,CAAC,GAEJ,MAAM,IAAI,EAAc,oBAAoB,yBAAyB;EAMtE,IAAI,CAHY,EAAK,OAAO,KAAK,MAAS,EAAc,CAAI,CAC1C,CAAA,CAAQ,MAAM,MAAS,EAAK,SAAS,CAAQ,CAE1D,GACJ,MAAM,IAAI,EAAc,oBAAoB,8BAA8B,GAAU;CAEtF;CAEA,IAAI,OAAO,EAAK,YAAa,YAAY,EAAK,OAAO,EAAK,UACzD,MAAM,IAAI,EAAc,kBAAkB,yBAAyB,EAAK,KAAK,KAAK,EAAK,UAAU;AAEnG;AAEA,SAAS,EAAc,GAAY,GAAwC;CAC1E,OAAO,IAAI,SAAS,GAAS,MAAW;EACvC,IAAM,IAAS,IAAI,WAAW,GAExB,IAAc,EAAY,EAAK,QAAQ,CAAM;EAYnD,AAVA,EAAO,gBAAgB;GAEtB,AADA,IAAc,GACd,EAAO,IAAI,EAAc,eAAe,yCAAyC,EAAO,KAAK,CAAC;EAC/F,GAEA,EAAO,gBAAgB;GAEtB,AADA,IAAc,GACd,EAAO,IAAI,EAAc,gBAAgB,uBAAuB,CAAC;EAClE,GAEA,EAAO,eAAe;GACrB,IAAc;GACd,IAAM,IAAS,EAAO;GACtB,IAAI,OAAO,KAAW,UAAU;IAC/B,EAAO,IAAI,EAAc,eAAe,uCAAuC,CAAC;IAChF;GACD;GACA,EAAQ,CAAM;EACf;EAEA,IAAI;GACH,EAAO,cAAc,CAAI;EAC1B,SAAS,GAAO;GAEf,AADA,IAAc,GACd,EAAO,IAAI,EAAc,eAAe,uDAAuD,CAAK,CAAC;EACtG;CACD,CAAC;AACF;AAEA,SAAS,EAAkB,GAAY,GAA6C;CACnF,OAAO,IAAI,SAAS,GAAS,MAAW;EACvC,IAAM,IAAS,IAAI,WAAW,GAExB,IAAc,EAAY,EAAK,QAAQ,CAAM;EAYnD,AAVA,EAAO,gBAAgB;GAEtB,AADA,IAAc,GACd,EAAO,IAAI,EAAc,eAAe,8CAA8C,EAAO,KAAK,CAAC;EACpG,GAEA,EAAO,gBAAgB;GAEtB,AADA,IAAc,GACd,EAAO,IAAI,EAAc,gBAAgB,uBAAuB,CAAC;EAClE,GAEA,EAAO,eAAe;GACrB,IAAc;GACd,IAAM,IAAS,EAAO;GACtB,IAAI,EAAE,aAAkB,cAAc;IACrC,EAAO,IAAI,EAAc,eAAe,uCAAuC,CAAC;IAChF;GACD;GACA,EAAQ,CAAM;EACf;EAEA,IAAI;GACH,EAAO,kBAAkB,CAAI;EAC9B,SAAS,GAAO;GAEf,AADA,IAAc,GACd,EAAO,IAAI,EAAc,eAAe,qDAAqD,CAAK,CAAC;EACpG;CACD,CAAC;AACF;AAaA,eAAsB,EAAS,GAAY,IAAwB,CAAC,GAA4B;CAE/F,AADA,EAAiB,EAAK,MAAM,GAC5B,EAAa,GAAM,CAAI;CAEvB,IAAM,IAAiB,EAAK,QAAQ,YAC9B,IAAO,EAAU,CAAI;CAQ3B,OANI,MAAS,iBAEL;EAAE,MAAM;EAAgB;EAAM,QAAA,MADhB,EAAkB,GAAM,CAAI;EACJ;CAAK,IAI5C;EAAE,MAAM;EAAY;EAAM,SAAA,MADX,EAAc,GAAM,CAAI;EACJ;CAAK;AAChD;;;ACzJA,IAAa,IAAb,cAAoC,MAAM;CACzC;CACA;CAEA,YAAY,GAA0B,GAAkB,GAAiB;EAIxE,AAHA,MAAM,KAAW,CAAI,GACrB,KAAK,OAAO,kBACZ,KAAK,OAAO,GACZ,KAAK,QAAQ;CACd;AACD;;;ACVA,SAAS,EAAY,GAAuB;CAC3C,OAAO,EAAK,WAAW,QAAQ;AAChC;AAEA,eAAe,EAAsB,GAAiB,GAAgD;CACrG,EAAiB,CAAM;CAEvB,IAAM,IAAM,IAAI,MAAM;CAiCtB,OAhCA,EAAI,WAAW,SACf,EAAI,MAAM,GAcV,MAAM,IAAI,SAAe,GAAS,MAAW;EAC5C,IAAM,UAAgB;GAErB,AADA,EAAI,SAAS,MACb,EAAI,UAAU;EACf;EAMA,AAJA,EAAI,eAAe;GAElB,AADA,EAAQ,GACR,EAAQ;EACT,GACA,EAAI,WAAW,MAAO;GAErB,AADA,EAAQ,GACR,EAAO,CAAE;EACV;CACD,CAAC,GAED,EAAiB,CAAM,GAChB;EAAE,OAAO,EAAI;EAAc,QAAQ,EAAI;CAAc;AAC7D;AAaA,eAAsB,EACrB,GACA,IAAyB,CAAC,GACyC;CACnE,IAAI,CAAC,EAAY,EAAK,IAAI,GACzB,MAAM,IAAI,EAAe,gBAAgB,uCAAuC,EAAK,QAAQ,WAAW;CAGzG,IAAM,IAAW;EAAE,aAAa,EAAK;EAAa,UAAU,EAAK;EAAU,QAAQ,EAAK;CAAO;CAE/F,IAAI,EAAK,SAAS,gBACjB,OAAO,EAAS,GAAM;EAAE,GAAG;EAAU,MAAM;CAAe,CAAC;CAG5D,IAAM,IAAS,MAAM,EAAS,GAAM,CAAQ,GACtC,IAAa,MAAM,EAAsB,EAAO,SAAS,EAAK,MAAM;CAE1E,OAAO;EAAE,GAAG;EAAQ;CAAW;AAChC;;;AC9EA,IAAM,IAA2D;CAChE,KAAK;CACL,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;CACL,MAAM;AACP;AAEA,SAAgB,EAA2B,GAA2B;CAGrE,OAAO,EAFqB,EAAU,KAAK,CAAC,CAAC,YAEf,MAAwB;AACvD"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/file/fileSignatureConstants.ts","../../src/file/signature/hasSignatureBytes.ts","../../src/file/signature/checkArchiveSignature.ts","../../src/file/signature/checkPdfSignature.ts","../../src/file/checkFileSignature.ts","../../src/file/contentDisposition.ts","../../src/file/read-file/types.ts","../../src/file/read-file/assert.ts","../../src/file/read-file/readFile.ts","../../src/file/image/types.ts","../../src/file/image/readImageFile.ts","../../src/file/mimeType.ts"],"sourcesContent":["/** Приватный словарь результатов; значения сохраняют публичный string union. */\nexport const FILE_SIGNATURE_RESULT = {\n\tMATCH: \"match\",\n\tMISMATCH: \"mismatch\",\n\tUNSUPPORTED: \"unsupported\"\n} as const;\n\n/** Только dispatch поддержанных проверок; это не предметный allowlist caller. */\nexport const FILE_SIGNATURE_EXTENSION = {\n\tPNG: \"png\",\n\tJPG: \"jpg\",\n\tJPEG: \"jpeg\",\n\tBMP: \"bmp\",\n\tPSD: \"psd\",\n\tRTF: \"rtf\",\n\tRAR: \"rar\",\n\tZIP: \"zip\",\n\tSEVEN_ZIP: \"7z\",\n\tPDF: \"pdf\"\n} as const;\n","/** Читаем только переданный view: соседние bytes его backing buffer не участвуют. */\nexport function hasSignatureBytes(bytes: Uint8Array, signature: readonly number[], offset = 0): boolean {\n\tif (offset + signature.length > bytes.length) return false;\n\treturn signature.every((value, index) => bytes[offset + index] === value);\n}\n\n/** Точный оборванный prefix отличаем от неизвестного представления/SFX. */\nexport function isTruncatedSignature(bytes: Uint8Array, signature: readonly number[]): boolean {\n\treturn bytes.length < signature.length && bytes.every((value, index) => value === signature[index]);\n}\n","import { FILE_SIGNATURE_EXTENSION, FILE_SIGNATURE_RESULT } from \"../fileSignatureConstants\";\nimport { type ArchiveFileSignatureExtension, type FileSignatureCheckResult } from \"../fileSignatureTypes\";\n\nimport { hasSignatureBytes, isTruncatedSignature } from \"./hasSignatureBytes\";\n\nconst RAR_FAMILY = [0x52, 0x61, 0x72, 0x21];\nconst RAR_MARKER_SUFFIX = [0x1a, 0x07];\nconst RAR4_VERSION = 0;\nconst RAR5_VERSION = 1;\nconst RAR5_MARKER_END = 0;\nconst ZIP_FAMILY = [0x50, 0x4b];\nconst ZIP_LOCAL_FILE_MARKER = [0x03, 0x04];\nconst ZIP_END_OF_CENTRAL_DIRECTORY_MARKER = [0x05, 0x06];\nconst ZIP_LOCAL_FILE_HEADER_BYTES = 30;\nconst ZIP_END_OF_CENTRAL_DIRECTORY_BYTES = 22;\nconst SEVEN_ZIP_FAMILY = [0x37, 0x7a];\nconst SEVEN_ZIP_SIGNATURE = [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c];\nconst SEVEN_ZIP_VERSION_BYTES = 2;\nconst SEVEN_ZIP_SUPPORTED_MAJOR_VERSION = 0;\nconst SEVEN_ZIP_SIGNATURE_HEADER_BYTES = 32;\n\n/**\n * RARLAB marker распознаёт RAR4/5 без main/encryption header. Не ищем marker\n * внутри SFX: отсутствие начального семейства не доказывает неверный архив.\n */\nfunction checkRarSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (isTruncatedSignature(bytes, RAR_FAMILY)) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tif (!hasSignatureBytes(bytes, RAR_FAMILY)) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\tconst versionOffset = RAR_FAMILY.length + RAR_MARKER_SUFFIX.length;\n\tif (bytes.length <= versionOffset || !hasSignatureBytes(bytes, RAR_MARKER_SUFFIX, RAR_FAMILY.length))\n\t\treturn FILE_SIGNATURE_RESULT.MISMATCH;\n\tif (bytes[versionOffset] === RAR4_VERSION) return FILE_SIGNATURE_RESULT.MATCH;\n\tif (bytes[versionOffset] !== RAR5_VERSION) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\treturn bytes[versionOffset + 1] === RAR5_MARKER_END ? FILE_SIGNATURE_RESULT.MATCH : FILE_SIGNATURE_RESULT.MISMATCH;\n}\n\n/** PKWARE fixed header не проверяет payload, central directory, CRC или parts. */\nfunction checkZipSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (isTruncatedSignature(bytes, ZIP_FAMILY)) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tif (!hasSignatureBytes(bytes, ZIP_FAMILY)) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\tconst recordOffset = ZIP_FAMILY.length;\n\tif (bytes.length <= recordOffset) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tif (bytes[recordOffset] === ZIP_LOCAL_FILE_MARKER[0]) {\n\t\treturn bytes.length >= ZIP_LOCAL_FILE_HEADER_BYTES && hasSignatureBytes(bytes, ZIP_LOCAL_FILE_MARKER, recordOffset)\n\t\t\t? FILE_SIGNATURE_RESULT.MATCH\n\t\t\t: FILE_SIGNATURE_RESULT.MISMATCH;\n\t}\n\tif (bytes[recordOffset] === ZIP_END_OF_CENTRAL_DIRECTORY_MARKER[0]) {\n\t\treturn bytes.length >= ZIP_END_OF_CENTRAL_DIRECTORY_BYTES &&\n\t\t\thasSignatureBytes(bytes, ZIP_END_OF_CENTRAL_DIRECTORY_MARKER, recordOffset)\n\t\t\t? FILE_SIGNATURE_RESULT.MATCH\n\t\t\t: FILE_SIGNATURE_RESULT.MISMATCH;\n\t}\n\t// Split/spanning, SFX и прочие records не подтверждаем как обычный start header.\n\treturn FILE_SIGNATURE_RESULT.UNSUPPORTED;\n}\n\n/** SignatureHeader 7-Zip занимает 32 bytes; NextHeader и CRC здесь не проверяются. */\nfunction checkSevenZipSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (isTruncatedSignature(bytes, SEVEN_ZIP_SIGNATURE)) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tif (!hasSignatureBytes(bytes, SEVEN_ZIP_SIGNATURE)) {\n\t\treturn hasSignatureBytes(bytes, SEVEN_ZIP_FAMILY) ? FILE_SIGNATURE_RESULT.MISMATCH : FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\t}\n\tif (bytes.length < SEVEN_ZIP_SIGNATURE.length + SEVEN_ZIP_VERSION_BYTES) return FILE_SIGNATURE_RESULT.MISMATCH;\n\t// У официального reader major=0; minor не является запретом новой версии.\n\tif (bytes[SEVEN_ZIP_SIGNATURE.length] !== SEVEN_ZIP_SUPPORTED_MAJOR_VERSION) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\treturn bytes.length >= SEVEN_ZIP_SIGNATURE_HEADER_BYTES ? FILE_SIGNATURE_RESULT.MATCH : FILE_SIGNATURE_RESULT.MISMATCH;\n}\n\nexport function checkArchiveSignature(extension: ArchiveFileSignatureExtension, bytes: Uint8Array): FileSignatureCheckResult {\n\tif (extension === FILE_SIGNATURE_EXTENSION.RAR) return checkRarSignature(bytes);\n\tif (extension === FILE_SIGNATURE_EXTENSION.ZIP) return checkZipSignature(bytes);\n\treturn checkSevenZipSignature(bytes);\n}\n","import { FILE_SIGNATURE_RESULT } from \"../fileSignatureConstants\";\nimport { type FileSignatureCheckResult } from \"../fileSignatureTypes\";\n\nimport { hasSignatureBytes, isTruncatedSignature } from \"./hasSignatureBytes\";\n\nconst PDF_FAMILY = [0x25, 0x50, 0x44, 0x46];\nconst PDF_SCAN_LIMIT = 1024;\n// Позиции относятся к локальному %PDF-x.y header, а не ко всему документу.\nconst PDF_HEADER_BYTES = 8;\nconst PDF_FAMILY_SEPARATOR_OFFSET = 4;\nconst PDF_FAMILY_SEPARATOR = 0x2d;\nconst PDF_MAJOR_VERSION_OFFSET = 5;\nconst PDF_VERSION_SEPARATOR_OFFSET = 6;\nconst PDF_VERSION_SEPARATOR = 0x2e;\nconst PDF_MINOR_VERSION_OFFSET = 7;\nconst PDF_VERSION_DIGIT_FIRST = 0x30;\nconst PDF_VERSION_DIGIT_LAST = 0x39;\nconst PDF_VERSION_1_MAJOR = 0x31;\nconst PDF_VERSION_1_MINOR_LAST = 0x37;\nconst PDF_VERSION_2_MAJOR = 0x32;\nconst PDF_VERSION_2_MINOR = 0x30;\n\n/**\n * Adobe документирует header с ненулевым offset. Проверяем только первые 1024\n * bytes без копии/полного scan; альтернативный PS header и неизвестные версии\n * не объявляем mismatch. Версия в header не доказывает структуру документа.\n */\nexport function checkPdfSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (isTruncatedSignature(bytes, PDF_FAMILY)) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tconst limit = Math.min(bytes.length, PDF_SCAN_LIMIT);\n\tlet malformedHeader = false;\n\tlet unknownRepresentation = false;\n\n\tfor (let offset = 0; offset + PDF_FAMILY.length <= limit; offset += 1) {\n\t\tif (!hasSignatureBytes(bytes, PDF_FAMILY, offset)) continue;\n\t\tif (offset + PDF_HEADER_BYTES > limit) {\n\t\t\t// EOF доказывает усечение; граница scan не является концом файла.\n\t\t\tif (offset + PDF_HEADER_BYTES > bytes.length) malformedHeader = true;\n\t\t\telse unknownRepresentation = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconst major = bytes[offset + PDF_MAJOR_VERSION_OFFSET];\n\t\tconst minor = bytes[offset + PDF_MINOR_VERSION_OFFSET];\n\t\tif (\n\t\t\tbytes[offset + PDF_FAMILY_SEPARATOR_OFFSET] !== PDF_FAMILY_SEPARATOR ||\n\t\t\tbytes[offset + PDF_VERSION_SEPARATOR_OFFSET] !== PDF_VERSION_SEPARATOR ||\n\t\t\tmajor < PDF_VERSION_DIGIT_FIRST ||\n\t\t\tmajor > PDF_VERSION_DIGIT_LAST ||\n\t\t\tminor < PDF_VERSION_DIGIT_FIRST ||\n\t\t\tminor > PDF_VERSION_DIGIT_LAST\n\t\t) {\n\t\t\tmalformedHeader = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (\n\t\t\t(major === PDF_VERSION_1_MAJOR && minor <= PDF_VERSION_1_MINOR_LAST) ||\n\t\t\t(major === PDF_VERSION_2_MAJOR && minor === PDF_VERSION_2_MINOR)\n\t\t)\n\t\t\treturn FILE_SIGNATURE_RESULT.MATCH;\n\t\tunknownRepresentation = true;\n\t}\n\n\treturn unknownRepresentation\n\t\t? FILE_SIGNATURE_RESULT.UNSUPPORTED\n\t\t: malformedHeader\n\t\t\t? FILE_SIGNATURE_RESULT.MISMATCH\n\t\t\t: FILE_SIGNATURE_RESULT.UNSUPPORTED;\n}\n","import { FILE_SIGNATURE_EXTENSION, FILE_SIGNATURE_RESULT } from \"./fileSignatureConstants\";\nimport { type FileSignatureCheckOptions, type FileSignatureCheckResult } from \"./fileSignatureTypes\";\nimport { checkArchiveSignature } from \"./signature/checkArchiveSignature\";\nimport { checkPdfSignature } from \"./signature/checkPdfSignature\";\nimport { hasSignatureBytes, isTruncatedSignature } from \"./signature/hasSignatureBytes\";\n\nconst PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\nconst JPEG_MARKER_PREFIX = 0xff;\nconst JPEG_START_OF_IMAGE_MARKER = 0xd8;\n// Ограничение работы checker, а не максимальная длина fill bytes формата JPEG.\nconst JPEG_MARKER_SCAN_BYTES = 32;\nconst BITMAP_SIGNATURE = [0x42, 0x4d];\nconst BITMAP_FILE_HEADER_BYTES = 14;\nconst BITMAP_RESERVED_OFFSET = 6;\nconst BITMAP_RESERVED_BYTES = [0, 0, 0, 0];\nconst PSD_SIGNATURE = [0x38, 0x42, 0x50, 0x53];\nconst PSD_SUPPORTED_VERSION = [0, 1];\nconst PSD_RESERVED_BYTES = [0, 0, 0, 0, 0, 0];\nconst RTF_FAMILY = [0x7b, 0x5c, 0x72, 0x74, 0x66];\nconst RTF_SIGNATURE = [0x7b, 0x5c, 0x72, 0x74, 0x66, 0x31];\n// Цифра или буква продолжает control word вместо завершения версии rtf1.\nconst RTF_CONTROL_WORD_RANGES = {\n\tdigit: [0x30, 0x39],\n\tuppercase: [0x41, 0x5a],\n\tlowercase: [0x61, 0x7a]\n} as const;\n\nfunction checkJpegSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (bytes[0] !== JPEG_MARKER_PREFIX) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tlet offset = 1;\n\t// T.81 допускает FF fill перед marker. Длинный prefix не превращаем в scan файла.\n\twhile (offset < Math.min(bytes.length, JPEG_MARKER_SCAN_BYTES) && bytes[offset] === JPEG_MARKER_PREFIX) offset += 1;\n\tif (offset === JPEG_MARKER_SCAN_BYTES && bytes.length > offset) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\treturn bytes[offset] === JPEG_START_OF_IMAGE_MARKER ? FILE_SIGNATURE_RESULT.MATCH : FILE_SIGNATURE_RESULT.MISMATCH;\n}\n\nfunction checkBitmapSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (isTruncatedSignature(bytes, BITMAP_SIGNATURE)) return FILE_SIGNATURE_RESULT.MISMATCH;\n\t// OS/2/DIB и неизвестные альтернативы не запрещаем по Windows BM header.\n\tif (!hasSignatureBytes(bytes, BITMAP_SIGNATURE)) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\tif (bytes.length < BITMAP_FILE_HEADER_BYTES) return FILE_SIGNATURE_RESULT.MISMATCH;\n\treturn hasSignatureBytes(bytes, BITMAP_RESERVED_BYTES, BITMAP_RESERVED_OFFSET)\n\t\t? FILE_SIGNATURE_RESULT.MATCH\n\t\t: FILE_SIGNATURE_RESULT.MISMATCH;\n}\n\nfunction checkPhotoshopSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (!hasSignatureBytes(bytes, PSD_SIGNATURE)) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tconst reservedOffset = PSD_SIGNATURE.length + PSD_SUPPORTED_VERSION.length;\n\tif (bytes.length < reservedOffset) return FILE_SIGNATURE_RESULT.MISMATCH;\n\t// PSB=2 и неизвестные версии имеют общее семейство, но не подтверждают PSD=1.\n\tif (!hasSignatureBytes(bytes, PSD_SUPPORTED_VERSION, PSD_SIGNATURE.length)) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\treturn hasSignatureBytes(bytes, PSD_RESERVED_BYTES, reservedOffset) ? FILE_SIGNATURE_RESULT.MATCH : FILE_SIGNATURE_RESULT.MISMATCH;\n}\n\nfunction checkRtfSignature(bytes: Uint8Array): FileSignatureCheckResult {\n\tif (isTruncatedSignature(bytes, RTF_SIGNATURE)) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tif (!hasSignatureBytes(bytes, RTF_FAMILY)) return FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\tif (!hasSignatureBytes(bytes, RTF_SIGNATURE) || bytes.length <= RTF_SIGNATURE.length) return FILE_SIGNATURE_RESULT.MISMATCH;\n\tconst delimiter = bytes[RTF_SIGNATURE.length];\n\t// Полный control word не совпадает с \\rtf10 или \\rtf1abc.\n\treturn (delimiter >= RTF_CONTROL_WORD_RANGES.digit[0] && delimiter <= RTF_CONTROL_WORD_RANGES.digit[1]) ||\n\t\t(delimiter >= RTF_CONTROL_WORD_RANGES.uppercase[0] && delimiter <= RTF_CONTROL_WORD_RANGES.uppercase[1]) ||\n\t\t(delimiter >= RTF_CONTROL_WORD_RANGES.lowercase[0] && delimiter <= RTF_CONTROL_WORD_RANGES.lowercase[1])\n\t\t? FILE_SIGNATURE_RESULT.MISMATCH\n\t\t: FILE_SIGNATURE_RESULT.MATCH;\n}\n\n/**\n * Частичная pure-проверка fixed headers: нет FileReader, Node API, декодирования,\n * распаковки или чтения всего файла. Любой match относится только к header.\n * Общий ZIP/OLE marker не подтверждает Office/ODF/XFL; allowlist принадлежит caller.\n */\nexport function checkFileSignature({ extension, bytes }: FileSignatureCheckOptions): FileSignatureCheckResult {\n\tconst normalizedExtension = extension.trim().toLowerCase();\n\tswitch (normalizedExtension) {\n\t\tcase FILE_SIGNATURE_EXTENSION.PNG:\n\t\t\treturn hasSignatureBytes(bytes, PNG_SIGNATURE) ? FILE_SIGNATURE_RESULT.MATCH : FILE_SIGNATURE_RESULT.MISMATCH;\n\t\tcase FILE_SIGNATURE_EXTENSION.JPG:\n\t\tcase FILE_SIGNATURE_EXTENSION.JPEG:\n\t\t\treturn checkJpegSignature(bytes);\n\t\tcase FILE_SIGNATURE_EXTENSION.BMP:\n\t\t\treturn checkBitmapSignature(bytes);\n\t\tcase FILE_SIGNATURE_EXTENSION.PSD:\n\t\t\treturn checkPhotoshopSignature(bytes);\n\t\tcase FILE_SIGNATURE_EXTENSION.RTF:\n\t\t\treturn checkRtfSignature(bytes);\n\t\tcase FILE_SIGNATURE_EXTENSION.RAR:\n\t\tcase FILE_SIGNATURE_EXTENSION.ZIP:\n\t\tcase FILE_SIGNATURE_EXTENSION.SEVEN_ZIP:\n\t\t\treturn checkArchiveSignature(normalizedExtension, bytes);\n\t\tcase FILE_SIGNATURE_EXTENSION.PDF:\n\t\t\treturn checkPdfSignature(bytes);\n\t\tdefault:\n\t\t\treturn FILE_SIGNATURE_RESULT.UNSUPPORTED;\n\t}\n}\n","export type ContentDispositionKind = \"inline\" | \"attachment\";\n\nexport type ContentDispositionOptions = {\n\treadonly disposition: ContentDispositionKind;\n\treadonly filename: string;\n\treadonly asciiFallback?: string;\n};\n\nexport function sanitizeFileName(value: string, fallback = \"file\"): string {\n\tconst normalized = value\n\t\t.replace(/[\\r\\n]/g, \" \")\n\t\t.replace(/\"/g, \"'\")\n\t\t.trim();\n\n\treturn normalized.length > 0 ? normalized : fallback;\n}\n\nexport function encodeFilenameRFC5987(value: string): string {\n\treturn encodeURIComponent(value)\n\t\t.replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)\n\t\t.replace(/\\*/g, \"%2A\");\n}\n\nexport function createAsciiFilenameFallback(value: string, fallback = \"file.bin\"): string {\n\tconst asciiValue = value\n\t\t.replace(/[\\r\\n]/g, \" \")\n\t\t.replace(/[\"\\\\]/g, \"_\")\n\t\t.replace(/[^\\x20-\\x7E]/g, \"_\")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n\n\treturn asciiValue.length > 0 ? asciiValue : fallback;\n}\n\nexport function createContentDispositionHeader({ disposition, filename, asciiFallback = \"file.bin\" }: ContentDispositionOptions): string {\n\tconst sanitizedFilename = sanitizeFileName(filename, asciiFallback);\n\tconst asciiFilename = createAsciiFilenameFallback(sanitizedFilename, asciiFallback);\n\tconst encodedFilename = encodeFilenameRFC5987(sanitizedFilename);\n\n\treturn `${disposition}; filename=\"${asciiFilename}\"; filename*=UTF-8''${encodedFilename}`;\n}\n","export type ReadMode = \"data-url\" | \"array-buffer\";\n\nexport type ReadFileErrorCode = \"NO_FILE\" | \"MIME_NOT_ALLOWED\" | \"FILE_TOO_LARGE\" | \"READ_ABORTED\" | \"READ_FAILED\";\n\nexport class ReadFileError extends Error {\n\tpublic readonly code: ReadFileErrorCode;\n\tpublic readonly cause?: unknown;\n\n\tconstructor(code: ReadFileErrorCode, message?: string, cause?: unknown) {\n\t\tsuper(message ?? code);\n\t\tthis.name = \"ReadFileError\";\n\t\tthis.code = code;\n\t\tthis.cause = cause;\n\t}\n}\n\nexport type FileMeta = {\n\tmime: string;\n\tsize: number;\n\tname: string;\n\tlastModified: number;\n};\n\nexport type ReadFileOptionsBase = {\n\taccept?: readonly string[];\n\t/** Ограничение по MIME (если не указано — любые файлы) */\n\tallowedMime?: readonly string[];\n\t/** Ограничение по размеру файла (байты) */\n\tmaxBytes?: number;\n\t/** AbortController для отмены */\n\tsignal?: AbortSignal;\n};\n\nexport type ReadFileAsDataUrlOptions = ReadFileOptionsBase & {\n\tmode?: \"data-url\";\n};\n\nexport type ReadFileAsArrayBufferOptions = ReadFileOptionsBase & {\n\tmode: \"array-buffer\";\n};\n\nexport type ReadFileOptions = ReadFileAsDataUrlOptions | ReadFileAsArrayBufferOptions;\n\nexport type ReadFileAsDataUrlResult = {\n\tmode: \"data-url\";\n\tmeta: FileMeta;\n\tdataUrl: string;\n\tfile: File;\n};\n\nexport type ReadFileAsArrayBufferResult = {\n\tmode: \"array-buffer\";\n\tmeta: FileMeta;\n\tbuffer: ArrayBuffer;\n\tfile: File;\n};\n\nexport type ReadFileResult = ReadFileAsDataUrlResult | ReadFileAsArrayBufferResult;\n","import { ReadFileError } from \"./types\";\n\nconst MIME_PART_PATTERN = \"[a-z0-9!#$&^_.+-]+\";\nconst STRICT_MIME_PATTERN = new RegExp(`^${MIME_PART_PATTERN}\\\\/${MIME_PART_PATTERN}$`, \"i\");\nconst ACCEPT_MIME_PATTERN = new RegExp(`^${MIME_PART_PATTERN}\\\\/(?:${MIME_PART_PATTERN}|\\\\*)$`, \"i\");\nconst ACCEPT_EXTENSION_PATTERN = /^\\.[^\\s,]+$/;\n\nexport function assertValidAllowedMime(allowedMime?: readonly string[]): void {\n\tif (!allowedMime) return;\n\n\tfor (const [index, mime] of allowedMime.entries()) {\n\t\tif (!STRICT_MIME_PATTERN.test(mime)) {\n\t\t\tthrow new Error(\n\t\t\t\t`DropZone: некорректный allowedMime[${index}]=\"${mime}\". Ожидается полный MIME-тип вида \"application/pdf\" без wildcard и параметров.`\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function assertValidAccept(accept?: readonly string[]): void {\n\tif (!accept) return;\n\n\tfor (const [index, specifier] of accept.entries()) {\n\t\tconst isValidSpecifier = ACCEPT_EXTENSION_PATTERN.test(specifier) || ACCEPT_MIME_PATTERN.test(specifier);\n\n\t\tif (!isValidSpecifier) {\n\t\t\tthrow new Error(\n\t\t\t\t`DropZone: некорректный accept[${index}]=\"${specifier}\". Ожидается расширение вида \".pdf\" или MIME-тип вида \"application/pdf\" / \"image/*\".`\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function assertNotAborted(signal?: AbortSignal): void {\n\tif (signal?.aborted) {\n\t\tthrow new ReadFileError(\"READ_ABORTED\", \"Прервано по сигналу\");\n\t}\n}\n","import { assertNotAborted } from \"./assert\";\nimport { FileMeta, ReadFileError, ReadFileOptions, ReadFileResult, ReadMode } from \"./types\";\n\nfunction normalizeMime(mime?: string): string {\n\treturn mime?.trim().toLowerCase() ?? \"\";\n}\n\nfunction getFileExtension(fileName: string): string {\n\tconst extension = fileName.split(\".\").pop();\n\treturn normalizeMime(extension);\n}\n\nexport function buildMeta(file: File): FileMeta {\n\treturn {\n\t\tmime: file.type,\n\t\tsize: file.size,\n\t\tname: file.name,\n\t\tlastModified: file.lastModified\n\t};\n}\n\nexport function attachAbort(signal: AbortSignal | undefined, reader: FileReader): (() => void) | undefined {\n\tif (!signal) return undefined;\n\n\tconst onAbort = () => {\n\t\ttry {\n\t\t\treader.abort();\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t};\n\n\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\treturn () => signal.removeEventListener(\"abort\", onAbort);\n}\n\nfunction validateFile(file: File, opts: ReadFileOptions): void {\n\tif (opts.allowedMime && opts.allowedMime.length > 0) {\n\t\tconst allowed = new Set(opts.allowedMime.map((mime) => normalizeMime(mime)));\n\t\tconst fileMime = normalizeMime(file.type);\n\t\tif (!allowed.has(fileMime)) {\n\t\t\tthrow new ReadFileError(\"MIME_NOT_ALLOWED\", `Неподдерживаемый MIME тип: ${file.type}`);\n\t\t}\n\t} else if (opts.accept && opts.accept.length > 0) {\n\t\t// Берём расширение файла, если его нет, пытаемся вытащить из MIME\n\t\tconst fileMime = getFileExtension(file.name) || file.type.split(\"/\").pop();\n\t\tif (!fileMime) {\n\t\t\t// NOTE: Если неизвестный mime и нет расширения? Пока запретить такой сценарий\n\t\t\tthrow new ReadFileError(\"MIME_NOT_ALLOWED\", `Неопределенный MIME тип`);\n\t\t}\n\n\t\tconst allowed = opts.accept.map((mime) => normalizeMime(mime));\n\t\tconst isAllowed = allowed.some((mime) => mime.includes(fileMime));\n\n\t\tif (!isAllowed) {\n\t\t\tthrow new ReadFileError(\"MIME_NOT_ALLOWED\", `Неподдерживаемый MIME тип: ${fileMime}`);\n\t\t}\n\t}\n\n\tif (typeof opts.maxBytes === \"number\" && file.size > opts.maxBytes) {\n\t\tthrow new ReadFileError(\"FILE_TOO_LARGE\", `Файл слишком большой: ${file.size} > ${opts.maxBytes}`);\n\t}\n}\n\nfunction readAsDataUrl(file: File, opts: ReadFileOptions): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\n\t\tconst detachAbort = attachAbort(opts.signal, reader);\n\n\t\treader.onerror = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Не удалось прочитать файл как DataURL\", reader.error));\n\t\t};\n\n\t\treader.onabort = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_ABORTED\", \"Чтение файла прервано\"));\n\t\t};\n\n\t\treader.onload = () => {\n\t\t\tdetachAbort?.();\n\t\t\tconst result = reader.result;\n\t\t\tif (typeof result !== \"string\") {\n\t\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Неожиданный тип результата FileReader\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(result);\n\t\t};\n\n\t\ttry {\n\t\t\treader.readAsDataURL(file);\n\t\t} catch (cause) {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"При чтении DataURL функция FileReader выдала ошибку\", cause));\n\t\t}\n\t});\n}\n\nfunction readAsArrayBuffer(file: File, opts: ReadFileOptions): Promise<ArrayBuffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\n\t\tconst detachAbort = attachAbort(opts.signal, reader);\n\n\t\treader.onerror = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Не удалось прочитать файл как ArrayBuffer.\", reader.error));\n\t\t};\n\n\t\treader.onabort = () => {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_ABORTED\", \"Чтение файла прервано\"));\n\t\t};\n\n\t\treader.onload = () => {\n\t\t\tdetachAbort?.();\n\t\t\tconst result = reader.result;\n\t\t\tif (!(result instanceof ArrayBuffer)) {\n\t\t\t\treject(new ReadFileError(\"READ_FAILED\", \"Неожиданный тип результата FileReader\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(result);\n\t\t};\n\n\t\ttry {\n\t\t\treader.readAsArrayBuffer(file);\n\t\t} catch (cause) {\n\t\t\tdetachAbort?.();\n\t\t\treject(new ReadFileError(\"READ_FAILED\", \"При чтении ArrayBuffer возникла ошибка FileReader\", cause));\n\t\t}\n\t});\n}\n\n// Перегрузки для строгой типизации результата\n\nexport async function readFile(\n\tfile: File,\n\topts?: Omit<ReadFileOptions, \"mode\"> & { mode?: \"data-url\" }\n): Promise<Extract<ReadFileResult, { mode: \"data-url\" }>>;\nexport async function readFile(\n\tfile: File,\n\topts: Omit<ReadFileOptions, \"mode\"> & { mode: \"array-buffer\" }\n): Promise<Extract<ReadFileResult, { mode: \"array-buffer\" }>>;\n\nexport async function readFile(file: File, opts: ReadFileOptions = {}): Promise<ReadFileResult> {\n\tassertNotAborted(opts.signal);\n\tvalidateFile(file, opts);\n\n\tconst mode: ReadMode = opts.mode ?? \"data-url\";\n\tconst meta = buildMeta(file);\n\n\tif (mode === \"array-buffer\") {\n\t\tconst buffer = await readAsArrayBuffer(file, opts);\n\t\treturn { mode: \"array-buffer\", meta, buffer, file };\n\t}\n\n\tconst dataUrl = await readAsDataUrl(file, opts);\n\treturn { mode: \"data-url\", meta, dataUrl, file };\n}\n","import type { ReadFileAsArrayBufferResult, ReadFileAsDataUrlResult, ReadFileErrorCode, ReadFileOptionsBase } from \"../read-file\";\n\nexport type ImageMime = \"image/png\" | \"image/jpeg\" | \"image/webp\" | \"image/gif\" | \"image/svg+xml\" | \"image/avif\";\n\nexport type ReadImageErrorCode = ReadFileErrorCode | \"NOT_AN_IMAGE\" | \"IMAGE_DECODE_FAILED\";\n\nexport class ReadImageError extends Error {\n\tpublic readonly code: ReadImageErrorCode;\n\tpublic readonly cause?: unknown;\n\n\tconstructor(code: ReadImageErrorCode, message?: string, cause?: unknown) {\n\t\tsuper(message ?? code);\n\t\tthis.name = \"ReadImageError\";\n\t\tthis.code = code;\n\t\tthis.cause = cause;\n\t}\n}\n\nexport type ReadImageOptionsBase = Omit<ReadFileOptionsBase, \"allowedMime\"> & {\n\t/** Ограничение по MIME (если не указано — любые image/*) */\n\tallowedMime?: readonly ImageMime[];\n};\n\nexport type ReadImageAsDataUrlOptions = ReadImageOptionsBase & {\n\tmode?: \"data-url\";\n};\n\nexport type ReadImageAsArrayBufferOptions = ReadImageOptionsBase & {\n\tmode: \"array-buffer\";\n};\n\nexport type ReadImageOptions = ReadImageAsDataUrlOptions | ReadImageAsArrayBufferOptions;\n\nexport type ImageDimensions = {\n\twidth: number;\n\theight: number;\n};\n\nexport type ReadImageAsDataUrlResult = ReadFileAsDataUrlResult & {\n\t/** безопасно вытащенные размеры (через decode) */\n\tdimensions?: ImageDimensions;\n};\n\nexport type ReadImageAsArrayBufferResult = ReadFileAsArrayBufferResult;\n\nexport type ReadImageResult = ReadImageAsDataUrlResult | ReadImageAsArrayBufferResult;\n","import { assertNotAborted, readFile } from \"../read-file\";\n\nimport { ReadImageError } from \"./types\";\n\nimport type { ImageDimensions, ReadImageAsArrayBufferResult, ReadImageAsDataUrlResult, ReadImageOptions } from \"./types\";\n\nfunction isImageMime(mime: string): boolean {\n\treturn mime.startsWith(\"image/\");\n}\n\nasync function decodeImageDimensions(dataUrl: string, signal?: AbortSignal): Promise<ImageDimensions> {\n\tassertNotAborted(signal);\n\n\tconst img = new Image();\n\timg.decoding = \"async\";\n\timg.src = dataUrl;\n\n\t// decode() поддерживается в современных браузерах; fallback на onload\n\t// NOTE: большого размера png падают...\n\t// if (typeof img.decode === \"function\") {\n\t// \ttry {\n\t// \t\tawait img.decode();\n\t// \t\tassertNotAborted(signal);\n\t// \t\treturn { width: img.naturalWidth, height: img.naturalHeight };\n\t// \t} catch (cause) {\n\t// \t\tthrow new ReadImageError(\"IMAGE_DECODE_FAILED\", \"Не удалось декодировать изображение\", cause);\n\t// \t}\n\t// }\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst cleanup = () => {\n\t\t\timg.onload = null;\n\t\t\timg.onerror = null;\n\t\t};\n\n\t\timg.onload = () => {\n\t\t\tcleanup();\n\t\t\tresolve();\n\t\t};\n\t\timg.onerror = (ev) => {\n\t\t\tcleanup();\n\t\t\treject(ev);\n\t\t};\n\t});\n\n\tassertNotAborted(signal);\n\treturn { width: img.naturalWidth, height: img.naturalHeight };\n}\n\n// Перегрузки для строгой типизации результата\n\nexport async function readImageFile(\n\tfile: File,\n\topts?: Omit<ReadImageOptions, \"mode\"> & { mode?: \"data-url\" }\n): Promise<ReadImageAsDataUrlResult>;\nexport async function readImageFile(\n\tfile: File,\n\topts: Omit<ReadImageOptions, \"mode\"> & { mode: \"array-buffer\" }\n): Promise<ReadImageAsArrayBufferResult>;\n\nexport async function readImageFile(\n\tfile: File,\n\topts: ReadImageOptions = {}\n): Promise<ReadImageAsDataUrlResult | ReadImageAsArrayBufferResult> {\n\tif (!isImageMime(file.type)) {\n\t\tthrow new ReadImageError(\"NOT_AN_IMAGE\", `Файл не является изображением. mime=${file.type || \"unknown\"}`);\n\t}\n\n\tconst baseOpts = { allowedMime: opts.allowedMime, maxBytes: opts.maxBytes, signal: opts.signal };\n\n\tif (opts.mode === \"array-buffer\") {\n\t\treturn readFile(file, { ...baseOpts, mode: \"array-buffer\" });\n\t}\n\n\tconst result = await readFile(file, baseOpts);\n\tconst dimensions = await decodeImageDimensions(result.dataUrl, opts.signal);\n\n\treturn { ...result, dimensions };\n}\n","const EXTENSION_CONTENT_TYPE: Readonly<Record<string, string>> = {\n\tpdf: \"application/pdf\",\n\tdoc: \"application/msword\",\n\tdocx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n\txls: \"application/vnd.ms-excel\",\n\txlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\tppt: \"application/vnd.ms-powerpoint\",\n\tpptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n\ttxt: \"text/plain; charset=UTF-8\",\n\trtf: \"application/rtf\",\n\todt: \"application/vnd.oasis.opendocument.text\",\n\tzip: \"application/zip\",\n\trar: \"application/vnd.rar\",\n\tjpg: \"image/jpeg\",\n\tjpeg: \"image/jpeg\",\n\tpng: \"image/png\",\n\tgif: \"image/gif\",\n\twebp: \"image/webp\"\n};\n\nexport function resolveMimeTypeByExtension(extension: string): string {\n\tconst normalizedExtension = extension.trim().toLowerCase();\n\n\treturn EXTENSION_CONTENT_TYPE[normalizedExtension] ?? \"application/octet-stream\";\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ArchiveFileSignatureExtension, type FileSignatureCheckResult } from "../fileSignatureTypes";
|
|
2
|
+
export declare function checkArchiveSignature(extension: ArchiveFileSignatureExtension, bytes: Uint8Array): FileSignatureCheckResult;
|
|
3
|
+
//# sourceMappingURL=checkArchiveSignature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkArchiveSignature.d.ts","sourceRoot":"","sources":["../../../src/file/signature/checkArchiveSignature.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,6BAA6B,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAoE1G,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,6BAA6B,EAAE,KAAK,EAAE,UAAU,GAAG,wBAAwB,CAI3H"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FileSignatureCheckResult } from "../fileSignatureTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Adobe документирует header с ненулевым offset. Проверяем только первые 1024
|
|
4
|
+
* bytes без копии/полного scan; альтернативный PS header и неизвестные версии
|
|
5
|
+
* не объявляем mismatch. Версия в header не доказывает структуру документа.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkPdfSignature(bytes: Uint8Array): FileSignatureCheckResult;
|
|
8
|
+
//# sourceMappingURL=checkPdfSignature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkPdfSignature.d.ts","sourceRoot":"","sources":["../../../src/file/signature/checkPdfSignature.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAqBtE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,wBAAwB,CAwC7E"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Читаем только переданный view: соседние bytes его backing buffer не участвуют. */
|
|
2
|
+
export declare function hasSignatureBytes(bytes: Uint8Array, signature: readonly number[], offset?: number): boolean;
|
|
3
|
+
/** Точный оборванный prefix отличаем от неизвестного представления/SFX. */
|
|
4
|
+
export declare function isTruncatedSignature(bytes: Uint8Array, signature: readonly number[]): boolean;
|
|
5
|
+
//# sourceMappingURL=hasSignatureBytes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hasSignatureBytes.d.ts","sourceRoot":"","sources":["../../../src/file/signature/hasSignatureBytes.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,SAAI,GAAG,OAAO,CAGtG;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAE7F"}
|
package/dist/media/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useCallback as e, useSyncExternalStore as t } from "react";
|
|
2
2
|
//#region src/media/breakpoints.ts
|
|
3
3
|
var n = {
|
|
4
|
-
mobileMax:
|
|
5
|
-
tabletMin:
|
|
4
|
+
mobileMax: 479.98 / 16,
|
|
5
|
+
tabletMin: 480 / 16,
|
|
6
6
|
tabletMax: 1151.98 / 16,
|
|
7
7
|
laptopMin: 1152 / 16
|
|
8
8
|
}, r = 16, i = 1.2;
|
package/dist/media/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/media/breakpoints.ts","../../src/media/getCurrentFontSize.ts","../../src/media/pxToEm.ts","../../src/media/responsive.ts","../../src/media/useMediaQuery.ts","../../src/media/useMatchMedia.ts"],"sourcesContent":["export const BREAKPOINTS_EM = {\n\tmobileMax: 767.98 / 16,\n\ttabletMin: 768 / 16,\n\ttabletMax: 1151.98 / 16,\n\tlaptopMin: 1152 / 16\n} as const;\n\nexport type Breakpoint = \"mobile\" | \"tablet\" | \"laptop\";\nexport type MediaMatches = Record<Breakpoint, boolean>;\n","const DEFAULT_FONT_SIZE = 16;\nconst DEFAULT_LINE_HEIGHT = 1.2;\n\nexport function getCurrentFontSize(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst rootFontSize = window.getComputedStyle(target).fontSize;\n\tconst fontSize = Number.parseFloat(rootFontSize);\n\n\tif (!Number.isFinite(fontSize) || fontSize <= 0) {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\treturn fontSize;\n}\n\nexport function getCurrentLineHeight(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst computedStyles = window.getComputedStyle(target);\n\tconst rootLineHeight = computedStyles.lineHeight;\n\tconst lineHeight = Number.parseFloat(rootLineHeight);\n\n\tif (!Number.isFinite(lineHeight) || lineHeight <= 0) {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\treturn lineHeight / DEFAULT_FONT_SIZE;\n}\n\nexport function getControlHeight() {\n\tconst fontSize = getCurrentFontSize();\n\tconst lineHeight = getCurrentLineHeight();\n\n\t// Правильно расчета --control-height\n\treturn fontSize + lineHeight * 10;\n}\n","import { getCurrentFontSize } from \"./getCurrentFontSize\";\n\nexport function pxToEm(px: number, baseEm?: number): string {\n\tconst emSize = baseEm ?? getCurrentFontSize(); // динамическая привязка\n\treturn (px / emSize).toString();\n}\n","import { Breakpoint } from \"./breakpoints\";\n\nexport type ResponsiveMap<T> = {\n\tmobile?: T;\n\ttablet?: T;\n\tlaptop?: T;\n};\nexport type ResponsiveValue<T> = T | ResponsiveMap<T>;\nexport type UnwrapResponsive<T> = T extends ResponsiveValue<infer U> ? U : T;\n\nexport function isResponsiveMap<T>(v: ResponsiveValue<T>): v is ResponsiveMap<T> {\n\tif (typeof v !== \"object\" || v === null) return false;\n\n\t// ключевой момент: отличаем “responsive-map” от произвольного объекта T\n\treturn \"mobile\" in v || \"tablet\" in v || \"laptop\" in v || \"desktop\" in v;\n}\n\n/**\n * Превращает объект пропсов, где некоторые поля ResponsiveValue<...>,\n * в объект, где эти поля уже \"плоские\" (U).\n */\nexport type ResolvedProps<T extends object> = {\n\t[K in keyof T]: UnwrapResponsive<T[K]>;\n};\n\n/**\n * Резолвит responsive значение под текущий брейкпоинт.\n * Приоритеты:\n * - exact match (mobile/tablet/laptop)\n * - fallback на \"первое определённое\" в предсказуемом порядке\n */\nexport function resolveResponsiveValue<T>(value: ResponsiveValue<T> | undefined, bp: Breakpoint): T | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isResponsiveMap(value)) {\n\t\t// здесь value точно T (в т.ч. может быть объектом — и это ок)\n\t\treturn value;\n\t}\n\n\tif (bp === \"mobile\") return value.mobile ?? value.tablet ?? value.laptop;\n\tif (bp === \"tablet\") return value.tablet ?? value.mobile ?? value.laptop;\n\n\treturn value.laptop ?? value.tablet ?? value.mobile;\n}\n\n/**\n * Типобезопасно резолвит все поля объекта по списку ключей, которые responsive.\n * (TS-магия: на выходе будет ResolvedProps<T>.)\n */\nexport function resolveProps<T extends object>(props: T, bp: Breakpoint, responsiveKeys: readonly (keyof T)[]): ResolvedProps<T> {\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst out: any = { ...props };\n\n\tfor (const key of responsiveKeys) {\n\t\tout[key] = resolveResponsiveValue(out[key], bp);\n\t}\n\n\treturn out as ResolvedProps<T>;\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\n\ntype UseMediaQueryOptions = {\n\treadonly ssrFallback?: boolean;\n};\n\nconst noop = () => {};\n\nfunction readMediaQuery(query: string, fallback: boolean): boolean {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return fallback;\n\n\treturn window.matchMedia(query).matches;\n}\n\nfunction subscribeMediaQuery(query: string, listener: () => void): () => void {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return noop;\n\n\tconst mql = window.matchMedia(query);\n\n\tif (typeof mql.addEventListener === \"function\") {\n\t\tmql.addEventListener(\"change\", listener);\n\t\treturn () => mql.removeEventListener(\"change\", listener);\n\t}\n\n\tmql.addListener(listener);\n\treturn () => mql.removeListener(listener);\n}\n\n/**\n * SSR-безопасная подписка на произвольный media query.\n * Используется для клиентских предпочтений, где значение должно совпадать с SSR fallback на гидрации.\n */\nexport function useMediaQuery(query: string, { ssrFallback = false }: UseMediaQueryOptions = {}) {\n\tconst subscribe = useCallback((listener: () => void) => subscribeMediaQuery(query, listener), [query]);\n\tconst getSnapshot = useCallback(() => readMediaQuery(query, ssrFallback), [query, ssrFallback]);\n\tconst getServerSnapshot = useCallback(() => ssrFallback, [ssrFallback]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","import { useSyncExternalStore } from \"react\";\n\nimport { BREAKPOINTS_EM, type Breakpoint, type MediaMatches } from \"./breakpoints\";\nimport { getCurrentFontSize } from \"./getCurrentFontSize\";\n\n/**\n * Публичный снимок состояния, который возвращает хук/store.\n * Содержит сырые совпадения media query и вычисленный активный брейкпоинт.\n */\ntype MatchMediaSnapshot = {\n\tmatches: MediaMatches;\n\tactiveBreakpoint: Breakpoint;\n};\n\nfunction subscribeMql(mql: MediaQueryList, cb: () => void) {\n\tif (\"addEventListener\" in mql) {\n\t\tmql.addEventListener(\"change\", cb);\n\t\treturn () => mql.removeEventListener(\"change\", cb);\n\t}\n\t// Safari legacy\n\t// @ts-expect-error legacy api\n\tmql.addListener(cb);\n\t// @ts-expect-error legacy api\n\treturn () => mql.removeListener(cb);\n}\n\nfunction buildQueriesPx(emSize: number) {\n\tconst toPx = (em: number) => em * emSize;\n\n\tconst mobileMax = toPx(BREAKPOINTS_EM.mobileMax);\n\tconst tabletMin = toPx(BREAKPOINTS_EM.tabletMin);\n\tconst tabletMax = toPx(BREAKPOINTS_EM.tabletMax);\n\tconst laptopMin = toPx(BREAKPOINTS_EM.laptopMin);\n\n\treturn {\n\t\tmobile: `(width <= ${mobileMax}px)`,\n\t\ttablet: `(width >= ${tabletMin}px) and (width <= ${tabletMax}px)`,\n\t\tlaptop: `(width >= ${laptopMin}px)`\n\t} satisfies Record<Breakpoint, string>;\n}\n\nconst DEFAULT_MATCHES: MediaMatches = {\n\tmobile: false,\n\ttablet: false,\n\tlaptop: false\n};\n\nconst DEFAULT_SNAPSHOT: MatchMediaSnapshot = {\n\tmatches: DEFAULT_MATCHES,\n\tactiveBreakpoint: \"laptop\"\n};\n\n/**\n * Игнорируем микроколебания `em`, чтобы не перестраивать media query без необходимости.\n */\nconst EM_CHANGE_THRESHOLD = 0.25;\n/**\n * Склеиваем частые события observer-ов в одно перестроение.\n */\nconst EM_REBUILD_DEBOUNCE_MS = 80;\n\nlet snapshot: MatchMediaSnapshot = DEFAULT_SNAPSHOT;\nlet currentMql: Record<Breakpoint, MediaQueryList> | null = null;\nlet mqlUnsubs: Array<() => void> = [];\nlet emSizeRef = 0;\nlet initialized = false;\nlet mutationObserver: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet emProbeElement: HTMLDivElement | null = null;\nlet rebuildTimer: ReturnType<typeof setTimeout> | null = null;\n\nconst listeners = new Set<() => void>();\n\n/**\n * Выбирает один активный брейкпоинт по приоритету: mobile -> tablet -> laptop.\n */\nfunction getActiveBreakpoint(matches: MediaMatches): Breakpoint {\n\tif (matches.mobile) return \"mobile\";\n\tif (matches.tablet) return \"tablet\";\n\treturn \"laptop\";\n}\n\n/**\n * Поверхностное сравнение media-состояния, чтобы пропускать пустые уведомления.\n */\nfunction matchesEqual(a: MediaMatches, b: MediaMatches) {\n\treturn a.mobile === b.mobile && a.tablet === b.tablet && a.laptop === b.laptop;\n}\n\n/**\n * Обновляет глобальный snapshot и уведомляет подписчиков только при реальных изменениях.\n */\nfunction emitIfChanged(nextMatches: MediaMatches) {\n\tif (matchesEqual(snapshot.matches, nextMatches)) return;\n\tsnapshot = {\n\t\tmatches: nextMatches,\n\t\tactiveBreakpoint: getActiveBreakpoint(nextMatches)\n\t};\n\tlisteners.forEach((listener) => listener());\n}\n\n/**\n * Читает текущие значения `MediaQueryList` и публикует новый snapshot.\n */\nfunction recomputeFromMql() {\n\tif (!currentMql) return;\n\n\temitIfChanged({\n\t\tmobile: currentMql.mobile.matches,\n\t\ttablet: currentMql.tablet.matches,\n\t\tlaptop: currentMql.laptop.matches\n\t});\n}\n\n/**\n * Снимает активные mql-подписки перед перестроением/очисткой.\n */\nfunction clearMqlSubscriptions() {\n\tmqlUnsubs.forEach((unsubscribe) => unsubscribe());\n\tmqlUnsubs = [];\n}\n\n/**\n * Перестраивает media query, когда корневой `em` изменился достаточно сильно (или принудительно).\n * Это дорогой путь, поэтому он специально ограничен проверками.\n */\nfunction rebuildIfEmChanged(force = false) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tconst em = getCurrentFontSize();\n\tif (!force && currentMql && Math.abs(em - emSizeRef) < EM_CHANGE_THRESHOLD) return;\n\n\temSizeRef = em;\n\tclearMqlSubscriptions();\n\n\tconst queries = buildQueriesPx(em);\n\tcurrentMql = {\n\t\tmobile: window.matchMedia(queries.mobile),\n\t\ttablet: window.matchMedia(queries.tablet),\n\t\tlaptop: window.matchMedia(queries.laptop)\n\t};\n\n\tmqlUnsubs = (Object.keys(currentMql) as Breakpoint[]).map((key) => subscribeMql(currentMql![key], recomputeFromMql));\n\trecomputeFromMql();\n}\n\n/**\n * Дебаунс-обертка для перестроения query по событиям observer-ов.\n */\nfunction scheduleRebuild() {\n\tif (rebuildTimer) clearTimeout(rebuildTimer);\n\trebuildTimer = setTimeout(() => {\n\t\trebuildTimer = null;\n\t\trebuildIfEmChanged();\n\t}, EM_REBUILD_DEBOUNCE_MS);\n}\n\n/**\n * Наблюдает за изменением style/class у root и размером probe-элемента `1em`.\n * Это позволяет ловить изменения font-size/zoom без слушателя window resize.\n */\nfunction setupEmObservers() {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tmutationObserver = new MutationObserver(scheduleRebuild);\n\tmutationObserver.observe(document.documentElement, {\n\t\tattributes: true,\n\t\tattributeFilter: [\"style\", \"class\", \"data-font-size\"]\n\t});\n\n\tif (typeof ResizeObserver === \"undefined\") return;\n\n\temProbeElement = document.createElement(\"div\");\n\temProbeElement.style.position = \"absolute\";\n\temProbeElement.style.visibility = \"hidden\";\n\temProbeElement.style.pointerEvents = \"none\";\n\temProbeElement.style.width = \"1em\";\n\temProbeElement.style.height = \"1px\";\n\temProbeElement.style.overflow = \"hidden\";\n\temProbeElement.setAttribute(\"aria-hidden\", \"true\");\n\n\t(document.body ?? document.documentElement).appendChild(emProbeElement);\n\n\tresizeObserver = new ResizeObserver(scheduleRebuild);\n\tresizeObserver.observe(emProbeElement);\n}\n\n/**\n * Полностью освобождает глобальные ресурсы, когда не осталось подписчиков.\n */\nfunction cleanupStore() {\n\tclearMqlSubscriptions();\n\tcurrentMql = null;\n\n\tif (rebuildTimer) {\n\t\tclearTimeout(rebuildTimer);\n\t\trebuildTimer = null;\n\t}\n\n\tif (mutationObserver) {\n\t\tmutationObserver.disconnect();\n\t\tmutationObserver = null;\n\t}\n\n\tif (resizeObserver) {\n\t\tresizeObserver.disconnect();\n\t\tresizeObserver = null;\n\t}\n\n\tif (emProbeElement?.parentNode) {\n\t\temProbeElement.parentNode.removeChild(emProbeElement);\n\t}\n\temProbeElement = null;\n\n\tinitialized = false;\n}\n\n/**\n * Лениво инициализирует singleton-store при первом подписчике.\n */\nfunction initializeStore() {\n\tif (initialized) return;\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tinitialized = true;\n\trebuildIfEmChanged(true);\n\tsetupEmObservers();\n}\n\n/**\n * API подписки для `useSyncExternalStore`.\n * Держит ровно один глобальный набор observer-ов и mql на всё приложение.\n */\nfunction subscribeStore(listener: () => void) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn () => {};\n\t}\n\n\tlisteners.add(listener);\n\tinitializeStore();\n\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t\tif (listeners.size === 0) {\n\t\t\tcleanupStore();\n\t\t}\n\t};\n}\n\n/**\n * Getter клиентского snapshot для `useSyncExternalStore`.\n */\nfunction getSnapshot() {\n\treturn snapshot;\n}\n\n/**\n * SSR-безопасный getter snapshot.\n */\nfunction getServerSnapshot() {\n\treturn DEFAULT_SNAPSHOT;\n}\n\n/**\n * Общий хук брейкпоинтов, работающий через singleton external store.\n */\nexport function useMatchMedia() {\n\treturn useSyncExternalStore(subscribeStore, getSnapshot, getServerSnapshot);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/media/breakpoints.ts","../../src/media/getCurrentFontSize.ts","../../src/media/pxToEm.ts","../../src/media/responsive.ts","../../src/media/useMediaQuery.ts","../../src/media/useMatchMedia.ts"],"sourcesContent":["export const BREAKPOINTS_EM = {\n\tmobileMax: 479.98 / 16,\n\ttabletMin: 480 / 16,\n\ttabletMax: 1151.98 / 16,\n\tlaptopMin: 1152 / 16\n} as const;\n\nexport type Breakpoint = \"mobile\" | \"tablet\" | \"laptop\";\nexport type MediaMatches = Record<Breakpoint, boolean>;\n","const DEFAULT_FONT_SIZE = 16;\nconst DEFAULT_LINE_HEIGHT = 1.2;\n\nexport function getCurrentFontSize(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst rootFontSize = window.getComputedStyle(target).fontSize;\n\tconst fontSize = Number.parseFloat(rootFontSize);\n\n\tif (!Number.isFinite(fontSize) || fontSize <= 0) {\n\t\treturn DEFAULT_FONT_SIZE;\n\t}\n\n\treturn fontSize;\n}\n\nexport function getCurrentLineHeight(element?: HTMLElement | null): number {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\tconst target = element ?? document.documentElement;\n\tconst computedStyles = window.getComputedStyle(target);\n\tconst rootLineHeight = computedStyles.lineHeight;\n\tconst lineHeight = Number.parseFloat(rootLineHeight);\n\n\tif (!Number.isFinite(lineHeight) || lineHeight <= 0) {\n\t\treturn DEFAULT_LINE_HEIGHT;\n\t}\n\n\treturn lineHeight / DEFAULT_FONT_SIZE;\n}\n\nexport function getControlHeight() {\n\tconst fontSize = getCurrentFontSize();\n\tconst lineHeight = getCurrentLineHeight();\n\n\t// Правильно расчета --control-height\n\treturn fontSize + lineHeight * 10;\n}\n","import { getCurrentFontSize } from \"./getCurrentFontSize\";\n\nexport function pxToEm(px: number, baseEm?: number): string {\n\tconst emSize = baseEm ?? getCurrentFontSize(); // динамическая привязка\n\treturn (px / emSize).toString();\n}\n","import { Breakpoint } from \"./breakpoints\";\n\nexport type ResponsiveMap<T> = {\n\tmobile?: T;\n\ttablet?: T;\n\tlaptop?: T;\n};\nexport type ResponsiveValue<T> = T | ResponsiveMap<T>;\nexport type UnwrapResponsive<T> = T extends ResponsiveValue<infer U> ? U : T;\n\nexport function isResponsiveMap<T>(v: ResponsiveValue<T>): v is ResponsiveMap<T> {\n\tif (typeof v !== \"object\" || v === null) return false;\n\n\t// ключевой момент: отличаем “responsive-map” от произвольного объекта T\n\treturn \"mobile\" in v || \"tablet\" in v || \"laptop\" in v || \"desktop\" in v;\n}\n\n/**\n * Превращает объект пропсов, где некоторые поля ResponsiveValue<...>,\n * в объект, где эти поля уже \"плоские\" (U).\n */\nexport type ResolvedProps<T extends object> = {\n\t[K in keyof T]: UnwrapResponsive<T[K]>;\n};\n\n/**\n * Резолвит responsive значение под текущий брейкпоинт.\n * Приоритеты:\n * - exact match (mobile/tablet/laptop)\n * - fallback на \"первое определённое\" в предсказуемом порядке\n */\nexport function resolveResponsiveValue<T>(value: ResponsiveValue<T> | undefined, bp: Breakpoint): T | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isResponsiveMap(value)) {\n\t\t// здесь value точно T (в т.ч. может быть объектом — и это ок)\n\t\treturn value;\n\t}\n\n\tif (bp === \"mobile\") return value.mobile ?? value.tablet ?? value.laptop;\n\tif (bp === \"tablet\") return value.tablet ?? value.mobile ?? value.laptop;\n\n\treturn value.laptop ?? value.tablet ?? value.mobile;\n}\n\n/**\n * Типобезопасно резолвит все поля объекта по списку ключей, которые responsive.\n * (TS-магия: на выходе будет ResolvedProps<T>.)\n */\nexport function resolveProps<T extends object>(props: T, bp: Breakpoint, responsiveKeys: readonly (keyof T)[]): ResolvedProps<T> {\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst out: any = { ...props };\n\n\tfor (const key of responsiveKeys) {\n\t\tout[key] = resolveResponsiveValue(out[key], bp);\n\t}\n\n\treturn out as ResolvedProps<T>;\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\n\ntype UseMediaQueryOptions = {\n\treadonly ssrFallback?: boolean;\n};\n\nconst noop = () => {};\n\nfunction readMediaQuery(query: string, fallback: boolean): boolean {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return fallback;\n\n\treturn window.matchMedia(query).matches;\n}\n\nfunction subscribeMediaQuery(query: string, listener: () => void): () => void {\n\tif (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return noop;\n\n\tconst mql = window.matchMedia(query);\n\n\tif (typeof mql.addEventListener === \"function\") {\n\t\tmql.addEventListener(\"change\", listener);\n\t\treturn () => mql.removeEventListener(\"change\", listener);\n\t}\n\n\tmql.addListener(listener);\n\treturn () => mql.removeListener(listener);\n}\n\n/**\n * SSR-безопасная подписка на произвольный media query.\n * Используется для клиентских предпочтений, где значение должно совпадать с SSR fallback на гидрации.\n */\nexport function useMediaQuery(query: string, { ssrFallback = false }: UseMediaQueryOptions = {}) {\n\tconst subscribe = useCallback((listener: () => void) => subscribeMediaQuery(query, listener), [query]);\n\tconst getSnapshot = useCallback(() => readMediaQuery(query, ssrFallback), [query, ssrFallback]);\n\tconst getServerSnapshot = useCallback(() => ssrFallback, [ssrFallback]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","import { useSyncExternalStore } from \"react\";\n\nimport { BREAKPOINTS_EM, type Breakpoint, type MediaMatches } from \"./breakpoints\";\nimport { getCurrentFontSize } from \"./getCurrentFontSize\";\n\n/**\n * Публичный снимок состояния, который возвращает хук/store.\n * Содержит сырые совпадения media query и вычисленный активный брейкпоинт.\n */\ntype MatchMediaSnapshot = {\n\tmatches: MediaMatches;\n\tactiveBreakpoint: Breakpoint;\n};\n\nfunction subscribeMql(mql: MediaQueryList, cb: () => void) {\n\tif (\"addEventListener\" in mql) {\n\t\tmql.addEventListener(\"change\", cb);\n\t\treturn () => mql.removeEventListener(\"change\", cb);\n\t}\n\t// Safari legacy\n\t// @ts-expect-error legacy api\n\tmql.addListener(cb);\n\t// @ts-expect-error legacy api\n\treturn () => mql.removeListener(cb);\n}\n\nfunction buildQueriesPx(emSize: number) {\n\tconst toPx = (em: number) => em * emSize;\n\n\tconst mobileMax = toPx(BREAKPOINTS_EM.mobileMax);\n\tconst tabletMin = toPx(BREAKPOINTS_EM.tabletMin);\n\tconst tabletMax = toPx(BREAKPOINTS_EM.tabletMax);\n\tconst laptopMin = toPx(BREAKPOINTS_EM.laptopMin);\n\n\treturn {\n\t\tmobile: `(width <= ${mobileMax}px)`,\n\t\ttablet: `(width >= ${tabletMin}px) and (width <= ${tabletMax}px)`,\n\t\tlaptop: `(width >= ${laptopMin}px)`\n\t} satisfies Record<Breakpoint, string>;\n}\n\nconst DEFAULT_MATCHES: MediaMatches = {\n\tmobile: false,\n\ttablet: false,\n\tlaptop: false\n};\n\nconst DEFAULT_SNAPSHOT: MatchMediaSnapshot = {\n\tmatches: DEFAULT_MATCHES,\n\tactiveBreakpoint: \"laptop\"\n};\n\n/**\n * Игнорируем микроколебания `em`, чтобы не перестраивать media query без необходимости.\n */\nconst EM_CHANGE_THRESHOLD = 0.25;\n/**\n * Склеиваем частые события observer-ов в одно перестроение.\n */\nconst EM_REBUILD_DEBOUNCE_MS = 80;\n\nlet snapshot: MatchMediaSnapshot = DEFAULT_SNAPSHOT;\nlet currentMql: Record<Breakpoint, MediaQueryList> | null = null;\nlet mqlUnsubs: Array<() => void> = [];\nlet emSizeRef = 0;\nlet initialized = false;\nlet mutationObserver: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet emProbeElement: HTMLDivElement | null = null;\nlet rebuildTimer: ReturnType<typeof setTimeout> | null = null;\n\nconst listeners = new Set<() => void>();\n\n/**\n * Выбирает один активный брейкпоинт по приоритету: mobile -> tablet -> laptop.\n */\nfunction getActiveBreakpoint(matches: MediaMatches): Breakpoint {\n\tif (matches.mobile) return \"mobile\";\n\tif (matches.tablet) return \"tablet\";\n\treturn \"laptop\";\n}\n\n/**\n * Поверхностное сравнение media-состояния, чтобы пропускать пустые уведомления.\n */\nfunction matchesEqual(a: MediaMatches, b: MediaMatches) {\n\treturn a.mobile === b.mobile && a.tablet === b.tablet && a.laptop === b.laptop;\n}\n\n/**\n * Обновляет глобальный snapshot и уведомляет подписчиков только при реальных изменениях.\n */\nfunction emitIfChanged(nextMatches: MediaMatches) {\n\tif (matchesEqual(snapshot.matches, nextMatches)) return;\n\tsnapshot = {\n\t\tmatches: nextMatches,\n\t\tactiveBreakpoint: getActiveBreakpoint(nextMatches)\n\t};\n\tlisteners.forEach((listener) => listener());\n}\n\n/**\n * Читает текущие значения `MediaQueryList` и публикует новый snapshot.\n */\nfunction recomputeFromMql() {\n\tif (!currentMql) return;\n\n\temitIfChanged({\n\t\tmobile: currentMql.mobile.matches,\n\t\ttablet: currentMql.tablet.matches,\n\t\tlaptop: currentMql.laptop.matches\n\t});\n}\n\n/**\n * Снимает активные mql-подписки перед перестроением/очисткой.\n */\nfunction clearMqlSubscriptions() {\n\tmqlUnsubs.forEach((unsubscribe) => unsubscribe());\n\tmqlUnsubs = [];\n}\n\n/**\n * Перестраивает media query, когда корневой `em` изменился достаточно сильно (или принудительно).\n * Это дорогой путь, поэтому он специально ограничен проверками.\n */\nfunction rebuildIfEmChanged(force = false) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tconst em = getCurrentFontSize();\n\tif (!force && currentMql && Math.abs(em - emSizeRef) < EM_CHANGE_THRESHOLD) return;\n\n\temSizeRef = em;\n\tclearMqlSubscriptions();\n\n\tconst queries = buildQueriesPx(em);\n\tcurrentMql = {\n\t\tmobile: window.matchMedia(queries.mobile),\n\t\ttablet: window.matchMedia(queries.tablet),\n\t\tlaptop: window.matchMedia(queries.laptop)\n\t};\n\n\tmqlUnsubs = (Object.keys(currentMql) as Breakpoint[]).map((key) => subscribeMql(currentMql![key], recomputeFromMql));\n\trecomputeFromMql();\n}\n\n/**\n * Дебаунс-обертка для перестроения query по событиям observer-ов.\n */\nfunction scheduleRebuild() {\n\tif (rebuildTimer) clearTimeout(rebuildTimer);\n\trebuildTimer = setTimeout(() => {\n\t\trebuildTimer = null;\n\t\trebuildIfEmChanged();\n\t}, EM_REBUILD_DEBOUNCE_MS);\n}\n\n/**\n * Наблюдает за изменением style/class у root и размером probe-элемента `1em`.\n * Это позволяет ловить изменения font-size/zoom без слушателя window resize.\n */\nfunction setupEmObservers() {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tmutationObserver = new MutationObserver(scheduleRebuild);\n\tmutationObserver.observe(document.documentElement, {\n\t\tattributes: true,\n\t\tattributeFilter: [\"style\", \"class\", \"data-font-size\"]\n\t});\n\n\tif (typeof ResizeObserver === \"undefined\") return;\n\n\temProbeElement = document.createElement(\"div\");\n\temProbeElement.style.position = \"absolute\";\n\temProbeElement.style.visibility = \"hidden\";\n\temProbeElement.style.pointerEvents = \"none\";\n\temProbeElement.style.width = \"1em\";\n\temProbeElement.style.height = \"1px\";\n\temProbeElement.style.overflow = \"hidden\";\n\temProbeElement.setAttribute(\"aria-hidden\", \"true\");\n\n\t(document.body ?? document.documentElement).appendChild(emProbeElement);\n\n\tresizeObserver = new ResizeObserver(scheduleRebuild);\n\tresizeObserver.observe(emProbeElement);\n}\n\n/**\n * Полностью освобождает глобальные ресурсы, когда не осталось подписчиков.\n */\nfunction cleanupStore() {\n\tclearMqlSubscriptions();\n\tcurrentMql = null;\n\n\tif (rebuildTimer) {\n\t\tclearTimeout(rebuildTimer);\n\t\trebuildTimer = null;\n\t}\n\n\tif (mutationObserver) {\n\t\tmutationObserver.disconnect();\n\t\tmutationObserver = null;\n\t}\n\n\tif (resizeObserver) {\n\t\tresizeObserver.disconnect();\n\t\tresizeObserver = null;\n\t}\n\n\tif (emProbeElement?.parentNode) {\n\t\temProbeElement.parentNode.removeChild(emProbeElement);\n\t}\n\temProbeElement = null;\n\n\tinitialized = false;\n}\n\n/**\n * Лениво инициализирует singleton-store при первом подписчике.\n */\nfunction initializeStore() {\n\tif (initialized) return;\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n\tinitialized = true;\n\trebuildIfEmChanged(true);\n\tsetupEmObservers();\n}\n\n/**\n * API подписки для `useSyncExternalStore`.\n * Держит ровно один глобальный набор observer-ов и mql на всё приложение.\n */\nfunction subscribeStore(listener: () => void) {\n\tif (typeof window === \"undefined\" || typeof document === \"undefined\") {\n\t\treturn () => {};\n\t}\n\n\tlisteners.add(listener);\n\tinitializeStore();\n\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t\tif (listeners.size === 0) {\n\t\t\tcleanupStore();\n\t\t}\n\t};\n}\n\n/**\n * Getter клиентского snapshot для `useSyncExternalStore`.\n */\nfunction getSnapshot() {\n\treturn snapshot;\n}\n\n/**\n * SSR-безопасный getter snapshot.\n */\nfunction getServerSnapshot() {\n\treturn DEFAULT_SNAPSHOT;\n}\n\n/**\n * Общий хук брейкпоинтов, работающий через singleton external store.\n */\nexport function useMatchMedia() {\n\treturn useSyncExternalStore(subscribeStore, getSnapshot, getServerSnapshot);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/file/README.mdx
CHANGED
|
@@ -4,14 +4,15 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
4
4
|
|
|
5
5
|
# Работа с пользовательскими файлами
|
|
6
6
|
|
|
7
|
-
Модуль `file` содержит browser helpers для
|
|
7
|
+
Модуль `file` содержит browser helpers и чистые функции для связанных задач:
|
|
8
8
|
|
|
9
9
|
- прочитать выбранный пользователем `File` как data URL или `ArrayBuffer`;
|
|
10
10
|
- проверить MIME-тип и размер файла перед чтением;
|
|
11
11
|
- прочитать изображение и получить его естественные размеры;
|
|
12
12
|
- определить MIME по расширению и безопаснее сформировать HTTP-заголовок `Content-Disposition` с Unicode-именем файла.
|
|
13
|
+
- проверить ограниченный набор сигнатур в `Uint8Array`, различая совпадение, противоречие и неподдержанный вариант.
|
|
13
14
|
|
|
14
|
-
Модуль является низкоуровневым foundation-слоем. Он не рисует `<input type="file">`, не отправляет файл на backend, не показывает preview или progress, не
|
|
15
|
+
Модуль является низкоуровневым foundation-слоем. Он не рисует `<input type="file">`, не отправляет файл на backend, не показывает preview или progress, не выполняет полную валидацию содержимого файла и не заменяет серверную policy.
|
|
15
16
|
|
|
16
17
|
## Содержание
|
|
17
18
|
|
|
@@ -29,6 +30,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
29
30
|
- [Ошибки `readFile`](#ошибки-readfile)
|
|
30
31
|
- [`readImageFile`](#readimagefile)
|
|
31
32
|
- [Ошибки `readImageFile`](#ошибки-readimagefile)
|
|
33
|
+
- [`checkFileSignature`](#checkfilesignature)
|
|
32
34
|
- [`resolveMimeTypeByExtension`](#resolvemimetypebyextension)
|
|
33
35
|
- [Helpers для `Content-Disposition`](#helpers-для-content-disposition)
|
|
34
36
|
- [Низкоуровневые helpers](#низкоуровневые-helpers)
|
|
@@ -54,6 +56,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
54
56
|
- получает `naturalWidth` и `naturalHeight` изображения в режиме data URL;
|
|
55
57
|
- сопоставляет ограниченный список расширений с MIME;
|
|
56
58
|
- формирует `Content-Disposition` одновременно с ASCII fallback и UTF-8 `filename*`.
|
|
59
|
+
- проверяет поддержанные начальные headers; PDF header ищет только в первых 1024 bytes.
|
|
57
60
|
|
|
58
61
|
### Чего модуль не делает
|
|
59
62
|
|
|
@@ -64,7 +67,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
64
67
|
- не скачивает `Blob` в браузере;
|
|
65
68
|
- не превращает произвольный base64 в `Blob`;
|
|
66
69
|
- не читает файл потоково и не сообщает progress;
|
|
67
|
-
- не проверяет
|
|
70
|
+
- не проверяет целостность и полную структуру PDF, ZIP, изображения или документа;
|
|
68
71
|
- не обнаруживает вирусы и вредоносное содержимое;
|
|
69
72
|
- не гарантирует, что `File.type` соответствует байтам;
|
|
70
73
|
- не сжимает и не изменяет изображение;
|
|
@@ -136,13 +139,14 @@ import { readFile } from "@ryuzaki13/react-foundation-lib/src/file/read-file/rea
|
|
|
136
139
|
|
|
137
140
|
Чистые helpers не зависят от DOM:
|
|
138
141
|
|
|
142
|
+
- `checkFileSignature`;
|
|
139
143
|
- `resolveMimeTypeByExtension`;
|
|
140
144
|
- `sanitizeFileName`;
|
|
141
145
|
- `createAsciiFilenameFallback`;
|
|
142
146
|
- `encodeFilenameRFC5987`;
|
|
143
147
|
- `createContentDispositionHeader`.
|
|
144
148
|
|
|
145
|
-
Их можно вызывать на серверной boundary
|
|
149
|
+
Их можно вызывать на серверной boundary. Проверке сигнатуры нужен только стандартный `Uint8Array`; строковым helpers — стандартные строковые функции и `encodeURIComponent`.
|
|
146
150
|
|
|
147
151
|
Важно различать:
|
|
148
152
|
|
|
@@ -151,6 +155,58 @@ import { readFile } from "@ryuzaki13/react-foundation-lib/src/file/read-file/rea
|
|
|
151
155
|
|
|
152
156
|
Сам импорт не начинает чтение файла. Browser API используется только при вызове соответствующей функции.
|
|
153
157
|
|
|
158
|
+
## `checkFileSignature`
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { checkFileSignature, type FileSignatureCheckResult } from "@ryuzaki13/react-foundation-lib/file";
|
|
162
|
+
|
|
163
|
+
const result: FileSignatureCheckResult = checkFileSignature({ extension: "png", bytes });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Функция принимает `FileSignatureCheckOptions = Readonly<{ extension: string; bytes: Uint8Array }>`.
|
|
167
|
+
Расширение передаётся без точки и имени файла; применяется только `trim().toLowerCase()`.
|
|
168
|
+
Bytes не копируются и не меняются, учитывается именно переданный view с его `byteOffset` и длиной.
|
|
169
|
+
Функция не использует Node API, DOM, MIME или filesystem и не вводит список разрешённых приложению форматов.
|
|
170
|
+
|
|
171
|
+
| Результат | Значение |
|
|
172
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| `match` | Совпала проверяемая сигнатура/header; остальное содержимое не проверено. |
|
|
174
|
+
| `mismatch` | Поддержанная сигнатура противоречит bytes либо распознанный обязательный header усечён/повреждён. |
|
|
175
|
+
| `unsupported` | Этот формат или представление не распознано достаточно надёжно этой частичной проверкой. Это не совпадение и не самостоятельный запрет файла. |
|
|
176
|
+
|
|
177
|
+
### Покрытие и пределы
|
|
178
|
+
|
|
179
|
+
| Расширение | Проверка |
|
|
180
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
181
|
+
| `png` | Восемь начальных bytes `89 50 4E 47 0D 0A 1A 0A`; без chunks/CRC. |
|
|
182
|
+
| `jpg`, `jpeg` | Начальный SOI `FF D8`; JFIF, Exif и конкретный APP marker не обязательны. Дополнительные FF fill bytes обрабатываются в первых 32 bytes; более длинный неизвестный prefix даёт `unsupported`. |
|
|
183
|
+
| `bmp` | Windows `BM`, полный 14-byte BITMAPFILEHEADER и нулевые reserved fields. Версия следующего DIB header не ограничивается; альтернативное начало даёт `unsupported`. |
|
|
184
|
+
| `psd` | `8BPS`, big-endian version 1, шесть нулевых reserved bytes. PSB version 2 и неизвестные версии дают `unsupported`. |
|
|
185
|
+
| `rtf` | Обычный текстовый header `{\rtf1` с границей control word; неизвестное начало и обёртки дают `unsupported`. |
|
|
186
|
+
| `rar` | Начальный RAR4/5 marker; неизвестные версии, SFX и неизвестное начало дают `unsupported`. Main/encryption header не проверяется. |
|
|
187
|
+
| `zip` | Local header `PK 03 04` с фиксированной частью 30 bytes или EOCD `PK 05 06` с частью 22 bytes. Split/SFX/неизвестное начало дают `unsupported`. Entries, payload, CRC и central directory не проверяются. |
|
|
188
|
+
| `7z` | Начальная сигнатура, major 0 и наличие SignatureHeader 32 bytes; minor не ограничивается. Неизвестный major, SFX и неизвестное начало дают `unsupported`. CRC/NextHeader не проверяются. |
|
|
189
|
+
| `pdf` | `%PDF-1.0`…`%PDF-1.7` или `%PDF-2.0` целиком в первых 1024 bytes, включая ненулевой offset. Неизвестные версии, альтернативный PS header и отсутствие распознанного header дают `unsupported`. Пересечение scan limit не считается EOF. |
|
|
190
|
+
|
|
191
|
+
Общий ZIP/CFB header **не доказывает** Office/ODF/XFL формат. Расширения `doc`, `ppt`, `xls`, `dot`, `docx`, `pptx`, `xlsx`, `docm`, `odt`, `fla`, `ai`, `txt`, `bak`, `cdr`, `tga`, `dt`, `mdb`, `accdb`, `erwin` и прочие не перечисленные выше возвращают `unsupported` независимо от общего контейнера.
|
|
192
|
+
|
|
193
|
+
Архивный header без начального marker не объявляется неверным архивом: SFX может иметь исполняемый prefix.
|
|
194
|
+
Но распознанный поддержанный header с неверными обязательными bytes или усечением не прячется за `unsupported`.
|
|
195
|
+
Например, `Rar!` с повреждёнными marker fields и `PK 03 04` короче 30 bytes дают `mismatch`.
|
|
196
|
+
|
|
197
|
+
Проверка читает фиксированные заголовки, максимум 32 bytes для JPEG и 1024 для PDF.
|
|
198
|
+
Она не сканирует весь файл, не распаковывает контейнеры и не валидирует структуру или безопасность payload.
|
|
199
|
+
Злоумышленник может добавить правильный header к произвольным bytes; результат останется `match`.
|
|
200
|
+
Обрабатывать `unsupported`, ограничивать размер/форматы и решать вопрос публикации обязан caller.
|
|
201
|
+
`readFile` не вызывает эту функцию автоматически и сохраняет прежнее поведение.
|
|
202
|
+
|
|
203
|
+
### Основания проверки
|
|
204
|
+
|
|
205
|
+
- [W3C PNG](https://www.w3.org/TR/PNG-Structure.html), [ITU-T T.81, Annex B](https://www.w3.org/Graphics/JPEG/itu-t81.pdf).
|
|
206
|
+
- [Microsoft BITMAPFILEHEADER](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader), [Adobe Photoshop file format](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [Microsoft RTF header](https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxrtfex/3cf14977-3883-404a-8ed8-57c99263fb76).
|
|
207
|
+
- [RARLAB](https://www.rarlab.com/technote.htm), [PKWARE APPNOTE](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT), [7-Zip format](https://raw.githubusercontent.com/ip7z/7zip/main/DOC/7zFormat.txt).
|
|
208
|
+
- [RFC 8118](https://www.rfc-editor.org/rfc/rfc8118.html), [Adobe PDF Reference, Appendix H](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf).
|
|
209
|
+
|
|
154
210
|
## Минимальные понятия
|
|
155
211
|
|
|
156
212
|
### Что такое `File`
|
|
@@ -1612,7 +1668,7 @@ Backend обязан повторно проверить:
|
|
|
1612
1668
|
|
|
1613
1669
|
### `File.type` недоверенный
|
|
1614
1670
|
|
|
1615
|
-
`allowedMime` сравнивает только `file.type`.
|
|
1671
|
+
`allowedMime` сравнивает только `file.type`. `readFile` не открывает заголовок для подтверждения signature; отдельный `checkFileSignature` работает с уже полученными bytes.
|
|
1616
1672
|
|
|
1617
1673
|
### Расширение недоверенное
|
|
1618
1674
|
|
|
@@ -1775,7 +1831,7 @@ expect(contentDisposition).toContain("filename*=UTF-8''%D0%9E");
|
|
|
1775
1831
|
9. `assertValidAllowedMime` и `assertValidAccept` не вызываются автоматически.
|
|
1776
1832
|
10. `maxBytes` не валидируется как положительное конечное число.
|
|
1777
1833
|
11. Файл читается целиком; progress и streaming отсутствуют.
|
|
1778
|
-
12. MIME и extension
|
|
1834
|
+
12. `readFile` не сверяет MIME и extension с реальными bytes. Отдельный `checkFileSignature` имеет частичное покрытие headers, описанное выше.
|
|
1779
1835
|
13. Resolver принимает только расширение без точки и поддерживает ограниченную таблицу.
|
|
1780
1836
|
14. Filename helpers предназначены для header, а не для безопасного filesystem path.
|
|
1781
1837
|
|
|
@@ -1818,6 +1874,7 @@ const result = await readImageFile(file, {
|
|
|
1818
1874
|
| `assertNotAborted` | Бросает `READ_ABORTED` для уже отменённого signal |
|
|
1819
1875
|
| `attachAbort` | Связывает signal с `FileReader.abort()` и возвращает cleanup |
|
|
1820
1876
|
| `buildMeta` | Копирует четыре metadata-поля `File` в новый объект |
|
|
1877
|
+
| `checkFileSignature` | Частично проверяет header в `Uint8Array`, не весь файл |
|
|
1821
1878
|
|
|
1822
1879
|
### MIME и HTTP filename
|
|
1823
1880
|
|
package/src/media/README.mdx
CHANGED
|
@@ -276,8 +276,8 @@ const invalid: Breakpoint = "desktop";
|
|
|
276
276
|
|
|
277
277
|
```ts
|
|
278
278
|
const BREAKPOINTS_EM = {
|
|
279
|
-
mobileMax:
|
|
280
|
-
tabletMin:
|
|
279
|
+
mobileMax: 479.98 / 16,
|
|
280
|
+
tabletMin: 480 / 16,
|
|
281
281
|
tabletMax: 1151.98 / 16,
|
|
282
282
|
laptopMin: 1152 / 16
|
|
283
283
|
} as const;
|
|
@@ -285,8 +285,8 @@ const BREAKPOINTS_EM = {
|
|
|
285
285
|
|
|
286
286
|
| Поле | Значение в `em` | При базе `16px` | Назначение |
|
|
287
287
|
| ----------- | --------------: | --------------: | ---------------------- |
|
|
288
|
-
| `mobileMax` | `
|
|
289
|
-
| `tabletMin` | `
|
|
288
|
+
| `mobileMax` | `29.99875` | `479.98px` | верхняя граница mobile |
|
|
289
|
+
| `tabletMin` | `30` | `480px` | нижняя граница tablet |
|
|
290
290
|
| `tabletMax` | `71.99875` | `1151.98px` | верхняя граница tablet |
|
|
291
291
|
| `laptopMin` | `72` | `1152px` | нижняя граница laptop |
|
|
292
292
|
|
|
@@ -320,7 +320,7 @@ type MediaMatches = {
|
|
|
320
320
|
|
|
321
321
|
| Root font-size | Mobile | Tablet | Laptop |
|
|
322
322
|
| -------------: | --------------- | ----------------------- | ----------- |
|
|
323
|
-
| `16px` | до `
|
|
323
|
+
| `16px` | до `479.98px` | `480px` — `1151.98px` | от `1152px` |
|
|
324
324
|
| `18px` | до `863.9775px` | `864px` — `1295.9775px` | от `1296px` |
|
|
325
325
|
| `20px` | до `959.975px` | `960px` — `1439.975px` | от `1440px` |
|
|
326
326
|
|