diff --git a/maxdiff/freezing_utils.py b/maxdiff/freezing_utils.py index cafe935..872f033 100644 --- a/maxdiff/freezing_utils.py +++ b/maxdiff/freezing_utils.py @@ -86,10 +86,11 @@ def get_patcher_dict(entry: device_entry_with_data): device_data_text = "" try: - if patch_data[len(patch_data) - 1] == 0: - device_data_text = patch_data[: len(patch_data) - 1].decode("utf-8") - else: - device_data_text = patch_data.decode("utf-8") + device_data_text = patch_data.decode("utf-8") + + # Past Max versions could write patcher data with a trailing 0 + # or with trailing garbage. + device_data_text = strip_trailing_nonjson(device_data_text) except Exception as e: print(f"Error getting patch data as text for entry {name}: {e}") return {} @@ -106,3 +107,29 @@ def get_patcher_dict(entry: device_entry_with_data): except: print(f"Content of entry {name} does not seem to be a patcher") return {} + + +def strip_trailing_nonjson(device_data_text: str) -> str: + """ + This function returns (possibly trimmed) JSON text to pass to json.loads(). + """ + decoder = json.JSONDecoder() + try: + _, end = decoder.raw_decode(device_data_text) + return device_data_text[:end].rstrip() + except json.JSONDecodeError: + pass + + # find all candidate closing positions for objects/arrays + closers = [i for i, ch in enumerate(device_data_text) if ch in ("}", "]")] + # try from last to first — prefer the longest valid prefix + for pos in reversed(closers): + candidate = device_data_text[: pos + 1] + try: + json.loads(candidate) + return candidate + except Exception: + continue + + # nothing worked; return original so caller can log/handle the parse error + return device_data_text diff --git a/maxdiff/tests/test.py b/maxdiff/tests/test.py index 3eac5cf..7ffeecd 100644 --- a/maxdiff/tests/test.py +++ b/maxdiff/tests/test.py @@ -102,6 +102,16 @@ def test_parse_maxpat_with_merge_conficts(self): actual = parse(test_path) self.assertEqual(expected, actual) + def test_parse_with_garbage(self): + self.maxDiff = None + + expected_path, test_path = get_test_path_files("WithGarbage.amxd") + + with open(expected_path, mode="r") as expected_file: + expected = expected_file.read() + actual = parse(test_path) + self.assertEqual(expected, actual) + def get_test_path_files(file_name): expected = get_test_path_file(f"test_baselines/{file_name}.txt") diff --git a/maxdiff/tests/test_baselines/WithGarbage.amxd.txt b/maxdiff/tests/test_baselines/WithGarbage.amxd.txt new file mode 100644 index 0000000..88812f3 --- /dev/null +++ b/maxdiff/tests/test_baselines/WithGarbage.amxd.txt @@ -0,0 +1,12 @@ +Audio Effect Device +------------------- +Device is frozen +----- Contents ----- +Livesync Leader.amxd: 88337 bytes, modified at 2020/06/14 09:47:41 UTC <= Device + +Total - Counting every abstraction instance - Indicates loading time + Object instances: 130 + Connections: 151 +Unique - Counting abstractions once - Indicates maintainability + Object instances: 130 + Connections: 151 diff --git a/maxdiff/tests/test_files/WithGarbage.amxd b/maxdiff/tests/test_files/WithGarbage.amxd new file mode 100644 index 0000000..2040cce Binary files /dev/null and b/maxdiff/tests/test_files/WithGarbage.amxd differ diff --git a/maxdiff/tests/test_rewrite_baselines.py b/maxdiff/tests/test_rewrite_baselines.py index 390cac9..14b1db6 100644 --- a/maxdiff/tests/test_rewrite_baselines.py +++ b/maxdiff/tests/test_rewrite_baselines.py @@ -14,6 +14,7 @@ def run(): rewrite_file("Test.amxd") rewrite_file("EncryptedTest.amxd") rewrite_file("FrozenTest.amxd") + rewrite_file("WithGarbage.amxd") rewrite_file("Test.maxpat") rewrite_file("Test Project/Zipped.als") rewrite_file("Test Project/Test.als")