Skip to content

Commit

Permalink
Added Motorola header get/set/del
Browse files Browse the repository at this point in the history
Signed-off-by: TexZK <[email protected]>
  • Loading branch information
TexZK committed Nov 9, 2023
1 parent fc4440e commit a1d8bc0
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/_autosummary/hexrec.formats.motorola.Record.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ hexrec.formats.motorola.Record
~Record.fit_count_tag
~Record.fit_data_tag
~Record.fix_tags
~Record.get_header
~Record.get_metadata
~Record.is_data
~Record.load_blocks
Expand All @@ -44,6 +45,7 @@ hexrec.formats.motorola.Record
~Record.save_blocks
~Record.save_memory
~Record.save_records
~Record.set_header
~Record.split
~Record.unmarshal
~Record.update_checksum
Expand Down
154 changes: 152 additions & 2 deletions src/hexrec/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@
Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration
"""
import sys
from typing import Callable
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Type

import click

from .__init__ import __version__ as _version
from .formats.motorola import Record as _MotorolaRecord
from .records import RECORD_TYPES as _RECORD_TYPES
from .records import Record as _Record
from .records import convert_file as _convert_file
Expand All @@ -55,7 +58,9 @@
from .records import merge_files as _merge_files
from .records import register_default_record_types as _register_default_record_types
from .records import save_memory as _save_memory
from .utils import hexlify as _hexlify
from .utils import parse_int as _parse_int
from .utils import unhexlify as _unhexlify
from .xxd import xxd as _xxd

_register_default_record_types()
Expand All @@ -68,7 +73,7 @@ def convert(self, value, param, ctx):
try:
return _parse_int(value)
except ValueError:
self.fail('%s is not a valid integer' % value, param, ctx)
self.fail(f'{value} is not a valid integer', param, ctx)


class ByteIntParamType(click.ParamType):
Expand All @@ -81,7 +86,7 @@ def convert(self, value, param, ctx):
raise ValueError()
return b
except ValueError:
self.fail('%s is not a valid byte' % value, param, ctx)
self.fail(f'{value} is not a valid byte', param, ctx)


BASED_INT = BasedIntParamType()
Expand All @@ -93,6 +98,40 @@ def convert(self, value, param, ctx):

RECORD_FORMAT_CHOICE = click.Choice(list(sorted(_RECORD_TYPES.keys())))

DATA_FMT_FORMATTERS: Mapping[str, Callable[[bytes], str]] = {
'ascii': lambda b: b.decode('ascii'),
'hex': lambda b: _hexlify(b, upper=False),
'HEX': lambda b: _hexlify(b, upper=True),
'hex.': lambda b: _hexlify(b, sep='.', upper=False),
'HEX.': lambda b: _hexlify(b, sep='.', upper=True),
'hex-': lambda b: _hexlify(b, sep='-', upper=False),
'HEX-': lambda b: _hexlify(b, sep='-', upper=True),
'hex:': lambda b: _hexlify(b, sep=':', upper=False),
'HEX:': lambda b: _hexlify(b, sep=':', upper=True),
'hex_': lambda b: _hexlify(b, sep='_', upper=False),
'HEX_': lambda b: _hexlify(b, sep='_', upper=True),
'hex ': lambda b: _hexlify(b, sep=' ', upper=False),
'HEX ': lambda b: _hexlify(b, sep=' ', upper=True),
}

DATA_FMT_PARSERS: Mapping[str, Callable[[str], bytes]] = {
'ascii': lambda t: t.encode('ascii'),
'hex': lambda t: _unhexlify(t),
'HEX': lambda t: _unhexlify(t),
'hex.': lambda t: _unhexlify(t.replace('.', '')),
'HEX.': lambda t: _unhexlify(t.replace('.', '')),
'hex-': lambda t: _unhexlify(t.replace('-', '')),
'HEX-': lambda t: _unhexlify(t.replace('-', '')),
'hex:': lambda t: _unhexlify(t.replace(':', '')),
'HEX:': lambda t: _unhexlify(t.replace(':', '')),
'hex_': lambda t: _unhexlify(t.replace('_', '')),
'HEX_': lambda t: _unhexlify(t.replace('_', '')),
'hex ': lambda t: _unhexlify(t),
'HEX ': lambda t: _unhexlify(t),
}

DATA_FMT_CHOICE = click.Choice(list(DATA_FMT_FORMATTERS.keys()))


# ----------------------------------------------------------------------------

Expand Down Expand Up @@ -587,6 +626,117 @@ def shift(
_save_memory(outfile, memory, record_type=output_type)


# ----------------------------------------------------------------------------

@main.command()
@click.option('-i', '--input-format', type=RECORD_FORMAT_CHOICE, help="""
Forces the input file format.
Required for the standard input.
""")
@click.argument('infile', type=FILE_PATH_IN)
def validate(
input_format: str,
infile: str,
) -> None:
r"""Validates a record file.
``INFILE`` is the path of the input file.
Set to ``-`` to read from standard input; input format required.
"""
input_type, _ = find_types(input_format, None, infile, '-')
records = input_type.load_records(infile)
input_type.check_sequence(records)


# ----------------------------------------------------------------------------

@main.group()
def motorola() -> None:
"""Motorola SREC specific"""
pass


# ----------------------------------------------------------------------------

# noinspection PyShadowingBuiltins
@motorola.command()
@click.option('-f', '--format', 'format', type=DATA_FMT_CHOICE,
default='ascii', help='Header data format.')
@click.argument('infile', type=FILE_PATH_IN)
def get_header(
format: str,
infile: str,
) -> None:
r"""Gets the header data.
``INFILE`` is the path of the input file; 'motorola' record type.
Set to ``-`` to read from standard input.
"""
formatter = DATA_FMT_FORMATTERS[format]
record_type = _MotorolaRecord
records = record_type.load_records(infile)
record = record_type.get_header(records)
if record is not None:
header_text = formatter(record.data)
print(header_text)


# ----------------------------------------------------------------------------

# noinspection PyShadowingBuiltins
@motorola.command()
@click.option('-f', '--format', 'format', type=DATA_FMT_CHOICE,
default='ascii', help='Header data format.')
@click.argument('header', type=str)
@click.argument('infile', type=FILE_PATH_IN)
@click.argument('outfile', type=FILE_PATH_OUT)
def set_header(
format: str,
header: str,
infile: str,
outfile: str,
) -> None:
r"""Sets the header data record.
``INFILE`` is the path of the input file; 'motorola' record type.
Set to ``-`` to read from standard input.
``OUTFILE`` is the path of the output file.
Set to ``-`` to write to standard output.
"""
parser = DATA_FMT_PARSERS[format]
data = parser(header)
record_type = _MotorolaRecord
records = record_type.load_records(infile)
records = record_type.set_header(records, data)
record_type.save_records(outfile, records)


# ----------------------------------------------------------------------------

# noinspection PyShadowingBuiltins
@motorola.command()
@click.argument('infile', type=FILE_PATH_IN)
@click.argument('outfile', type=FILE_PATH_OUT)
def del_header(
infile: str,
outfile: str,
) -> None:
r"""Deletes the header data record.
``INFILE`` is the path of the input file; 'motorola' record type.
Set to ``-`` to read from standard input.
``OUTFILE`` is the path of the output file.
Set to ``-`` to write to standard output.
"""
record_type = _MotorolaRecord
records = record_type.load_records(infile)
header_tag = record_type.TAG_TYPE.HEADER
records = [r for r in records if r.tag != header_tag]
record_type.save_records(outfile, records)


# ----------------------------------------------------------------------------

@main.command(context_settings=dict(help_option_names=['-h', '--help']))
Expand Down
58 changes: 58 additions & 0 deletions src/hexrec/formats/motorola.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,3 +792,61 @@ def fix_tags(
start_tag = cls.TAG_TYPE(cls.MATCHING_TAG.index(max_tag))
for index in start_ids:
records[index].tag = start_tag

@classmethod
def get_header(
cls,
records: RecordSequence,
) -> Optional['Record']:
r"""Gets the header record.
Arguments:
records (list of records):
A sequence of records.
Returns:
record: The header record, or ``None``.
"""
header_tag = cls.TAG_TYPE.HEADER
for record in records:
if record.tag == header_tag:
return record
return None

@classmethod
def set_header(
cls,
records: RecordSequence,
data: AnyBytes,
) -> RecordSequence:
r"""Sets the header data.
If existing, the header record is updated in-place.
If missing, the header record is prepended.
Arguments:
records (list of records):
A sequence of records.
data (bytes):
Optional header data.
Returns:
list of records: Updated record list.
"""
header_tag = cls.TAG_TYPE.HEADER
found = None
for record in records:
if record.tag == header_tag:
found = record
break

if found is None:
records = list(records)
records.insert(0, cls.build_header(data))
else:
found.data = data
found.address = 0
found.update_count()
found.update_checksum()
return records
68 changes: 68 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,74 @@ def test_merge_nothing():
assert result.output == ''


# ============================================================================

def test_validate_nothing():
runner = CliRunner()
with pytest.raises(ValueError, match='missing count'):
runner.invoke(main, 'validate -i motorola -'.split(), catch_exceptions=False)


def test_validate_headless(datapath):
runner = CliRunner()
path_in = str(datapath / 'headless.mot')
with pytest.raises(ValueError, match='missing header'):
runner.invoke(main, f'validate {path_in}'.split(), catch_exceptions=False)


def test_validate(datapath):
runner = CliRunner()
path_in = str(datapath / 'bytes.mot')
result = runner.invoke(main, f'validate {path_in}'.split())

assert result.exit_code == 0
assert result.output == ''


# ============================================================================

def test_motorola_dummy(datapath):
runner = CliRunner()
result = runner.invoke(main, f'motorola -h'.split())
assert result.exit_code == 2


def test_motorola_get_header_headless(datapath):
runner = CliRunner()
path_in = str(datapath / 'headless.mot')
result = runner.invoke(main, f'motorola get-header {path_in}'.split())

assert result.exit_code == 0
assert result.output == ''


def test_motorola_get_header_empty(datapath):
runner = CliRunner()
path_in = str(datapath / 'bytes.mot')
result = runner.invoke(main, f'motorola get-header {path_in}'.split())

assert result.exit_code == 0
assert result.output == '\n'


def test_motorola_get_header_ascii(datapath):
runner = CliRunner()
path_in = str(datapath / 'header.mot')
result = runner.invoke(main, f'motorola get-header -f ascii {path_in}'.split())

assert result.exit_code == 0
assert result.output == 'ABC\n'


def test_motorola_get_header_hex(datapath):
runner = CliRunner()
path_in = str(datapath / 'header.mot')
result = runner.invoke(main, f'motorola get-header -f hex {path_in}'.split())

assert result.exit_code == 0
assert result.output == '414243\n'


# ============================================================================

def test_xxd_version():
Expand Down
19 changes: 19 additions & 0 deletions tests/test_cli/header.mot
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
S006000041424333
S1130000000102030405060708090A0B0C0D0E0F74
S1130010101112131415161718191A1B1C1D1E1F64
S1130020202122232425262728292A2B2C2D2E2F54
S1130030303132333435363738393A3B3C3D3E3F44
S1130040404142434445464748494A4B4C4D4E4F34
S1130050505152535455565758595A5B5C5D5E5F24
S1130060606162636465666768696A6B6C6D6E6F14
S1130070707172737475767778797A7B7C7D7E7F04
S1130080808182838485868788898A8B8C8D8E8FF4
S1130090909192939495969798999A9B9C9D9E9FE4
S11300A0A0A1A2A3A4A5A6A7A8A9AAABACADAEAFD4
S11300B0B0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC4
S11300C0C0C1C2C3C4C5C6C7C8C9CACBCCCDCECFB4
S11300D0D0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFA4
S11300E0E0E1E2E3E4E5E6E7E8E9EAEBECEDEEEF94
S11300F0F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF84
S5030010EC
S9030000FC
18 changes: 18 additions & 0 deletions tests/test_cli/headless.mot
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
S1130000000102030405060708090A0B0C0D0E0F74
S1130010101112131415161718191A1B1C1D1E1F64
S1130020202122232425262728292A2B2C2D2E2F54
S1130030303132333435363738393A3B3C3D3E3F44
S1130040404142434445464748494A4B4C4D4E4F34
S1130050505152535455565758595A5B5C5D5E5F24
S1130060606162636465666768696A6B6C6D6E6F14
S1130070707172737475767778797A7B7C7D7E7F04
S1130080808182838485868788898A8B8C8D8E8FF4
S1130090909192939495969798999A9B9C9D9E9FE4
S11300A0A0A1A2A3A4A5A6A7A8A9AAABACADAEAFD4
S11300B0B0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC4
S11300C0C0C1C2C3C4C5C6C7C8C9CACBCCCDCECFB4
S11300D0D0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFA4
S11300E0E0E1E2E3E4E5E6E7E8E9EAEBECEDEEEF94
S11300F0F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF84
S5030010EC
S9030000FC
Loading

0 comments on commit a1d8bc0

Please sign in to comment.