feat(inbox): .xlsx extraction — formula gate, hidden sheets, cell comments (stage 2h)
This commit is contained in:
parent
a46a96db0a
commit
ca26e117ea
3 changed files with 132 additions and 1 deletions
|
|
@ -199,6 +199,51 @@ def _extract_pptx_text(fs_path) -> str:
|
|||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _extract_xlsx(fs_path):
|
||||
"""Extract text from an ``.xlsx``, including the regions a human reading the
|
||||
workbook in Excel does not see: cells on a *hidden* sheet (still worksheets, so
|
||||
iterated) and cell comments. Returns ``(text, formula_cells)`` — the text is
|
||||
materialized as the concept body (so a hidden-sheet / comment injection rides
|
||||
into the stage-2 scan) and ``formula_cells`` holds the formula-lead cells the
|
||||
front-end refuses (a spreadsheet threat, RCE/DDE when a human opens the file).
|
||||
|
||||
``openpyxl`` is imported lazily — a dev/showcase-scoped parser, not a core
|
||||
dependency. It reads formulas as their string (default ``data_only=False``).
|
||||
"""
|
||||
from openpyxl import load_workbook
|
||||
|
||||
wb = load_workbook(str(fs_path))
|
||||
parts: list = []
|
||||
formula_cells: list = []
|
||||
for ws in wb.worksheets: # includes hidden / very-hidden sheets
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
value = cell.value
|
||||
if value is not None and value != "":
|
||||
parts.append(str(value))
|
||||
# Only a genuine text/formula cell can carry the injection lead;
|
||||
# a numeric cell is typed (int/float) by openpyxl, so a negative
|
||||
# number never trips the gate — unlike CSV, where every cell is
|
||||
# text and a leading '-'/'+' is the accepted false-positive.
|
||||
if isinstance(value, str) and _is_formula_cell(value):
|
||||
formula_cells.append(value)
|
||||
comment = cell.comment
|
||||
if comment is not None and comment.text:
|
||||
parts.append(comment.text)
|
||||
return "\n".join(parts), formula_cells
|
||||
|
||||
|
||||
def _ingest_xlsx(rel_name, fs_path, bundle, provenance, rejected):
|
||||
"""Materialize an ``.xlsx`` (all sheets incl. hidden, + cell comments) as a
|
||||
concept and flag formula-injection cells — mirrors :func:`_ingest_csv`."""
|
||||
text, formula_cells = _extract_xlsx(fs_path)
|
||||
if formula_cells:
|
||||
rejected.append(
|
||||
(rel_name, f"XLSX formula-injection lead in {len(formula_cells)} cell(s): {formula_cells[0][:24]!r}")
|
||||
)
|
||||
_materialize_text(rel_name, text, "xlsx", bundle, provenance)
|
||||
|
||||
|
||||
def _ingest_regular_file(fs_path, rel_name, bundle, provenance, rejected, *, strict):
|
||||
"""Dispatch one on-disk file by suffix. ``strict`` raises on an unsupported
|
||||
suffix (a top-level drop); a folder walk passes ``strict=False`` to skip it."""
|
||||
|
|
@ -210,6 +255,8 @@ def _ingest_regular_file(fs_path, rel_name, bundle, provenance, rejected, *, str
|
|||
_materialize_text(rel_name, _extract_docx_text(fs_path), "docx", bundle, provenance)
|
||||
elif suffix == ".pptx":
|
||||
_materialize_text(rel_name, _extract_pptx_text(fs_path), "pptx", bundle, provenance)
|
||||
elif suffix == ".xlsx":
|
||||
_ingest_xlsx(rel_name, fs_path, bundle, provenance, rejected)
|
||||
elif suffix in _TEXT_SUFFIXES:
|
||||
text = Path(fs_path).read_text(encoding="utf-8", errors="replace")
|
||||
_materialize_text(rel_name, text, suffix.lstrip("."), bundle, provenance)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue