"""Resource bounds shared by every XLSX importer. Lived in ``app/domains/auditor/`` until 2026-08-30, when the support domain needed the same archive-amplification guards for the Feishu-helpdesk FAQ import. Domain modules do not import each other, so the guards moved here. """ from __future__ import annotations import io import zipfile from app.core.exceptions import ValidationError MAX_XLSX_MEMBERS = 1_000 MAX_XLSX_UNCOMPRESSED_BYTES = 64 * 1024 * 1024 MAX_XLSX_MEMBER_BYTES = 32 * 1024 * 1024 MAX_XLSX_COMPRESSION_RATIO = 200.0 MAX_XLSX_COLUMNS = 100 MAX_XLSX_CELL_CHARS = 32_767 def preflight_xlsx(payload: bytes) -> None: """Reject archive amplification before openpyxl reads workbook XML.""" try: with zipfile.ZipFile(io.BytesIO(payload)) as archive: members = [item for item in archive.infolist() if not item.is_dir()] except (zipfile.BadZipFile, OSError) as exc: raise ValidationError("could not read xlsx: not a valid workbook") from exc if len(members) > MAX_XLSX_MEMBERS: raise ValidationError("workbook archive contains too many members") total = 0 for member in members: if member.flag_bits & 0x1: raise ValidationError("encrypted workbooks are not supported") if member.file_size > MAX_XLSX_MEMBER_BYTES: raise ValidationError("workbook archive member is too large") total += member.file_size if total > MAX_XLSX_UNCOMPRESSED_BYTES: raise ValidationError("workbook contents exceed the 64 MB inflated limit") if member.file_size / max(member.compress_size, 1) > MAX_XLSX_COMPRESSION_RATIO: raise ValidationError("workbook compression ratio is suspiciously high") def bounded_row(row: tuple[object, ...]) -> tuple[object, ...]: """Validate the sentinel column and cell sizes, then trim empty tail cells.""" if len(row) > MAX_XLSX_COLUMNS and row[MAX_XLSX_COLUMNS] is not None: raise ValidationError(f"workbook exceeds {MAX_XLSX_COLUMNS} columns") values = row[:MAX_XLSX_COLUMNS] for value in values: if isinstance(value, str) and len(value) > MAX_XLSX_CELL_CHARS: raise ValidationError("workbook cell text is too long") last = len(values) while last and values[last - 1] is None: last -= 1 return values[:last]