@ryuzaki13/react-foundation-lib 1.3.1 → 1.4.1

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.
Files changed (38) hide show
  1. package/README.md +20 -16
  2. package/dist/table/index.d.ts +1 -0
  3. package/dist/table/index.d.ts.map +1 -1
  4. package/dist/table/index.js +77 -64
  5. package/dist/table/index.js.map +1 -1
  6. package/dist/table/tanStackFeatures.d.ts +19 -0
  7. package/dist/table/tanStackFeatures.d.ts.map +1 -0
  8. package/dist/table/types.d.ts +17 -2
  9. package/dist/table/types.d.ts.map +1 -1
  10. package/package.json +9 -9
  11. package/src/array/README.mdx +1630 -0
  12. package/src/binary/README.mdx +1405 -0
  13. package/src/copy/README.mdx +992 -0
  14. package/src/crypto/README.mdx +1052 -0
  15. package/src/dom/README.mdx +2179 -0
  16. package/src/file/README.mdx +1957 -0
  17. package/src/formatters/README.mdx +469 -0
  18. package/src/formatters/boolean/README.mdx +494 -0
  19. package/src/formatters/date/README.mdx +732 -0
  20. package/src/formatters/number/README.mdx +542 -0
  21. package/src/formatters/pipeline/README.mdx +629 -0
  22. package/src/formatters/rowBased/README.mdx +486 -0
  23. package/src/formatters/strings/README.mdx +442 -0
  24. package/src/formatters/valueState/README.mdx +459 -0
  25. package/src/formulas/README.mdx +1283 -0
  26. package/src/binary/README.md +0 -110
  27. package/src/docs/browserAndReact.mdx +0 -150
  28. package/src/docs/filesAndExcel.mdx +0 -163
  29. package/src/docs/formatters-runtime.mdx +0 -455
  30. package/src/docs/formatters.mdx +0 -380
  31. package/src/docs/odataAndTables.mdx +0 -178
  32. package/src/docs/pureUtilities.mdx +0 -96
  33. package/src/docs/runtimeAndState.mdx +0 -155
  34. package/src/file/README.md +0 -289
  35. package/src/formatters/date/README.md +0 -520
  36. package/src/formatters/number/README.md +0 -123
  37. package/src/formatters/valueState/README.md +0 -275
  38. package/src/formulas/README.md +0 -42
