oaknut-romfs 12.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oaknut_romfs-12.3.0/LICENSE +21 -0
- oaknut_romfs-12.3.0/PKG-INFO +67 -0
- oaknut_romfs-12.3.0/README.md +37 -0
- oaknut_romfs-12.3.0/pyproject.toml +59 -0
- oaknut_romfs-12.3.0/setup.cfg +4 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/__init__.py +46 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/block.py +142 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/cli.py +76 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/crc.py +34 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/exceptions.py +49 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/filesystem.py +299 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/handler.py +237 -0
- oaknut_romfs-12.3.0/src/oaknut/romfs/romfs.py +643 -0
- oaknut_romfs-12.3.0/src/oaknut_romfs.egg-info/PKG-INFO +67 -0
- oaknut_romfs-12.3.0/src/oaknut_romfs.egg-info/SOURCES.txt +26 -0
- oaknut_romfs-12.3.0/src/oaknut_romfs.egg-info/dependency_links.txt +1 -0
- oaknut_romfs-12.3.0/src/oaknut_romfs.egg-info/entry_points.txt +5 -0
- oaknut_romfs-12.3.0/src/oaknut_romfs.egg-info/requires.txt +7 -0
- oaknut_romfs-12.3.0/src/oaknut_romfs.egg-info/top_level.txt +1 -0
- oaknut_romfs-12.3.0/tests/test_block.py +106 -0
- oaknut_romfs-12.3.0/tests/test_crc.py +55 -0
- oaknut_romfs-12.3.0/tests/test_create.py +78 -0
- oaknut_romfs-12.3.0/tests/test_filesystem.py +196 -0
- oaknut_romfs-12.3.0/tests/test_handler.py +42 -0
- oaknut_romfs-12.3.0/tests/test_header_edit.py +89 -0
- oaknut_romfs-12.3.0/tests/test_package.py +20 -0
- oaknut_romfs-12.3.0/tests/test_romfs.py +140 -0
- oaknut_romfs-12.3.0/tests/test_write.py +76 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Smallshire
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oaknut-romfs
|
|
3
|
+
Version: 12.3.0
|
|
4
|
+
Summary: Python library for Acorn ROM Filing System (ROMFS) paged-ROM images
|
|
5
|
+
Author-email: Robert Smallshire <robert@smallshire.org.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-romfs
|
|
8
|
+
Project-URL: Documentation, https://rob-smallshire.github.io/oaknut/disc/api/reference/romfs.html
|
|
9
|
+
Project-URL: Repository, https://github.com/rob-smallshire/oaknut
|
|
10
|
+
Project-URL: Issues, https://github.com/rob-smallshire/oaknut/issues
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: oaknut-file>=10.0
|
|
24
|
+
Requires-Dist: oaknut-discimage>=10.0
|
|
25
|
+
Requires-Dist: oaknut-filesystem>=10.0
|
|
26
|
+
Requires-Dist: oaknut-basic>=10.0
|
|
27
|
+
Provides-Extra: cli
|
|
28
|
+
Requires-Dist: oaknut-cli>=10.0; extra == "cli"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# oaknut-romfs
|
|
32
|
+
|
|
33
|
+
Acorn ROM Filing System (ROMFS) support for the [oaknut](https://github.com/rob-smallshire/oaknut)
|
|
34
|
+
family of packages.
|
|
35
|
+
|
|
36
|
+
ROMFS is the filing system for paged ROMs on the BBC Micro and Acorn
|
|
37
|
+
Electron — sideways ROMs and cartridges. It stores files in the same
|
|
38
|
+
block layout as the Cassette Filing System (CFS), with the ROM image
|
|
39
|
+
standing in for the tape: each file is a chain of CFS-format blocks
|
|
40
|
+
carrying Acorn load and execution addresses, a block number, a length,
|
|
41
|
+
a flag byte, and header and data CRCs, introduced by a standard
|
|
42
|
+
paged-ROM service header.
|
|
43
|
+
|
|
44
|
+
The medium is read-only ROM, so the filing system is flat: there are no
|
|
45
|
+
directories, and a file's metadata is the load/exec pair plus a lock
|
|
46
|
+
bit, exactly as on cassette.
|
|
47
|
+
|
|
48
|
+
This package contributes ROMFS to the `oaknut.filesystem` extension axis,
|
|
49
|
+
so ROMFS images are identified, listed and read through the `disc` CLI
|
|
50
|
+
alongside the disc-based filing systems.
|
|
51
|
+
|
|
52
|
+
## Status
|
|
53
|
+
|
|
54
|
+
Pre-alpha. The package is being built up format-first: see
|
|
55
|
+
[`docs/romfs-format-spec.md`](docs/romfs-format-spec.md) for the on-ROM
|
|
56
|
+
byte layout and [`docs/architecture.md`](docs/architecture.md) for the
|
|
57
|
+
package design and its mapping onto the `oaknut.filesystem` contract.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
uv add oaknut-romfs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Licence
|
|
66
|
+
|
|
67
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# oaknut-romfs
|
|
2
|
+
|
|
3
|
+
Acorn ROM Filing System (ROMFS) support for the [oaknut](https://github.com/rob-smallshire/oaknut)
|
|
4
|
+
family of packages.
|
|
5
|
+
|
|
6
|
+
ROMFS is the filing system for paged ROMs on the BBC Micro and Acorn
|
|
7
|
+
Electron — sideways ROMs and cartridges. It stores files in the same
|
|
8
|
+
block layout as the Cassette Filing System (CFS), with the ROM image
|
|
9
|
+
standing in for the tape: each file is a chain of CFS-format blocks
|
|
10
|
+
carrying Acorn load and execution addresses, a block number, a length,
|
|
11
|
+
a flag byte, and header and data CRCs, introduced by a standard
|
|
12
|
+
paged-ROM service header.
|
|
13
|
+
|
|
14
|
+
The medium is read-only ROM, so the filing system is flat: there are no
|
|
15
|
+
directories, and a file's metadata is the load/exec pair plus a lock
|
|
16
|
+
bit, exactly as on cassette.
|
|
17
|
+
|
|
18
|
+
This package contributes ROMFS to the `oaknut.filesystem` extension axis,
|
|
19
|
+
so ROMFS images are identified, listed and read through the `disc` CLI
|
|
20
|
+
alongside the disc-based filing systems.
|
|
21
|
+
|
|
22
|
+
## Status
|
|
23
|
+
|
|
24
|
+
Pre-alpha. The package is being built up format-first: see
|
|
25
|
+
[`docs/romfs-format-spec.md`](docs/romfs-format-spec.md) for the on-ROM
|
|
26
|
+
byte layout and [`docs/architecture.md`](docs/architecture.md) for the
|
|
27
|
+
package design and its mapping onto the `oaknut.filesystem` contract.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
uv add oaknut-romfs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Licence
|
|
36
|
+
|
|
37
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oaknut-romfs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [{ name = "Robert Smallshire", email = "robert@smallshire.org.uk" }]
|
|
9
|
+
description = "Python library for Acorn ROM Filing System (ROMFS) paged-ROM images"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"oaknut-file>=10.0",
|
|
27
|
+
"oaknut-discimage>=10.0",
|
|
28
|
+
"oaknut-filesystem>=10.0",
|
|
29
|
+
"oaknut-basic>=10.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# The `cli` extra pulls the shared CLI toolkit so this package can
|
|
33
|
+
# contribute `disc romfs` subcommands. The library core never imports it,
|
|
34
|
+
# so a bare install stays CLI-free.
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
cli = ["oaknut-cli>=10.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-romfs"
|
|
40
|
+
Documentation = "https://rob-smallshire.github.io/oaknut/disc/api/reference/romfs.html"
|
|
41
|
+
Repository = "https://github.com/rob-smallshire/oaknut"
|
|
42
|
+
Issues = "https://github.com/rob-smallshire/oaknut/issues"
|
|
43
|
+
|
|
44
|
+
# Filesystem contributed to the oaknut.filesystem axis — discovered at
|
|
45
|
+
# runtime via this entry point, so the detector ships with the format it
|
|
46
|
+
# detects.
|
|
47
|
+
[project.entry-points."oaknut.filesystem"]
|
|
48
|
+
acorn-romfs = "oaknut.romfs.filesystem:AcornROMFS"
|
|
49
|
+
|
|
50
|
+
# Admin subcommands contributed to the `disc` CLI on the oaknut.command axis:
|
|
51
|
+
# `disc romfs <subcommand>`. Loaded only with the [cli] extra.
|
|
52
|
+
[project.entry-points."oaknut.command"]
|
|
53
|
+
romfs = "oaknut.romfs.cli:romfs"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.dynamic]
|
|
56
|
+
version = { attr = "oaknut.romfs.__version__" }
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = ["src"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Acorn ROM Filing System (ROMFS).
|
|
2
|
+
|
|
3
|
+
ROMFS is Acorn's filing system for *paged ROMs* — the sideways ROM and
|
|
4
|
+
cartridge format used on the BBC Micro and Acorn Electron. It stores
|
|
5
|
+
files in the same block layout as the Cassette Filing System (CFS): the
|
|
6
|
+
backing store is simply a linear ROM image rather than a tape, so a
|
|
7
|
+
file is a chain of CFS-format blocks (load/exec addresses, block number,
|
|
8
|
+
length, flag byte, header and data CRCs) preceded by a standard paged-ROM
|
|
9
|
+
service header.
|
|
10
|
+
|
|
11
|
+
Because the medium is read-only ROM the filing system is flat — there are
|
|
12
|
+
no directories — and a file's metadata is the Acorn load/exec pair plus a
|
|
13
|
+
lock bit, exactly as on cassette. This package adapts that on-ROM format
|
|
14
|
+
to the :mod:`oaknut.filesystem` extension contract so ROMFS images are
|
|
15
|
+
identifiable, listable and readable through the ``disc`` CLI alongside the
|
|
16
|
+
disc-based filing systems.
|
|
17
|
+
|
|
18
|
+
See ``docs/romfs-format-spec.md`` for the on-ROM byte layout and
|
|
19
|
+
``docs/architecture.md`` for how the native API maps onto the
|
|
20
|
+
``oaknut.filesystem`` plug-in interface.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from oaknut.romfs.crc import crc16_ccitt
|
|
26
|
+
from oaknut.romfs.exceptions import (
|
|
27
|
+
CRCError,
|
|
28
|
+
NotAROMFSError,
|
|
29
|
+
ROMFSError,
|
|
30
|
+
ROMFullError,
|
|
31
|
+
TruncatedROMError,
|
|
32
|
+
)
|
|
33
|
+
from oaknut.romfs.romfs import ROMFS, ROMFSFile
|
|
34
|
+
|
|
35
|
+
__version__ = "12.3.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ROMFS",
|
|
39
|
+
"ROMFSFile",
|
|
40
|
+
"ROMFSError",
|
|
41
|
+
"NotAROMFSError",
|
|
42
|
+
"CRCError",
|
|
43
|
+
"TruncatedROMError",
|
|
44
|
+
"ROMFullError",
|
|
45
|
+
"crc16_ccitt",
|
|
46
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""The CFS block-header codec — parse and serialise one ROMFS block.
|
|
2
|
+
|
|
3
|
+
A ROMFS file is a chain of Cassette Filing System blocks. A *header
|
|
4
|
+
block* begins with the sync byte ``&2A`` followed by the fields modelled
|
|
5
|
+
by :class:`BlockHeader`; *continuation* blocks within a multi-block file
|
|
6
|
+
use a single ``&23`` marker instead of a header and are implicitly 256
|
|
7
|
+
data bytes. The filing system ends with a single ``&2B`` byte.
|
|
8
|
+
|
|
9
|
+
This module owns the byte-level header format (field layout, flag bits,
|
|
10
|
+
CRC placement). Walking a chain into files and assembling files into a
|
|
11
|
+
chain lives one layer up, in :mod:`oaknut.romfs.romfs`. See
|
|
12
|
+
``docs/romfs-format-spec.md`` §2.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from oaknut.romfs.crc import crc16_ccitt
|
|
21
|
+
from oaknut.romfs.exceptions import CRCError, TruncatedROMError
|
|
22
|
+
|
|
23
|
+
#: A header block starts with this synchronisation byte (``'*'``).
|
|
24
|
+
SYNC_BYTE = 0x2A
|
|
25
|
+
#: A continuation (data-only) block within a multi-block file (``'#'``).
|
|
26
|
+
INTER_BLOCK_MARKER = 0x23
|
|
27
|
+
#: A single byte marking the end of the filing-system data (``'+'``).
|
|
28
|
+
END_OF_FILESYSTEM = 0x2B
|
|
29
|
+
|
|
30
|
+
#: Flag bit: this is the last block of its file.
|
|
31
|
+
FLAG_LAST = 0x80
|
|
32
|
+
#: Flag bit: this block carries no data (mkromfs sets it; Acorn does not —
|
|
33
|
+
#: test ``block_length == 0`` rather than this bit to detect an empty file).
|
|
34
|
+
FLAG_EMPTY = 0x40
|
|
35
|
+
#: Flag bit: the file is *RUN-only — a form of copy protection. The MOS
|
|
36
|
+
#: refuses to ``*LOAD`` / ``*EXEC`` / ``CHAIN`` it (only ``*RUN``); the OS
|
|
37
|
+
#: calls this "locked", but it is read-protection, not the disc filing
|
|
38
|
+
#: systems' delete-lock (see ``oaknut.file.Access.X``).
|
|
39
|
+
FLAG_RUN_ONLY = 0x01
|
|
40
|
+
|
|
41
|
+
#: Maximum file-name length (characters), excluding the NUL terminator.
|
|
42
|
+
MAX_NAME_LENGTH = 10
|
|
43
|
+
#: A full block carries this many data bytes; a continuation block always does.
|
|
44
|
+
BLOCK_DATA_SIZE = 256
|
|
45
|
+
|
|
46
|
+
# Fixed-size part of the header following the NUL-terminated name:
|
|
47
|
+
# load (4), exec (4), block number (2), block length (2), flag (1),
|
|
48
|
+
# end-of-file address (4). The two-byte header CRC follows.
|
|
49
|
+
_FIXED = struct.Struct("<IIHHBI")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class BlockHeader:
|
|
54
|
+
"""The header of one CFS block, in decoded form.
|
|
55
|
+
|
|
56
|
+
The multi-byte integer fields are stored little-endian on the ROM; the
|
|
57
|
+
trailing header CRC (handled by :meth:`to_bytes` / :meth:`parse`) is
|
|
58
|
+
stored big-endian. *end_address* is the paged-ROM address of the byte
|
|
59
|
+
just past this file — i.e. where the next file begins.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
load_address: int
|
|
64
|
+
exec_address: int
|
|
65
|
+
block_number: int
|
|
66
|
+
block_length: int
|
|
67
|
+
flag: int
|
|
68
|
+
end_address: int
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_last(self) -> bool:
|
|
72
|
+
"""Whether this is the final block of its file (flag bit 7)."""
|
|
73
|
+
return bool(self.flag & FLAG_LAST)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_empty(self) -> bool:
|
|
77
|
+
"""Whether the block carries no data (length 0)."""
|
|
78
|
+
return self.block_length == 0
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def is_run_only(self) -> bool:
|
|
82
|
+
"""Whether the file is *RUN-only / copy-protected (flag bit 0)."""
|
|
83
|
+
return bool(self.flag & FLAG_RUN_ONLY)
|
|
84
|
+
|
|
85
|
+
def field_bytes(self) -> bytes:
|
|
86
|
+
"""The header bytes the CRC is taken over: name through end address.
|
|
87
|
+
|
|
88
|
+
This is everything after the sync byte and before the header CRC.
|
|
89
|
+
"""
|
|
90
|
+
return (
|
|
91
|
+
self.name.encode("latin-1")
|
|
92
|
+
+ b"\x00"
|
|
93
|
+
+ _FIXED.pack(
|
|
94
|
+
self.load_address,
|
|
95
|
+
self.exec_address,
|
|
96
|
+
self.block_number,
|
|
97
|
+
self.block_length,
|
|
98
|
+
self.flag,
|
|
99
|
+
self.end_address,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def to_bytes(self) -> bytes:
|
|
104
|
+
"""The full on-ROM header: sync byte, fields, big-endian header CRC."""
|
|
105
|
+
fields = self.field_bytes()
|
|
106
|
+
crc = crc16_ccitt(fields)
|
|
107
|
+
return bytes([SYNC_BYTE]) + fields + crc.to_bytes(2, "big")
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def parse(cls, buf, offset: int = 0) -> tuple["BlockHeader", int]:
|
|
111
|
+
"""Parse a header block at *offset* (which must hold the sync byte).
|
|
112
|
+
|
|
113
|
+
Returns ``(header, data_offset)`` where *data_offset* is the offset
|
|
114
|
+
of the block's data, just past the two-byte header CRC. Raises
|
|
115
|
+
:class:`CRCError` if the stored header CRC does not match, and
|
|
116
|
+
:class:`TruncatedROMError` if the buffer ends mid-header.
|
|
117
|
+
"""
|
|
118
|
+
view = memoryview(buf)
|
|
119
|
+
if offset >= len(view) or view[offset] != SYNC_BYTE:
|
|
120
|
+
raise TruncatedROMError(f"no block sync byte at offset {offset}")
|
|
121
|
+
name_start = offset + 1
|
|
122
|
+
nul = buf.find(b"\x00", name_start)
|
|
123
|
+
if nul < 0:
|
|
124
|
+
raise TruncatedROMError("unterminated block-header name")
|
|
125
|
+
fixed_start = nul + 1
|
|
126
|
+
crc_start = fixed_start + _FIXED.size
|
|
127
|
+
crc_end = crc_start + 2
|
|
128
|
+
if crc_end > len(view):
|
|
129
|
+
raise TruncatedROMError("block header runs past the end of the ROM")
|
|
130
|
+
name = bytes(view[name_start:nul]).decode("latin-1")
|
|
131
|
+
load, execa, block_number, block_length, flag, end_address = _FIXED.unpack(
|
|
132
|
+
view[fixed_start:crc_start]
|
|
133
|
+
)
|
|
134
|
+
stored_crc = int.from_bytes(view[crc_start:crc_end], "big")
|
|
135
|
+
computed = crc16_ccitt(view[name_start:crc_start])
|
|
136
|
+
if stored_crc != computed:
|
|
137
|
+
raise CRCError(
|
|
138
|
+
f"header CRC mismatch for {name!r}: "
|
|
139
|
+
f"stored &{stored_crc:04X}, computed &{computed:04X}"
|
|
140
|
+
)
|
|
141
|
+
header = cls(name, load, execa, block_number, block_length, flag, end_address)
|
|
142
|
+
return header, crc_end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""ROMFS contributed ``disc`` commands.
|
|
2
|
+
|
|
3
|
+
The ``disc romfs`` command group, contributed to the CLI on the
|
|
4
|
+
``oaknut.command`` axis (see ``docs/dev/contributed-commands.md``). It holds
|
|
5
|
+
the ROMFS-specific paged-ROM *header* properties that do not fit the generic
|
|
6
|
+
mount model — the copyright string and the binary version byte — as simple
|
|
7
|
+
getters and setters on an existing image. Creating an image is the generic
|
|
8
|
+
``disc create``; this group only queries and tweaks header metadata.
|
|
9
|
+
|
|
10
|
+
This module imports Click and is loaded only when ``oaknut-romfs`` is
|
|
11
|
+
installed with its ``[cli]`` extra; the library core never imports it.
|
|
12
|
+
Errors raised as :class:`~oaknut.romfs.exceptions.ROMFSError` are reported
|
|
13
|
+
by the CLI's shared error boundary.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
_IMAGE = click.argument("image", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
def romfs() -> None:
|
|
27
|
+
"""Acorn ROM Filing System administration."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@romfs.command(name="get-copyright")
|
|
31
|
+
@_IMAGE
|
|
32
|
+
def get_copyright_command(image: Path) -> None:
|
|
33
|
+
"""Print the paged-ROM copyright string of IMAGE."""
|
|
34
|
+
from oaknut.romfs.romfs import get_copyright
|
|
35
|
+
|
|
36
|
+
click.echo(get_copyright(image.read_bytes()))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@romfs.command(name="set-copyright")
|
|
40
|
+
@_IMAGE
|
|
41
|
+
@click.argument("copyright")
|
|
42
|
+
def set_copyright_command(image: Path, copyright: str) -> None:
|
|
43
|
+
"""Set the paged-ROM copyright string of IMAGE (must begin "(C)").
|
|
44
|
+
|
|
45
|
+
A same-length string is written in place. A different length moves the
|
|
46
|
+
service handler, so the ROM is rebuilt — done only for a created-style
|
|
47
|
+
ROM (no language entry, nothing after the filing system); other ROMs are
|
|
48
|
+
refused to avoid disturbing their code.
|
|
49
|
+
"""
|
|
50
|
+
from oaknut.romfs.romfs import set_copyright
|
|
51
|
+
|
|
52
|
+
image.write_bytes(set_copyright(image.read_bytes(), copyright))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@romfs.command(name="get-version")
|
|
56
|
+
@_IMAGE
|
|
57
|
+
def get_version_command(image: Path) -> None:
|
|
58
|
+
"""Print the paged-ROM binary version byte of IMAGE."""
|
|
59
|
+
from oaknut.romfs.romfs import get_version
|
|
60
|
+
|
|
61
|
+
click.echo(get_version(image.read_bytes()))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@romfs.command(name="set-version")
|
|
65
|
+
@_IMAGE
|
|
66
|
+
@click.argument("version")
|
|
67
|
+
def set_version_command(image: Path, version: str) -> None:
|
|
68
|
+
"""Set the paged-ROM binary version byte of IMAGE (0-255).
|
|
69
|
+
|
|
70
|
+
VERSION honours a base prefix, like the address commands: ``0x`` hex
|
|
71
|
+
(e.g. ``0x80``), ``0o`` octal, ``0b`` binary, or a plain decimal value.
|
|
72
|
+
"""
|
|
73
|
+
from oaknut.file import parse_address
|
|
74
|
+
from oaknut.romfs.romfs import set_version
|
|
75
|
+
|
|
76
|
+
image.write_bytes(set_version(image.read_bytes(), parse_address(version)))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""The CRC used by ROMFS / Cassette Filing System blocks.
|
|
2
|
+
|
|
3
|
+
Both the block header CRC and the block data CRC are CRC-16/XMODEM:
|
|
4
|
+
polynomial ``0x1021``, initial value ``0x0000``, processed
|
|
5
|
+
most-significant-bit first with no input/output reflection. On the ROM the
|
|
6
|
+
16-bit result is stored **big-endian** (most-significant byte first),
|
|
7
|
+
unlike the little-endian multi-byte integer fields of the header.
|
|
8
|
+
|
|
9
|
+
See ``docs/romfs-format-spec.md`` §3.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
|
|
16
|
+
_POLYNOMIAL = 0x1021
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def crc16_ccitt(data: Iterable[int]) -> int:
|
|
20
|
+
"""Return the CRC-16/XMODEM of *data* (any iterable of byte values).
|
|
21
|
+
|
|
22
|
+
Accepts ``bytes``, ``bytearray``, ``memoryview`` or any iterable of
|
|
23
|
+
integers in ``0..255``. The empty input has CRC ``0x0000``; the
|
|
24
|
+
canonical check string ``b"123456789"`` has CRC ``0x31C3``.
|
|
25
|
+
"""
|
|
26
|
+
crc = 0
|
|
27
|
+
for byte in data:
|
|
28
|
+
crc ^= byte << 8
|
|
29
|
+
for _ in range(8):
|
|
30
|
+
if crc & 0x8000:
|
|
31
|
+
crc = ((crc << 1) ^ _POLYNOMIAL) & 0xFFFF
|
|
32
|
+
else:
|
|
33
|
+
crc = (crc << 1) & 0xFFFF
|
|
34
|
+
return crc
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Exception hierarchy for oaknut.romfs.
|
|
2
|
+
|
|
3
|
+
All ROMFS-specific exceptions derive from :class:`ROMFSError`, which in
|
|
4
|
+
turn derives from the shared :class:`~oaknut.file.exceptions.FSError` base
|
|
5
|
+
(a ``DataError``). The CLI boundary reads an exception's ``exit_code`` to
|
|
6
|
+
decide its exit status, so each subclass pins a specific :class:`ExitCode`.
|
|
7
|
+
|
|
8
|
+
Hierarchy::
|
|
9
|
+
|
|
10
|
+
FSError (oaknut.file.exceptions, a DataError)
|
|
11
|
+
└── ROMFSError
|
|
12
|
+
├── NotAROMFSError (ExitCode.DATA_ERR)
|
|
13
|
+
├── CRCError (ExitCode.DATA_ERR)
|
|
14
|
+
├── TruncatedROMError (ExitCode.DATA_ERR)
|
|
15
|
+
└── ROMFullError (ExitCode.CANT_CREATE)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from exit_codes import ExitCode
|
|
21
|
+
from oaknut.file.exceptions import FSError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ROMFSError(FSError):
|
|
25
|
+
"""Base exception for all ROMFS errors."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NotAROMFSError(ROMFSError):
|
|
29
|
+
"""The image is not a recognisable ROMFS paged ROM."""
|
|
30
|
+
|
|
31
|
+
_exit_code = ExitCode.DATA_ERR
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CRCError(ROMFSError):
|
|
35
|
+
"""A stored header or data CRC does not match the computed value."""
|
|
36
|
+
|
|
37
|
+
_exit_code = ExitCode.DATA_ERR
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TruncatedROMError(ROMFSError):
|
|
41
|
+
"""The ROM ends in the middle of a block header or its data."""
|
|
42
|
+
|
|
43
|
+
_exit_code = ExitCode.DATA_ERR
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ROMFullError(ROMFSError):
|
|
47
|
+
"""The files no longer fit within the ROM's capacity."""
|
|
48
|
+
|
|
49
|
+
_exit_code = ExitCode.CANT_CREATE
|