From 3a462e1e2c4b56542c0d55a1966e923f7559b925 Mon Sep 17 00:00:00 2001 From: "va@resident" Date: Tue, 6 Oct 2026 15:29:41 +0500 Subject: [PATCH] Fix spurious ADB state parse error on devices without stored keys Since #957 the parsed body keeps the closing brace of debugging_manager and the blank lines before it. The brace popped two levels, and a blank line after system_keys raised IndexError. Don't pop when the indentation drops on a closing brace, and skip blank lines outside multiline values. Fixes #967. --- src/mvt/android/artifacts/dumpsys_adb.py | 10 ++- tests/android/test_artifact_dumpsys_adb.py | 79 ++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/mvt/android/artifacts/dumpsys_adb.py b/src/mvt/android/artifacts/dumpsys_adb.py index c57cf8d6..2e493c27 100644 --- a/src/mvt/android/artifacts/dumpsys_adb.py +++ b/src/mvt/android/artifacts/dumpsys_adb.py @@ -33,7 +33,12 @@ class DumpsysADBArtifact(AndroidArtifact): line = line.removesuffix(b"\r") # Track the level of indentation indent = len(line) - len(line.lstrip()) - if indent < cur_indent: + # A closing brace is dedented from the block it closes, and the + # brace handler below pops that block. Popping on the dedent as + # well would close two levels for one brace. A blank line has no + # indentation to compare: it ends a multiline value (handled + # below) and is otherwise skipped. + if indent < cur_indent and line.strip() not in (b"", b"}"): # If the current line is less indented than the previous one, back out if len(stack) <= 1: # Dedenting below the outermost level means this is not the @@ -72,6 +77,9 @@ class DumpsysADBArtifact(AndroidArtifact): stack.pop() current_dict = stack[-1] + if not line.strip(): + continue + if key == "}": if len(stack) <= 1: self.log.error( diff --git a/tests/android/test_artifact_dumpsys_adb.py b/tests/android/test_artifact_dumpsys_adb.py index 0e5b59f4..b85c3973 100644 --- a/tests/android/test_artifact_dumpsys_adb.py +++ b/tests/android/test_artifact_dumpsys_adb.py @@ -209,6 +209,85 @@ class TestDumpsysADBArtifact: assert len(da_adb.results) == 1 assert da_adb.results[0]["user_keys"][0]["user"] == user.decode() + def test_state_without_keys_is_parsed_without_error(self, caplog): + # Shape printed by every device with no stored ADB keys (Samsung + # SM-A525F, Android 12). The block's closing brace is dedented from + # the last entry and must close exactly one level. + da_adb = DumpsysADBArtifact() + da_adb.parse( + b"ADB MANAGER STATE (dumpsys adb):\n" + b"{\n" + b" debugging_manager={\n" + b" connected_to_adb=false\n" + b" }\n" + b"}\n" + b"--------- 0.015s was the duration of dumpsys adb\n" + ) + + assert len(da_adb.results) == 1 + assert da_adb.results[0]["connected_to_adb"] is False + assert not [r for r in caplog.records if r.levelname == "ERROR"] + + def test_nested_block_as_last_entry_is_parsed_without_error(self, caplog): + # Shape printed by Pixel devices with no stored ADB keys. + da_adb = DumpsysADBArtifact() + da_adb.parse( + b"ADB MANAGER STATE (dumpsys adb):\n" + b"{\n" + b" debugging_manager={\n" + b" connected_to_adb=false\n" + b" adb_wifi={\n" + b" enabled=false\n" + b" tls_port=0\n" + b" }\n" + b" }\n" + b"}\n" + ) + + assert len(da_adb.results) == 1 + assert da_adb.results[0]["connected_to_adb"] is False + assert da_adb.results[0]["adb_wifi"]["tls_port"] == b"0" + assert not [r for r in caplog.records if r.levelname == "ERROR"] + + def test_entry_after_nested_block_stays_in_debugging_manager(self): + da_adb = DumpsysADBArtifact() + da_adb.parse( + b"ADB MANAGER STATE (dumpsys adb):\n" + b"{\n" + b" debugging_manager={\n" + b" adb_wifi={\n" + b" enabled=false\n" + b" }\n" + b" connected_to_adb=true\n" + b" }\n" + b"}\n" + ) + + assert len(da_adb.results) == 1 + assert da_adb.results[0]["connected_to_adb"] is True + + def test_blank_line_after_single_line_value_does_not_raise(self): + # `system_keys` is the content of /adb_keys and ends with a newline, + # so a blank line follows it. It is not a multiline field, so the + # blank line is not a multiline terminator either. + da_adb = DumpsysADBArtifact() + da_adb.parse( + b"ADB MANAGER STATE (dumpsys adb):\n" + b"{\n" + b" debugging_manager={\n" + b" connected_to_adb=true\n" + b" user_keys=QUJDRA== host@example\n" + b"\n" + b" system_keys=RUZHSA== system@example\n" + b"\n" + b" }\n" + b"}\n" + ) + + assert len(da_adb.results) == 1 + assert da_adb.results[0]["user_keys"][0]["user"] == "host@example" + assert da_adb.results[0]["system_keys"] == "RUZHSA== system@example" + def test_unbalanced_state_is_reported_rather_than_raising(self): da_adb = DumpsysADBArtifact() da_adb.parse(