@@ -0,0 +1,1405 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/API/Binary" />
4
+
5
+ # Работа с бинарными данными через `@ryuzaki13/react-foundation-lib/binary`
6
+
7
+ Модуль `binary` преобразует строковое бинарное содержимое в браузерный `Blob`, определяет предполагаемый MIME-тип документа и предоставляет React hook для типового сценария «base64 → `{ blob, mime }`».
8
+
9
+ Основной сценарий:
10
+
11
+ 1. backend возвращает содержимое файла строкой base64;
12
+ 2. приложение определяет MIME-тип по имени файла, data URL или сигнатуре данных;
13
+ 3. base64 декодируется в байты;
14
+ 4. из байтов создаётся `Blob`;
15
+ 5. вызывающий код передаёт `Blob` просмотрщику, скачивает файл или отправляет его дальше.
16
+
17
+ Модуль является предметно-независимым foundation-слоем. Он не знает бизнес-коды документов, не выполняет HTTP/OData-запросы, не загружает файлы с диска, не создаёт viewer и не проверяет, безопасно ли содержимое.
18
+
19
+ README рассчитан на разработчика, который использует опубликованный пакет без доступа к исходному коду. Здесь объясняются базовые понятия, все публичные функции, точный приоритет MIME, поддерживаемые форматы, ошибки, React lifecycle, безопасность и управление object URL.
20
+
21
+ ## Содержание
22
+
23
+ - [Назначение и границы модуля](#назначение-и-границы-модуля)
24
+ - [Установка и импорт](#установка-и-импорт)
25
+ - [Минимальные понятия](#минимальные-понятия)
26
+ - [Как выбрать API](#как-выбрать-api)
27
+ - [Быстрый старт](#быстрый-старт)
28
+ - [Поддерживаемые входные форматы](#поддерживаемые-входные-форматы)
29
+ - [`binaryToBlob`](#binarytoblob)
30
+ - [`detectMimeType`](#detectmimetype)
31
+ - [`useBinaryFile`](#usebinaryfile)
32
+ - [Работа с готовым `Blob`](#работа-с-готовым-blob)
33
+ - [Готовые рецепты](#готовые-рецепты)
34
+ - [Ошибки и диагностика](#ошибки-и-диагностика)
35
+ - [Безопасность](#безопасность)
36
+ - [Производительность и память](#производительность-и-память)
37
+ - [Тестирование](#тестирование)
38
+ - [Краткий справочник API](#краткий-справочник-api)
39
+ - [Вопросы и ответы](#вопросы-и-ответы)
40
+
41
+ ## Назначение и границы модуля
42
+
43
+ ### Что делает `binary`
44
+
45
+ - принимает base64, base64 data URL или raw binary string;
46
+ - нормализует пробелы, URL-safe алфавит и отсутствующий base64 padding;
47
+ - создаёт `Blob`;
48
+ - предполагает MIME по имени файла, объявлению data URL или небольшой сигнатуре;
49
+ - предоставляет React hook для base64-данных.
50
+
51
+ ### Чего модуль не делает
52
+
53
+ - не отправляет запросы и не получает данные с backend;
54
+ - не читает пользовательский `File` через `FileReader`;
55
+ - не скачивает `Blob` автоматически;
56
+ - не создаёт и не освобождает object URL;
57
+ - не выполняет полную проверку формата файла;
58
+ - не сканирует файл на вредоносное содержимое;
59
+ - не преобразует обычный Unicode-текст в UTF-8;
60
+ - не распаковывает ZIP, DOCX, XLSX или другие контейнеры;
61
+ - не исправляет ошибочный MIME из внешнего источника.
62
+
63
+ ### Соседние модули
64
+
65
+ | Задача | Модуль |
66
+ | ---------------------------------------------------------------- | ---------------------------------------------------------- |
67
+ | Декодировать строковый base64/data URL в `Blob` | `@ryuzaki13/react-foundation-lib/binary` |
68
+ | Прочитать выбранный пользователем `File` | `@ryuzaki13/react-foundation-lib/file` |
69
+ | Скачать готовый `Blob` или object URL | `@ryuzaki13/react-foundation-lib/dom` |
70
+ | Преобразовать base64 непосредственно в `Blob` без MIME-detection | `@ryuzaki13/react-foundation-lib/binary` |
71
+ | Полностью проверить структуру PDF, изображения или архива | Специализированный parser/validator на доверенной boundary |
72
+
73
+ Не копируйте логику скачивания рядом с `binaryToBlob`. Для готового `Blob` уже существует `downloadFileFromBlob` в `/dom`.
74
+
75
+ ## Установка и импорт
76
+
77
+ Установите пакет:
78
+
79
+ ```bash
80
+ npm install @ryuzaki13/react-foundation-lib
81
+ ```
82
+
83
+ Импортируйте API только из опубликованного subpath `/binary`:
84
+
85
+ ```ts
86
+ import { binaryToBlob, detectMimeType, useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
87
+ ```
88
+
89
+ Корневой импорт пакета не поддерживается:
90
+
91
+ ```ts
92
+ // Неправильно.
93
+ import { binaryToBlob } from "@ryuzaki13/react-foundation-lib";
94
+
95
+ // Правильно.
96
+ import { binaryToBlob } from "@ryuzaki13/react-foundation-lib/binary";
97
+ ```
98
+
99
+ ### React peer-зависимость
100
+
101
+ Entrypoint `/binary` экспортирует `useBinaryFile` и поэтому загружает React API. В текущем контракте пакета consumer должен предоставить совместимый React peer `>=19.2.0 <20.0.0`, даже если конкретный файл импортирует только `binaryToBlob` или `detectMimeType`.
102
+
103
+ Для React-приложения зависимость обычно уже установлена:
104
+
105
+ ```bash
106
+ npm install react react-dom
107
+ ```
108
+
109
+ Не подключайте отдельную вторую копию React ради этого модуля. Версия должна согласовываться с React host-приложения.
110
+
111
+ ### Runtime
112
+
113
+ Модуль ориентирован на браузер и использует:
114
+
115
+ - `atob`;
116
+ - `Blob`;
117
+ - `ArrayBuffer`;
118
+ - `Uint8Array`;
119
+ - React 19 hooks для `useBinaryFile`.
120
+
121
+ В тестовой или серверной среде эти Web APIs могут отсутствовать либо вести себя иначе. Наличие похожих globals в конкретной версии Node.js не превращает browser contract в гарантированный server API.
122
+
123
+ ## Минимальные понятия
124
+
125
+ ### Байт
126
+
127
+ Байт — целое число от `0` до `255`. Файл на низком уровне является последовательностью байтов.
128
+
129
+ Например, ASCII-строка `"ABC"` представлена байтами:
130
+
131
+ | Символ | Код байта |
132
+ | ------ | --------: |
133
+ | `A` | `65` |
134
+ | `B` | `66` |
135
+ | `C` | `67` |
136
+
137
+ ### Бинарные данные
138
+
139
+ Бинарные данные — произвольные байты файла: PDF, изображения, архивы, офисные документы и так далее. Не каждый набор байтов является корректным текстом.
140
+
141
+ ### Base64
142
+
143
+ Base64 кодирует байты ограниченным набором печатных символов:
144
+
145
+ ```text
146
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ
147
+ abcdefghijklmnopqrstuvwxyz
148
+ 0123456789
149
+ +/
150
+ ```
151
+
152
+ Пример:
153
+
154
+ ```text
155
+ байты текста "Hello" → SGVsbG8=
156
+ ```
157
+
158
+ Base64 не шифрует данные и не делает их безопасными. Любой, кто получил строку, может декодировать содержимое.
159
+
160
+ ### Padding `=`
161
+
162
+ В конце стандартного base64 могут находиться один или два символа `=`. Они дополняют длину строки до блока из четырёх символов.
163
+
164
+ Модуль умеет добавить отсутствующий padding:
165
+
166
+ ```text
167
+ SGVsbG8 → SGVsbG8=
168
+ ```
169
+
170
+ Строка, длина которой после нормализации даёт остаток `1` при делении на `4`, не может быть корректно дополнена и отклоняется.
171
+
172
+ ### URL-safe base64
173
+
174
+ URL-safe вариант заменяет:
175
+
176
+ - `+` на `-`;
177
+ - `/` на `_`.
178
+
179
+ Модуль возвращает эти символы в стандартный base64-алфавит перед декодированием.
180
+
181
+ ### Data URL
182
+
183
+ Data URL содержит metadata и данные в одной строке:
184
+
185
+ ```text
186
+ data:application/pdf;base64,JVBERi0xLjc...
187
+ ```
188
+
189
+ Структура:
190
+
191
+ ```text
192
+ data:<MIME>;base64,<payload>
193
+ ```
194
+
195
+ Модуль поддерживает только base64 data URL с обязательным маркером `;base64,`. Обычный percent-encoded data URL вида `data:text/plain,Hello%20world` не поддерживается.
196
+
197
+ ### MIME-тип
198
+
199
+ MIME сообщает, как интерпретировать содержимое:
200
+
201
+ | MIME | Тип содержимого |
202
+ | -------------------------- | ------------------------------ |
203
+ | `application/pdf` | PDF |
204
+ | `image/png` | PNG |
205
+ | `text/plain` | Обычный текст |
206
+ | `application/zip` | ZIP |
207
+ | `application/octet-stream` | Неопределённые бинарные данные |
208
+
209
+ MIME — metadata, а не доказательство реального формата. Строка может заявлять `image/png`, хотя внутри находится другой файл.
210
+
211
+ ### `Blob`
212
+
213
+ `Blob` — браузерный объект, который хранит последовательность байтов и MIME:
214
+
215
+ ```ts
216
+ const blob = new Blob([new Uint8Array([65, 66, 67])], { type: "text/plain" });
217
+ ```
218
+
219
+ У `Blob` есть:
220
+
221
+ - `blob.type` — MIME;
222
+ - `blob.size` — размер в байтах;
223
+ - `blob.text()` — прочитать как текст;
224
+ - `blob.arrayBuffer()` — получить байты.
225
+
226
+ ### Object URL
227
+
228
+ Object URL — временная браузерная ссылка на `Blob`:
229
+
230
+ ```ts
231
+ const objectUrl = URL.createObjectURL(blob);
232
+ ```
233
+
234
+ Она похожа на:
235
+
236
+ ```text
237
+ blob:https://example.test/...
238
+ ```
239
+
240
+ Object URL не содержит base64 и не является data URL. После использования его нужно освободить:
241
+
242
+ ```ts
243
+ URL.revokeObjectURL(objectUrl);
244
+ ```
245
+
246
+ Сам `Blob` не требует ручного `revoke`; освобождать нужно созданные через `URL.createObjectURL` URL.
247
+
248
+ ### Raw binary string
249
+
250
+ Raw binary string — JavaScript-строка, в которой код каждого символа рассматривается как один байт.
251
+
252
+ ```ts
253
+ const raw = String.fromCharCode(65, 66, 67);
254
+ ```
255
+
256
+ Это не обычная Unicode-строка и не UTF-8. Режим нужен только для legacy API, которые уже вернули байты в такой форме.
257
+
258
+ ## Как выбрать API
259
+
260
+ | Нужно | Использовать |
261
+ | ------------------------------------------------------ | ------------------------------------------------- |
262
+ | Превратить base64 в `Blob` | `binaryToBlob` |
263
+ | Превратить base64 data URL в `Blob` | `binaryToBlob` |
264
+ | Превратить raw binary string в `Blob` | `binaryToBlob(..., ..., false)` |
265
+ | Предположить MIME по имени и данным | `detectMimeType` |
266
+ | В React получить `{ blob, mime }` из base64 prop/state | `useBinaryFile` |
267
+ | Скачать готовый `Blob` | `downloadFileFromBlob` из `/dom` |
268
+ | Прочитать пользовательский `File` | `readFile` или `readImageFile` из `/file` |
269
+ | Использовать известные filename и MIME | Вызвать `detectMimeType`/`binaryToBlob` вручную |
270
+ | Полностью проверить файл | Специализированный validator, не `detectMimeType` |
271
+
272
+ ## Быстрый старт
273
+
274
+ ### Base64 и имя файла
275
+
276
+ ```ts
277
+ import { binaryToBlob, detectMimeType } from "@ryuzaki13/react-foundation-lib/binary";
278
+ import { downloadFileFromBlob } from "@ryuzaki13/react-foundation-lib/dom";
279
+
280
+ type BackendFile = {
281
+ fileName: string;
282
+ fileContent: string;
283
+ };
284
+
285
+ function downloadBackendFile(file: BackendFile) {
286
+ const mime = detectMimeType(file.fileName, file.fileContent);
287
+ const blob = binaryToBlob(file.fileContent, mime);
288
+
289
+ downloadFileFromBlob(file.fileName, blob);
290
+ }
291
+ ```
292
+
293
+ ### Base64 data URL
294
+
295
+ ```ts
296
+ const dataUrl = "data:text/plain;base64,SGVsbG8=";
297
+
298
+ const mime = detectMimeType(undefined, dataUrl);
299
+ const blob = binaryToBlob(dataUrl, mime);
300
+
301
+ console.log(mime); // "text/plain"
302
+ console.log(blob.type); // "text/plain"
303
+ console.log(await blob.text()); // "Hello"
304
+ ```
305
+
306
+ Важно: `binaryToBlob(dataUrl)` самостоятельно не возьмёт MIME из data URL. Без второго аргумента `blob.type` будет `application/octet-stream`.
307
+
308
+ ### Общий поток
309
+
310
+ ```text
311
+ fileName ───────────────────────┐
312
+ ├─> detectMimeType ─> MIME ─┐
313
+ base64 / base64 data URL ──────┘ │
314
+ ├─> Blob
315
+ base64 / base64 data URL ─────────> binaryToBlob ─────────┘
316
+ │
317
+ ├─> viewer
318
+ ├─> download
319
+ └─> FormData
320
+ ```
321
+
322
+ ## Поддерживаемые входные форматы
323
+
324
+ | Вход | Поддержка | Пример/комментарий |
325
+ | ------------------------------- | ------------------------------- | ------------------------------------------------ |
326
+ | Стандартный base64 | Да | `SGVsbG8=` |
327
+ | Base64 без padding | Да | `SGVsbG8` |
328
+ | Base64 с пробелами и переносами | Да | Пробельные символы удаляются |
329
+ | URL-safe base64 | Да | `-` и `_` заменяются на `+` и `/` |
330
+ | Base64 data URL | Да | `data:image/png;base64,...` |
331
+ | Data URL с параметрами | Да | `data:text/plain;charset=utf-8;base64,...` |
332
+ | Data URL без MIME | Да | `data:;base64,...` |
333
+ | Raw binary string | Да, только с `isBase64 = false` | Каждый JS-символ превращается в один байт |
334
+ | Обычная Unicode-строка | Нет как raw binary | Это не UTF-8 encoder |
335
+ | Percent-encoded data URL | Нет | `data:text/plain,Hello%20world` |
336
+ | Hex-строка | Нет | `25504446` не трактуется как байты PDF |
337
+ | `Uint8Array` | Нет | API принимает `string`; создайте `Blob` напрямую |
338
+ | `ArrayBuffer` | Нет | API принимает `string`; создайте `Blob` напрямую |
339
+ | Пустая строка | Нет | Выбрасывается ошибка |
340
+
341
+ ## `binaryToBlob`
342
+
343
+ ### Сигнатура
344
+
345
+ ```ts
346
+ function binaryToBlob(binaryData: string, mimeType?: string, isBase64?: boolean): Blob;
347
+ ```
348
+
349
+ Фактические значения по умолчанию:
350
+
351
+ ```ts
352
+ mimeType = "application/octet-stream";
353
+ isBase64 = true;
354
+ ```
355
+
356
+ ### Параметры
357
+
358
+ | Параметр | Тип | По умолчанию | Назначение |
359
+ | ------------ | --------- | -------------------------- | ------------------------------------------------------ |
360
+ | `binaryData` | `string` | Обязательный | Base64/data URL или raw binary string |
361
+ | `mimeType` | `string` | `application/octet-stream` | MIME создаваемого `Blob` |
362
+ | `isBase64` | `boolean` | `true` | Декодировать base64 или читать коды символов как байты |
363
+
364
+ ### Результат
365
+
366
+ Возвращается новый непустой `Blob`.
367
+
368
+ ```ts
369
+ const blob = binaryToBlob("SGVsbG8=", "text/plain");
370
+
371
+ console.log(blob.size); // 5
372
+ console.log(blob.type); // "text/plain"
373
+ console.log(await blob.text()); // "Hello"
374
+ ```
375
+
376
+ Функция синхронная. Методы чтения самого `Blob`, например `text()` и `arrayBuffer()`, возвращают `Promise`.
377
+
378
+ ### Что происходит в base64-режиме
379
+
380
+ При `isBase64 = true` функция:
381
+
382
+ 1. проверяет, не является ли строка base64 data URL;
383
+ 2. для data URL отделяет payload после запятой;
384
+ 3. удаляет все пробельные символы;
385
+ 4. заменяет URL-safe `-` и `_`;
386
+ 5. проверяет допустимый алфавит и padding;
387
+ 6. добавляет отсутствующий padding;
388
+ 7. декодирует строку браузерным `atob`;
389
+ 8. переносит коды полученной binary string в `Uint8Array`;
390
+ 9. копирует байты в отдельный `ArrayBuffer`;
391
+ 10. создаёт `Blob` с переданным MIME.
392
+
393
+ ### Обычный base64
394
+
395
+ ```ts
396
+ const blob = binaryToBlob("SGVsbG8=", "text/plain");
397
+
398
+ console.log(await blob.text()); // "Hello"
399
+ ```
400
+
401
+ ### Пробелы и переносы строк
402
+
403
+ Пробельные символы удаляются во всей base64-строке:
404
+
405
+ ```ts
406
+ const blob = binaryToBlob("SGVs\n bG8=\t", "text/plain");
407
+
408
+ console.log(await blob.text()); // "Hello"
409
+ ```
410
+
411
+ Это удобно для MIME-wrapped base64, но не исправляет данные, в которых символ `+` был случайно заменён пробелом при URL/form-декодировании. В таком случае байт будет потерян.
412
+
413
+ ### Отсутствующий padding
414
+
415
+ ```ts
416
+ const blob = binaryToBlob("SGVsbG8", "text/plain");
417
+
418
+ console.log(await blob.text()); // "Hello"
419
+ ```
420
+
421
+ ### URL-safe base64
422
+
423
+ ```ts
424
+ const blob = binaryToBlob("__8", "application/octet-stream");
425
+
426
+ const bytes = new Uint8Array(await blob.arrayBuffer());
427
+
428
+ console.log([...bytes]); // [255, 255]
429
+ ```
430
+
431
+ ### Data URL
432
+
433
+ ```ts
434
+ const dataUrl = "data:text/plain;base64,SGVsbG8=";
435
+
436
+ const blob = binaryToBlob(dataUrl, "text/plain");
437
+
438
+ console.log(await blob.text()); // "Hello"
439
+ ```
440
+
441
+ Поддерживается data URL с дополнительными параметрами:
442
+
443
+ ```ts
444
+ const dataUrl = "data:text/plain;charset=utf-8;base64,SGVsbG8=";
445
+ ```
446
+
447
+ Распознавание префикса выполняется без учёта регистра для `data:` и `base64`.
448
+
449
+ ### MIME data URL не применяется автоматически
450
+
451
+ `binaryToBlob` отделяет payload data URL, но намеренно использует только аргумент `mimeType`:
452
+
453
+ ```ts
454
+ const dataUrl = "data:image/png;base64,iVBORw0KGgo=";
455
+
456
+ const blob = binaryToBlob(dataUrl);
457
+
458
+ console.log(blob.type);
459
+ // "application/octet-stream"
460
+ ```
461
+
462
+ Для использования MIME из data URL сначала вызовите `detectMimeType`:
463
+
464
+ ```ts
465
+ const mime = detectMimeType(undefined, dataUrl);
466
+ const blob = binaryToBlob(dataUrl, mime);
467
+ ```
468
+
469
+ ### Пустой MIME
470
+
471
+ `undefined`, пустая строка или строка только из пробелов приводят к `application/octet-stream`:
472
+
473
+ ```ts
474
+ binaryToBlob("SGVsbG8=", undefined).type;
475
+ // "application/octet-stream"
476
+
477
+ binaryToBlob("SGVsbG8=", "").type;
478
+ // "application/octet-stream"
479
+
480
+ binaryToBlob("SGVsbG8=", " ").type;
481
+ // "application/octet-stream"
482
+ ```
483
+
484
+ Непустой MIME передаётся в конструктор `Blob` после `trim()`. Окончательная нормализация `blob.type` выполняется браузерной реализацией `Blob`.
485
+
486
+ ### Raw binary string
487
+
488
+ Чтобы отключить base64-декодирование:
489
+
490
+ ```ts
491
+ const blob = binaryToBlob("ABC", "application/octet-stream", false);
492
+
493
+ const bytes = new Uint8Array(await blob.arrayBuffer());
494
+
495
+ console.log([...bytes]); // [65, 66, 67]
496
+ ```
497
+
498
+ Каждый UTF-16 code unit строки записывается в `Uint8Array`. Значения больше `255` обрезаются до младшего байта.
499
+
500
+ Поэтому обычный Unicode-текст нельзя передавать как raw binary:
501
+
502
+ ```ts
503
+ // Неправильно для UTF-8.
504
+ binaryToBlob("Привет", "text/plain", false);
505
+ ```
506
+
507
+ Для текста используйте `TextEncoder` и создайте `Blob` напрямую:
508
+
509
+ ```ts
510
+ const bytes = new TextEncoder().encode("Привет");
511
+ const blob = new Blob([bytes], { type: "text/plain;charset=utf-8" });
512
+ ```
513
+
514
+ ### Функция не выполняет MIME-detection
515
+
516
+ ```ts
517
+ const blob = binaryToBlob(pdfBase64);
518
+
519
+ console.log(blob.type);
520
+ // "application/octet-stream", даже если внутри PDF.
521
+ ```
522
+
523
+ Если MIME неизвестен:
524
+
525
+ ```ts
526
+ const mime = detectMimeType(fileName, pdfBase64);
527
+ const blob = binaryToBlob(pdfBase64, mime);
528
+ ```
529
+
530
+ ### Ошибки
531
+
532
+ | Условие | Сообщение |
533
+ | ------------------------------------------------------ | ----------------------------------------- |
534
+ | `binaryData === ""` | `Не переданы бинарные данные` |
535
+ | После удаления префикса/пробелов payload пуст | `После преобразования не осталось данных` |
536
+ | Недопустимый base64-символ | `Некорректный формат base64` |
537
+ | Длина base64 имеет невозможный остаток | `Некорректный формат base64` |
538
+ | `atob` отклонил редкий синтаксически прошедший вариант | Сообщение browser `DOMException` |
539
+ | Низкоуровневый API выбросил не-`Error` | `Некорректный формат бинарных данных` |
540
+
541
+ Пример обработки:
542
+
543
+ ```ts
544
+ try {
545
+ const blob = binaryToBlob(payload, "application/pdf");
546
+
547
+ showDocument(blob);
548
+ } catch (error) {
549
+ const message = error instanceof Error ? error.message : "Не удалось декодировать файл";
550
+
551
+ showError(message);
552
+ }
553
+ ```
554
+
555
+ Не включайте исходную base64-строку в сообщение, лог или error report: она может содержать персональные или конфиденциальные данные.
556
+
557
+ ## `detectMimeType`
558
+
559
+ ### Сигнатура
560
+
561
+ ```ts
562
+ function detectMimeType(fileName?: string, binaryData?: string): string;
563
+ ```
564
+
565
+ Функция всегда возвращает строку и не выбрасывает ошибку для неизвестного либо некорректного base64. Если MIME определить не удалось, возвращается `application/pdf`.
566
+
567
+ ### Приоритет источников
568
+
569
+ Проверки выполняются строго в таком порядке:
570
+
571
+ | Приоритет | Источник | Когда используется |
572
+ | --------: | ------------------------------- | --------------------------------- |
573
+ | 1 | Расширение `fileName` | Расширение известно таблице |
574
+ | 2 | MIME из base64 data URL | Filename не дал известный MIME |
575
+ | 3 | Сигнатура декодированных данных | Data URL не объявил непустой MIME |
576
+ | 4 | Fallback PDF | Ничего не распознано |
577
+
578
+ Первый успешный источник побеждает. Функция не сравнивает источники между собой.
579
+
580
+ ```ts
581
+ const pngDataUrl = "data:image/png;base64,iVBORw0KGgo=";
582
+
583
+ detectMimeType("report.pdf", pngDataUrl);
584
+ // "application/pdf": расширение filename имеет приоритет.
585
+ ```
586
+
587
+ ### MIME по расширению файла
588
+
589
+ Поддерживается последняя часть имени после точки. Регистр не важен.
590
+
591
+ | Расширение | MIME |
592
+ | ---------- | --------------------------------------------------------------------------- |
593
+ | `bmp` | `image/bmp` |
594
+ | `csv` | `text/csv` |
595
+ | `doc` | `application/msword` |
596
+ | `docx` | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` |
597
+ | `gif` | `image/gif` |
598
+ | `jpeg` | `image/jpeg` |
599
+ | `jpg` | `image/jpeg` |
600
+ | `json` | `application/json` |
601
+ | `pdf` | `application/pdf` |
602
+ | `png` | `image/png` |
603
+ | `ppt` | `application/vnd.ms-powerpoint` |
604
+ | `pptx` | `application/vnd.openxmlformats-officedocument.presentationml.presentation` |
605
+ | `svg` | `image/svg+xml` |
606
+ | `tif` | `image/tiff` |
607
+ | `tiff` | `image/tiff` |
608
+ | `txt` | `text/plain` |
609
+ | `webp` | `image/webp` |
610
+ | `xls` | `application/vnd.ms-excel` |
611
+ | `xlsx` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
612
+ | `xml` | `application/xml` |
613
+ | `zip` | `application/zip` |
614
+
615
+ Примеры:
616
+
617
+ ```ts
618
+ detectMimeType("report.xlsx");
619
+ // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
620
+
621
+ detectMimeType("PHOTO.JPG");
622
+ // "image/jpeg"
623
+
624
+ detectMimeType("archive.backup.zip");
625
+ // "application/zip"
626
+ ```
627
+
628
+ Filename не очищается от query/hash и пробелов:
629
+
630
+ ```ts
631
+ detectMimeType("photo.jpg?download=1");
632
+ // "application/pdf", если binaryData не передан.
633
+
634
+ detectMimeType("photo.jpg ");
635
+ // "application/pdf", если binaryData не передан.
636
+ ```
637
+
638
+ Передавайте именно имя файла, а не URL. Если есть URL, сначала безопасно извлеките pathname/filename на соответствующей boundary.
639
+
640
+ ### MIME из data URL
641
+
642
+ ```ts
643
+ const dataUrl = "data:image/png;base64,iVBORw0KGgo=";
644
+
645
+ detectMimeType(undefined, dataUrl);
646
+ // "image/png"
647
+ ```
648
+
649
+ Для параметров используется только часть до первого `;`:
650
+
651
+ ```ts
652
+ const dataUrl = "data:text/plain;charset=utf-8;base64,SGVsbG8=";
653
+
654
+ detectMimeType(undefined, dataUrl);
655
+ // "text/plain"
656
+ ```
657
+
658
+ Если MIME пуст:
659
+
660
+ ```ts
661
+ const dataUrl = "data:;base64,JVBERi0xLjc=";
662
+
663
+ detectMimeType(undefined, dataUrl);
664
+ // Проверка продолжится по сигнатуре.
665
+ ```
666
+
667
+ Объявленный MIME возвращается без декодирования и проверки payload:
668
+
669
+ ```ts
670
+ detectMimeType(undefined, "data:image/png;base64,not-valid-base64");
671
+ // "image/png"
672
+ ```
673
+
674
+ Поэтому data URL MIME нельзя считать доказательством формата.
675
+
676
+ ### MIME по сигнатуре
677
+
678
+ Если filename и data URL не дали тип, функция пытается декодировать начало base64. Ошибка декодирования скрывается, после чего используется fallback.
679
+
680
+ Проверяются следующие признаки:
681
+
682
+ | Начало данных | Результат | Ограничение |
683
+ | -------------------------------------------------- | ----------------- | -------------------------------------------------- |
684
+ | bytes `25 50 44 46` (`%PDF`) | `application/pdf` | Проверяются только первые четыре байта |
685
+ | bytes `FF D8 FF` | `image/jpeg` | Проверяются только первые три байта |
686
+ | bytes `89 50 4E 47` | `image/png` | Полная восьмибайтовая PNG signature не проверяется |
687
+ | текст `GIF` | `image/gif` | Версия `87a/89a` не проверяется |
688
+ | текст `RIFF` | `image/webp` | Маркер `WEBP` дальше в header не проверяется |
689
+ | bytes `50 4B` (`PK`) | `application/zip` | OOXML-файлы без filename тоже выглядят как ZIP |
690
+ | первый непустой текст начинается с `<?xml` или `<` | `application/xml` | HTML и SVG тоже могут попасть в эту ветку |
691
+
692
+ Примеры:
693
+
694
+ ```ts
695
+ detectMimeType(undefined, pdfBase64);
696
+ // "application/pdf"
697
+
698
+ detectMimeType(undefined, jpegBase64);
699
+ // "image/jpeg"
700
+
701
+ detectMimeType(undefined, zipBase64);
702
+ // "application/zip"
703
+ ```
704
+
705
+ DOCX, XLSX и PPTX являются ZIP-контейнерами. Без имени файла они обычно определятся как `application/zip`, а не как конкретный Office MIME.
706
+
707
+ Любое RIFF-содержимое соответствует текущей короткой проверке и может быть ошибочно определено как `image/webp`. Например, WAV/AVI без filename не должны доверять этому результату.
708
+
709
+ Текст, начинающийся после пробелов с `<`, считается XML-подобным. Это эвристика, а не XML parser.
710
+
711
+ ### Fallback
712
+
713
+ Если ничего не распознано:
714
+
715
+ ```ts
716
+ detectMimeType();
717
+ // "application/pdf"
718
+
719
+ detectMimeType(undefined, "не base64");
720
+ // "application/pdf"
721
+
722
+ detectMimeType("unknown.bin");
723
+ // "application/pdf"
724
+ ```
725
+
726
+ Fallback PDF является осознанным предметно-независимым default текущего контракта, оптимизированным под документные сценарии пакета. Это не означает, что неизвестные данные действительно являются PDF.
727
+
728
+ Если вызывающий код не может принимать такое предположение, он должен:
729
+
730
+ - требовать известный MIME от backend;
731
+ - валидировать формат специализированным parser;
732
+ - или применять собственный fallback после отдельной проверки.
733
+
734
+ ### `detectMimeType` не является validator
735
+
736
+ Функция отвечает только на вопрос «какой MIME предположить». Она не отвечает на вопросы:
737
+
738
+ - полностью ли корректен файл;
739
+ - соответствует ли содержимое расширению;
740
+ - безопасно ли показывать файл;
741
+ - не повреждён ли архив;
742
+ - разрешён ли тип бизнес-правилами;
743
+ - можно ли доверять MIME из внешнего источника.
744
+
745
+ ## `useBinaryFile`
746
+
747
+ ### Сигнатура
748
+
749
+ ```ts
750
+ function useBinaryFile(bin: string | undefined): {
751
+ data: {
752
+ blob: Blob;
753
+ mime: string;
754
+ } | null;
755
+ error: string | null;
756
+ };
757
+ ```
758
+
759
+ Hook принимает только base64/base64 data URL. Он всегда вызывает:
760
+
761
+ ```ts
762
+ const mime = detectMimeType(undefined, bin);
763
+ const blob = binaryToBlob(bin, mime, true);
764
+ ```
765
+
766
+ Filename передать нельзя, raw binary mode использовать нельзя.
767
+
768
+ ### Возвращаемое состояние
769
+
770
+ | Поле | Значение | Смысл |
771
+ | ------- | ---------------- | ------------------------------------ |
772
+ | `data` | `{ blob, mime }` | Декодирование завершилось успешно |
773
+ | `data` | `null` | Нет результата либо произошла ошибка |
774
+ | `error` | `string` | Строковое представление ошибки |
775
+ | `error` | `null` | Ошибки нет |
776
+
777
+ ### Lifecycle
778
+
779
+ Преобразование выполняется после commit в React effect:
780
+
781
+ 1. первый render возвращает `{ data: null, error: null }`;
782
+ 2. после commit effect обрабатывает `bin`;
783
+ 3. успех записывает `{ blob, mime }` и очищает ошибку;
784
+ 4. ошибка очищает `data` и записывает `String(error)`;
785
+ 5. `undefined` или пустая строка очищают оба поля.
786
+
787
+ Пример последовательности:
788
+
789
+ | `bin` | Состояние после effect |
790
+ | --------------------- | ---------------------------------------------------------- |
791
+ | `undefined` | `{ data: null, error: null }` |
792
+ | `""` | `{ data: null, error: null }` |
793
+ | Корректный base64 PDF | `{ data: { blob, mime: "application/pdf" }, error: null }` |
794
+ | Некорректный base64 | `{ data: null, error: "Error: ..." }` |
795
+ | Строка из пробелов | Ошибка после удаления пробелов |
796
+
797
+ У hook нет отдельного `isLoading`/`isProcessing`. На первом render `data: null` и `error: null` может означать как отсутствие входа, так и ещё не выполненный effect.
798
+
799
+ ### Смена `bin`
800
+
801
+ При смене одного непустого `bin` на другой предыдущие `data`/`error` могут оставаться видимыми до выполнения следующего effect. Hook не очищает состояние синхронно во время render.
802
+
803
+ Если нельзя кратковременно показать предыдущий документ:
804
+
805
+ - разместите hook в компоненте, который remount-ится по стабильному ID документа;
806
+ - или выполните преобразование на явной границе загрузки;
807
+ - или добавьте внешний state machine с identity результата.
808
+
809
+ Не используйте саму большую base64-строку как React `key`. Используйте безопасный стабильный ID документа:
810
+
811
+ ```tsx
812
+ <BinaryDocument key={document.id} bin={document.content} />
813
+ ```
814
+
815
+ ### Определение MIME внутри hook
816
+
817
+ Hook не знает filename. Приоритет будет таким:
818
+
819
+ 1. MIME из data URL;
820
+ 2. сигнатура;
821
+ 3. fallback `application/pdf`.
822
+
823
+ Обычный base64 текста без data URL и известной сигнатуры станет `Blob` с `application/pdf`:
824
+
825
+ ```tsx
826
+ const { data } = useBinaryFile("SGVsbG8=");
827
+
828
+ // После effect:
829
+ // data?.mime === "application/pdf"
830
+ ```
831
+
832
+ Если известны имя или MIME, вызывайте `detectMimeType` и `binaryToBlob` вручную.
833
+
834
+ ### Ошибки и logging
835
+
836
+ При ошибке hook:
837
+
838
+ 1. вызывает внутренний `logError(error)`;
839
+ 2. `logError` пишет ошибку в `console.error`;
840
+ 3. возвращает `error: String(error)`;
841
+ 4. очищает `data`.
842
+
843
+ Для обычного `Error` строка включает имя:
844
+
845
+ ```text
846
+ Error: Некорректный формат base64
847
+ ```
848
+
849
+ Не сравнивайте `error` с точным текстом browser `DOMException`: сообщение низкоуровневого `atob` может отличаться между средами.
850
+
851
+ ### Базовое использование
852
+
853
+ ```tsx
854
+ import { useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
855
+
856
+ type BinaryDocumentProps = {
857
+ readonly bin: string | undefined;
858
+ };
859
+
860
+ export function BinaryDocument(props: BinaryDocumentProps) {
861
+ const { data, error } = useBinaryFile(props.bin);
862
+
863
+ if (error) {
864
+ return <p role="alert">Не удалось подготовить документ</p>;
865
+ }
866
+
867
+ if (!props.bin) {
868
+ return <p>Документ не передан</p>;
869
+ }
870
+
871
+ if (!data) {
872
+ return <p>Подготовка документа…</p>;
873
+ }
874
+
875
+ return <DocumentViewer blob={data.blob} mime={data.mime} />;
876
+ }
877
+ ```
878
+
879
+ `DocumentViewer` в примере — компонент host-приложения. Пакет `binary` его не предоставляет.
880
+
881
+ ### Hook не создаёт object URL
882
+
883
+ `Blob` можно передать компоненту, который принимает его напрямую. Если viewer требует строковый URL, вызывающий компонент отвечает за создание и cleanup.
884
+
885
+ Корректная синхронизация с `iframe`:
886
+
887
+ ```tsx
888
+ import { useEffect, useRef } from "react";
889
+
890
+ type BlobFrameProps = {
891
+ readonly blob: Blob;
892
+ readonly title: string;
893
+ };
894
+
895
+ export function BlobFrame(props: BlobFrameProps) {
896
+ const frameRef = useRef<HTMLIFrameElement>(null);
897
+
898
+ useEffect(() => {
899
+ const frame = frameRef.current;
900
+
901
+ if (!frame) return;
902
+
903
+ const objectUrl = URL.createObjectURL(props.blob);
904
+
905
+ frame.src = objectUrl;
906
+
907
+ return () => {
908
+ frame.removeAttribute("src");
909
+ URL.revokeObjectURL(objectUrl);
910
+ };
911
+ }, [props.blob]);
912
+
913
+ return <iframe ref={frameRef} title={props.title} />;
914
+ }
915
+ ```
916
+
917
+ Effect синхронизирует React с внешним browser API и симметрично освобождает URL. Такой cleanup устойчив к повторному setup/cleanup в React Strict Mode.
918
+
919
+ ### Когда hook не подходит
920
+
921
+ Не используйте `useBinaryFile`, если:
922
+
923
+ - код выполняется вне React;
924
+ - MIME уже известен и не равен PDF fallback;
925
+ - нужно учитывать filename;
926
+ - данные являются raw binary string;
927
+ - результат нужен немедленно в event handler;
928
+ - нужен явный processing/error state machine;
929
+ - преобразование большого payload нужно вынести с main thread.
930
+
931
+ ## Работа с готовым `Blob`
932
+
933
+ ### Скачать файл
934
+
935
+ Используйте `/dom`:
936
+
937
+ ```ts
938
+ import { downloadFileFromBlob } from "@ryuzaki13/react-foundation-lib/dom";
939
+
940
+ downloadFileFromBlob(fileName, blob);
941
+ ```
942
+
943
+ Helper создаёт object URL, запускает скачивание и освобождает URL.
944
+
945
+ ### Передать в viewer
946
+
947
+ Если viewer принимает `Blob`:
948
+
949
+ ```tsx
950
+ <DocumentViewer blob={blob} mime={blob.type} />
951
+ ```
952
+
953
+ Если viewer принимает URL, создайте object URL на его lifecycle boundary и обязательно выполните cleanup.
954
+
955
+ ### Прочитать байты
956
+
957
+ ```ts
958
+ const buffer = await blob.arrayBuffer();
959
+ const bytes = new Uint8Array(buffer);
960
+
961
+ console.log(bytes.length);
962
+ ```
963
+
964
+ ### Прочитать текст
965
+
966
+ ```ts
967
+ const text = await blob.text();
968
+ ```
969
+
970
+ `blob.text()` декодирует данные как текст. Для PDF, ZIP и изображений результат не имеет полезного смысла.
971
+
972
+ ### Отправить через `FormData`
973
+
974
+ ```ts
975
+ const formData = new FormData();
976
+
977
+ formData.append("file", blob, fileName);
978
+ ```
979
+
980
+ Transport, authentication и обработка ответа не принадлежат модулю `binary`.
981
+
982
+ ### Object URL cleanup
983
+
984
+ Неправильно:
985
+
986
+ ```ts
987
+ const src = URL.createObjectURL(blob);
988
+ image.src = src;
989
+
990
+ // URL никогда не освобождается.
991
+ ```
992
+
993
+ Правильно:
994
+
995
+ ```ts
996
+ const src = URL.createObjectURL(blob);
997
+ image.src = src;
998
+
999
+ // Когда URL больше не нужен:
1000
+ image.removeAttribute("src");
1001
+ URL.revokeObjectURL(src);
1002
+ ```
1003
+
1004
+ Не вызывайте `revokeObjectURL` до того, как consumer закончил использовать URL.
1005
+
1006
+ ## Готовые рецепты
1007
+
1008
+ ### Скачать backend-файл с известным именем
1009
+
1010
+ ```ts
1011
+ import { binaryToBlob, detectMimeType } from "@ryuzaki13/react-foundation-lib/binary";
1012
+ import { downloadFileFromBlob } from "@ryuzaki13/react-foundation-lib/dom";
1013
+
1014
+ function downloadFile(fileName: string, fileContent: string) {
1015
+ const mime = detectMimeType(fileName, fileContent);
1016
+ const blob = binaryToBlob(fileContent, mime);
1017
+
1018
+ downloadFileFromBlob(fileName, blob);
1019
+ }
1020
+ ```
1021
+
1022
+ ### Создать PDF Blob при известном контракте backend
1023
+
1024
+ Если backend-контракт гарантирует PDF, detection не нужен:
1025
+
1026
+ ```ts
1027
+ const blob = binaryToBlob(fileContent, "application/pdf");
1028
+ ```
1029
+
1030
+ Это надёжнее эвристики, если MIME гарантирован и проверяется владельцем transport/domain boundary.
1031
+
1032
+ ### Обработать data URL
1033
+
1034
+ ```ts
1035
+ function dataUrlToBlob(dataUrl: string) {
1036
+ const mime = detectMimeType(undefined, dataUrl);
1037
+
1038
+ return binaryToBlob(dataUrl, mime);
1039
+ }
1040
+ ```
1041
+
1042
+ ### Обработать URL-safe base64
1043
+
1044
+ ```ts
1045
+ const blob = binaryToBlob(urlSafePayload, "application/octet-stream");
1046
+ ```
1047
+
1048
+ Дополнительных замен в consumer-коде не требуется.
1049
+
1050
+ ### Обработать raw binary string
1051
+
1052
+ ```ts
1053
+ const blob = binaryToBlob(rawBinaryString, "application/pdf", false);
1054
+ ```
1055
+
1056
+ Используйте только когда upstream явно гарантирует raw binary string.
1057
+
1058
+ ### Показать base64-документ в React
1059
+
1060
+ ```tsx
1061
+ function DocumentPanel(props: { readonly documentId: string; readonly base64: string | undefined }) {
1062
+ return <BinaryDocument key={props.documentId} bin={props.base64} />;
1063
+ }
1064
+ ```
1065
+
1066
+ ### Отладочно проверить первые байты
1067
+
1068
+ ```ts
1069
+ const blob = binaryToBlob(payload, "application/octet-stream");
1070
+ const bytes = new Uint8Array(await blob.arrayBuffer());
1071
+
1072
+ console.log([...bytes.slice(0, 8)]);
1073
+ ```
1074
+
1075
+ Не оставляйте logging бинарного содержимого в production-коде.
1076
+
1077
+ ## Ошибки и диагностика
1078
+
1079
+ ### «Не переданы бинарные данные»
1080
+
1081
+ Причина:
1082
+
1083
+ ```ts
1084
+ binaryToBlob("");
1085
+ ```
1086
+
1087
+ Решение: различайте отсутствие payload и корректное содержимое до вызова функции.
1088
+
1089
+ ### «После преобразования не осталось данных»
1090
+
1091
+ Причины:
1092
+
1093
+ - data URL имеет пустой payload;
1094
+ - base64 состоит только из пробелов;
1095
+ - после удаления whitespace не осталось символов.
1096
+
1097
+ ```ts
1098
+ binaryToBlob("data:text/plain;base64,");
1099
+ ```
1100
+
1101
+ ### «Некорректный формат base64»
1102
+
1103
+ Причины:
1104
+
1105
+ - присутствуют недопустимые символы;
1106
+ - строка имеет невозможную длину;
1107
+ - передан обычный текст вместо base64;
1108
+ - передан percent-encoded data URL;
1109
+ - payload был повреждён transport-слоем.
1110
+
1111
+ ```ts
1112
+ binaryToBlob("not valid *");
1113
+ ```
1114
+
1115
+ ### Base64 из query/form потерял `+`
1116
+
1117
+ В `application/x-www-form-urlencoded` символ `+` может интерпретироваться как пробел. Модуль удалит пробел и не сможет восстановить потерянный байт.
1118
+
1119
+ Передавайте base64:
1120
+
1121
+ - в JSON body;
1122
+ - как URL-safe base64;
1123
+ - или с корректным percent-encoding.
1124
+
1125
+ ### `Blob.type` равен `application/octet-stream`
1126
+
1127
+ Причина: `binaryToBlob` не выполняет detection и не использует MIME data URL автоматически.
1128
+
1129
+ Решение:
1130
+
1131
+ ```ts
1132
+ const mime = detectMimeType(fileName, payload);
1133
+ const blob = binaryToBlob(payload, mime);
1134
+ ```
1135
+
1136
+ ### Hook определил текст как PDF
1137
+
1138
+ Причина: `useBinaryFile` не получает filename, сигнатура текста неизвестна, поэтому применяется PDF fallback.
1139
+
1140
+ Решения:
1141
+
1142
+ - передать data URL с MIME;
1143
+ - вызвать функции вручную с известным MIME;
1144
+ - не использовать hook для этого контракта.
1145
+
1146
+ ### MIME не совпал с содержимым
1147
+
1148
+ Проверьте приоритет:
1149
+
1150
+ 1. filename;
1151
+ 2. data URL;
1152
+ 3. signature;
1153
+ 4. PDF fallback.
1154
+
1155
+ Известное расширение filename всегда выигрывает, даже если данные заявляют другой тип.
1156
+
1157
+ ### Data URL не распознан
1158
+
1159
+ Поддерживается форма:
1160
+
1161
+ ```text
1162
+ data:<mime>[;parameters];base64,<payload>
1163
+ ```
1164
+
1165
+ Не поддерживается:
1166
+
1167
+ ```text
1168
+ data:<mime>,<percent-encoded-payload>
1169
+ ```
1170
+
1171
+ ### Ошибка появляется в `console.error`
1172
+
1173
+ Это текущее поведение `useBinaryFile`: hook логирует пойманную ошибку перед записью `error` state. Для ручного `binaryToBlob` logging не выполняется.
1174
+
1175
+ ### Object URL перестал работать
1176
+
1177
+ Проверьте, что:
1178
+
1179
+ - URL не был освобождён слишком рано;
1180
+ - cleanup старого URL не затронул новый URL;
1181
+ - `Blob` ещё соответствует текущему документу;
1182
+ - browser CSP/viewer разрешает `blob:` source.
1183
+
1184
+ ## Безопасность
1185
+
1186
+ ### MIME нельзя использовать как проверку безопасности
1187
+
1188
+ Следующие источники контролируемы внешними данными:
1189
+
1190
+ - filename;
1191
+ - MIME data URL;
1192
+ - первые байты payload.
1193
+
1194
+ `detectMimeType` не гарантирует, что файл безопасен или соответствует типу.
1195
+
1196
+ ### Активное содержимое
1197
+
1198
+ SVG, HTML, XML и некоторые PDF могут содержать активные или внешние ссылки. Не вставляйте непроверенное содержимое напрямую в DOM.
1199
+
1200
+ Для недоверенных файлов:
1201
+
1202
+ - используйте безопасный viewer;
1203
+ - учитывайте sandbox/CSP;
1204
+ - запрещайте опасные типы на доверенной server boundary;
1205
+ - не полагайтесь только на расширение;
1206
+ - не выполняйте содержимое как script/module.
1207
+
1208
+ ### Ограничивайте размер
1209
+
1210
+ Перед декодированием проверяйте допустимый размер ответа на transport/domain boundary. Base64 и несколько промежуточных копий могут значительно увеличить потребление памяти.
1211
+
1212
+ ### Не логируйте payload
1213
+
1214
+ Base64 может содержать:
1215
+
1216
+ - персональные данные;
1217
+ - договоры и документы;
1218
+ - изображения;
1219
+ - токены, если upstream ошибочно включил их;
1220
+ - другие чувствительные сведения.
1221
+
1222
+ В diagnostics передавайте безопасные metadata: источник, ожидаемый тип, размер строки и технический код ошибки — без самого содержимого.
1223
+
1224
+ ### Не доверяйте filename
1225
+
1226
+ Filename может содержать нежелательные символы или обманное расширение. Санитизация имени и бизнес-политика разрешённых форматов принадлежат boundary, которая принимает внешний файл.
1227
+
1228
+ ## Производительность и память
1229
+
1230
+ ### Синхронное декодирование
1231
+
1232
+ `binaryToBlob` синхронно:
1233
+
1234
+ - нормализует всю строку;
1235
+ - вызывает `atob`;
1236
+ - создаёт `Uint8Array`;
1237
+ - создаёт отдельный `ArrayBuffer`;
1238
+ - создаёт `Blob`.
1239
+
1240
+ Большой payload может заблокировать main thread и временно существовать в нескольких представлениях одновременно.
1241
+
1242
+ ### Base64 больше исходных байтов
1243
+
1244
+ Base64 обычно примерно на треть больше исходного binary payload, не считая префикса data URL и JavaScript string storage.
1245
+
1246
+ ### `detectMimeType` декодирует только начало
1247
+
1248
+ Signature detection декодирует ограниченное начало base64, поэтому дешевле полного декодирования. При этом распознавание data URL выполняется регулярным выражением над входной строкой и может просканировать весь payload. Последующий `binaryToBlob` в любом случае обрабатывает полный payload.
1249
+
1250
+ ### Рекомендации
1251
+
1252
+ - ограничивайте размер на backend и transport boundary;
1253
+ - не декодируйте один payload несколько раз без причины;
1254
+ - не храните одновременно много больших base64-строк и `Blob`;
1255
+ - освобождайте object URL;
1256
+ - для действительно больших файлов предпочитайте streaming/download endpoint;
1257
+ - не выполняйте тяжёлое декодирование каждого render;
1258
+ - не используйте `useBinaryFile` как cache больших backend-файлов.
1259
+
1260
+ ### Сложность
1261
+
1262
+ Для payload длины `n`:
1263
+
1264
+ - время `binaryToBlob` — `O(n)`;
1265
+ - дополнительная память — `O(n)`;
1266
+ - `detectMimeType` — `O(n)` в худшем случае из-за разбора data URL, хотя для signature декодируется только ограниченный header.
1267
+
1268
+ ## Тестирование
1269
+
1270
+ ### Тесты consumer-кода
1271
+
1272
+ Проверяйте:
1273
+
1274
+ - `blob.type`;
1275
+ - `blob.size`;
1276
+ - текст для текстового fixture;
1277
+ - байты через `arrayBuffer()`;
1278
+ - ожидаемые ошибки;
1279
+ - cleanup object URL в UI boundary.
1280
+
1281
+ Пример:
1282
+
1283
+ ```ts
1284
+ import { describe, expect, it } from "vitest";
1285
+
1286
+ import { binaryToBlob, detectMimeType } from "@ryuzaki13/react-foundation-lib/binary";
1287
+
1288
+ describe("binary document", () => {
1289
+ it("создаёт текстовый Blob", async () => {
1290
+ const blob = binaryToBlob("SGVsbG8=", "text/plain");
1291
+
1292
+ expect(blob.type).toBe("text/plain");
1293
+ expect(await blob.text()).toBe("Hello");
1294
+ });
1295
+
1296
+ it("определяет PDF по имени", () => {
1297
+ expect(detectMimeType("report.pdf")).toBe("application/pdf");
1298
+ });
1299
+ });
1300
+ ```
1301
+
1302
+ Тестовая среда должна предоставлять `Blob`, `atob` и при UI-тестах React DOM. В пакете pure-функции проверяются в `jsdom`.
1303
+
1304
+ ### Узкий тест пакета
1305
+
1306
+ ```bash
1307
+ npm run test -- src/binary/binaryToBlob.test.ts
1308
+ ```
1309
+
1310
+ ## Краткий справочник API
1311
+
1312
+ Полный импорт:
1313
+
1314
+ ```ts
1315
+ import { binaryToBlob, detectMimeType, useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
1316
+ ```
1317
+
1318
+ | API | Вход | Результат | Ошибка |
1319
+ | ---------------- | ------------------------------- | ----------------- | ---------------------------------------- |
1320
+ | `binaryToBlob` | Строка, MIME, base64/raw флаг | Непустой `Blob` | Выбрасывает `Error`/browser decode error |
1321
+ | `detectMimeType` | Filename и/или base64/data URL | MIME string | Не выбрасывает; fallback PDF |
1322
+ | `useBinaryFile` | Base64/data URL или `undefined` | `{ data, error }` | Логирует и возвращает строку ошибки |
1323
+
1324
+ ### Defaults
1325
+
1326
+ | Контракт | Значение |
1327
+ | ---------------------------------- | -------------------------- |
1328
+ | `binaryToBlob` MIME | `application/octet-stream` |
1329
+ | `binaryToBlob` input mode | base64 |
1330
+ | `detectMimeType` fallback | `application/pdf` |
1331
+ | `useBinaryFile` initial `data` | `null` |
1332
+ | `useBinaryFile` initial `error` | `null` |
1333
+ | `useBinaryFile` raw binary support | Нет |
1334
+ | Автоматическое создание object URL | Нет |
1335
+
1336
+ ## Вопросы и ответы
1337
+
1338
+ ### Base64 — это шифрование?
1339
+
1340
+ Нет. Это текстовое кодирование байтов. Оно не обеспечивает секретность, целостность или подлинность.
1341
+
1342
+ ### Почему `binaryToBlob` не определяет MIME сам?
1343
+
1344
+ Преобразование байтов и выбор MIME являются разными ответственностями. Вызывающий код может знать MIME точнее любой эвристики. Для предположения существует отдельная `detectMimeType`.
1345
+
1346
+ ### Почему fallback у `binaryToBlob` и `detectMimeType` разный?
1347
+
1348
+ `binaryToBlob` без информации создаёт нейтральный `application/octet-stream`. `detectMimeType` ориентирован на документный контур и возвращает `application/pdf`, если эвристики не сработали.
1349
+
1350
+ ### Почему MIME data URL не попал в `Blob`?
1351
+
1352
+ `binaryToBlob` использует data URL только для извлечения base64 payload. Передайте MIME вторым аргументом:
1353
+
1354
+ ```ts
1355
+ const mime = detectMimeType(undefined, dataUrl);
1356
+ const blob = binaryToBlob(dataUrl, mime);
1357
+ ```
1358
+
1359
+ ### Почему DOCX определился как ZIP?
1360
+
1361
+ DOCX, XLSX и PPTX технически являются ZIP-контейнерами. Без filename короткая signature видит только `PK`. Передавайте корректное имя или известный MIME.
1362
+
1363
+ ### Почему неизвестный файл определился как PDF?
1364
+
1365
+ Это fallback `detectMimeType`. Он не доказывает, что файл является PDF.
1366
+
1367
+ ### Можно ли передать `Uint8Array`?
1368
+
1369
+ Нет, публичный API принимает строку. Для готовых байтов создайте `Blob` напрямую:
1370
+
1371
+ ```ts
1372
+ const blob = new Blob([bytes], { type: mime });
1373
+ ```
1374
+
1375
+ ### Можно ли передать обычный текст?
1376
+
1377
+ Если текст уже корректно закодирован в base64 — да. Raw mode не является UTF-8 encoder. Для обычного Unicode-текста используйте `TextEncoder`.
1378
+
1379
+ ### Нужно ли вручную освобождать `Blob`?
1380
+
1381
+ Нет. Нужно освобождать object URL, созданный через `URL.createObjectURL(blob)`.
1382
+
1383
+ ### Создаёт ли `useBinaryFile` object URL?
1384
+
1385
+ Нет. Hook возвращает только `Blob` и MIME.
1386
+
1387
+ ### Можно ли использовать `useBinaryFile` вне React-компонента?
1388
+
1389
+ Нет. Это React hook, к нему применяются Rules of Hooks. Для обычной функции используйте `binaryToBlob` и `detectMimeType`.
1390
+
1391
+ ### Почему entrypoint требует React, если нужна только pure-функция?
1392
+
1393
+ Текущий `/binary` entrypoint также экспортирует `useBinaryFile`, а собранный ESM-модуль импортирует React. Поэтому React должен быть доступен при разрешении entrypoint.
1394
+
1395
+ ### Валидирует ли `detectMimeType` файл?
1396
+
1397
+ Нет. Это только эвристический выбор MIME.
1398
+
1399
+ ### Можно ли использовать результат для проверки разрешённого типа?
1400
+
1401
+ Не как единственную проверку. Политику разрешённых форматов и проверку недоверенного содержимого выполняйте на доверенной boundary.
1402
+
1403
+ ### Что выбрать: data URL или object URL?
1404
+
1405
+ Data URL содержит данные внутри строки и часто приходит извне. Object URL является временной ссылкой браузера на уже созданный `Blob`. Для отображения большого `Blob` обычно удобнее object URL с обязательным cleanup.