├── test ├── __init__.py ├── data │ └── f_loader.hob ├── test_trdos.py ├── test_hobeta.py └── test_zeus2txt.py ├── zeus2txt.jpg ├── .pylintrc ├── zxtools ├── __init__.py ├── common.py ├── trdos.py ├── zeus2txt.py └── hobeta.py ├── Makefile ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ └── python-package.yml ├── .gitignore ├── LICENSE ├── setup.py ├── README.rst └── CODE_OF_CONDUCT.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | # Empty 2 | -------------------------------------------------------------------------------- /zeus2txt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeatcpp/zxtools/HEAD/zeus2txt.jpg -------------------------------------------------------------------------------- /test/data/f_loader.hob: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeatcpp/zxtools/HEAD/test/data/f_loader.hob -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-classes=Header 3 | 4 | [SIMILARITIES] 5 | 6 | # Minimum lines number of a similarity. 7 | min-similarity-lines=7 8 | 9 | [MISCELLANEOUS] 10 | # List of note tags to take in consideration, separated by a comma. 11 | notes=FIXME,XXX 12 | -------------------------------------------------------------------------------- /zxtools/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2016 Kirill V. Lyadvinsky 5 | # http://www.codeatcpp.com 6 | # 7 | # Licensed under the BSD 3-Clause license. 8 | # See LICENSE file in the project root for full license information. 9 | # 10 | """Global constants""" 11 | 12 | __version__ = '1.0.24' 13 | CHUNK_SIZE = 512 * 1024 # 512 KBytes 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test clean coverage lint distclean release 2 | 3 | PYTHON ?= python3 4 | 5 | test: 6 | $(PYTHON) -m unittest discover -v -b 7 | 8 | clean: 9 | rm -rf dist/ build/ *.egg-info 10 | rm -rf coverage.xml 11 | find . -name '__pycache__' | xargs rm -rf 12 | 13 | distclean: clean 14 | 15 | coverage: 16 | pytest --cov=zxtools --cov-report=term-missing 17 | # coverage report -m --fail-under=80 18 | 19 | lint: 20 | pylint zxtools -f parseable -r n 21 | 22 | release: clean lint coverage clean 23 | $(PYTHON) setup.py sdist bdist_wheel upload 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /zxtools/common.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2016 Kirill V. Lyadvinsky 5 | # http://www.codeatcpp.com 6 | # Licensed under the BSD 3-Clause license. 7 | # See LICENSE file in the project root for full license information. 8 | # 9 | """ Common functions """ 10 | 11 | import sys 12 | import logging 13 | 14 | 15 | def safe_parse_args(parser, args): 16 | """Safely parse arguments""" 17 | try: 18 | options = parser.parse_args(args) 19 | if len(args) == 0: 20 | raise ValueError 21 | except ValueError: 22 | parser.print_help() 23 | sys.exit(0) 24 | 25 | return options 26 | 27 | 28 | def default_main(parser): 29 | """ Default entry point implementation """ 30 | args = safe_parse_args(parser, sys.argv[1:]) 31 | if args.verbose: 32 | logging.basicConfig(level=logging.DEBUG) 33 | 34 | if hasattr(args, 'func'): 35 | args.func(args) 36 | 37 | return args 38 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: zxtools tests 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: [3.9, 3.12, 3.13, 3.14] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install setuptools pytest pytest-cov mock 31 | pip install -e . 32 | - name: Run tests 33 | run: | 34 | pytest --cov=zxtools --cov-report=term-missing 35 | - name: Upload coverage to Codecov 36 | run: bash <(curl -s https://codecov.io/bash) 37 | if: success() 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # IDE files 10 | .idea/ 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | #Ipython Notebook 65 | .ipynb_checkpoints 66 | -------------------------------------------------------------------------------- /test/test_trdos.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # vim: set fileencoding=utf-8 : 3 | # -*- coding: utf-8 -*- 4 | # 5 | # Copyright (c) 2016 Kirill V. Lyadvinsky 6 | # http://www.codeatcpp.com 7 | # 8 | # Licensed under the BSD 3-Clause license. 9 | # See LICENSE file in the project root for full license information. 10 | # 11 | """ trdos.py tests """ 12 | 13 | import unittest 14 | import struct 15 | from zxtools import trdos 16 | 17 | 18 | class TestTRDos(unittest.TestCase): 19 | def test_fat_format(self): 20 | data = b"filenameC\x00\x80\xf9\x06\x07\xC1\x01" 21 | record = trdos.FATRecord._make( 22 | struct.unpack_from(trdos.FAT_RECORD_FMT, data)) 23 | 24 | self.assertEqual(record.filename, b"filename") 25 | self.assertEqual(record.filetype, ord("C")) 26 | self.assertEqual(record.start, 32768) 27 | self.assertEqual(record.length, 1785) 28 | self.assertEqual(record.occupied_sectors, 0x07) 29 | self.assertEqual(record.first_sector, 0xC1) 30 | self.assertEqual(record.first_track, 0x01) 31 | 32 | 33 | if __name__ == '__main__': 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Kirill V. Lyadvinsky 2 | http://www.codeatcpp.com 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /zxtools/trdos.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2016 Kirill V. Lyadvinsky 5 | # http://www.codeatcpp.com 6 | # 7 | # Licensed under the BSD 3-Clause license. 8 | # See LICENSE file in the project root for full license information. 9 | # 10 | """ TR-DOS diskette structure utils """ 11 | 12 | from collections import namedtuple 13 | 14 | # TR-DOS diskette structure description 15 | # 16 | # 0 track contains FAT in sectors 0..7 17 | ############################################################################### 18 | # Each FAT record has the following format: 19 | # 20 | # 0 1 21 | # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 22 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 23 | # |FILENAME |E| S | L |N|F|T| 24 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 25 | # 26 | # FILENAME File name in ASCII 27 | # E File extension. Standard TR-DOS types are B, C, D, #. 28 | # S Depends on extension: 29 | # : file size excluding variables 30 | # : base address to load the code 31 | # L File size in bytes. This can be not accurate. 32 | # N File size in sectors. 33 | # F The number of the first sector with data. 34 | # T The number of the first tract with data. 35 | # 36 | FAT_RECORD_FMT = '<8sBHHBBB' 37 | FATRecord = namedtuple('FATRecord', 'filename filetype start length ' 38 | 'occupied_sectors first_sector first_track') 39 | 40 | ############################################################################### 41 | # 42 | # Sector 8 contains disk information in the following format: 43 | # 44 | # 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | 6 | from os.path import join, dirname 7 | 8 | from setuptools import setup, find_packages 9 | 10 | with open(join(dirname(__file__), 'zxtools', '__init__.py'), 'r') as f: 11 | version_info = re.match(r".*__version__ = '(.*?)'", f.read(), re.S).group(1) 12 | 13 | with open('README.rst') as f: 14 | long_readme = f.read() 15 | 16 | dev_requires = [ 17 | 'pytest>=2.8', 18 | 'coverage>=3.7.1', 19 | ] 20 | 21 | setup( 22 | name='zxtools', 23 | version=version_info, 24 | description='Tools to manipulate files from ZX Spectrum', 25 | keywords='spectrum sinclair 48k z80 zeus zeus-asm', 26 | long_description=long_readme, 27 | long_description_content_type='text/x-rst', 28 | author='Kirill V. Lyadvinsky', 29 | author_email='mail@codeatcpp.com', 30 | download_url='https://github.com/codeatcpp/zxtools', 31 | url='http://www.codeatcpp.com', 32 | license='BSD-3-Clause', 33 | packages=find_packages(exclude=('test', 'docs')), 34 | python_requires='>=3.7', 35 | extras_require={ 36 | 'test': dev_requires, 37 | }, 38 | test_suite='test', 39 | zip_safe=True, 40 | classifiers=[ 41 | 'Development Status :: 4 - Beta', 42 | 'Operating System :: OS Independent', 43 | 'Environment :: Console', 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved :: BSD License', 46 | 'Programming Language :: Python :: 3', 47 | 'Programming Language :: Python :: 3.7', 48 | 'Programming Language :: Python :: 3.8', 49 | 'Programming Language :: Python :: 3.9', 50 | 'Programming Language :: Python :: 3.10', 51 | 'Programming Language :: Python :: 3.11', 52 | 'Programming Language :: Python :: 3.12', 53 | 'Programming Language :: Python :: 3.13', 54 | 'Programming Language :: Python :: 3.14', 55 | 'Programming Language :: Python :: Implementation :: CPython', 56 | 'Topic :: Software Development', 57 | 'Topic :: Utilities', 58 | ], 59 | entry_points={ 60 | 'console_scripts': [ 61 | 'zeus2txt = zxtools.zeus2txt:main', 62 | 'hobeta = zxtools.hobeta:main', 63 | ], 64 | }, 65 | project_urls={ 66 | "Repository": "https://github.com/codeatcpp/zxtools", 67 | "Issues": "https://github.com/codeatcpp/zxtools/issues", 68 | }, 69 | ) 70 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================== 2 | Tools to manipulate ZX Spectrum files 3 | ===================================== 4 | 5 | 6 | .. image:: https://github.com/codeatcpp/zxtools/actions/workflows/python-package.yml/badge.svg 7 | :target: https://github.com/codeatcpp/zxtools/actions 8 | 9 | .. image:: https://codecov.io/gh/codeatcpp/zxtools/branch/master/graph/badge.svg 10 | :target: https://codecov.io/gh/codeatcpp/zxtools 11 | 12 | .. image:: https://img.shields.io/github/release/codeatcpp/zxtools.svg?style=flat 13 | :target: https://github.com/codeatcpp/zxtools/releases 14 | 15 | .. image:: https://img.shields.io/pypi/v/zxtools.svg?style=flat 16 | :target: https://pypi.python.org/pypi/zxtools 17 | 18 | .. image:: https://img.shields.io/github/issues/codeatcpp/zxtools.svg 19 | :target: https://github.com/codeatcpp/zxtools/issues 20 | 21 | .. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjia3ep%2Fzxtools.svg?type=shield 22 | :target: https://app.fossa.io/projects/git%2Bgithub.com%2Fjia3ep%2Fzxtools?ref=badge_shield 23 | 24 | Here's a set of utils to manipulate files that were copied from a TR-DOS diskette or from a tape. 25 | 26 | Originally the tools were written to simplify the following workflow: 27 | 28 | 1. Grab diskette image using `Hobeta `_ tool. 29 | 2. Strip the file header and save the result to a new file. 30 | 3. Convert resulting `Zeus Z80 assembler `_ file to the plain text format. 31 | 32 | TODO: I have future plans to implement some more tools I need to restore my old ZX Spectrum projects. 33 | 34 | But you can use them in the way you need. And it's very easy to use: download the package, run ``setup.py`` (or install via ``pip install zxtools``), invoke in the following way:: 35 | 36 | $ python3 -m zxtools.hobeta strip input.hobeta result.zeus 37 | $ python3 -m zxtools.zeus2txt result.zeus listing.asm --include-code 38 | 39 | .. image:: https://raw.githubusercontent.com/codeatcpp/zxtools/master/zeus2txt.jpg 40 | 41 | NOTE: Python 3 is required to use this package, and Python 2 is not supported but you are welcome to fix it. 42 | 43 | To view the resulting files with syntax colorization you can use special `Visual Studio Code plugin `_: 44 | 45 | .. image:: https://raw.githubusercontent.com/codeatcpp/vscode-language-z80-asm/master/vscode.png 46 | :target: https://marketplace.visualstudio.com/items?itemName=jia3ep.zeus-z80-asm 47 | 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at oss@codeatcpp.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /zxtools/zeus2txt.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2016 Kirill V. Lyadvinsky 5 | # http://www.codeatcpp.com 6 | # 7 | # Licensed under the BSD 3-Clause license. 8 | # See LICENSE file in the project root for full license information. 9 | # 10 | """ Convert Zeus Z80 assembler file to a plain text """ 11 | 12 | import argparse 13 | import logging 14 | import io 15 | 16 | from zxtools import CHUNK_SIZE 17 | from zxtools.common import default_main 18 | 19 | CODE_ALIGN_WIDTH = 35 20 | 21 | 22 | def show_info(*parsed_args): 23 | """Show some statistic about Zeus file""" 24 | # TODO Implement this function 25 | return parsed_args 26 | 27 | 28 | def read_file(src_file): 29 | """Read source file for future processing""" 30 | with src_file: 31 | while True: 32 | chunk = src_file.read(CHUNK_SIZE) 33 | if chunk: 34 | for cur_char in chunk: 35 | yield cur_char 36 | else: 37 | break 38 | 39 | 40 | ASM_FIRST_TOKEN = 128 41 | ASM_META = [ 42 | "A", "ADC ", "ADD ", "AF'", "AF", "AND ", "B", "BC", "BIT ", "C", 43 | "CALL ", "CCF", "CP ", "CPD", "CPDR", "CPI", "CPIR", "CPL", "D", "DAA", 44 | "DE", "DEC ", "DEFB ", "DEFM ", "DEFS ", "DEFW ", "DI", "DISP ", "DJNZ ", 45 | "E", "EI", "ENT", "EQU ", "EX ", "EXX", "H", "HALT", "HL", "I", "IM ", 46 | "IN ", "INC ", "IND", "INDR", "INI", "INIR", "IX", "IY", "JP ", "JR ", 47 | "L", "LD ", "LDD", "LDDR", "LDI", "LDIR", "M", "NC", "NEG", "NOP", "NV", 48 | "NZ", "OR ", "ORG ", "OTDR", "OTIR", "OUT ", "OUTD", "OUTI", "P", "PE", 49 | "PO", "POP ", "PUSH ", "R", "RES ", "RET", "RETI", "RETN", "RL ", "RLA", 50 | "RLC ", "RLCA", "RLD", "RR ", "RRA", "RRC ", "RRCA", "RRD", "RST ", 51 | "SBC ", "SCF", "SET ", "SLA ", "SP", "SRA ", "SRL ", "SUB ", "V", "XOR ", 52 | "Z"] 53 | 54 | 55 | def convert_file(parsed_args): 56 | """ Convert Zeus Z80 assembler file specified in zeus_file to the plain 57 | text and print it to the output_file """ 58 | logger = logging.getLogger('convert_file') 59 | 60 | process_string = False 61 | strnum_lo = False, 0 62 | tab = False 63 | output = parsed_args.output_file 64 | strnum = 0 65 | cur_buffer = "" 66 | cur_line = io.StringIO() 67 | for cur_char in read_file(parsed_args.zeus_file): 68 | if process_string: 69 | cur_buffer += "0x%02X " % cur_char 70 | if not cur_char: # End of string 71 | process_string = False 72 | strnum_lo = False, 0 73 | cur_str = cur_line.getvalue() 74 | print(cur_str, end="", file=output) 75 | if parsed_args.include_code: 76 | print(" "*(CODE_ALIGN_WIDTH-len(cur_str))+";", 77 | "0x%04X " % strnum + cur_buffer, file=output) 78 | else: 79 | print(file=output) 80 | continue 81 | if tab: 82 | print(" "*cur_char, end="", file=cur_line) 83 | tab = False 84 | continue 85 | if cur_char == 0x0A: 86 | tab = True 87 | continue 88 | if cur_char < ASM_FIRST_TOKEN: # Printable character 89 | print(chr(cur_char), end="", file=cur_line) 90 | continue 91 | try: 92 | print(ASM_META[cur_char-ASM_FIRST_TOKEN], end="", file=cur_line) 93 | except IndexError: 94 | logger.warning("Token not defined: 0x%02X (%d), at line %05d. " 95 | "Skipped.", cur_char, cur_char, strnum) 96 | else: 97 | if not strnum_lo[0]: 98 | strnum_lo = True, cur_char 99 | else: 100 | strnum = strnum_lo[1] + cur_char*256 101 | if strnum == 0xFFFF: # End of file 102 | print(file=output) 103 | break 104 | cur_line = io.StringIO() 105 | cur_line.truncate(0) 106 | cur_buffer = "" 107 | print("%05d" % strnum, end=" ", file=cur_line) 108 | process_string = True 109 | output.close() 110 | 111 | 112 | def create_parser(): 113 | """ Parse command line arguments """ 114 | parser = argparse.ArgumentParser( 115 | description="Zeus Z80 assembler files converter") 116 | parser.add_argument( 117 | '-v', '--verbose', help="Increase output verbosity", 118 | action='store_true') 119 | 120 | subparsers = parser.add_subparsers(help="Available commands") 121 | subparsers.required = False 122 | 123 | info_parser = subparsers.add_parser( 124 | 'info', 125 | help="Show information about the specified Zeus Z80 assembler file") 126 | info_parser.add_argument( 127 | 'zeus_file', metavar='zeus-file', type=argparse.FileType('rb', 0), 128 | help="Input file with Zeus Z80 assembler (usually FILENAME.$C)") 129 | info_parser.set_defaults(func=show_info) 130 | 131 | convert_parser = subparsers.add_parser( 132 | 'convert', help="Convert Zeus Z80 assembler file to a plain text file") 133 | convert_parser.add_argument( 134 | 'zeus_file', metavar='zeus-file', type=argparse.FileType('rb', 0), 135 | help="Input file with Zeus Z80 assembler (usually FILENAME.$C)") 136 | convert_parser.add_argument( 137 | 'output_file', metavar='output-file', 138 | type=argparse.FileType('w'), help="Path to the output file") 139 | convert_parser.add_argument( 140 | '--include-code', dest='include_code', 141 | action='store_true', help="Include original code in the output file") 142 | convert_parser.set_defaults(func=convert_file) 143 | 144 | return parser 145 | 146 | 147 | def main(): 148 | """Entry point""" 149 | default_main(create_parser()) 150 | 151 | 152 | if __name__ == '__main__': 153 | main() 154 | -------------------------------------------------------------------------------- /zxtools/hobeta.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2016 Kirill V. Lyadvinsky 5 | # http://www.codeatcpp.com 6 | # 7 | # Licensed under the BSD 3-Clause license. 8 | # See LICENSE file in the project root for full license information. 9 | # 10 | """ Hobeta file utils """ 11 | 12 | import os 13 | import logging 14 | import struct 15 | from collections import namedtuple 16 | import argparse 17 | 18 | from zxtools import CHUNK_SIZE 19 | from zxtools.common import default_main 20 | 21 | HEADER_FMT = '<8sBHHBBH' 22 | Header = namedtuple( 23 | 'Header', 24 | 'filename filetype start length first_sector occupied_sectors check_sum') 25 | 26 | 27 | def hobeta_help(*parsed_args): 28 | """Shows help""" 29 | print( 30 | "Hobeta file has the following format:\n" 31 | "(this is according to http://speccy.info/Hobeta)\n" 32 | "\n" 33 | "0 1 2 3\n" 34 | "0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n" 35 | "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n" 36 | "|TR-DOS FILENAME|T| S | L |F|C|CHK| RAW FILE DATA...\n" 37 | "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n" 38 | "\n" 39 | " T - File type. Standard types are B, C, D, #.\n" 40 | " S - TR-DOS START parameter (ORG for CODE, LEN for BASIC)\n" 41 | " L - TR-DOS LENGTH parameter (size in bytes)\n" 42 | " F - The first occupied sector or just padding?\n" 43 | " C - File size in TR-DOS sectors\n" 44 | " CHK - Checksum of this header (excluding CHK)") 45 | return parsed_args 46 | 47 | 48 | def calc_checksum(data): 49 | """ Calculate checksum for data """ 50 | check_sum = 0 51 | for i, value in enumerate(data): 52 | check_sum = (check_sum + value*257 + i) % 0x10000 53 | return check_sum 54 | 55 | 56 | def parse_info(hobeta_file): 57 | """ Parse Hobeta header """ 58 | logger = logging.getLogger('parse_info') 59 | 60 | header_len = struct.calcsize(HEADER_FMT) 61 | logger.debug(header_len) 62 | data = hobeta_file.read(header_len) 63 | 64 | actual_check_sum = calc_checksum(data[0:header_len-2]) 65 | header = Header._make(struct.unpack_from(HEADER_FMT, data)) 66 | logger.debug(header) 67 | 68 | return header, actual_check_sum 69 | 70 | 71 | def show_info(parsed_args): 72 | """ Show info from Hobeta header """ 73 | header, crc = parse_info(parsed_args.hobeta_file) 74 | 75 | print( 76 | ("File name:\t" + header.filename.decode("ascii") + "\n" + 77 | "Extension:\t" + chr(header.filetype) + "\n" + 78 | ("Prg LEN:\t" if header.filetype == ord('B') else "Place at:\t") + 79 | str(header.start) + "\n" + 80 | "File size:\t" + str(header.length) + "\n" + 81 | "First sector:\t" + str(header.first_sector) + "\n" + 82 | "Occupied sectors:\t" + str(header.occupied_sectors) + "\n" + 83 | "Check sum:\t" + str(header.check_sum) + " " + 84 | ("(OK)" if crc == header.check_sum 85 | else "(WRONG! Should be " + str(crc) + ")")).expandtabs(20)) 86 | 87 | 88 | def strip_header(parsed_args): 89 | """ Copy the source file to the output file excluding Hobeta header """ 90 | logger = logging.getLogger('strip_header') 91 | 92 | header, crc = parse_info(parsed_args.hobeta_file) 93 | if header.check_sum != crc: 94 | print("WARNING: wrong checksum in the header.") 95 | 96 | header_size = struct.calcsize(HEADER_FMT) 97 | 98 | with parsed_args.hobeta_file as src_file: 99 | src_file.seek(0, os.SEEK_END) 100 | hobeta_file_size = src_file.tell() 101 | if parsed_args.ignore_header: 102 | bytes_to_copy = hobeta_file_size-header_size 103 | else: 104 | bytes_to_copy = header.length 105 | logger.debug(bytes_to_copy) 106 | 107 | length = bytes_to_copy 108 | src_file.seek(header_size) 109 | with parsed_args.output_file as dst_file: 110 | while length: 111 | chunk_size = min(CHUNK_SIZE, length) 112 | data = src_file.read(chunk_size) 113 | if not data: 114 | break 115 | dst_file.write(data) 116 | length -= len(data) 117 | print("Created file %s, %d bytes copied." % 118 | (dst_file.name, bytes_to_copy-length)) 119 | dst_file.close() 120 | src_file.close() 121 | return bytes_to_copy-length 122 | 123 | 124 | def create_parser(): 125 | """ Parse command line arguments """ 126 | parser = argparse.ArgumentParser(description="Hobeta files converter") 127 | parser.add_argument( 128 | '-v', '--verbose', help="Increase output verbosity", 129 | action='store_true') 130 | 131 | subparsers = parser.add_subparsers(help="Available commands") 132 | subparsers.required = False 133 | 134 | info_parser = subparsers.add_parser( 135 | 'info', 136 | help="Show information about the specified Hobeta file") 137 | info_parser.add_argument( 138 | 'hobeta_file', metavar='hobeta-file', type=argparse.FileType('rb', 0), 139 | help="Input file in Hobeta format (usually FILENAME.$C)") 140 | info_parser.set_defaults(func=show_info) 141 | 142 | strip_parser = subparsers.add_parser('strip', help="Strip Hobeta header") 143 | strip_parser.add_argument( 144 | 'hobeta_file', metavar='hobeta-file', type=argparse.FileType('rb', 0), 145 | help="Input file in Hobeta format (usually FILENAME.$C)") 146 | strip_parser.add_argument( 147 | 'output_file', metavar='output-file', 148 | type=argparse.FileType('wb', 0), help="Path to the output file") 149 | strip_parser.add_argument( 150 | '--ignore-header', dest='ignore_header', 151 | action='store_true', help="Ignore the file size from Hobeta header") 152 | strip_parser.set_defaults(func=strip_header) 153 | 154 | help_parser = subparsers.add_parser( 155 | 'hobeta-help', 156 | help="Show Hobeta header format description") 157 | help_parser.set_defaults(func=hobeta_help) 158 | 159 | return parser 160 | 161 | 162 | def main(): 163 | """Entry point""" 164 | return default_main(create_parser()) 165 | 166 | 167 | if __name__ == '__main__': 168 | main() 169 | -------------------------------------------------------------------------------- /test/test_hobeta.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # vim: set fileencoding=utf-8 : 3 | # -*- coding: utf-8 -*- 4 | # 5 | # Copyright (c) 2016 Kirill V. Lyadvinsky 6 | # http://www.codeatcpp.com 7 | # 8 | # Licensed under the BSD 3-Clause license. 9 | # See LICENSE file in the project root for full license information. 10 | # 11 | """ hobeta.py tests """ 12 | 13 | import os 14 | import io 15 | import struct 16 | import unittest 17 | import tempfile 18 | from collections import namedtuple 19 | 20 | from zxtools import hobeta 21 | from zxtools.common import safe_parse_args 22 | 23 | from mock import patch 24 | 25 | 26 | class TestHobeta(unittest.TestCase): 27 | def test_args_parser(self): 28 | with self.assertRaises(SystemExit): 29 | with patch('sys.argv', ["hobeta.py", "-h", "-v"]): 30 | hobeta.main() 31 | 32 | with self.assertRaises(SystemExit): 33 | with patch('sys.argv', ["hobeta.py"]): 34 | hobeta.main() 35 | 36 | with patch('sys.argv', ["hobeta.py", "hobeta-help"]): 37 | hobeta.main() 38 | 39 | args_parser = hobeta.create_parser() 40 | 41 | temp_in_file = tempfile.mkstemp()[1] 42 | input_file = open(temp_in_file, "w") 43 | input_file.close() 44 | temp_out_file = tempfile.mkstemp()[1] 45 | try: 46 | args = safe_parse_args(args_parser, ["info", temp_in_file]) 47 | self.assertEqual(args.func, hobeta.show_info) 48 | args.hobeta_file.close() 49 | 50 | args = safe_parse_args(args_parser, 51 | ["strip", temp_in_file, temp_out_file]) 52 | self.assertEqual(args.func, hobeta.strip_header) 53 | args.hobeta_file.close() 54 | args.output_file.close() 55 | finally: 56 | os.remove(temp_in_file) 57 | os.remove(temp_out_file) 58 | 59 | def test_checksum(self): 60 | data = b'F.load.AC\x00\x80\xf9\x06\x00\x07' 61 | self.assertEqual(hobeta.calc_checksum(data), 20661) 62 | 63 | def test_format_size(self): 64 | header_len = struct.calcsize(hobeta.HEADER_FMT) 65 | self.assertEqual(header_len, 17) 66 | 67 | def test_format(self): 68 | data = b"\x46\x2E\x6C\x6F\x61\x64\x2E\x41" \ 69 | b"\x43\x00\x80\xF9\x06\x00\x07\xB5\x50" 70 | record = hobeta.Header._make(struct.unpack_from(hobeta.HEADER_FMT, 71 | data)) 72 | 73 | self.assertEqual(record.filename, b"F.load.A") 74 | self.assertEqual(record.filetype, ord('C')) 75 | self.assertEqual(record.start, 32768) 76 | self.assertEqual(record.length, 1785) 77 | self.assertEqual(record.first_sector, 0) 78 | self.assertEqual(record.occupied_sectors, 7) 79 | self.assertEqual(record.check_sum, 20661) 80 | 81 | def test_parse_info(self): 82 | test_file = io.BytesIO(b"\x46\x2E\x6C\x6F\x61\x64\x2E\x41" 83 | b"\x43\x00\x80\xF9\x06\x00\x07\xB5" 84 | b"\x50\x00\x00\x3B\x20\x4C\x4F\x41" 85 | b"\x44\x45\x52\x20\x66\x6F\x72\x20") 86 | header, crc = hobeta.parse_info(test_file) 87 | 88 | self.assertEqual(header.filename, b"F.load.A") 89 | self.assertEqual(header.filetype, ord('C')) 90 | self.assertEqual(header.start, 32768) 91 | self.assertEqual(header.length, 1785) 92 | self.assertEqual(header.first_sector, 0) 93 | self.assertEqual(header.occupied_sectors, 7) 94 | self.assertEqual(header.check_sum, 20661) 95 | self.assertEqual(header.check_sum, crc) 96 | 97 | def test_parse_info2(self): 98 | test_file = io.BytesIO(b"\x46\x2E\x6C\x6F\x61\x64\x2E\x41" 99 | b"\x43\x00\x80\x02\x00\x00\x07\xB5" 100 | b"\x50\x00\x00\x3B\x20\x4C\x4F\x41" 101 | b"\x44\x45\x52\x20\x66\x6F\x72\x20") 102 | header, crc = hobeta.parse_info(test_file) 103 | 104 | self.assertEqual(header.filename, b"F.load.A") 105 | self.assertEqual(header.filetype, ord('C')) 106 | self.assertEqual(header.start, 32768) 107 | self.assertEqual(header.length, 2) 108 | self.assertEqual(header.first_sector, 0) 109 | self.assertEqual(header.occupied_sectors, 7) 110 | self.assertEqual(header.check_sum, 20661) 111 | self.assertNotEqual(header.check_sum, crc) 112 | 113 | @staticmethod 114 | def strip_header(test_input_file, ignore_header_info): 115 | temp_output_path = tempfile.mkstemp()[1] 116 | temp_output_file = open(temp_output_path, "wb") 117 | args = namedtuple('Args', "hobeta_file output_file ignore_header") 118 | parsed_args = args(test_input_file, 119 | temp_output_file, 120 | ignore_header_info) 121 | copied_bytes = hobeta.strip_header(parsed_args) 122 | 123 | return temp_output_path, copied_bytes 124 | 125 | def test_strip_header1(self): 126 | test_input_file = io.BytesIO(b"\x46\x2E\x6C\x6F\x61\x64\x2E\x41" 127 | b"\x43\x00\x80\xF9\x06\x00\x07\xB5" 128 | b"\x50\x00\x00\x3B\x20\x4C\x4F\x41" 129 | b"\x44\x45\x52\x20\x66\x6F\x72") 130 | temp_output_path, bytes_count = self.strip_header( 131 | test_input_file, True) 132 | 133 | temp_output_file = open(temp_output_path, "rb") 134 | temp_output_file.seek(0, os.SEEK_END) 135 | try: 136 | self.assertEqual(temp_output_file.tell(), 14) 137 | self.assertEqual(bytes_count, 14) 138 | finally: 139 | temp_output_file.close() 140 | os.remove(temp_output_path) 141 | 142 | def test_strip_header2(self): 143 | test_input_file = io.BytesIO(b"\x46\x2E\x6C\x6F\x61\x64\x2E\x41" 144 | b"\x43\x00\x80\xF9\x06\x00\x07\xB5" 145 | b"\x50\x00\x00\x3B\x20\x4C\x4F\x41" 146 | b"\x44\x45\x52\x20\x66\x6F\x72\x20\x20") 147 | temp_output_path, bytes_count = self.strip_header( 148 | test_input_file, False) 149 | 150 | temp_output_file = open(temp_output_path, "rb") 151 | temp_output_file.seek(0, os.SEEK_END) 152 | try: 153 | self.assertEqual(temp_output_file.tell(), 16) 154 | self.assertEqual(bytes_count, 16) 155 | finally: 156 | temp_output_file.close() 157 | os.remove(temp_output_path) 158 | 159 | def test_strip_header3(self): 160 | test_input_file = io.BytesIO(b"\x46\x2E\x6C\x6F\x61\x64\x2E\x41" 161 | b"\x43\x00\x80\x0A\x00\x00\x07\xB5" 162 | b"\x50\x00\x00\x3B\x20\x4C\x4F\x41" 163 | b"\x44\x45\x52\x20\x66\x6F\x72\x20") 164 | temp_output_path, bytes_count = self.strip_header( 165 | test_input_file, False) 166 | 167 | temp_output_file = open(temp_output_path, "rb") 168 | temp_output_file.seek(0, os.SEEK_END) 169 | try: 170 | self.assertEqual(temp_output_file.tell(), 10) 171 | self.assertEqual(bytes_count, 10) 172 | finally: 173 | temp_output_file.close() 174 | os.remove(temp_output_path) 175 | 176 | 177 | if __name__ == '__main__': 178 | unittest.main() 179 | -------------------------------------------------------------------------------- /test/test_zeus2txt.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # vim: set fileencoding=utf-8 : 3 | # -*- coding: utf-8 -*- 4 | # 5 | # Copyright (c) 2016 Kirill V. Lyadvinsky 6 | # http://www.codeatcpp.com 7 | # 8 | # Licensed under the BSD 3-Clause license. 9 | # See LICENSE file in the project root for full license information. 10 | # 11 | """ zeus2txt.py tests """ 12 | 13 | import io 14 | import os 15 | import tempfile 16 | import unittest 17 | from collections import namedtuple 18 | import logging 19 | from mock import patch 20 | 21 | from zxtools import zeus2txt 22 | from zxtools.common import safe_parse_args 23 | 24 | 25 | 26 | class TestZeus2Txt(unittest.TestCase): 27 | def test_args_parser(self): 28 | args_parser = zeus2txt.create_parser() 29 | 30 | with self.assertRaises(SystemExit): 31 | with patch('sys.argv', ["zeus2txt.py", "-h", "-v"]): 32 | zeus2txt.main() 33 | 34 | with self.assertRaises(SystemExit): 35 | with patch('sys.argv', ["zeus2txt.py"]): 36 | zeus2txt.main() 37 | 38 | temp_in_file = tempfile.mkstemp()[1] 39 | input_file = open(temp_in_file, "w") 40 | input_file.close() 41 | temp_out_file = tempfile.mkstemp()[1] 42 | try: 43 | args = safe_parse_args(args_parser, ["info", temp_in_file]) 44 | self.assertEqual(args.func, zeus2txt.show_info) 45 | args.zeus_file.close() 46 | 47 | args = safe_parse_args(args_parser, 48 | ["convert", temp_in_file, temp_out_file]) 49 | self.assertEqual(args.func, zeus2txt.convert_file) 50 | args.zeus_file.close() 51 | args.output_file.close() 52 | finally: 53 | os.remove(temp_in_file) 54 | os.remove(temp_out_file) 55 | 56 | @staticmethod 57 | def prepare_convert_args(test_data, include_code=False): 58 | test_file = io.BytesIO(test_data) 59 | temp_output_path = tempfile.mkstemp()[1] 60 | temp_output_file = open(temp_output_path, "w") 61 | 62 | args = namedtuple('Args', "zeus_file output_file include_code") 63 | parsed_args = args(test_file, temp_output_file, include_code) 64 | return parsed_args, temp_output_path, temp_output_file 65 | 66 | def test_undefined_token(self): 67 | logging.basicConfig(level=logging.DEBUG) 68 | args, temp_output_path, temp_output_file = self.prepare_convert_args( 69 | b"\x0A\x00\x0A\x06\xFF\x2C\x34\x32\x00\xFF\xFF") 70 | 71 | try: 72 | zeus2txt.convert_file(args) 73 | temp_output_file.close() 74 | temp_output_file = open(temp_output_path, "r") 75 | lines = temp_output_file.read().splitlines() 76 | self.assertEqual(lines, ["00010 ,42", ""]) 77 | finally: 78 | temp_output_file.close() 79 | os.remove(temp_output_path) 80 | 81 | def test_no_eof(self): 82 | args, temp_output_path, temp_output_file = self.prepare_convert_args( 83 | b"\x0A\x00\x0A\x06\x82\x87\x2C\x34\x32\x00") 84 | 85 | try: 86 | zeus2txt.convert_file(args) 87 | temp_output_file.close() 88 | temp_output_file = open(temp_output_path, "r") 89 | lines = temp_output_file.read().splitlines() 90 | self.assertEqual(lines, ["00010 ADD BC,42", ]) 91 | finally: 92 | temp_output_file.close() 93 | os.remove(temp_output_path) 94 | 95 | def test_include_code(self): 96 | args, temp_output_path, temp_output_file = self.prepare_convert_args( 97 | b"\x0A\x00\x0A\x06\x82\x87\x2C\x34\x32\x00", True) 98 | 99 | try: 100 | zeus2txt.convert_file(args) 101 | temp_output_file.close() 102 | temp_output_file = open(temp_output_path, "r") 103 | lines = temp_output_file.read().splitlines() 104 | self.assertEqual(lines, ["00010 ADD BC,42" 105 | " ; 0x000A " 106 | "0x0A 0x06 0x82 0x87 " 107 | "0x2C 0x34 0x32 0x00 ", ]) 108 | finally: 109 | temp_output_file.close() 110 | os.remove(temp_output_path) 111 | 112 | def test_convert(self): 113 | args, temp_output_path, temp_output_file = self.prepare_convert_args( 114 | self.test_data) 115 | 116 | try: 117 | zeus2txt.convert_file(args) 118 | temp_output_file.close() 119 | temp_output_file = open(temp_output_path, "rb") 120 | lines = temp_output_file.read().splitlines() 121 | expected_lines = self.test_output.split(b"\n") 122 | self.assertEqual(lines, expected_lines) 123 | finally: 124 | temp_output_file.close() 125 | os.remove(temp_output_path) 126 | 127 | def setUp(self): 128 | self.test_data = ( 129 | b"\x00\x00\x3B\x20\x4C\x4F\x41\x44\x45\x52\x20\x66\x6F\x72\x20\x46" 130 | b"\x2E\x45\x44\x49\x54\x4F\x52\x00\x00\x00\x3B\x20\x4C\x2E\x4B\x2E" 131 | b"\x50\x72\x6F\x64\x75\x63\x74\x69\x6F\x6E\x00\x0A\x00\x0A\x06\xBF" 132 | b"\x35\x30\x30\x30\x30\x00\x14\x00\x0A\x06\x8A\x43\x4C\x53\x00\x1E" 133 | b"\x00\x0A\x06\x8A\x53\x48\x52\x00\x28\x00\x0A\x06\x8A\x4E\x45\x57" 134 | b"\x53\x48\x00\x32\x00\x0A\x06\xB3\x94\x2C\x4D\x53\x47\x31\x00\x3C" 135 | b"\x00\x0A\x06\xB3\x87\x2C\x33\x39\x00\x46\x00\x0A\x06\x8A\x23\x32" 136 | b"\x30\x33\x43\x00\x50\x00\x0A\x06\x8A\x53\x54\x41\x4E\x44\x00\x5A" 137 | b"\x00\x0A\x06\xB3\xA5\x2C\x32\x32\x37\x38\x34\x00\x64\x00\x0A\x06" 138 | b"\xB3\x94\x2C\x32\x32\x37\x38\x33\x00\x6E\x00\x0A\x06\xB3\x87\x2C" 139 | b"\x35\x31\x31\x00\x78\x00\x0A\x06\xB3\x28\xA5\x29\x2C\x30\x00\x82" 140 | b"\x00\x0A\x06\xB7\x00\x8C\x00\x0A\x06\xB0\x4C\x4F\x41\x44\x00\x3F" 141 | b"\x9C\x45\x4E\x44\x0A\x03\xCC\x00\x40\x9C\x43\x4C\x53\x0A\x03\xB3" 142 | b"\xA5\x2C\x31\x36\x33\x38\x34\x00\x4A\x9C\x0A\x06\xB3\x94\x2C\x31" 143 | b"\x36\x33\x38\x35\x00\x54\x9C\x0A\x06\xB3\x87\x2C\x36\x31\x34\x33" 144 | b"\x00\x5E\x9C\x0A\x06\xB3\x28\xA5\x29\x2C\x30\x00\x68\x9C\x0A\x06" 145 | b"\xB7\x00\x72\x9C\x0A\x06\xB3\xA5\x2C\x32\x32\x35\x32\x38\x00\x7C" 146 | b"\x9C\x0A\x06\xB3\x94\x2C\x32\x32\x35\x32\x39\x00\x86\x9C\x0A\x06" 147 | b"\xB3\x87\x2C\x37\x36\x37\x00\x90\x9C\x0A\x06\xB3\x28\xA5\x29\x2C" 148 | b"\x37\x00\x9A\x9C\x0A\x06\xB7\x00\xA4\x9C\x0A\x06\xE3\x80\x00\xAE" 149 | b"\x9C\xC2\x28\x32\x35\x34\x29\x2C\x80\x3A\xB3\x80\x2C\x37\x00\xAF" 150 | b"\x9C\x0A\x06\xB3\x28\x32\x33\x36\x32\x34\x29\x2C\x80\x00\xB8\x9C" 151 | b"\x43\x48\x4F\x50\x45\x20\xB3\x80\x2C\x32\x00\xC2\x9C\x0A\x06\x8A" 152 | b"\x23\x31\x36\x30\x31\x00\xCC\x9C\x0A\x06\xCC\x00\xD6\x9C\x53\x48" 153 | b"\x52\x0A\x03\xB3\xA5\x2C\x31\x35\x36\x31\x36\x00\xE0\x9C\x0A\x06" 154 | b"\xB3\x94\x2C\x33\x30\x30\x30\x30\x00\xEA\x9C\x0A\x06\xB3\x87\x2C" 155 | b"\x37\x36\x38\x00\xF4\x9C\x0A\x06\xB7\x00\xFE\x9C\x0A\x06\xB3\xA5" 156 | b"\x2C\x33\x30\x30\x30\x30\x00\x08\x9D\x0A\x06\xB3\x86\x2C\x39\x36" 157 | b"\x00\x12\x9D\x53\x48\x32\x0A\x03\xC9\x87\x00\x1C\x9D\x0A\x06\xB3" 158 | b"\x86\x2C\x34\x00\x26\x9D\x53\x48\x33\x0A\x03\xA9\xA5\x00\x30\x9D" 159 | b"\x0A\x06\x9C\x53\x48\x33\x00\x3A\x9D\x0A\x06\xB3\x86\x2C\x34\x00" 160 | b"\x44\x9D\x53\x48\x34\x0A\x03\xB3\x80\x2C\x28\xA5\x29\x00\x4E\x9D" 161 | b"\x0A\x06\xD2\x00\x58\x9D\x0A\x06\xBE\x28\xA5\x29\x00\x62\x9D\x0A" 162 | b"\x06\xB3\x28\xA5\x29\x2C\x80\x00\x6C\x9D\x0A\x06\xA9\xA5\x00\x76" 163 | b"\x9D\x0A\x06\x9C\x53\x48\x34\x00\x80\x9D\x0A\x06\xC8\x87\x00\x8A" 164 | b"\x9D\x0A\x06\x9C\x53\x48\x32\x00\x94\x9D\x0A\x06\xCC\x00\x9E\x9D" 165 | b"\x53\x54\x41\x4E\x44\x20\xB3\xA5\x2C\x31\x35\x36\x31\x36\x00\xA8" 166 | b"\x9D\x0A\x06\x95\xA3\x00\xB2\x9D\x0A\x06\xB3\x28\x32\x33\x36\x30" 167 | b"\x36\x29\x2C\xA5\x00\xBC\x9D\x0A\x06\xCC\x00\xC6\x9D\x4E\x45\x57" 168 | b"\x53\x48\x20\xB3\xA5\x2C\x41\x44\x52\x53\x48\x00\xD0\x9D\x0A\x06" 169 | b"\x95\xA3\x00\xDA\x9D\x0A\x06\xB3\x28\x32\x33\x36\x30\x36\x29\x2C" 170 | b"\xA5\x00\xE4\x9D\x0A\x06\xCC\x00\xEE\x9D\x46\x46\x49\x4C\x45\x20" 171 | b"\xB3\x80\x2C\x28\x32\x30\x37\x30\x38\x29\x00\xF8\x9D\x0A\x06\x8C" 172 | b"\x30\x00\x02\x9E\x0A\x06\xB1\xE4\x2C\x4E\x46\x49\x4C\x45\x00\x0C" 173 | b"\x9E\x0A\x06\xB3\x86\x2C\x80\x00\x16\x9E\x0A\x06\xB3\x94\x2C\x31" 174 | b"\x38\x34\x33\x32\x00\x20\x9E\x46\x46\x32\x0A\x03\xB3\xA5\x2C\x46" 175 | b"\x4E\x41\x4D\x45\x00\x2A\x9E\x0A\x06\xB3\x28\x50\x44\x45\x29\x2C" 176 | b"\x94\x00\x34\x9E\x0A\x06\xB3\x28\x50\x42\x43\x29\x2C\x87\x00\x3E" 177 | b"\x9E\x0A\x06\xB3\x86\x2C\x38\x00\x48\x9E\x46\x46\x33\x0A\x03\xB3" 178 | b"\x80\x2C\x28\x94\x29\x00\x52\x9E\x0A\x06\x8C\x28\xA5\x29\x00\x5C" 179 | b"\x9E\x0A\x06\xB1\xBD\x2C\x4E\x45\x58\x54\x46\x00\x66\x9E\x0A\x06" 180 | b"\xA9\x94\x00\x70\x9E\x0A\x06\xA9\xA5\x00\x7A\x9E\x0A\x06\x9C\x46" 181 | b"\x46\x33\x00\x84\x9E\x0A\x06\xB3\x80\x2C\x28\x94\x29\x00\x8E\x9E" 182 | b"\x0A\x06\x8C\x36\x39\x00\x98\x9E\x0A\x06\xB1\xBD\x2C\x4E\x45\x58" 183 | b"\x54\x46\x00\xA2\x9E\x0A\x06\xB3\xA5\x2C\x36\x00\xAC\x9E\x0A\x06" 184 | b"\x82\xA5\x2C\x94\x00\xB6\x9E\x0A\x06\xB3\x80\x2C\x28\xA5\x29\x00" 185 | b"\xC0\x9E\x0A\x06\xB3\x28\x53\x45\x43\x29\x2C\x80\x00\xCA\x9E\x0A" 186 | b"\x06\xA9\xA5\x00\xD4\x9E\x0A\x06\xB3\x80\x2C\x28\xA5\x29\x00\xDE" 187 | b"\x9E\x0A\x06\xB3\x28\x54\x52\x43\x29\x2C\x80\x00\xE8\x9E\x0A\x06" 188 | b"\xB0\x4C\x4F\x41\x32\x00\xF2\x9E\x4E\x45\x58\x54\x46\x20\xB3\x94" 189 | b"\x2C\x28\x50\x44\x45\x29\x00\xFC\x9E\x0A\x06\xB3\xA5\x2C\x31\x36" 190 | b"\x00\x06\x9F\x0A\x06\x82\xA5\x2C\x94\x00\x10\x9F\x0A\x06\xA1\x94" 191 | b"\x2C\xA5\x00\x1A\x9F\x0A\x06\xB3\x87\x2C\x28\x50\x42\x43\x29\x00" 192 | b"\x24\x9F\x0A\x06\x9C\x46\x46\x32\x00\x2E\x9F\x4E\x46\x49\x4C\x45" 193 | b"\x20\x8A\x4E\x45\x57\x53\x48\x00\x38\x9F\x0A\x06\xB3\x94\x2C\x4D" 194 | b"\x53\x47\x32\x00\x42\x9F\x0A\x06\xB3\x87\x2C\x4D\x53\x47\x33\x2D" 195 | b"\x4D\x53\x47\x32\x00\x4C\x9F\x0A\x06\x8A\x23\x32\x30\x33\x43\x00" 196 | b"\x4D\x9F\x0A\x06\x8A\x42\x45\x45\x50\x00\x4E\x9F\x0A\x06\xB3\x94" 197 | b"\x2C\x4D\x53\x47\x33\x00\x4F\x9F\x0A\x06\xB3\x87\x2C\x54\x52\x43" 198 | b"\x2D\x4D\x53\x47\x33\x00\x50\x9F\x0A\x06\x8A\x23\x32\x30\x33\x43" 199 | b"\x00\x60\x9F\x8A\x50\x41\x55\x53\x3A\x8A\x53\x54\x41\x4E\x44\x00" 200 | b"\x6A\x9F\x0A\x06\xB0\x4C\x4F\x41\x44\x00\x74\x9F\x50\x41\x55\x53" 201 | b"\x0A\x02\xE3\x80\x00\x7E\x9F\x0A\x06\xB3\x28\x32\x33\x35\x36\x30" 202 | b"\x29\x2C\x80\x00\x88\x9F\x50\x41\x32\x0A\x03\xB3\x80\x2C\x28\x32" 203 | b"\x33\x35\x36\x30\x29\x00\x92\x9F\x0A\x06\x8C\x30\x00\x9C\x9F\x0A" 204 | b"\x06\xCC\x20\xBD\x00\xA6\x9F\x0A\x06\xB1\x50\x41\x32\x00\xB0\x9F" 205 | b"\x42\x45\x45\x50\x0A\x02\xB3\x94\x2C\x23\x30\x31\x30\x35\x00\xBA" 206 | b"\x9F\x0A\x06\xB3\xA5\x2C\x23\x30\x36\x36\x36\x00\xC4\x9F\x0A\x06" 207 | b"\x8A\x23\x30\x33\x42\x35\x00\xCE\x9F\x0A\x06\xCC\x00\xD8\x9F\x4C" 208 | b"\x4F\x41\x44\x0A\x02\xB3\xA5\x2C\x32\x32\x35\x36\x30\x00\xE2\x9F" 209 | b"\x0A\x06\xB3\x94\x2C\x32\x32\x35\x36\x31\x00\xEC\x9F\x0A\x06\xB3" 210 | b"\x87\x2C\x37\x33\x36\x00\xF6\x9F\x0A\x06\xB3\x28\xA5\x29\x2C\x30" 211 | b"\x00\x00\xA0\x0A\x06\xB7\x00\x0A\xA0\x0A\x06\xB3\x87\x2C\x23\x30" 212 | b"\x39\x30\x35\x00\x14\xA0\x0A\x06\xB3\x94\x2C\x30\x00\x1E\xA0\x0A" 213 | b"\x06\xB3\xA5\x2C\x31\x38\x34\x33\x32\x00\x28\xA0\x0A\x06\x8A\x31" 214 | b"\x35\x36\x33\x35\x00\x32\xA0\x0A\x06\xB0\x46\x46\x49\x4C\x45\x00" 215 | b"\x3C\xA0\x4C\x4F\x41\x32\x0A\x02\xB3\x80\x2C\x28\x53\x45\x43\x29" 216 | b"\x00\x46\xA0\x0A\x06\xB3\x9D\x2C\x80\x00\x50\xA0\x0A\x06\xB3\x80" 217 | b"\x2C\x28\x54\x52\x43\x29\x00\x5A\xA0\x0A\x06\xB3\x92\x2C\x80\x00" 218 | b"\x64\xA0\x0A\x06\xB3\x87\x2C\x23\x32\x36\x30\x35\x00\x6E\xA0\x0A" 219 | b"\x06\xB3\xA5\x2C\x33\x30\x30\x30\x30\x00\x78\xA0\x0A\x06\x8A\x31" 220 | b"\x35\x36\x33\x35\x00\x82\xA0\x0A\x06\xB0\x33\x31\x36\x39\x33\x00" 221 | b"\x60\xEA\x41\x44\x52\x53\x48\x20\xA0\x33\x30\x30\x30\x30\x00\x6A" 222 | b"\xEA\x4D\x53\x47\x31\x0A\x02\x96\x32\x32\x2C\x30\x2C\x30\x2C\x31" 223 | b"\x37\x2C\x30\x00\x74\xEA\x0A\x06\x96\x31\x36\x2C\x37\x00\x7E\xEA" 224 | b"\x97\x22\x46\x4F\x4E\x54\x20\x45\x44\x49\x54\x4F\x52\x20\x62\x79" 225 | b"\x20\x4C\x79\x61\x22\x00\x88\xEA\x97\x22\x64\x76\x69\x6E\x73\x6B" 226 | b"\x79\x20\x4B\x69\x72\x69\x6C\x6C\x22\x00\x92\xEA\x46\x4E\x41\x4D" 227 | b"\x45\x20\x97\x22\x65\x64\x69\x74\x6F\x72\x0A\x02\x22\x00\x9C\xEA" 228 | b"\x50\x44\x45\x0A\x03\x99\x30\x00\xA6\xEA\x50\x42\x43\x0A\x03\x99" 229 | b"\x30\x00\xB0\xEA\x4D\x53\x47\x32\x0A\x02\x96\x32\x32\x2C\x35\x2C" 230 | b"\x30\x2C\x31\x37\x2C\x30\x00\xBA\xEA\x96\x31\x36\x2C\x37\x2C\x31" 231 | b"\x39\x2C\x31\x00\xC4\xEA\x97\x22\x46\x69\x6C\x65\x20\x27\x65\x64" 232 | b"\x69\x74\x6F\x72\x0A\x02\x3C\x9D\x3E\x27\x22\x00\xCE\xEA\x97\x22" 233 | b"\x20\x6E\x6F\x74\x20\x66\x6F\x75\x6E\x64\x22\x00\xD8\xEA\x4D\x53" 234 | b"\x47\x33\x0A\x02\x96\x32\x32\x2C\x36\x2C\x30\x2C\x31\x37\x2C\x30" 235 | b"\x00\xDD\xEA\x0A\x06\x96\x31\x36\x2C\x37\x2C\x31\x39\x2C\x30\x00" 236 | b"\xE2\xEA\x97\x22\x50\x52\x45\x53\x53\x20\x41\x4E\x59\x20\x4B\x45" 237 | b"\x59\x20\x46\x4F\x52\x20\x22\x00\xEC\xEA\x97\x22\x52\x45\x4C\x4F" 238 | b"\x41\x44\x20\x46\x49\x4C\x45\x22\x00\x00\xEB\x54\x52\x43\x0A\x03" 239 | b"\x96\x30\x00\x0A\xEB\x53\x45\x43\x0A\x03\x96\x30\x00\x14\xEB\x45" 240 | b"\x4E\x44\x32\x0A\x02\xBB\x00\xFF\xFF" 241 | ) 242 | self.test_output = ( 243 | b"\x30\x30\x30\x30\x30\x20\x3B\x20\x4C\x4F\x41\x44\x45\x52\x20\x66" 244 | b"\x6F\x72\x20\x46\x2E\x45\x44\x49\x54\x4F\x52\x0A\x30\x30\x30\x30" 245 | b"\x30\x20\x3B\x20\x4C\x2E\x4B\x2E\x50\x72\x6F\x64\x75\x63\x74\x69" 246 | b"\x6F\x6E\x0A\x30\x30\x30\x31\x30\x20\x20\x20\x20\x20\x20\x20\x4F" 247 | b"\x52\x47\x20\x35\x30\x30\x30\x30\x0A\x30\x30\x30\x32\x30\x20\x20" 248 | b"\x20\x20\x20\x20\x20\x43\x41\x4C\x4C\x20\x43\x4C\x53\x0A\x30\x30" 249 | b"\x30\x33\x30\x20\x20\x20\x20\x20\x20\x20\x43\x41\x4C\x4C\x20\x53" 250 | b"\x48\x52\x0A\x30\x30\x30\x34\x30\x20\x20\x20\x20\x20\x20\x20\x43" 251 | b"\x41\x4C\x4C\x20\x4E\x45\x57\x53\x48\x0A\x30\x30\x30\x35\x30\x20" 252 | b"\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x44\x45\x2C\x4D\x53\x47\x31" 253 | b"\x0A\x30\x30\x30\x36\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20" 254 | b"\x42\x43\x2C\x33\x39\x0A\x30\x30\x30\x37\x30\x20\x20\x20\x20\x20" 255 | b"\x20\x20\x43\x41\x4C\x4C\x20\x23\x32\x30\x33\x43\x0A\x30\x30\x30" 256 | b"\x38\x30\x20\x20\x20\x20\x20\x20\x20\x43\x41\x4C\x4C\x20\x53\x54" 257 | b"\x41\x4E\x44\x0A\x30\x30\x30\x39\x30\x20\x20\x20\x20\x20\x20\x20" 258 | b"\x4C\x44\x20\x48\x4C\x2C\x32\x32\x37\x38\x34\x0A\x30\x30\x31\x30" 259 | b"\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x44\x45\x2C\x32\x32" 260 | b"\x37\x38\x33\x0A\x30\x30\x31\x31\x30\x20\x20\x20\x20\x20\x20\x20" 261 | b"\x4C\x44\x20\x42\x43\x2C\x35\x31\x31\x0A\x30\x30\x31\x32\x30\x20" 262 | b"\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28\x48\x4C\x29\x2C\x30\x0A" 263 | b"\x30\x30\x31\x33\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x49\x52" 264 | b"\x0A\x30\x30\x31\x34\x30\x20\x20\x20\x20\x20\x20\x20\x4A\x50\x20" 265 | b"\x4C\x4F\x41\x44\x0A\x33\x39\x39\x39\x39\x20\x45\x4E\x44\x20\x20" 266 | b"\x20\x52\x45\x54\x0A\x34\x30\x30\x30\x30\x20\x43\x4C\x53\x20\x20" 267 | b"\x20\x4C\x44\x20\x48\x4C\x2C\x31\x36\x33\x38\x34\x0A\x34\x30\x30" 268 | b"\x31\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x44\x45\x2C\x31" 269 | b"\x36\x33\x38\x35\x0A\x34\x30\x30\x32\x30\x20\x20\x20\x20\x20\x20" 270 | b"\x20\x4C\x44\x20\x42\x43\x2C\x36\x31\x34\x33\x0A\x34\x30\x30\x33" 271 | b"\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28\x48\x4C\x29\x2C" 272 | b"\x30\x0A\x34\x30\x30\x34\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44" 273 | b"\x49\x52\x0A\x34\x30\x30\x35\x30\x20\x20\x20\x20\x20\x20\x20\x4C" 274 | b"\x44\x20\x48\x4C\x2C\x32\x32\x35\x32\x38\x0A\x34\x30\x30\x36\x30" 275 | b"\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x44\x45\x2C\x32\x32\x35" 276 | b"\x32\x39\x0A\x34\x30\x30\x37\x30\x20\x20\x20\x20\x20\x20\x20\x4C" 277 | b"\x44\x20\x42\x43\x2C\x37\x36\x37\x0A\x34\x30\x30\x38\x30\x20\x20" 278 | b"\x20\x20\x20\x20\x20\x4C\x44\x20\x28\x48\x4C\x29\x2C\x37\x0A\x34" 279 | b"\x30\x30\x39\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x49\x52\x0A" 280 | b"\x34\x30\x31\x30\x30\x20\x20\x20\x20\x20\x20\x20\x58\x4F\x52\x20" 281 | b"\x41\x0A\x34\x30\x31\x31\x30\x20\x4F\x55\x54\x20\x28\x32\x35\x34" 282 | b"\x29\x2C\x41\x3A\x4C\x44\x20\x41\x2C\x37\x0A\x34\x30\x31\x31\x31" 283 | b"\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28\x32\x33\x36\x32\x34" 284 | b"\x29\x2C\x41\x0A\x34\x30\x31\x32\x30\x20\x43\x48\x4F\x50\x45\x20" 285 | b"\x4C\x44\x20\x41\x2C\x32\x0A\x34\x30\x31\x33\x30\x20\x20\x20\x20" 286 | b"\x20\x20\x20\x43\x41\x4C\x4C\x20\x23\x31\x36\x30\x31\x0A\x34\x30" 287 | b"\x31\x34\x30\x20\x20\x20\x20\x20\x20\x20\x52\x45\x54\x0A\x34\x30" 288 | b"\x31\x35\x30\x20\x53\x48\x52\x20\x20\x20\x4C\x44\x20\x48\x4C\x2C" 289 | b"\x31\x35\x36\x31\x36\x0A\x34\x30\x31\x36\x30\x20\x20\x20\x20\x20" 290 | b"\x20\x20\x4C\x44\x20\x44\x45\x2C\x33\x30\x30\x30\x30\x0A\x34\x30" 291 | b"\x31\x37\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42\x43\x2C" 292 | b"\x37\x36\x38\x0A\x34\x30\x31\x38\x30\x20\x20\x20\x20\x20\x20\x20" 293 | b"\x4C\x44\x49\x52\x0A\x34\x30\x31\x39\x30\x20\x20\x20\x20\x20\x20" 294 | b"\x20\x4C\x44\x20\x48\x4C\x2C\x33\x30\x30\x30\x30\x0A\x34\x30\x32" 295 | b"\x30\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42\x2C\x39\x36" 296 | b"\x0A\x34\x30\x32\x31\x30\x20\x53\x48\x32\x20\x20\x20\x50\x55\x53" 297 | b"\x48\x20\x42\x43\x0A\x34\x30\x32\x32\x30\x20\x20\x20\x20\x20\x20" 298 | b"\x20\x4C\x44\x20\x42\x2C\x34\x0A\x34\x30\x32\x33\x30\x20\x53\x48" 299 | b"\x33\x20\x20\x20\x49\x4E\x43\x20\x48\x4C\x0A\x34\x30\x32\x34\x30" 300 | b"\x20\x20\x20\x20\x20\x20\x20\x44\x4A\x4E\x5A\x20\x53\x48\x33\x0A" 301 | b"\x34\x30\x32\x35\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42" 302 | b"\x2C\x34\x0A\x34\x30\x32\x36\x30\x20\x53\x48\x34\x20\x20\x20\x4C" 303 | b"\x44\x20\x41\x2C\x28\x48\x4C\x29\x0A\x34\x30\x32\x37\x30\x20\x20" 304 | b"\x20\x20\x20\x20\x20\x52\x4C\x43\x41\x0A\x34\x30\x32\x38\x30\x20" 305 | b"\x20\x20\x20\x20\x20\x20\x4F\x52\x20\x28\x48\x4C\x29\x0A\x34\x30" 306 | b"\x32\x39\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28\x48\x4C" 307 | b"\x29\x2C\x41\x0A\x34\x30\x33\x30\x30\x20\x20\x20\x20\x20\x20\x20" 308 | b"\x49\x4E\x43\x20\x48\x4C\x0A\x34\x30\x33\x31\x30\x20\x20\x20\x20" 309 | b"\x20\x20\x20\x44\x4A\x4E\x5A\x20\x53\x48\x34\x0A\x34\x30\x33\x32" 310 | b"\x30\x20\x20\x20\x20\x20\x20\x20\x50\x4F\x50\x20\x42\x43\x0A\x34" 311 | b"\x30\x33\x33\x30\x20\x20\x20\x20\x20\x20\x20\x44\x4A\x4E\x5A\x20" 312 | b"\x53\x48\x32\x0A\x34\x30\x33\x34\x30\x20\x20\x20\x20\x20\x20\x20" 313 | b"\x52\x45\x54\x0A\x34\x30\x33\x35\x30\x20\x53\x54\x41\x4E\x44\x20" 314 | b"\x4C\x44\x20\x48\x4C\x2C\x31\x35\x36\x31\x36\x0A\x34\x30\x33\x36" 315 | b"\x30\x20\x20\x20\x20\x20\x20\x20\x44\x45\x43\x20\x48\x0A\x34\x30" 316 | b"\x33\x37\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28\x32\x33" 317 | b"\x36\x30\x36\x29\x2C\x48\x4C\x0A\x34\x30\x33\x38\x30\x20\x20\x20" 318 | b"\x20\x20\x20\x20\x52\x45\x54\x0A\x34\x30\x33\x39\x30\x20\x4E\x45" 319 | b"\x57\x53\x48\x20\x4C\x44\x20\x48\x4C\x2C\x41\x44\x52\x53\x48\x0A" 320 | b"\x34\x30\x34\x30\x30\x20\x20\x20\x20\x20\x20\x20\x44\x45\x43\x20" 321 | b"\x48\x0A\x34\x30\x34\x31\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44" 322 | b"\x20\x28\x32\x33\x36\x30\x36\x29\x2C\x48\x4C\x0A\x34\x30\x34\x32" 323 | b"\x30\x20\x20\x20\x20\x20\x20\x20\x52\x45\x54\x0A\x34\x30\x34\x33" 324 | b"\x30\x20\x46\x46\x49\x4C\x45\x20\x4C\x44\x20\x41\x2C\x28\x32\x30" 325 | b"\x37\x30\x38\x29\x0A\x34\x30\x34\x34\x30\x20\x20\x20\x20\x20\x20" 326 | b"\x20\x43\x50\x20\x30\x0A\x34\x30\x34\x35\x30\x20\x20\x20\x20\x20" 327 | b"\x20\x20\x4A\x52\x20\x5A\x2C\x4E\x46\x49\x4C\x45\x0A\x34\x30\x34" 328 | b"\x36\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42\x2C\x41\x0A" 329 | b"\x34\x30\x34\x37\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x44" 330 | b"\x45\x2C\x31\x38\x34\x33\x32\x0A\x34\x30\x34\x38\x30\x20\x46\x46" 331 | b"\x32\x20\x20\x20\x4C\x44\x20\x48\x4C\x2C\x46\x4E\x41\x4D\x45\x0A" 332 | b"\x34\x30\x34\x39\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28" 333 | b"\x50\x44\x45\x29\x2C\x44\x45\x0A\x34\x30\x35\x30\x30\x20\x20\x20" 334 | b"\x20\x20\x20\x20\x4C\x44\x20\x28\x50\x42\x43\x29\x2C\x42\x43\x0A" 335 | b"\x34\x30\x35\x31\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42" 336 | b"\x2C\x38\x0A\x34\x30\x35\x32\x30\x20\x46\x46\x33\x20\x20\x20\x4C" 337 | b"\x44\x20\x41\x2C\x28\x44\x45\x29\x0A\x34\x30\x35\x33\x30\x20\x20" 338 | b"\x20\x20\x20\x20\x20\x43\x50\x20\x28\x48\x4C\x29\x0A\x34\x30\x35" 339 | b"\x34\x30\x20\x20\x20\x20\x20\x20\x20\x4A\x52\x20\x4E\x5A\x2C\x4E" 340 | b"\x45\x58\x54\x46\x0A\x34\x30\x35\x35\x30\x20\x20\x20\x20\x20\x20" 341 | b"\x20\x49\x4E\x43\x20\x44\x45\x0A\x34\x30\x35\x36\x30\x20\x20\x20" 342 | b"\x20\x20\x20\x20\x49\x4E\x43\x20\x48\x4C\x0A\x34\x30\x35\x37\x30" 343 | b"\x20\x20\x20\x20\x20\x20\x20\x44\x4A\x4E\x5A\x20\x46\x46\x33\x0A" 344 | b"\x34\x30\x35\x38\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x41" 345 | b"\x2C\x28\x44\x45\x29\x0A\x34\x30\x35\x39\x30\x20\x20\x20\x20\x20" 346 | b"\x20\x20\x43\x50\x20\x36\x39\x0A\x34\x30\x36\x30\x30\x20\x20\x20" 347 | b"\x20\x20\x20\x20\x4A\x52\x20\x4E\x5A\x2C\x4E\x45\x58\x54\x46\x0A" 348 | b"\x34\x30\x36\x31\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x48" 349 | b"\x4C\x2C\x36\x0A\x34\x30\x36\x32\x30\x20\x20\x20\x20\x20\x20\x20" 350 | b"\x41\x44\x44\x20\x48\x4C\x2C\x44\x45\x0A\x34\x30\x36\x33\x30\x20" 351 | b"\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x41\x2C\x28\x48\x4C\x29\x0A" 352 | b"\x34\x30\x36\x34\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28" 353 | b"\x53\x45\x43\x29\x2C\x41\x0A\x34\x30\x36\x35\x30\x20\x20\x20\x20" 354 | b"\x20\x20\x20\x49\x4E\x43\x20\x48\x4C\x0A\x34\x30\x36\x36\x30\x20" 355 | b"\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x41\x2C\x28\x48\x4C\x29\x0A" 356 | b"\x34\x30\x36\x37\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x28" 357 | b"\x54\x52\x43\x29\x2C\x41\x0A\x34\x30\x36\x38\x30\x20\x20\x20\x20" 358 | b"\x20\x20\x20\x4A\x50\x20\x4C\x4F\x41\x32\x0A\x34\x30\x36\x39\x30" 359 | b"\x20\x4E\x45\x58\x54\x46\x20\x4C\x44\x20\x44\x45\x2C\x28\x50\x44" 360 | b"\x45\x29\x0A\x34\x30\x37\x30\x30\x20\x20\x20\x20\x20\x20\x20\x4C" 361 | b"\x44\x20\x48\x4C\x2C\x31\x36\x0A\x34\x30\x37\x31\x30\x20\x20\x20" 362 | b"\x20\x20\x20\x20\x41\x44\x44\x20\x48\x4C\x2C\x44\x45\x0A\x34\x30" 363 | b"\x37\x32\x30\x20\x20\x20\x20\x20\x20\x20\x45\x58\x20\x44\x45\x2C" 364 | b"\x48\x4C\x0A\x34\x30\x37\x33\x30\x20\x20\x20\x20\x20\x20\x20\x4C" 365 | b"\x44\x20\x42\x43\x2C\x28\x50\x42\x43\x29\x0A\x34\x30\x37\x34\x30" 366 | b"\x20\x20\x20\x20\x20\x20\x20\x44\x4A\x4E\x5A\x20\x46\x46\x32\x0A" 367 | b"\x34\x30\x37\x35\x30\x20\x4E\x46\x49\x4C\x45\x20\x43\x41\x4C\x4C" 368 | b"\x20\x4E\x45\x57\x53\x48\x0A\x34\x30\x37\x36\x30\x20\x20\x20\x20" 369 | b"\x20\x20\x20\x4C\x44\x20\x44\x45\x2C\x4D\x53\x47\x32\x0A\x34\x30" 370 | b"\x37\x37\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42\x43\x2C" 371 | b"\x4D\x53\x47\x33\x2D\x4D\x53\x47\x32\x0A\x34\x30\x37\x38\x30\x20" 372 | b"\x20\x20\x20\x20\x20\x20\x43\x41\x4C\x4C\x20\x23\x32\x30\x33\x43" 373 | b"\x0A\x34\x30\x37\x38\x31\x20\x20\x20\x20\x20\x20\x20\x43\x41\x4C" 374 | b"\x4C\x20\x42\x45\x45\x50\x0A\x34\x30\x37\x38\x32\x20\x20\x20\x20" 375 | b"\x20\x20\x20\x4C\x44\x20\x44\x45\x2C\x4D\x53\x47\x33\x0A\x34\x30" 376 | b"\x37\x38\x33\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42\x43\x2C" 377 | b"\x54\x52\x43\x2D\x4D\x53\x47\x33\x0A\x34\x30\x37\x38\x34\x20\x20" 378 | b"\x20\x20\x20\x20\x20\x43\x41\x4C\x4C\x20\x23\x32\x30\x33\x43\x0A" 379 | b"\x34\x30\x38\x30\x30\x20\x43\x41\x4C\x4C\x20\x50\x41\x55\x53\x3A" 380 | b"\x43\x41\x4C\x4C\x20\x53\x54\x41\x4E\x44\x0A\x34\x30\x38\x31\x30" 381 | b"\x20\x20\x20\x20\x20\x20\x20\x4A\x50\x20\x4C\x4F\x41\x44\x0A\x34" 382 | b"\x30\x38\x32\x30\x20\x50\x41\x55\x53\x20\x20\x58\x4F\x52\x20\x41" 383 | b"\x0A\x34\x30\x38\x33\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20" 384 | b"\x28\x32\x33\x35\x36\x30\x29\x2C\x41\x0A\x34\x30\x38\x34\x30\x20" 385 | b"\x50\x41\x32\x20\x20\x20\x4C\x44\x20\x41\x2C\x28\x32\x33\x35\x36" 386 | b"\x30\x29\x0A\x34\x30\x38\x35\x30\x20\x20\x20\x20\x20\x20\x20\x43" 387 | b"\x50\x20\x30\x0A\x34\x30\x38\x36\x30\x20\x20\x20\x20\x20\x20\x20" 388 | b"\x52\x45\x54\x20\x4E\x5A\x0A\x34\x30\x38\x37\x30\x20\x20\x20\x20" 389 | b"\x20\x20\x20\x4A\x52\x20\x50\x41\x32\x0A\x34\x30\x38\x38\x30\x20" 390 | b"\x42\x45\x45\x50\x20\x20\x4C\x44\x20\x44\x45\x2C\x23\x30\x31\x30" 391 | b"\x35\x0A\x34\x30\x38\x39\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44" 392 | b"\x20\x48\x4C\x2C\x23\x30\x36\x36\x36\x0A\x34\x30\x39\x30\x30\x20" 393 | b"\x20\x20\x20\x20\x20\x20\x43\x41\x4C\x4C\x20\x23\x30\x33\x42\x35" 394 | b"\x0A\x34\x30\x39\x31\x30\x20\x20\x20\x20\x20\x20\x20\x52\x45\x54" 395 | b"\x0A\x34\x30\x39\x32\x30\x20\x4C\x4F\x41\x44\x20\x20\x4C\x44\x20" 396 | b"\x48\x4C\x2C\x32\x32\x35\x36\x30\x0A\x34\x30\x39\x33\x30\x20\x20" 397 | b"\x20\x20\x20\x20\x20\x4C\x44\x20\x44\x45\x2C\x32\x32\x35\x36\x31" 398 | b"\x0A\x34\x30\x39\x34\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20" 399 | b"\x42\x43\x2C\x37\x33\x36\x0A\x34\x30\x39\x35\x30\x20\x20\x20\x20" 400 | b"\x20\x20\x20\x4C\x44\x20\x28\x48\x4C\x29\x2C\x30\x0A\x34\x30\x39" 401 | b"\x36\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x49\x52\x0A\x34\x30" 402 | b"\x39\x37\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x42\x43\x2C" 403 | b"\x23\x30\x39\x30\x35\x0A\x34\x30\x39\x38\x30\x20\x20\x20\x20\x20" 404 | b"\x20\x20\x4C\x44\x20\x44\x45\x2C\x30\x0A\x34\x30\x39\x39\x30\x20" 405 | b"\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x48\x4C\x2C\x31\x38\x34\x33" 406 | b"\x32\x0A\x34\x31\x30\x30\x30\x20\x20\x20\x20\x20\x20\x20\x43\x41" 407 | b"\x4C\x4C\x20\x31\x35\x36\x33\x35\x0A\x34\x31\x30\x31\x30\x20\x20" 408 | b"\x20\x20\x20\x20\x20\x4A\x50\x20\x46\x46\x49\x4C\x45\x0A\x34\x31" 409 | b"\x30\x32\x30\x20\x4C\x4F\x41\x32\x20\x20\x4C\x44\x20\x41\x2C\x28" 410 | b"\x53\x45\x43\x29\x0A\x34\x31\x30\x33\x30\x20\x20\x20\x20\x20\x20" 411 | b"\x20\x4C\x44\x20\x45\x2C\x41\x0A\x34\x31\x30\x34\x30\x20\x20\x20" 412 | b"\x20\x20\x20\x20\x4C\x44\x20\x41\x2C\x28\x54\x52\x43\x29\x0A\x34" 413 | b"\x31\x30\x35\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x44\x2C" 414 | b"\x41\x0A\x34\x31\x30\x36\x30\x20\x20\x20\x20\x20\x20\x20\x4C\x44" 415 | b"\x20\x42\x43\x2C\x23\x32\x36\x30\x35\x0A\x34\x31\x30\x37\x30\x20" 416 | b"\x20\x20\x20\x20\x20\x20\x4C\x44\x20\x48\x4C\x2C\x33\x30\x30\x30" 417 | b"\x30\x0A\x34\x31\x30\x38\x30\x20\x20\x20\x20\x20\x20\x20\x43\x41" 418 | b"\x4C\x4C\x20\x31\x35\x36\x33\x35\x0A\x34\x31\x30\x39\x30\x20\x20" 419 | b"\x20\x20\x20\x20\x20\x4A\x50\x20\x33\x31\x36\x39\x33\x0A\x36\x30" 420 | b"\x30\x30\x30\x20\x41\x44\x52\x53\x48\x20\x45\x51\x55\x20\x33\x30" 421 | b"\x30\x30\x30\x0A\x36\x30\x30\x31\x30\x20\x4D\x53\x47\x31\x20\x20" 422 | b"\x44\x45\x46\x42\x20\x32\x32\x2C\x30\x2C\x30\x2C\x31\x37\x2C\x30" 423 | b"\x0A\x36\x30\x30\x32\x30\x20\x20\x20\x20\x20\x20\x20\x44\x45\x46" 424 | b"\x42\x20\x31\x36\x2C\x37\x0A\x36\x30\x30\x33\x30\x20\x44\x45\x46" 425 | b"\x4D\x20\x22\x46\x4F\x4E\x54\x20\x45\x44\x49\x54\x4F\x52\x20\x62" 426 | b"\x79\x20\x4C\x79\x61\x22\x0A\x36\x30\x30\x34\x30\x20\x44\x45\x46" 427 | b"\x4D\x20\x22\x64\x76\x69\x6E\x73\x6B\x79\x20\x4B\x69\x72\x69\x6C" 428 | b"\x6C\x22\x0A\x36\x30\x30\x35\x30\x20\x46\x4E\x41\x4D\x45\x20\x44" 429 | b"\x45\x46\x4D\x20\x22\x65\x64\x69\x74\x6F\x72\x20\x20\x22\x0A\x36" 430 | b"\x30\x30\x36\x30\x20\x50\x44\x45\x20\x20\x20\x44\x45\x46\x57\x20" 431 | b"\x30\x0A\x36\x30\x30\x37\x30\x20\x50\x42\x43\x20\x20\x20\x44\x45" 432 | b"\x46\x57\x20\x30\x0A\x36\x30\x30\x38\x30\x20\x4D\x53\x47\x32\x20" 433 | b"\x20\x44\x45\x46\x42\x20\x32\x32\x2C\x35\x2C\x30\x2C\x31\x37\x2C" 434 | b"\x30\x0A\x36\x30\x30\x39\x30\x20\x44\x45\x46\x42\x20\x31\x36\x2C" 435 | b"\x37\x2C\x31\x39\x2C\x31\x0A\x36\x30\x31\x30\x30\x20\x44\x45\x46" 436 | b"\x4D\x20\x22\x46\x69\x6C\x65\x20\x27\x65\x64\x69\x74\x6F\x72\x20" 437 | b"\x20\x3C\x45\x3E\x27\x22\x0A\x36\x30\x31\x31\x30\x20\x44\x45\x46" 438 | b"\x4D\x20\x22\x20\x6E\x6F\x74\x20\x66\x6F\x75\x6E\x64\x22\x0A\x36" 439 | b"\x30\x31\x32\x30\x20\x4D\x53\x47\x33\x20\x20\x44\x45\x46\x42\x20" 440 | b"\x32\x32\x2C\x36\x2C\x30\x2C\x31\x37\x2C\x30\x0A\x36\x30\x31\x32" 441 | b"\x35\x20\x20\x20\x20\x20\x20\x20\x44\x45\x46\x42\x20\x31\x36\x2C" 442 | b"\x37\x2C\x31\x39\x2C\x30\x0A\x36\x30\x31\x33\x30\x20\x44\x45\x46" 443 | b"\x4D\x20\x22\x50\x52\x45\x53\x53\x20\x41\x4E\x59\x20\x4B\x45\x59" 444 | b"\x20\x46\x4F\x52\x20\x22\x0A\x36\x30\x31\x34\x30\x20\x44\x45\x46" 445 | b"\x4D\x20\x22\x52\x45\x4C\x4F\x41\x44\x20\x46\x49\x4C\x45\x22\x0A" 446 | b"\x36\x30\x31\x36\x30\x20\x54\x52\x43\x20\x20\x20\x44\x45\x46\x42" 447 | b"\x20\x30\x0A\x36\x30\x31\x37\x30\x20\x53\x45\x43\x20\x20\x20\x44" 448 | b"\x45\x46\x42\x20\x30\x0A\x36\x30\x31\x38\x30\x20\x45\x4E\x44\x32" 449 | b"\x20\x20\x4E\x4F\x50\x0A" 450 | ) 451 | 452 | 453 | if __name__ == '__main__': 454 | unittest.main() 455 | --------------------------------------------------------------------------------