From 471ae7087360cb2abf2bf62959a66fdb5301e07f Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 5 Oct 2026 17:18:32 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=E2=80=94=20=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B5=20=D1=80=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D1=80=D0=B5=D0=B4=D1=81=D0=BA=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20merge-=D0=B4?= =?UTF-8?q?=D0=B8=D0=B0=D0=BF=D0=B0=D0=B7=D0=BE=D0=BD=D0=BE=D0=B2=20(R-26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 88f04f8..e2530c5 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,8 @@ groups: Любой реальный файл разбирается быстрее 5 с (измерено: максимум ≈ 0,6 с). Скан объединённых ячеек xlsx (ruling R-25) повторяет то, что разложит openpyxl, с запасом в сторону отказа: -сканируются все записи zip `*.xml` и записи, начало которых похоже на XML (openpyxl идёт к листу по цели связи в -`workbook.xml.rels`, каталог и расширение части не проверяет); считается элемент с локальным именем `mergeCell` в любом +сканируются все записи zip `*.xml` и записи, первые 64 байта которых (после BOM и пробелов) начинаются с `<` (openpyxl идёт к +листу по цели связи в `workbook.xml.rels`, расширение части не проверяет); считается элемент с локальным именем `mergeCell` в любом пространстве имён (openpyxl берёт дочерние `mergeCell` по локальному имени) и только атрибут `ref` без префикса; ссылки на символы (`2`) раскрывает парсер, `>` в кавычках тег не обрывает; площадь — по строке `ref` (`CellRange` openpyxl), ячейки не перебираются; отказ — сразу при превышении. Битый XML части `*.xml` и `ref`, который openpyxl не @@ -215,8 +215,18 @@ openpyxl 0,58 с; синтетический лист в 2 млн ячеек (63 - XML-бомбы entity expansion: `defusedxml` не подключён; от экспоненты «billion laughs» защищает только сам expat ≥ 2.4 (и в скане, и в openpyxl, который разбирает части тем же expat). -Жёсткий лимит памяти контейнера, `defusedxml` или разбор в подпроцессе с `RLIMIT_AS` — решения платформы. Книга в -пределах лимитов читается библиотекой целиком. +- **известный обход предсканирования (R-26):** предскан — эшелон защиты, а не граница безопасности. Намеренно собранный + файл, у которого лист лежит в части без расширения `.xml`, начинается с ≥ 64 байт пробелов (или BOM UTF-16 и пробелов) и + не имеет XML-декларации, пропускает предскан: файл в 1,8 КБ читается `read_book` 27 с и занимает 1,4 ГБ. Первый + follow-up: убрать «sniff» и лениво отдавать expat каждую некаталожную запись архива (+ тест); +- **память и CPU самого предскана** не ограничены размером чанка: «мусорная» часть `xl/junk.xml` с одним атрибутом в + 400 МБ даёт 1,3 ГБ за 1,2 с, 400 МБ тегов `` — 8,5 с CPU (в пределах лимита распакованного zip, 500 МиБ); +- **xls с очень многим числом крупных merged-диапазонов** (каждый площадью > 10 000, например 100 000 штук): индекс + `Sheet` просматривает их линейно (≈ 4,4 мс на обращение), останавливает только `PARSE_TIMEOUT_SECONDS` (`TIMEOUT`). + Follow-up: потолок на число таких диапазонов или интервальный индекс. + +Жёсткий лимит памяти контейнера, `defusedxml` или разбор в подпроцессе с `RLIMIT_AS` — решения платформы (они же — +настоящая граница безопасности чтения). Книга в пределах лимитов читается библиотекой целиком. ## Тесты на реальных данных и golden-снимки