diff --git a/src/py65/tests/utils/test_hexdump.py b/src/py65/tests/utils/test_hexdump.py new file mode 100644 index 0000000..2a9197a --- /dev/null +++ b/src/py65/tests/utils/test_hexdump.py @@ -0,0 +1,134 @@ +import unittest +import sys +from py65.utils.hexdump import load, Loader + +class TopLevelHexdumpTests(unittest.TestCase): + def test_load(self): + text = 'c000: aa bb' + start, data = load(text) + self.assertEqual(0xC000, start) + self.assertEqual([0xAA, 0xBB], data) + +class HexdumpLoaderTests(unittest.TestCase): + def test_empty_string_does_nothing(self): + text = '' + loader = Loader(text) + self.assertEqual(None, loader.start_address) + self.assertEqual([], loader.data) + + def test_all_whitespace_does_nothing(self): + text = " \r\n \t \n" + loader = Loader(text) + self.assertEqual(None, loader.start_address) + self.assertEqual([], loader.data) + + def test_raises_when_start_address_not_found(self): + text = 'aa bb cc' + try: + Loader(text) + self.fail() + except ValueError, e: + msg = 'Start address was not found in data' + self.assert_(e.message.startswith('Start address')) + + def test_raises_when_start_address_is_invalid(self): + text = 'oops: aa bb cc' + try: + Loader(text) + self.fail() + except ValueError, e: + msg = 'Could not parse address: oops' + self.assertEqual(msg, e.message) + + def test_raises_when_start_address_is_too_short(self): + text = '01: aa bb cc' + try: + Loader(text) + self.fail() + except ValueError, e: + msg = 'Expected address to be 2 bytes, got 1' + self.assertEqual(msg, e.message) + + def test_raises_when_start_address_is_too_long(self): + text = '010304: aa bb cc' + try: + Loader(text) + self.fail() + except ValueError, e: + msg = 'Expected address to be 2 bytes, got 3' + self.assertEqual(msg, e.message) + + def test_raises_when_next_address_is_unexpected(self): + text = "c000: aa\nc002: cc" + try: + Loader(text) + self.fail() + except ValueError, e: + msg = 'Non-contigous block detected. Expected next ' \ + 'address to be $c001, label was $c002' + self.assertEqual(msg, e.message) + + def test_raises_when_data_is_invalid(self): + text = 'c000: foo' + try: + Loader(text) + self.fail() + except ValueError, e: + msg = 'Could not parse data: foo' + self.assertEqual(msg, e.message) + + def test_loads_data_without_dollar_signs(self): + text = 'c000: aa bb' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB], load.data) + + def test_loads_data_with_some_dollar_signs(self): + text = '$c000: aa $bb' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB], load.data) + + def test_loads_multiline_data_with_unix_endings(self): + text = '$c000: aa bb\n$c002: cc' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB, 0xCC], load.data) + + def test_loads_multiline_data_with_dos_endings(self): + text = '$c000: aa bb\r\n$c002: cc' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB, 0xCC], load.data) + + def test_ignores_semicolon_comments(self): + text = 'c000: aa bb ;comment' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB], load.data) + + def test_ignores_double_dash_comments(self): + text = 'c000: aa bb -- comment' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB], load.data) + + def test_ignores_pound_comments(self): + text = 'c000: aa bb # comment' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB], load.data) + + def test_ignores_pound_comments(self): + text = 'c000: aa bb # comment' + load = Loader(text) + self.assertEqual(0xC000, load.start_address) + self.assertEqual([0xAA, 0xBB], load.data) + + + +def test_suite(): + return unittest.findTestCases(sys.modules[__name__]) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/src/py65/utils/hexdump.py b/src/py65/utils/hexdump.py new file mode 100644 index 0000000..5395793 --- /dev/null +++ b/src/py65/utils/hexdump.py @@ -0,0 +1,80 @@ +from binascii import a2b_hex + +def load(text): + load = Loader(text) + return (load.start_address, load.data) + +class Loader: + def __init__(self, text): + self.load(text) + + def load(self, text): + self._reset() + + for line in text.splitlines(): + self._parse_line(line) + + def _reset(self): + self.data = [] + self.start_address = None + self.current_address = None + + def _parse_line(self, line): + line = self._remove_comments(line) + pieces = line.strip().split() + + for piece in pieces: + if piece.startswith('$'): + piece = piece[1:] + + if piece.endswith(':'): + self._parse_address(piece[:-1]) + + else: + self._parse_bytes(piece) + + def _remove_comments(self, line): + for delimiter in (';', '--', '#'): + pos = line.find(delimiter) + if pos != -1: + line = line[:pos] + return line + + def _parse_address(self, piece): + try: + addr_bytes = [ ord(c) for c in a2b_hex(piece) ] + except (TypeError, ValueError): + msg = "Could not parse address: %s" % piece + raise ValueError, msg + + if len(addr_bytes) != 2: + msg = "Expected address to be 2 bytes, got %d" % ( + len(addr_bytes)) + raise ValueError, msg + + address = (addr_bytes[0] << 8) + addr_bytes[1] + + if self.start_address is None: + self.start_address = address + self.current_address = address + + elif address != (self.current_address): + msg = "Non-contigous block detected. Expected next address " \ + "to be $%04x, label was $%04x" % (self.current_address, + address) + raise ValueError, msg + + def _parse_bytes(self, piece): + if self.start_address is None: + msg = "Start address was not found in data" + raise ValueError, msg + + else: + try: + bytes = [ ord(c) for c in a2b_hex(piece) ] + except (TypeError, ValueError): + msg = "Could not parse data: %s" % piece + raise ValueError, msg + + self.current_address += len(bytes) + self.data.extend(bytes)