[794-5][@ant] fix(spa): пакет — восстановление UTF-8 имён записей zip — иначе «в пакете нет файлов»
CI / build (push) Failing after 7s

Десктоп-упаковщик пишет имена в UTF-8, но не ставит bit-11 (UTF-8) в zip → fflate
декодирует кириллицу как latin1 (мохибейк), и пути из manifest.json (UTF-8) не
находят записи. parseZip строит индекс с восстановлением latin1→utf8 и резолвит
манифест устойчиво; fallback-ветка без дублей (имя восстанавливается только для показа).
This commit is contained in:
coopops
2026-06-15 10:54:43 +00:00
parent c7d55ea1f9
commit e9b2a9b54c
+39 -5
View File
@@ -25,24 +25,58 @@ export async function parseSingle(pdf: File, sig: File): Promise<ParseResult> {
};
}
/**
* Восстанавливает UTF-8-имя записи ZIP, если оно пришло как latin1-мохибейк.
*
* Десктоп-упаковщик пишет имена файлов в UTF-8, но НЕ ставит флаг bit-11 (UTF-8) в zip —
* читатели (fflate) тогда декодируют байты как latin1, и кириллица превращается в мусор
* («ÐÑиложение…»). Это ломает поиск файлов по путям из manifest.json (там корректный
* UTF-8). Если имя целиком в диапазоне latin1 — пробуем перечитать его байты как UTF-8.
* Если строка не является валидным UTF-8 (настоящее latin1-имя) — оставляем как есть.
*/
function recoverUtf8Name(name: string): string {
for (let i = 0; i < name.length; i++) if (name.charCodeAt(i) > 0xff) return name;
try {
const bytes = Uint8Array.from(name, (c) => c.charCodeAt(0));
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch {
return name;
}
}
/** ZIP-пакет: распознаётся по manifest.json; парсит пары <doc>.pdf/<doc>.coopsig. */
export async function parseZip(zip: File): Promise<ParseResult> {
const files = unzipSync(new Uint8Array(await zip.arrayBuffer()));
const dec = new TextDecoder();
const manifestRaw = files['manifest.json'];
// Индекс по имени: храним и сырой ключ, и восстановленный из мохибейка UTF-8 —
// чтобы пути из manifest (UTF-8) находили записи, у которых имена побиты latin1.
const byName = new Map<string, Uint8Array>();
for (const [raw, data] of Object.entries(files)) {
byName.set(raw, data);
const fixed = recoverUtf8Name(raw);
if (fixed !== raw && !byName.has(fixed)) byName.set(fixed, data);
}
const pick = (p: string): Uint8Array | undefined =>
byName.get(p) ?? byName.get(recoverUtf8Name(p));
const manifestRaw = byName.get('manifest.json');
if (manifestRaw) {
const manifest = JSON.parse(dec.decode(manifestRaw)) as PackageManifest;
const documents: ParsedDocument[] = manifest.documents.map((entry) => {
const pdf = files[entry.document];
const sigRaw = files[entry.signature];
const pdf = pick(entry.document);
const sigRaw = pick(entry.signature);
if (!pdf || !sigRaw) throw new Error(`В пакете нет файлов для ${entry.name}`);
return { name: entry.name, pdf, sig: JSON.parse(dec.decode(sigRaw)) as SigFile, legacy: false };
});
return { documents, isPackage: true };
}
// нет manifest.json — ищем пары *.pdf + *.coopsig напрямую
// нет manifest.json — ищем пары *.pdf + *.coopsig напрямую.
// Итерируем исходные ключи (а не byName с алиасами) — иначе один документ задвоится
// через сырое и восстановленное имя. Поиск пары самосогласован (оба имени бьются
// одинаково), а UTF-8 восстанавливаем только для отображаемого имени.
const documents: ParsedDocument[] = [];
for (const path of Object.keys(files)) {
if (!path.endsWith('.pdf')) continue;
@@ -50,7 +84,7 @@ export async function parseZip(zip: File): Promise<ParseResult> {
const sigRaw = files[`${base}.coopsig`];
if (!sigRaw) continue;
documents.push({
name: base,
name: recoverUtf8Name(base),
pdf: files[path]!,
sig: JSON.parse(dec.decode(sigRaw)) as SigFile,
legacy: !!files[`${base}.signature.txt`],