diff --git a/README.md b/README.md index e584d65..16368c1 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,12 @@ The tools are also lint clean (save for line length lints which are useless driv flake8 --ignore E501 . ``` +The tools also have their own unit tests. To verify tests, run the following: + +``` +python3 -m unittest discover tests +``` + ## Including This Package By design, this code can be used as a library by other python code, and as it is Public Domain, diff --git a/arcadeutils/__init__.py b/arcadeutils/__init__.py index e69de29..d2bdc0c 100644 --- a/arcadeutils/__init__.py +++ b/arcadeutils/__init__.py @@ -0,0 +1,7 @@ +from .binary import BinaryDiffException, BinaryDiff, ByteUtil + +__all__ = [ + "BinaryDiffException", + "BinaryDiff", + "ByteUtil", +] diff --git a/arcadeutils/binary.py b/arcadeutils/binary.py index c9d43f2..7f3789c 100644 --- a/arcadeutils/binary.py +++ b/arcadeutils/binary.py @@ -30,8 +30,9 @@ class BinaryDiff: # Chunk the differences, assuming files are usually about the same, # for a massive speed boost. for offset in range(0, binlength, BinaryDiff.CHUNK_SIZE): - if bin1[offset:(offset + BinaryDiff.CHUNK_SIZE)] != bin2[offset:(offset + BinaryDiff.CHUNK_SIZE)]: - for i in range(BinaryDiff.CHUNK_SIZE): + length = min(BinaryDiff.CHUNK_SIZE, binlength - offset) + if bin1[offset:(offset + length)] != bin2[offset:(offset + length)]: + for i in range(length): byte1 = bin1[offset + i] byte2 = bin2[offset + i] @@ -91,7 +92,10 @@ class BinaryDiff: # This is a comment, ignore it, unless its a file-size comment patch = patch[1:].strip().lower() if patch.startswith('file size:'): - return int(patch[10:].strip()) + try: + return int(patch[10:].strip()) + except ValueError: + return None return None @staticmethod @@ -225,7 +229,11 @@ class BinaryDiff: f"Patch is for binary of size {file_size} but binary is {len(binary)} " f"bytes long!" ) - differences: List[Tuple[int, Optional[bytes], bytes]] = BinaryDiff._gather_differences(patchlines, reverse) + + try: + differences: List[Tuple[int, Optional[bytes], bytes]] = BinaryDiff._gather_differences(patchlines, reverse) + except BinaryDiffException as e: + return (False, str(e)) # Now, verify the changes to the binary data for diff in differences: diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_BinaryDiff.py b/tests/test_BinaryDiff.py new file mode 100644 index 0000000..ba75d80 --- /dev/null +++ b/tests/test_BinaryDiff.py @@ -0,0 +1,408 @@ +from arcadeutils import BinaryDiff, BinaryDiffException +import unittest + + +class TestBinaryDiff(unittest.TestCase): + + def test_diff_no_differences(self) -> None: + self.assertEqual( + BinaryDiff.diff(b"abcd", b"abcd"), + [], + ) + self.assertEqual( + BinaryDiff.diff(b"", b""), + [], + ) + + def test_diff_different_sizes(self) -> None: + with self.assertRaises(BinaryDiffException): + BinaryDiff.diff(b"1234", b"123") + with self.assertRaises(BinaryDiffException): + BinaryDiff.diff(b"123", b"1234") + + def test_diff_simple(self) -> None: + self.assertEqual( + BinaryDiff.diff(b"abcd1234", b"bbcd1234"), + [ + '# File size: 8', + '00: 61 -> 62', + ] + ) + self.assertEqual( + BinaryDiff.diff(b"abcd1234", b"abcd1235"), + [ + '# File size: 8', + '07: 34 -> 35', + ] + ) + self.assertEqual( + BinaryDiff.diff(b"abcd1234", b"abdc1224"), + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ] + ) + self.assertEqual( + BinaryDiff.diff(b"abcd1234", b"4321bcda"), + [ + '# File size: 8', + '00: 61 62 63 64 31 32 33 34 -> 34 33 32 31 62 63 64 61', + ] + ) + + def test_size(self) -> None: + self.assertEqual( + BinaryDiff.size([]), + None, + ) + self.assertEqual( + BinaryDiff.size(['# Comment']), + None, + ) + self.assertEqual( + BinaryDiff.size(['00: 01 -> 02']), + None, + ) + self.assertEqual( + BinaryDiff.size(['# File Size: 1024']), + 1024, + ) + self.assertEqual( + BinaryDiff.size(['# File Size: invalid']), + None, + ) + + def test_description(self) -> None: + self.assertEqual( + BinaryDiff.description([]), + None, + ) + self.assertEqual( + BinaryDiff.description(['# Comment']), + None, + ) + self.assertEqual( + BinaryDiff.description(['00: 01 -> 02']), + None, + ) + self.assertEqual( + BinaryDiff.description(['# Description: sample text']), + "sample text", + ) + + def test_needed_amount(self) -> None: + self.assertEqual( + BinaryDiff.needed_amount([]), + 0, + ) + self.assertEqual( + BinaryDiff.needed_amount( + [ + '# File size: 8', + '00: 61 -> 62', + ] + ), + 1, + ) + self.assertEqual( + BinaryDiff.needed_amount( + [ + '# File size: 8', + '07: 34 -> 35', + ] + ), + 8, + ) + self.assertEqual( + BinaryDiff.needed_amount( + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ] + ), + 7, + ) + self.assertEqual( + BinaryDiff.needed_amount( + [ + '# File size: 8', + '00: 61 62 63 64 31 32 33 34 -> 34 33 32 31 62 63 64 61', + ] + ), + 8, + ) + + def test_can_patch_normal(self) -> None: + self.assertEqual( + BinaryDiff.can_patch( + b"abcd1234", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ), + (True, ''), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abcd1234", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ), + (False, 'Patch is for binary of size 12 but binary is 8 bytes long!'), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abcd1234", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ignore_size_differences=True, + ), + (True, '') + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abcd", + [ + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ), + (False, 'Patch offset 06 is beyond the end of the binary!'), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"4321bcda", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ), + (False, 'Patch offset 02 expecting 63 but found 32!'), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abcd1234", + [ + '# File size: 8', + '06: * -> 32', + ], + ), + (True, ''), + ) + + def test_can_patch_reverse(self) -> None: + self.assertEqual( + BinaryDiff.can_patch( + b"abdc1224", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ), + (True, ''), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abdc1224", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ), + (False, 'Patch is for binary of size 12 but binary is 8 bytes long!'), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abdc1224", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ignore_size_differences=True, + ), + (True, ''), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abdc", + [ + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ), + (False, 'Patch offset 06 is beyond the end of the binary!'), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"4321bcda", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ), + (False, 'Patch offset 02 expecting 64 but found 32!'), + ) + self.assertEqual( + BinaryDiff.can_patch( + b"abcd1234", + [ + '# File size: 8', + '06: * -> 32', + ], + reverse=True, + ), + (False, 'Patch offset 06 specifies a wildcard and cannot be reversed!'), + ) + + def test_patch_normal(self) -> None: + self.assertEqual( + BinaryDiff.patch( + b"abcd1234", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ), + b'abdc1224', + ) + with self.assertRaises(BinaryDiffException) as context: + BinaryDiff.patch( + b"abcd1234", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ) + self.assertEqual(str(context.exception), 'Patch is for binary of size 12 but binary is 8 bytes long!') + self.assertEqual( + BinaryDiff.patch( + b"abcd1234", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ignore_size_differences=True, + ), + b'abdc1224', + ) + with self.assertRaises(BinaryDiffException) as context: + BinaryDiff.patch( + b"abcd", + [ + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ) + self.assertEqual(str(context.exception), 'Patch offset 06 is beyond the end of the binary!') + with self.assertRaises(BinaryDiffException) as context: + BinaryDiff.patch( + b"4321bcda", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + ) + self.assertEqual(str(context.exception), 'Patch offset 02 expecting 63 but found 32!') + self.assertEqual( + BinaryDiff.patch( + b"abcd1234", + [ + '# File size: 8', + '06: * -> 32', + ], + ), + b'abcd1224', + ) + + def test_patch_reverse(self) -> None: + self.assertEqual( + BinaryDiff.patch( + b"abdc1224", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ), + b'abcd1234', + ) + with self.assertRaises(BinaryDiffException) as context: + BinaryDiff.patch( + b"abdc1224", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ) + self.assertEqual(str(context.exception), 'Patch is for binary of size 12 but binary is 8 bytes long!') + self.assertEqual( + BinaryDiff.patch( + b"abdc1224", + [ + '# File size: 12', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ignore_size_differences=True, + ), + b'abcd1234', + ) + with self.assertRaises(BinaryDiffException) as context: + BinaryDiff.patch( + b"abdc", + [ + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ) + self.assertEqual(str(context.exception), 'Patch offset 06 is beyond the end of the binary!') + with self.assertRaises(BinaryDiffException) as context: + BinaryDiff.patch( + b"4321bcda", + [ + '# File size: 8', + '02: 63 64 -> 64 63', + '06: 33 -> 32', + ], + reverse=True, + ) + self.assertEqual(str(context.exception), 'Patch offset 02 expecting 64 but found 32!') + with self.assertRaises(BinaryDiffException) as context: + BinaryDiff.patch( + b"abcd1234", + [ + '# File size: 8', + '06: * -> 32', + ], + reverse=True, + ) + self.assertEqual(str(context.exception), 'Patch offset 06 specifies a wildcard and cannot be reversed!')