fix(inf-parser): tolerate bare-line sections in real vendor INFs
Real INFs (e.g. Ricoh oemsetup.inf) include [SourceDisksFiles] entries with bare filename lines (no '='), which strict configparser rejects with ParsingError, surfacing as a 500 on /drivers/upload. Pre-process the INF text to rewrite bare lines into synthetic __bare_N = <line> entries before parsing, and filter those synthetic keys out of DriverDesc extraction so they cannot leak into driver_names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,33 @@ def _detect_encoding(raw: bytes) -> str:
|
|||||||
return "cp1252" # ANSI / Windows-1252 safe fallback
|
return "cp1252" # ANSI / Windows-1252 safe fallback
|
||||||
|
|
||||||
|
|
||||||
|
def _neutralize_bare_lines(inf_text: str) -> str:
|
||||||
|
"""Rewrite INF lines that lack ``=`` so configparser can ingest the file.
|
||||||
|
|
||||||
|
Real vendor INFs contain sections like ``[SourceDisksFiles]`` or copy-list
|
||||||
|
sections whose entries are bare filenames (no key/value). configparser is
|
||||||
|
strict and aborts on those. We only care about ``key = value`` lines for
|
||||||
|
DriverDesc extraction, so it's safe to convert each bare payload line into
|
||||||
|
a synthetic ``__bare_N = <original>`` entry.
|
||||||
|
"""
|
||||||
|
out: list[str] = []
|
||||||
|
counter = 0
|
||||||
|
for line in inf_text.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if (
|
||||||
|
not stripped
|
||||||
|
or stripped.startswith(";")
|
||||||
|
or stripped.startswith("#")
|
||||||
|
or stripped.startswith("[")
|
||||||
|
or "=" in stripped
|
||||||
|
):
|
||||||
|
out.append(line)
|
||||||
|
continue
|
||||||
|
counter += 1
|
||||||
|
out.append(f"__bare_{counter} = {stripped}")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_tokens(value: str, strings: dict[str, str]) -> str:
|
def _resolve_tokens(value: str, strings: dict[str, str]) -> str:
|
||||||
"""Expand %TOKEN% placeholders using the [Strings] section lookup dict.
|
"""Expand %TOKEN% placeholders using the [Strings] section lookup dict.
|
||||||
|
|
||||||
@@ -92,7 +119,7 @@ def parse_inf(inf_text: str, inf_filename: str, zip_names: list[str]) -> ParsedI
|
|||||||
# into "acme superprint 9000". We disable that behaviour here and manually lowercase
|
# into "acme superprint 9000". We disable that behaviour here and manually lowercase
|
||||||
# only when building the [Strings] lookup dict.
|
# only when building the [Strings] lookup dict.
|
||||||
parser.optionxform = str # type: ignore[assignment]
|
parser.optionxform = str # type: ignore[assignment]
|
||||||
parser.read_string(inf_text)
|
parser.read_string(_neutralize_bare_lines(inf_text))
|
||||||
|
|
||||||
# Build strings lookup with LOWERCASED keys (case-insensitive token resolution).
|
# Build strings lookup with LOWERCASED keys (case-insensitive token resolution).
|
||||||
# INF token references are case-insensitive per WDK spec.
|
# INF token references are case-insensitive per WDK spec.
|
||||||
@@ -144,6 +171,8 @@ def parse_inf(inf_text: str, inf_filename: str, zip_names: list[str]) -> ParsedI
|
|||||||
|
|
||||||
# Each option key in a Models section is a device-description (DriverDesc)
|
# Each option key in a Models section is a device-description (DriverDesc)
|
||||||
for key, _val in parser.items(section):
|
for key, _val in parser.items(section):
|
||||||
|
if key.startswith("__bare_"):
|
||||||
|
continue
|
||||||
resolved = _resolve_tokens(key, strings)
|
resolved = _resolve_tokens(key, strings)
|
||||||
# Skip empty, purely numeric, or clearly non-driver-name entries
|
# Skip empty, purely numeric, or clearly non-driver-name entries
|
||||||
if resolved and not resolved.isdigit():
|
if resolved and not resolved.isdigit():
|
||||||
|
|||||||
@@ -279,3 +279,40 @@ def test_empty_models_section():
|
|||||||
"""INF with empty Models section returns empty driver_names list."""
|
"""INF with empty Models section returns empty driver_names list."""
|
||||||
result = parse_inf(EMPTY_MODELS_INF, "empty.inf", [])
|
result = parse_inf(EMPTY_MODELS_INF, "empty.inf", [])
|
||||||
assert result.driver_names == []
|
assert result.driver_names == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# parse_inf – tolerates bare-line sections (real-world Ricoh oemsetup.inf)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
BARE_LINE_INF = """\
|
||||||
|
[Version]
|
||||||
|
Signature="$Windows NT$"
|
||||||
|
|
||||||
|
[Manufacturer]
|
||||||
|
%MFG%=Models,NTamd64
|
||||||
|
|
||||||
|
[Models.NTamd64]
|
||||||
|
%RICOH_DRIVER%=Install,{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC}
|
||||||
|
|
||||||
|
[SourceDisksFiles]
|
||||||
|
ricu18ui.dll,ricu18ui.dl_
|
||||||
|
ricu18ui.irj
|
||||||
|
ricu18ui.rdj
|
||||||
|
ricu18gl.dll,ricu18gl.dl_
|
||||||
|
RD01Kd64.dll,RD01Kd64.dl_,,0x00000020
|
||||||
|
|
||||||
|
[Strings]
|
||||||
|
MFG="Ricoh"
|
||||||
|
RICOH_DRIVER="Ricoh PCL6 Universal"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_bare_line_sections_do_not_raise():
|
||||||
|
"""Real INFs (e.g. Ricoh oemsetup.inf) include [SourceDisksFiles] entries
|
||||||
|
with bare filename lines and no '=' — parse_inf must not raise and must
|
||||||
|
still extract DriverDesc from the Models section."""
|
||||||
|
result = parse_inf(BARE_LINE_INF, "oemsetup.inf", ["oemsetup.inf"])
|
||||||
|
assert "Ricoh PCL6 Universal" in result.driver_names
|
||||||
|
# Synthetic __bare_N keys must not leak into driver_names
|
||||||
|
assert not any(n.startswith("__bare_") for n in result.driver_names)
|
||||||
|
|||||||
Reference in New Issue
Block a user