diff --git a/docs/_autosummary/hexrec.utils.SparseMemoryIO.rst b/docs/_autosummary/hexrec.utils.SparseMemoryIO.rst new file mode 100644 index 0000000..7006dfb --- /dev/null +++ b/docs/_autosummary/hexrec.utils.SparseMemoryIO.rst @@ -0,0 +1,57 @@ +SparseMemoryIO +============== + +.. currentmodule:: hexrec.utils + +.. autoclass:: SparseMemoryIO + :members: + :inherited-members: + :private-members: + :special-members: + + + + + .. rubric:: Attributes + + .. autosummary:: + + ~SparseMemoryIO.closed + ~SparseMemoryIO.memory + + + + + + + .. rubric:: Methods + + .. autosummary:: + :nosignatures: + + ~SparseMemoryIO.__init__ + ~SparseMemoryIO.close + ~SparseMemoryIO.detach + ~SparseMemoryIO.fileno + ~SparseMemoryIO.flush + ~SparseMemoryIO.getbuffer + ~SparseMemoryIO.getvalue + ~SparseMemoryIO.isatty + ~SparseMemoryIO.peek + ~SparseMemoryIO.read + ~SparseMemoryIO.read1 + ~SparseMemoryIO.readable + ~SparseMemoryIO.readinto + ~SparseMemoryIO.readinto1 + ~SparseMemoryIO.readline + ~SparseMemoryIO.readlines + ~SparseMemoryIO.seek + ~SparseMemoryIO.seekable + ~SparseMemoryIO.skip_data + ~SparseMemoryIO.skip_hole + ~SparseMemoryIO.tell + ~SparseMemoryIO.truncate + ~SparseMemoryIO.writable + ~SparseMemoryIO.write + ~SparseMemoryIO.writelines + diff --git a/docs/_autosummary/hexrec.utils.rst b/docs/_autosummary/hexrec.utils.rst index 9dd90b0..ef75565 100644 --- a/docs/_autosummary/hexrec.utils.rst +++ b/docs/_autosummary/hexrec.utils.rst @@ -38,6 +38,15 @@ + .. rubric:: Classes + + .. autosummary:: + :toctree: + :template: custom-class-template.rst + :nosignatures: + + SparseMemoryIO + diff --git a/src/hexrec/cli.py b/src/hexrec/cli.py index 9b7261a..3ee9f9e 100644 --- a/src/hexrec/cli.py +++ b/src/hexrec/cli.py @@ -103,7 +103,7 @@ def parse_args(self, ctx, args): FILE_PATH_IN = click.Path(dir_okay=False, allow_dash=True, readable=True, exists=True) FILE_PATH_OUT = click.Path(dir_okay=False, allow_dash=True, writable=True) -RECORD_FORMAT_CHOICE = click.Choice(list(sorted(FILE_TYPES.keys()))) +FORMAT_CHOICE = click.Choice(list(sorted(FILE_TYPES.keys()))) DATA_FMT_FORMATTERS: Mapping[str, Callable[[bytes], bytes]] = { 'ascii': lambda b: b, @@ -313,11 +313,11 @@ def main() -> None: # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -361,11 +361,11 @@ def clear( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -400,11 +400,11 @@ def convert( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -456,11 +456,11 @@ def crop( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -504,11 +504,11 @@ def delete( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -556,11 +556,11 @@ def fill( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -674,7 +674,7 @@ def flood( @click.option('-U', '--upper', 'upper', is_flag=True, help=""" Uses upper case hex letters on address and data. """) -@click.option('-I', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-I', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) @@ -696,7 +696,7 @@ def hexdump( skip: Optional[int], no_squeezing: bool, upper: bool, - input_format: Optional[str], # TODO: + input_format: Optional[str], ) -> None: r"""Display file contents in hexadecimal, decimal, octal, or ascii. @@ -729,6 +729,11 @@ def hexdump( for param, value in hexdump.ordered_options if (param.name in kwargs) and value] + if input_format: + input_type = guess_input_type(infile, input_format) + input_file = input_type.load(infile) + infile = input_file.memory + hexdump_core( infile=infile, length=length, @@ -801,7 +806,7 @@ def hexdump( @click.option('-U', '--upper', 'upper', is_flag=True, help=""" Uses upper case hex letters on address and data. """) -@click.option('-I', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-I', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) @@ -822,7 +827,7 @@ def hd( skip: Optional[int], no_squeezing: bool, upper: bool, - input_format: Optional[str], # TODO: + input_format: Optional[str], ) -> None: r"""Display file contents in hexadecimal, decimal, octal, or ascii. @@ -856,6 +861,11 @@ def hd( format_order.insert(0, 'canonical') kwargs['canonical'] = True + if input_format: + input_type = guess_input_type(infile, input_format) + input_file = input_type.load(infile) + infile = input_file.memory + hexdump_core( infile=infile, length=length, @@ -870,11 +880,11 @@ def hd( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format for all input files. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -918,11 +928,11 @@ def merge( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) -@click.option('-o', '--output-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-o', '--output-format', type=FORMAT_CHOICE, help=""" Forces the output file format. By default it is that of the input file. """) @@ -960,7 +970,7 @@ def shift( # ---------------------------------------------------------------------------- @main.command() -@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help=""" +@click.option('-i', '--input-format', type=FORMAT_CHOICE, help=""" Forces the input file format. Required for the standard input. """) @@ -1230,6 +1240,9 @@ def xxd( Some parameters were changed to satisfy the POSIX-like command line parser. """ + infile = None if infile == '-' else infile + outfile = None if outfile == '-' else outfile + xxd_core( infile=infile, outfile=outfile, diff --git a/src/hexrec/hexdump.py b/src/hexrec/hexdump.py index 818ea56..7f90035 100644 --- a/src/hexrec/hexdump.py +++ b/src/hexrec/hexdump.py @@ -36,7 +36,10 @@ from typing import Sequence from typing import Union +from bytesparse.base import ImmutableMemory + from .base import AnyBytes +from .utils import SparseMemoryIO CHAR_PRINTABLE: Sequence[bytes] = [b.to_bytes(1, 'big') for b in ( b'................' @@ -92,13 +95,17 @@ b' 350', b' 351', b' 352', b' 353', b' 354', b' 355', b' 356', b' 357', b' 360', b' 361', b' 362', b' 363', b' 364', b' 365', b' 366', b' 367', b' 370', b' 371', b' 372', b' 373', b' 374', b' 375', b' 376', b' 377', + b' ---', b' >>>', b' <<<' ] r"""Character tokens lookup table.""" -_HEX_LOWER_SINGLE_TOKENS = [b' %02x' % b for b in range(256)] -_HEX_UPPER_SINGLE_TOKENS = [b' %02X' % b for b in range(256)] +_HEX_LOWER = [b'%02x' % b for b in range(256)] + [b'--', b'>>', b'<<'] +_HEX_UPPER = [b'%02X' % b for b in range(256)] + [b'--', b'>>', b'<<'] + +_HEX_LOWER_TOKENS = [b' %02x' % b for b in range(256)] + [b' --', b' >>', b' <<'] +_HEX_UPPER_TOKENS = [b' %02X' % b for b in range(256)] + [b' --', b' >>', b' <<'] -_OCTAL_SINGLE_TOKENS = [b' %03o' % b for b in range(256)] +_OCTAL_TOKENS = [b' %03o' % b for b in range(256)] + [b' ---', b' >>>', b' <<<'] DEFAULT_FORMAT_ORDER: Sequence[str] = [ 'one_byte_octal', @@ -122,13 +129,13 @@ def _format_default( address_fmt = b'%07X' if upper else b'%07x' tokens = [address_fmt % address] + table = _HEX_UPPER if upper else _HEX_LOWER size = len(chunk) - token_fmt = b' %04X' if upper else b' %04x' - tokens.extend(token_fmt % (chunk[offset] | (chunk[offset+1] << 8)) + tokens.extend((b' ' + table[chunk[offset+1]] + table[chunk[offset]]) for offset in range(0, size-1, 2)) if size & 1: - tokens.append(token_fmt % chunk[size-1]) + tokens.append(b' 00' + table[chunk[size-1]]) if size < width: tokens.extend(b' ' for _ in range(1, width - size, 2)) @@ -146,7 +153,7 @@ def _format_one_byte_octal( address_fmt = b'%07X' if upper else b'%07x' tokens = [address_fmt % address] - table = _OCTAL_SINGLE_TOKENS + table = _OCTAL_TOKENS tokens.extend(table[b] for b in chunk) size = len(chunk) @@ -166,7 +173,7 @@ def _format_one_byte_hex( address_fmt = b'%08X' if upper else b'%08x' tokens = [address_fmt % address] - table = _HEX_UPPER_SINGLE_TOKENS if upper else _HEX_LOWER_SINGLE_TOKENS + table = _HEX_UPPER_TOKENS if upper else _HEX_LOWER_TOKENS tokens.extend(table[b] for b in chunk) size = len(chunk) @@ -206,7 +213,7 @@ def _format_canonical( address_fmt = b'%08X' if upper else b'%08x' tokens = [address_fmt % address] - table = _HEX_UPPER_SINGLE_TOKENS if upper else _HEX_LOWER_SINGLE_TOKENS + table = _HEX_UPPER_TOKENS if upper else _HEX_LOWER_TOKENS size = len(chunk) offset = 0 append = tokens.append @@ -286,13 +293,13 @@ def _format_two_bytes_hex( address_fmt = b'%07X' if upper else b'%07x' tokens = [address_fmt % address] + table = _HEX_UPPER if upper else _HEX_LOWER size = len(chunk) - token_fmt = b' %04X' if upper else b' %04x' - tokens.extend(token_fmt % (chunk[offset] | (chunk[offset+1] << 8)) + tokens.extend((b' ' + table[chunk[offset+1]] + table[chunk[offset]]) for offset in range(0, size-1, 2)) if size & 1: - tokens.append(token_fmt % chunk[size-1]) + tokens.append(b' 00' + table[chunk[size-1]]) if size < width: tokens.extend(b' ' for _ in range(1, width - size, 2)) @@ -354,13 +361,13 @@ def hexdump_core( Input data. If :obj:`str`, it is considered as the input file path. If :obj:`bytes`, it is the input byte chunk. - If ``None`` or ``'-'``, it reads from the standard input. + If ``None``, it reads from the standard input. outfile (str or bytes): Output data. If :obj:`str`, it is considered as the output file path. If :obj:`bytes`, it is the output byte chunk. - If ``None`` or ``'-'``, it writes to the standard output. + If ``None``, it writes to the standard output. one_byte_octal (bool): One-byte octal display. Display the input offset in @@ -494,22 +501,24 @@ def hexdump_core( format_handlers = [_format_default] do_squeezing = not no_squeezing - instream: Optional[IO] = None - outstream: Optional[IO] = None + instream = None + outstream = None try: # Input stream binding - if infile is None or infile == '-': + if infile is None: infile = None instream = sys.stdin.buffer elif isinstance(infile, str): instream = open(infile, 'rb') elif isinstance(infile, (bytes, bytearray, memoryview)): instream = io.BytesIO(infile) + elif isinstance(infile, ImmutableMemory): + instream = SparseMemoryIO(memory=infile) else: instream = infile # Output stream binding - if outfile is None or outfile == '-': + if outfile is None: outfile = None outstream = sys.stdout.buffer elif isinstance(outfile, str): diff --git a/src/hexrec/utils.py b/src/hexrec/utils.py index 2d25366..4b8093c 100644 --- a/src/hexrec/utils.py +++ b/src/hexrec/utils.py @@ -33,8 +33,13 @@ from typing import List from typing import Mapping from typing import Optional +from typing import Sequence from typing import Union +from bytesparse import MemoryIO +from bytesparse.base import Address +from bytesparse.base import ImmutableMemory + from .base import AnyBytes from .base import EllipsisType @@ -103,7 +108,7 @@ def chop( Iterates through the vector grouping its items into windows. - Arguments: + Args: vector (items): Vector to chop. @@ -194,7 +199,7 @@ def parse_int( ) -> Optional[int]: r"""Parses an integer. - Arguments: + Args: value: A generic object to convert to integer. In case `value` is a :obj:`str` (case-insensitive), it can be @@ -298,3 +303,93 @@ def unhexlify( bytestr = binascii.unhexlify(hexstr) return bytestr + + +class SparseMemoryIO(MemoryIO): + r"""Sparse memory I/O wrapper. + + With respect to the parent class :class:`bytesparse.io.MemoryIO`, it allows + reading and writing memory *holes*. + + Such holes are marked by the following integer values (instead of ``None``): + + * ``0x100`` = hole byte within memory span + (:attr:`bytesparse.base.ImmutableMemory.span`); + + * ``0x101`` = hole byte before memory start address + (:attr:`bytesparse.base.ImmutableMemory.start`); + + * ``0x102`` = hole byte after memory end address + (:attr:`bytesparse.base.ImmutableMemory.endex`); + + These special values allow displaying dedicated stuff when dumping memory + data to standard output. + + See Also: + :class:`bytesparse.io.MemoryIO` + :attr:`bytesparse.base.ImmutableMemory.span` + :attr:`bytesparse.base.ImmutableMemory.start` + :attr:`bytesparse.base.ImmutableMemory.endex` + """ + + def read( + self, + size: Optional[Address] = -1, + asmemview: bool = False, + ) -> Union[bytes, memoryview, Address, Sequence[int]]: + # TODO: __doc__ + + if asmemview: + raise ValueError('memory view not supported') + + memory = self._memory + start = self._position + if start >= memory.endex: + return b'' + endex = None if size < 0 else start + size + buffer = b'' + try: + buffer = memory.view(start=start, endex=endex) + contiguous = True + except ValueError: + contiguous = False + + if contiguous: + size = len(buffer) + else: + buffer = list(memory.values(start=start, endex=endex)) + size = len(buffer) + offset_start = memory.start - start + offset_endex = memory.endex - start + + for offset in range(size): + if buffer[offset] is None: + if offset < offset_start: + buffer[offset] = 0x101 # before + elif offset >= offset_endex: + buffer[offset] = 0x102 # after + else: + buffer[offset] = 0x100 # within + + self._position = start + size + return buffer + + def write( + self, + buffer: Union[AnyBytes, ImmutableMemory, int, Sequence[int]], + ) -> Address: + # TODO: __doc__ + + if isinstance(buffer, (bytes, bytearray, memoryview, ImmutableMemory, int)): + return super().write(buffer) + + memory = self._memory + start = self._position + size = len(buffer) + + for offset in range(size): + value = buffer[offset] + memory.poke(start + offset, value if value < 0x100 else None) + + self._position = start + size + return size diff --git a/src/hexrec/xxd.py b/src/hexrec/xxd.py index c117f62..9de74b1 100644 --- a/src/hexrec/xxd.py +++ b/src/hexrec/xxd.py @@ -93,7 +93,7 @@ def humanize( ) -> bytes: r"""Translates bytes to a human-readable representation. - Arguments: + Args: chunk (bytes): A chunk of bytes. @@ -159,13 +159,13 @@ def xxd_core( Input data. If :obj:`str`, it is considered as the input file path. If :obj:`bytes`, it is the input byte chunk. - If ``None`` or ``'-'``, it reads from the standard input. + If ``None``, it reads from the standard input. outfile (str or bytes): Output data. If :obj:`str`, it is considered as the output file path. If :obj:`bytes`, it is the output byte chunk. - If ``None`` or ``'-'``, it writes to the standard output. + If ``None``, it writes to the standard output. autoskip (bool): Toggles autoskip. A single ``'*'`` replaces null lines. @@ -287,7 +287,7 @@ def xxd_core( outstream: Optional[IO] = None try: # Input stream binding - if infile is None or infile == '-': + if infile is None: infile = None instream = sys.stdin.buffer elif isinstance(infile, str): @@ -298,7 +298,7 @@ def xxd_core( instream = infile # Output stream binding - if outfile is None or outfile == '-': + if outfile is None: outfile = None outstream = sys.stdout.buffer elif isinstance(outfile, str): diff --git a/tests/test_hexdump.py b/tests/test_hexdump.py index 9e30a14..9b0577d 100644 --- a/tests/test_hexdump.py +++ b/tests/test_hexdump.py @@ -44,7 +44,6 @@ def test_by_filename_hexdump(tmppath, datapath): test_filenames = glob.glob(str(datapath / (prefix + '*.hexdump'))) for filename in test_filenames: - _of = filename filename = os.path.basename(filename) path_out = tmppath / filename path_ref = datapath / filename diff --git a/tests/test_hexdump/genfiles.sh b/tests/test_hexdump/genfiles.sh index aa20996..30f8d2f 100644 --- a/tests/test_hexdump/genfiles.sh +++ b/tests/test_hexdump/genfiles.sh @@ -7,10 +7,10 @@ hexdump -n 128 bytes.bin > test_hexdump_-n_128_bytes.bin.hexdump hexdump -o bytes.bin > test_hexdump_-o_bytes.bin.hexdump hexdump -s 128 -n 64 bytes.bin > test_hexdump_-s_128_-n_64_bytes.bin.hexdump hexdump -s 32 bytes.bin > test_hexdump_-s_32_bytes.bin.hexdump -# hexdump -U bytes.bin > test_hexdump_-U_bytes.bin.hexdump +# hexdump -U bytes.bin > test_hexdump_-U_bytes.bin.hexdump # custom hexdump -v wildcard.bin > test_hexdump_-v_wildcard.bin.hexdump hexdump -x bytes.bin > test_hexdump_-x_bytes.bin.hexdump -# hexdump -X bytes.bin > test_hexdump_-X__bytes.bin.hexdump +# hexdump -X bytes.bin > test_hexdump_-X__bytes.bin.hexdump # custom hexdump bytes.bin > test_hexdump_bytes.bin.hexdump hexdump wildcard.bin > test_hexdump_wildcard.bin.hexdump @@ -22,9 +22,9 @@ hd -n 128 bytes.bin > test_hd_-n_128_bytes.bin.hd hd -o bytes.bin > test_hd_-o_bytes.bin.hd hd -s 128 -n 64 bytes.bin > test_hd_-s_128_-n_64_bytes.bin.hd hd -s 32 bytes.bin > test_hd_-s_32_bytes.bin.hd -# hd -U bytes.bin > test_hd_-U_bytes.bin.hd +# hd -U bytes.bin > test_hd_-U_bytes.bin.hd # custom hd -v wildcard.bin > test_hd_-v_wildcard.bin.hd hd -x bytes.bin > test_hd_-x_bytes.bin.hd -# hd -X bytes.bin > test_hd_-X__bytes.bin.hd +# hd -X bytes.bin > test_hd_-X__bytes.bin.hd # custom hd bytes.bin > test_hd_bytes.bin.hd hd wildcard.bin > test_hd_wildcard.bin.hd diff --git a/tests/test_hexdump/holes.mot b/tests/test_hexdump/holes.mot new file mode 100644 index 0000000..9aeafdf --- /dev/null +++ b/tests/test_hexdump/holes.mot @@ -0,0 +1,7 @@ +S0030000FC +S1100013131415161718191A1B1C1D1E1F97 +S1130030303132333435363738393A3B3C3D3E3F44 +S1130050505152535455565758595A5B5C5D5E5F24 +S10F0070707172737475767778797A7BFE +S5030004F8 +S9030000FC diff --git a/tests/test_hexdump/test_hd_-X_-I_srec_holes.mot.hd b/tests/test_hexdump/test_hd_-X_-I_srec_holes.mot.hd new file mode 100644 index 0000000..137930b --- /dev/null +++ b/tests/test_hexdump/test_hd_-X_-I_srec_holes.mot.hd @@ -0,0 +1,17 @@ +00000000 >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> |>>>>>>>>>>>>>>>>| +00000000 >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> +00000010 >> >> >> 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f |>>>.............| +00000010 >> >> >> 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f +00000020 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | | +00000020 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +00000030 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f |0123456789:;<=>?| +00000030 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f +00000040 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | | +00000040 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +00000050 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f |PQRSTUVWXYZ[\]^_| +00000050 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f +00000060 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | | +00000060 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +00000070 70 71 72 73 74 75 76 77 78 79 7a 7b << << << << |pqrstuvwxyz{<<<<| +00000070 70 71 72 73 74 75 76 77 78 79 7a 7b << << << << +00000080 diff --git a/tests/test_hexdump/test_hexdump_-X_-I_srec_holes.mot.hexdump b/tests/test_hexdump/test_hexdump_-X_-I_srec_holes.mot.hexdump new file mode 100644 index 0000000..b19bc6c --- /dev/null +++ b/tests/test_hexdump/test_hexdump_-X_-I_srec_holes.mot.hexdump @@ -0,0 +1,9 @@ +00000000 >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> +00000010 >> >> >> 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f +00000020 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +00000030 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f +00000040 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +00000050 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f +00000060 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +00000070 70 71 72 73 74 75 76 77 78 79 7a 7b << << << << +00000080 diff --git a/tests/test_utils.py b/tests/test_utils.py index 6568689..e384538 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,15 @@ +from typing import Any +from typing import Mapping from typing import Type import pytest +from bytesparse import Memory -from hexrec.utils import * - -HEXBYTES = bytes(range(16)) - +from hexrec.utils import SparseMemoryIO +from hexrec.utils import chop +from hexrec.utils import hexlify +from hexrec.utils import parse_int +from hexrec.utils import unhexlify PARSE_INT_PASS: Mapping[Any, int] = { None: None, @@ -120,3 +124,56 @@ def test_unhexlify_doctest(): ans_out = unhexlify(b'AA/BB/CC', delete=b'/') ans_ref = b'\xaa\xbb\xcc' assert ans_out == ans_ref + + +class TestSparseMemoryIO: + + def test_read_after(self): + stream = SparseMemoryIO(Memory.from_bytes(b'\xAA\xBB\xCC')) + actual = stream.read(5) + assert actual == [0xAA, 0xBB, 0xCC, 0x102, 0x102] + + def test_read_before(self): + stream = SparseMemoryIO(Memory.from_bytes(b'\xAA\xBB\xCC', offset=2)) + actual = stream.read() + assert actual == [0x101, 0x101, 0xAA, 0xBB, 0xCC] + + def test_read_contiguous(self): + stream = SparseMemoryIO(Memory.from_bytes(b'abc')) + actual = stream.read() + assert actual == b'abc' + + def test_read_empty(self): + stream = SparseMemoryIO(Memory()) + actual = stream.read() + assert actual == b'' + + def test_read_hole(self): + blocks = [[0, b'\xAA\xBB\xCC'], [5, b'\xEE\xFF']] + stream = SparseMemoryIO(Memory.from_blocks(blocks)) + actual = stream.read() + assert actual == [0xAA, 0xBB, 0xCC, 0x100, 0x100, 0xEE, 0xFF] + + def test_read_raises_asmemview(self): + stream = SparseMemoryIO(Memory()) + with pytest.raises(ValueError, match='memory view not supported'): + stream.read(asmemview=True) + + def test_write_bytes(self): + memory = Memory() + stream = SparseMemoryIO(memory) + stream.write(b'abc') + assert memory.to_blocks() == [[0, b'abc']] + + def test_write_empty(self): + memory = Memory() + stream = SparseMemoryIO(memory) + stream.write([]) + assert memory.to_blocks() == [] + + def test_write_hole(self): + memory = Memory() + stream = SparseMemoryIO(memory) + stream.write([0xAA, 0xBB, 0xCC, 0x100, 0x100, 0xEE, 0xFF]) + expected = [[0, b'\xAA\xBB\xCC'], [5, b'\xEE\xFF']] + assert memory.to_blocks() == expected diff --git a/tests/test_xxd.py b/tests/test_xxd.py index f379f8d..013263e 100644 --- a/tests/test_xxd.py +++ b/tests/test_xxd.py @@ -1,15 +1,17 @@ -import argparse import glob import io import os import sys from pathlib import Path +from typing import Any +from typing import cast as _cast import pytest +from click.testing import CliRunner from test_base import replace_stdin from test_base import replace_stdout -from hexrec.utils import parse_int +from hexrec.cli import main as cli_main from hexrec.xxd import ZERO_BLOCK_SIZE from hexrec.xxd import parse_seek from hexrec.xxd import xxd_core @@ -57,34 +59,6 @@ def test_normalize_whitespace(): assert ans_ref == ans_out -def run_cli(args=None, namespace=None): - FLAG = 'store_true' - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument('-a', '--autoskip', action=FLAG) - parser.add_argument('-b', '--bits', action=FLAG) - parser.add_argument('-c', '--cols', type=parse_int) - parser.add_argument('-E', '--ebcdic', action=FLAG) - parser.add_argument('-e', '--endian', action=FLAG) - parser.add_argument('-g', '--groupsize', type=parse_int) - parser.add_argument('-i', '--include', action=FLAG) - parser.add_argument('-l', '--length', '--len', type=parse_int) - parser.add_argument('-o', '--offset', type=parse_int) - parser.add_argument('-p', '--postscript', '--ps', '--plain', action=FLAG) - parser.add_argument('-q', '--quadword', action=FLAG) - parser.add_argument('-r', '--revert', action=FLAG) - parser.add_argument('-seek', '--oseek', type=parse_int) - parser.add_argument('-s', '--iseek') - parser.add_argument('-u', '--upper', action=FLAG) - parser.add_argument('-U', '--upper-all', action=FLAG) - parser.add_argument('infile', nargs='?', default='-') - parser.add_argument('outfile', nargs='?', default='-') - - args = parser.parse_args(args, namespace) - kwargs = vars(args) - - xxd_core(**kwargs) - - def test_parse_seek(): assert parse_seek(None) == ('', 0) @@ -101,9 +75,11 @@ def test_by_filename_xxd(tmppath, datapath): cmdline = filename[len(prefix):].replace('_', ' ') args = cmdline.split() path_in = datapath / os.path.splitext(args[-1])[0] - args = args[:-1] + [str(path_in), str(path_out)] + args = ['xxd'] + args[:-1] + [str(path_in), str(path_out)] - run_cli(args) + runner = CliRunner() + result = runner.invoke(_cast(Any, cli_main), args) + assert result.exit_code == 0 ans_out = read_text(path_out) ans_ref = read_text(path_ref) @@ -123,9 +99,11 @@ def test_by_filename_bin(tmppath, datapath): cmdline = filename[len(prefix):].replace('_', ' ') args = cmdline.split() path_in = datapath / os.path.splitext(args[-1])[0] - args = args[:-1] + [str(path_in), str(path_out)] + args = ['xxd'] + args[:-1] + [str(path_in), str(path_out)] - run_cli(args) + runner = CliRunner() + result = runner.invoke(_cast(Any, cli_main), args) + assert result.exit_code == 0 ans_out = read_bytes(path_out) ans_ref = read_bytes(path_ref) @@ -145,9 +123,11 @@ def test_by_filename_c(tmppath, datapath): cmdline = filename[len(prefix):].replace('_', ' ') args = cmdline.split() path_in = datapath / os.path.splitext(args[-1])[0] - args = args[:-1] + [str(path_in), str(path_out)] + args = ['xxd'] + args[:-1] + [str(path_in), str(path_out)] - run_cli(args) + runner = CliRunner() + result = runner.invoke(_cast(Any, cli_main), args) + assert result.exit_code == 0 ans_out = read_text(path_out) ans_ref = read_text(path_ref) diff --git a/tests/test_xxd/test_xxd_-r_-p_-seek_256_bytes.hexstr.bin b/tests/test_xxd/test_xxd_-r_-k_256_bytes-shuffled.xxd.bin similarity index 100% rename from tests/test_xxd/test_xxd_-r_-p_-seek_256_bytes.hexstr.bin rename to tests/test_xxd/test_xxd_-r_-k_256_bytes-shuffled.xxd.bin diff --git a/tests/test_xxd/test_xxd_-r_-seek_256_bytes-shuffled.xxd.bin b/tests/test_xxd/test_xxd_-r_-k_256_bytes.xxd.bin similarity index 100% rename from tests/test_xxd/test_xxd_-r_-seek_256_bytes-shuffled.xxd.bin rename to tests/test_xxd/test_xxd_-r_-k_256_bytes.xxd.bin diff --git a/tests/test_xxd/test_xxd_-r_-seek_256_bytes.xxd.bin b/tests/test_xxd/test_xxd_-r_-p_-k_256_bytes.hexstr.bin similarity index 100% rename from tests/test_xxd/test_xxd_-r_-seek_256_bytes.xxd.bin rename to tests/test_xxd/test_xxd_-r_-p_-k_256_bytes.hexstr.bin