diff --git a/imptune/services/__init__.py b/imptune/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/sample.inf b/tests/fixtures/sample.inf new file mode 100644 index 0000000..b971283 --- /dev/null +++ b/tests/fixtures/sample.inf @@ -0,0 +1,14 @@ +[Version] +Signature="$Windows NT$" +Class=Printer +Provider=%MFG% + +[Manufacturer] +%MFG%=Models,NTamd64 + +[Models.NTamd64] +%DRIVER_NAME%=Install,{12345678-1234-1234-1234-123456789012} + +[Strings] +MFG="Test Manufacturer" +DRIVER_NAME="Test LaserJet Pro" diff --git a/tests/fixtures/sample_multi_model.inf b/tests/fixtures/sample_multi_model.inf new file mode 100644 index 0000000..c85358a --- /dev/null +++ b/tests/fixtures/sample_multi_model.inf @@ -0,0 +1,18 @@ +[Version] +Signature="$Windows NT$" +Class=Printer + +[Manufacturer] +%MFG%=Models,Models.NTamd64 + +[Models] +%DRIVER_A%=InstallA,{11111111-1111-1111-1111-111111111111} + +[Models.NTamd64] +%DRIVER_A%=InstallA,{11111111-1111-1111-1111-111111111111} +%DRIVER_B%=InstallB,{22222222-2222-2222-2222-222222222222} + +[Strings] +MFG="Multi Corp" +DRIVER_A="Multi Printer 1000" +DRIVER_B="Multi Printer 2000" diff --git a/tests/fixtures/sample_utf16.inf b/tests/fixtures/sample_utf16.inf new file mode 100644 index 0000000..6684166 Binary files /dev/null and b/tests/fixtures/sample_utf16.inf differ diff --git a/tests/test_inf_parser.py b/tests/test_inf_parser.py new file mode 100644 index 0000000..faefee5 --- /dev/null +++ b/tests/test_inf_parser.py @@ -0,0 +1,281 @@ +""" +Unit tests for imptune.services.inf_parser. + +Covers DRV-02 (INF parsing, token resolution, encoding detection, +multi-model) and DRV-05 (unused file detection). +""" +from pathlib import Path + +import pytest + +from imptune.services.inf_parser import ParsedInf, _detect_encoding, parse_inf + +FIXTURES = Path(__file__).parent / "fixtures" + +# --------------------------------------------------------------------------- +# _detect_encoding tests +# --------------------------------------------------------------------------- + + +def test_detect_encoding_utf16le(): + """UTF-16 LE BOM (\xff\xfe) is detected as 'utf-16'.""" + raw = b"\xff\xfe" + "[Version]\r\n".encode("utf-16-le") + assert _detect_encoding(raw) == "utf-16" + + +def test_detect_encoding_utf16be(): + """UTF-16 BE BOM (\xfe\xff) is also detected as 'utf-16'.""" + raw = b"\xfe\xff" + "[Version]\r\n".encode("utf-16-be") + assert _detect_encoding(raw) == "utf-16" + + +def test_detect_encoding_utf8bom(): + """UTF-8 BOM (\xef\xbb\xbf) is detected as 'utf-8-sig'.""" + raw = b"\xef\xbb\xbf" + b"[Version]\r\n" + assert _detect_encoding(raw) == "utf-8-sig" + + +def test_detect_encoding_ansi(): + """Byte sequence with no BOM falls back to cp1252 (ANSI).""" + raw = b"[Version]\r\nSignature=\"$Windows NT$\"\r\n" + assert _detect_encoding(raw) == "cp1252" + + +# --------------------------------------------------------------------------- +# parse_inf – simple INF with literal driver description +# --------------------------------------------------------------------------- + +SIMPLE_INF = """ +[Version] +Signature="$Windows NT$" +Class=Printer + +[Manufacturer] +Acme=AcmeModels,NTamd64 + +[AcmeModels.NTamd64] +Acme SuperPrint 9000=Install,{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA} + +[Strings] +""" + + +def test_simple_driver_desc(): + """parse_inf extracts literal DriverDesc from a Models section.""" + result = parse_inf(SIMPLE_INF, "acme.inf", ["acme.inf", "acme.dll"]) + assert "Acme SuperPrint 9000" in result.driver_names + + +# --------------------------------------------------------------------------- +# parse_inf – %TOKEN% resolution from [Strings] +# --------------------------------------------------------------------------- + +TOKEN_INF = """ +[Version] +Signature="$Windows NT$" + +[Manufacturer] +%MFG%=Models,NTamd64 + +[Models.NTamd64] +%HP_DRIVER%=Install,{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB} + +[Strings] +MFG="HP" +HP_DRIVER="HP LaserJet" +""" + + +def test_token_resolution(): + """parse_inf resolves %TOKEN% references via the [Strings] section.""" + result = parse_inf(TOKEN_INF, "hp.inf", ["hp.inf"]) + assert "HP LaserJet" in result.driver_names + # Raw token should NOT appear in the output + assert "%HP_DRIVER%" not in result.driver_names + + +# --------------------------------------------------------------------------- +# parse_inf – UTF-16 LE fixture +# --------------------------------------------------------------------------- + + +def test_utf16_encoding(): + """Reading a UTF-16 LE BOM fixture and passing decoded text to parse_inf produces correct driver_names.""" + raw = (FIXTURES / "sample_utf16.inf").read_bytes() + encoding = _detect_encoding(raw) + assert encoding == "utf-16" + text = raw.decode(encoding, errors="replace") + result = parse_inf(text, "sample_utf16.inf", ["sample_utf16.inf"]) + assert "Test LaserJet Pro" in result.driver_names + + +# --------------------------------------------------------------------------- +# parse_inf – multi-model INF (NTamd64 + undecorated) +# --------------------------------------------------------------------------- + + +def test_multi_model_inf(): + """INF with both NTamd64 and undecorated sections returns deduplicated driver_names.""" + text = (FIXTURES / "sample_multi_model.inf").read_text(encoding="cp1252") + result = parse_inf(text, "multi.inf", ["multi.inf"]) + # Both driver names must appear exactly once + assert "Multi Printer 1000" in result.driver_names + assert "Multi Printer 2000" in result.driver_names + assert result.driver_names.count("Multi Printer 1000") == 1 + assert result.driver_names.count("Multi Printer 2000") == 1 + + +# --------------------------------------------------------------------------- +# parse_inf – architecture detection +# --------------------------------------------------------------------------- + +AMD64_INF = """ +[Version] +Signature="$Windows NT$" + +[Manufacturer] +Mfg=Mfg.Models,NTamd64 + +[Mfg.Models.NTamd64] +Some Driver=Install,{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC} + +[Strings] +""" + +ARM64_INF = """ +[Version] +Signature="$Windows NT$" + +[Manufacturer] +Mfg=Mfg.Models,NTarm64 + +[Mfg.Models.NTarm64] +Some Driver=Install,{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC} + +[Strings] +""" + +UNDECORATED_INF = """ +[Version] +Signature="$Windows NT$" + +[Manufacturer] +Mfg=Mfg.Models + +[Mfg.Models] +Some Driver=Install,{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC} + +[Strings] +""" + +MIXED_INF = """ +[Version] +Signature="$Windows NT$" + +[Manufacturer] +Mfg=Mfg.Models,Mfg.Models.NTamd64 + +[Mfg.Models] +Some Driver=Install,{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC} + +[Mfg.Models.NTamd64] +Some Driver=Install,{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC} + +[Strings] +""" + + +def test_architecture_detection_amd64(): + """NTamd64 decoration -> architecture='x64'.""" + result = parse_inf(AMD64_INF, "amd64.inf", []) + assert result.architecture == "x64" + + +def test_architecture_detection_arm64(): + """NTarm64 decoration -> architecture='arm64'.""" + result = parse_inf(ARM64_INF, "arm64.inf", []) + assert result.architecture == "arm64" + + +def test_architecture_detection_undecorated(): + """Undecorated section only -> architecture='x86'.""" + result = parse_inf(UNDECORATED_INF, "x86.inf", []) + assert result.architecture == "x86" + + +def test_architecture_detection_mixed(): + """Mixed architectures -> architecture=None (ambiguous).""" + result = parse_inf(MIXED_INF, "mixed.inf", []) + assert result.architecture is None + + +# --------------------------------------------------------------------------- +# parse_inf – .cat file detection +# --------------------------------------------------------------------------- + + +def test_cat_file_detection_present(): + """zip_names containing a .cat file -> has_cat_file=True.""" + result = parse_inf(SIMPLE_INF, "acme.inf", ["acme.inf", "driver.cat", "acme.dll"]) + assert result.has_cat_file is True + + +def test_cat_file_detection_absent(): + """zip_names without a .cat file -> has_cat_file=False.""" + result = parse_inf(SIMPLE_INF, "acme.inf", ["acme.inf", "acme.dll"]) + assert result.has_cat_file is False + + +# --------------------------------------------------------------------------- +# parse_inf – unused files detection +# --------------------------------------------------------------------------- + + +def test_unused_files(): + """ZIP members not referenced in INF text appear in unused_files.""" + inf_text = """ +[Version] +Signature="$Windows NT$" + +[Manufacturer] +Mfg=Mfg.Models + +[Mfg.Models] +Some Driver=Install,{DDDDDDDD-DDDD-DDDD-DDDD-DDDDDDDDDDDD} + +[SourceDisksFiles] +driver.inf=1 +driver.dll=1 + +[Strings] +""" + zip_names = ["driver.inf", "driver.dll", "readme.txt"] + result = parse_inf(inf_text, "driver.inf", zip_names) + # readme.txt is not mentioned anywhere in inf_text + assert "readme.txt" in result.unused_files + # driver.inf and driver.dll ARE referenced in inf_text + assert "driver.inf" not in result.unused_files + assert "driver.dll" not in result.unused_files + + +# --------------------------------------------------------------------------- +# parse_inf – empty models section +# --------------------------------------------------------------------------- + +EMPTY_MODELS_INF = """ +[Version] +Signature="$Windows NT$" + +[Manufacturer] +Mfg=MfgModels + +[MfgModels] + +[Strings] +""" + + +def test_empty_models_section(): + """INF with empty Models section returns empty driver_names list.""" + result = parse_inf(EMPTY_MODELS_INF, "empty.inf", []) + assert result.driver_names == []