oaknut-hfe 13.6.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_hfe-13.6.0/LICENSE +21 -0
- oaknut_hfe-13.6.0/PKG-INFO +94 -0
- oaknut_hfe-13.6.0/README.md +65 -0
- oaknut_hfe-13.6.0/pyproject.toml +55 -0
- oaknut_hfe-13.6.0/setup.cfg +4 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/__init__.py +89 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/build.py +194 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/cli.py +243 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/container.py +348 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/decode.py +322 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/decoder.py +58 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/exceptions.py +22 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/flat.py +157 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/ibm.py +129 -0
- oaknut_hfe-13.6.0/src/oaknut/hfe/report.py +122 -0
- oaknut_hfe-13.6.0/src/oaknut_hfe.egg-info/PKG-INFO +94 -0
- oaknut_hfe-13.6.0/src/oaknut_hfe.egg-info/SOURCES.txt +26 -0
- oaknut_hfe-13.6.0/src/oaknut_hfe.egg-info/dependency_links.txt +1 -0
- oaknut_hfe-13.6.0/src/oaknut_hfe.egg-info/entry_points.txt +5 -0
- oaknut_hfe-13.6.0/src/oaknut_hfe.egg-info/requires.txt +7 -0
- oaknut_hfe-13.6.0/src/oaknut_hfe.egg-info/top_level.txt +1 -0
- oaknut_hfe-13.6.0/tests/test_container.py +182 -0
- oaknut_hfe-13.6.0/tests/test_decode.py +222 -0
- oaknut_hfe-13.6.0/tests/test_decoder_integration.py +50 -0
- oaknut_hfe-13.6.0/tests/test_flat.py +161 -0
- oaknut_hfe-13.6.0/tests/test_ibm.py +75 -0
- oaknut_hfe-13.6.0/tests/test_package.py +14 -0
- oaknut_hfe-13.6.0/tests/test_report.py +109 -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,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oaknut-hfe
|
|
3
|
+
Version: 13.6.0
|
|
4
|
+
Summary: Python library for reading HFE (HxC Floppy Emulator) flux-level disc 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-hfe
|
|
8
|
+
Project-URL: Documentation, https://rob-smallshire.github.io/oaknut/disc/api/reference/hfe.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.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: oaknut-exception>=10.0
|
|
23
|
+
Requires-Dist: oaknut-extension>=10.0
|
|
24
|
+
Requires-Dist: oaknut-filesystem>=13.5
|
|
25
|
+
Provides-Extra: cli
|
|
26
|
+
Requires-Dist: oaknut-cli>=10.0; extra == "cli"
|
|
27
|
+
Requires-Dist: oaknut-file>=10.0; extra == "cli"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# oaknut-hfe
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/oaknut-hfe/)
|
|
33
|
+
[](https://github.com/rob-smallshire/oaknut/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/oaknut-hfe/)
|
|
35
|
+
[](https://github.com/rob-smallshire/oaknut/blob/master/packages/oaknut-hfe/LICENSE)
|
|
36
|
+
|
|
37
|
+
HFE (HxC Floppy Emulator) disc-image support for the
|
|
38
|
+
[oaknut](https://github.com/rob-smallshire/oaknut) family of packages.
|
|
39
|
+
|
|
40
|
+
An HFE file records each track of a floppy disc as the raw bitstream the
|
|
41
|
+
drive head delivers. It is the native format of the HxC and Gotek floppy
|
|
42
|
+
emulators and a common output of preservation capture hardware such as
|
|
43
|
+
the Greaseweazle. Because it sits below the sector level, an HFE image
|
|
44
|
+
keeps everything an original disc carries, including the non-standard
|
|
45
|
+
sector IDs, deliberate CRC errors, deleted-data marks and extra tracks
|
|
46
|
+
that copy protection relies on.
|
|
47
|
+
|
|
48
|
+
This package reads HFE v1 and v3 images, decodes their FM and MFM tracks
|
|
49
|
+
into sectors, and flattens a decoded disc into the logical sector image
|
|
50
|
+
that the oaknut filesystems read.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Every `disc` command that reads an image opens an HFE file as the sector
|
|
55
|
+
image decoded from it:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
disc ls 'Dunjunz_D1S1.hfe:$'
|
|
59
|
+
disc export Dunjunz_D1S1.hfe dunjunz/
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `disc hfe` commands look below the sector level:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
disc hfe info Dunjunz_D1S1.hfe # header and decoded layout
|
|
66
|
+
disc hfe sectors --anomalies Dunjunz_D1S1.hfe # what protection put there
|
|
67
|
+
disc hfe flatten Dunjunz_D1S1.hfe Dunjunz_D1S1.ssd
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
From Python:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from oaknut.hfe import HFEImage, analyse, decode_image, flatten
|
|
74
|
+
|
|
75
|
+
disc = decode_image(HFEImage.from_filepath("Dunjunz_D1S1.hfe"))
|
|
76
|
+
flat = flatten(disc) # flat.data is the .ssd image
|
|
77
|
+
for finding in analyse(disc, flat.layout):
|
|
78
|
+
if finding.anomalous:
|
|
79
|
+
print(finding.track, finding.side, finding.remarks)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Status
|
|
83
|
+
|
|
84
|
+
Pre-alpha, read-only.
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
uv add oaknut-hfe
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# oaknut-hfe
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/oaknut-hfe/)
|
|
4
|
+
[](https://github.com/rob-smallshire/oaknut/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pypi.org/project/oaknut-hfe/)
|
|
6
|
+
[](https://github.com/rob-smallshire/oaknut/blob/master/packages/oaknut-hfe/LICENSE)
|
|
7
|
+
|
|
8
|
+
HFE (HxC Floppy Emulator) disc-image support for the
|
|
9
|
+
[oaknut](https://github.com/rob-smallshire/oaknut) family of packages.
|
|
10
|
+
|
|
11
|
+
An HFE file records each track of a floppy disc as the raw bitstream the
|
|
12
|
+
drive head delivers. It is the native format of the HxC and Gotek floppy
|
|
13
|
+
emulators and a common output of preservation capture hardware such as
|
|
14
|
+
the Greaseweazle. Because it sits below the sector level, an HFE image
|
|
15
|
+
keeps everything an original disc carries, including the non-standard
|
|
16
|
+
sector IDs, deliberate CRC errors, deleted-data marks and extra tracks
|
|
17
|
+
that copy protection relies on.
|
|
18
|
+
|
|
19
|
+
This package reads HFE v1 and v3 images, decodes their FM and MFM tracks
|
|
20
|
+
into sectors, and flattens a decoded disc into the logical sector image
|
|
21
|
+
that the oaknut filesystems read.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Every `disc` command that reads an image opens an HFE file as the sector
|
|
26
|
+
image decoded from it:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
disc ls 'Dunjunz_D1S1.hfe:$'
|
|
30
|
+
disc export Dunjunz_D1S1.hfe dunjunz/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `disc hfe` commands look below the sector level:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
disc hfe info Dunjunz_D1S1.hfe # header and decoded layout
|
|
37
|
+
disc hfe sectors --anomalies Dunjunz_D1S1.hfe # what protection put there
|
|
38
|
+
disc hfe flatten Dunjunz_D1S1.hfe Dunjunz_D1S1.ssd
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From Python:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from oaknut.hfe import HFEImage, analyse, decode_image, flatten
|
|
45
|
+
|
|
46
|
+
disc = decode_image(HFEImage.from_filepath("Dunjunz_D1S1.hfe"))
|
|
47
|
+
flat = flatten(disc) # flat.data is the .ssd image
|
|
48
|
+
for finding in analyse(disc, flat.layout):
|
|
49
|
+
if finding.anomalous:
|
|
50
|
+
print(finding.track, finding.side, finding.remarks)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Status
|
|
54
|
+
|
|
55
|
+
Pre-alpha, read-only.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
uv add oaknut-hfe
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oaknut-hfe"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [{ name = "Robert Smallshire", email = "robert@smallshire.org.uk" }]
|
|
9
|
+
description = "Python library for reading HFE (HxC Floppy Emulator) flux-level disc images"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.12"
|
|
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.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"oaknut-exception>=10.0",
|
|
26
|
+
"oaknut-extension>=10.0",
|
|
27
|
+
"oaknut-filesystem>=13.5",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# The `cli` extra pulls the shared CLI toolkit so this package can
|
|
31
|
+
# contribute `disc hfe` subcommands. The library core never imports it.
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
cli = ["oaknut-cli>=10.0", "oaknut-file>=10.0"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-hfe"
|
|
37
|
+
Documentation = "https://rob-smallshire.github.io/oaknut/disc/api/reference/hfe.html"
|
|
38
|
+
Repository = "https://github.com/rob-smallshire/oaknut"
|
|
39
|
+
Issues = "https://github.com/rob-smallshire/oaknut/issues"
|
|
40
|
+
|
|
41
|
+
# The image decoder contributed to the oaknut.decoder axis: any `disc`
|
|
42
|
+
# command reading an image opens an HFE file as its flattened sectors.
|
|
43
|
+
[project.entry-points."oaknut.decoder"]
|
|
44
|
+
hfe = "oaknut.hfe.decoder:HFEDecoder"
|
|
45
|
+
|
|
46
|
+
# Subcommands contributed to the `disc` CLI on the oaknut.command axis:
|
|
47
|
+
# `disc hfe <subcommand>`. Loaded only with the [cli] extra.
|
|
48
|
+
[project.entry-points."oaknut.command"]
|
|
49
|
+
hfe = "oaknut.hfe.cli:hfe"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.dynamic]
|
|
52
|
+
version = { attr = "oaknut.hfe.__version__" }
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""HFE (HxC Floppy Emulator) flux-level disc images.
|
|
2
|
+
|
|
3
|
+
An HFE file records each track of a floppy disc as the raw bitstream a
|
|
4
|
+
drive's read head would deliver, sampled on a fixed cell clock. It is the
|
|
5
|
+
native format of the HxC and Gotek floppy emulators and a common output
|
|
6
|
+
of preservation capture hardware such as the Greaseweazle. Because it sits
|
|
7
|
+
below the sector level, an HFE image keeps everything an original disc
|
|
8
|
+
carries — including the non-standard sector IDs, deliberate CRC errors,
|
|
9
|
+
deleted-data marks and extra tracks that copy protection relies on.
|
|
10
|
+
|
|
11
|
+
This package reads HFE v1 (``HXCPICFE``) and v3 (``HXCHFEV3``) images,
|
|
12
|
+
decodes their FM and MFM tracks into sectors, and flattens a decoded disc
|
|
13
|
+
into the logical sector image that the oaknut filesystems read.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
__version__ = "13.6.0"
|
|
19
|
+
|
|
20
|
+
from oaknut.hfe.build import (
|
|
21
|
+
TRACK_SAMPLES_300RPM_250KBPS,
|
|
22
|
+
FormattedSector,
|
|
23
|
+
build_track,
|
|
24
|
+
samples_per_half_cell,
|
|
25
|
+
track_samples,
|
|
26
|
+
)
|
|
27
|
+
from oaknut.hfe.container import (
|
|
28
|
+
HFE_V1_SIGNATURE,
|
|
29
|
+
HFE_V3_SIGNATURE,
|
|
30
|
+
HFEHeader,
|
|
31
|
+
HFEImage,
|
|
32
|
+
TrackEncoding,
|
|
33
|
+
TrackSamples,
|
|
34
|
+
write_hfe,
|
|
35
|
+
)
|
|
36
|
+
from oaknut.hfe.decode import (
|
|
37
|
+
DataMark,
|
|
38
|
+
DecodedDisc,
|
|
39
|
+
DecodedSector,
|
|
40
|
+
DecodedTrack,
|
|
41
|
+
Encoding,
|
|
42
|
+
SectorID,
|
|
43
|
+
decode_image,
|
|
44
|
+
decode_track,
|
|
45
|
+
)
|
|
46
|
+
from oaknut.hfe.decoder import HFEDecoder
|
|
47
|
+
from oaknut.hfe.exceptions import HFEError, HFEFormatError
|
|
48
|
+
from oaknut.hfe.flat import FlatImage, SectorLayout, flatten, flatten_hfe
|
|
49
|
+
from oaknut.hfe.report import SectorFinding, TrackFinding, analyse
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# The container
|
|
53
|
+
"HFEImage",
|
|
54
|
+
"HFEHeader",
|
|
55
|
+
"TrackSamples",
|
|
56
|
+
"TrackEncoding",
|
|
57
|
+
"HFE_V1_SIGNATURE",
|
|
58
|
+
"HFE_V3_SIGNATURE",
|
|
59
|
+
"write_hfe",
|
|
60
|
+
# Decoding
|
|
61
|
+
"Encoding",
|
|
62
|
+
"DataMark",
|
|
63
|
+
"SectorID",
|
|
64
|
+
"DecodedSector",
|
|
65
|
+
"DecodedTrack",
|
|
66
|
+
"DecodedDisc",
|
|
67
|
+
"decode_track",
|
|
68
|
+
"decode_image",
|
|
69
|
+
# Flattening
|
|
70
|
+
"SectorLayout",
|
|
71
|
+
"FlatImage",
|
|
72
|
+
"flatten",
|
|
73
|
+
"flatten_hfe",
|
|
74
|
+
# The sector report
|
|
75
|
+
"SectorFinding",
|
|
76
|
+
"TrackFinding",
|
|
77
|
+
"analyse",
|
|
78
|
+
# Building tracks
|
|
79
|
+
"FormattedSector",
|
|
80
|
+
"build_track",
|
|
81
|
+
"track_samples",
|
|
82
|
+
"samples_per_half_cell",
|
|
83
|
+
"TRACK_SAMPLES_300RPM_250KBPS",
|
|
84
|
+
# The oaknut.decoder plug-in
|
|
85
|
+
"HFEDecoder",
|
|
86
|
+
# Exceptions
|
|
87
|
+
"HFEError",
|
|
88
|
+
"HFEFormatError",
|
|
89
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Build FM and MFM tracks from sector descriptions.
|
|
2
|
+
|
|
3
|
+
The inverse of :mod:`oaknut.hfe.decode`: lay sectors out as an Acorn
|
|
4
|
+
controller would format them, then convert the half-cells to HFE samples
|
|
5
|
+
for :func:`oaknut.hfe.container.write_hfe`. Each :class:`FormattedSector`
|
|
6
|
+
can carry the defects copy protection uses — any ID, a bad ID or data
|
|
7
|
+
CRC, a deleted-data mark, a missing data field — so decoder behaviour can
|
|
8
|
+
be tested without real captures of every trick.
|
|
9
|
+
|
|
10
|
+
Ported from Beebium's ``TrackBuilder``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from oaknut.hfe.decode import Encoding, SectorID
|
|
19
|
+
from oaknut.hfe.ibm import (
|
|
20
|
+
DATA_MARK,
|
|
21
|
+
DELETED_DATA_MARK,
|
|
22
|
+
FM_DATA_MARK,
|
|
23
|
+
FM_DELETED_DATA_MARK,
|
|
24
|
+
FM_ID_MARK,
|
|
25
|
+
ID_MARK,
|
|
26
|
+
MFM_SYNC,
|
|
27
|
+
MFM_SYNC_CRC,
|
|
28
|
+
crc16,
|
|
29
|
+
fm_encode,
|
|
30
|
+
mfm_encode,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"FormattedSector",
|
|
35
|
+
"build_track",
|
|
36
|
+
"track_samples",
|
|
37
|
+
"samples_per_half_cell",
|
|
38
|
+
"TRACK_SAMPLES_300RPM_250KBPS",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
#: Samples in one revolution at 300 rpm on a 250 kbit/s HFE clock.
|
|
42
|
+
TRACK_SAMPLES_300RPM_250KBPS = 100_000
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class FormattedSector:
|
|
47
|
+
"""One sector as formatted onto a track."""
|
|
48
|
+
|
|
49
|
+
id: SectorID
|
|
50
|
+
#: The data field; padded with zeros, or truncated, to the ID's size.
|
|
51
|
+
data: bytes = b""
|
|
52
|
+
deleted: bool = False
|
|
53
|
+
#: Write no data field at all after the ID.
|
|
54
|
+
omit_data: bool = False
|
|
55
|
+
#: Corrupt the stored ID CRC.
|
|
56
|
+
bad_id_crc: bool = False
|
|
57
|
+
#: Corrupt the stored data CRC.
|
|
58
|
+
bad_data_crc: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class _Layout:
|
|
63
|
+
gap1: int
|
|
64
|
+
sync: int
|
|
65
|
+
gap2: int
|
|
66
|
+
gap3: int
|
|
67
|
+
gap_byte: int
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_FM_LAYOUT = _Layout(gap1=16, sync=6, gap2=11, gap3=21, gap_byte=0xFF)
|
|
71
|
+
_MFM_LAYOUT = _Layout(gap1=50, sync=12, gap2=22, gap3=24, gap_byte=0x4E)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_track(
|
|
75
|
+
sectors: Iterable[FormattedSector],
|
|
76
|
+
encoding: Encoding,
|
|
77
|
+
*,
|
|
78
|
+
half_cells: int | None = None,
|
|
79
|
+
) -> str:
|
|
80
|
+
"""The half-cells of a track holding *sectors* in the order given.
|
|
81
|
+
|
|
82
|
+
The track is padded with gap bytes to *half_cells* when given (one
|
|
83
|
+
revolution at 300 rpm is 50,000 FM or 100,000 MFM half-cells).
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: The sectors do not fit in *half_cells*.
|
|
87
|
+
"""
|
|
88
|
+
if encoding is Encoding.FM:
|
|
89
|
+
cells = _fm_track(sectors)
|
|
90
|
+
filler = fm_encode(bytes([_FM_LAYOUT.gap_byte]))
|
|
91
|
+
else:
|
|
92
|
+
cells = _mfm_track(sectors)
|
|
93
|
+
filler = mfm_encode(bytes([_MFM_LAYOUT.gap_byte]))
|
|
94
|
+
if half_cells is not None:
|
|
95
|
+
if len(cells) > half_cells:
|
|
96
|
+
raise ValueError(f"track needs {len(cells)} half-cells; only {half_cells} fit")
|
|
97
|
+
cells += (filler * (half_cells // len(filler) + 1))[: half_cells - len(cells)]
|
|
98
|
+
return cells
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _field(sector: FormattedSector, encoding: Encoding) -> tuple[bytes, bytes]:
|
|
102
|
+
identifier = sector.id
|
|
103
|
+
id_bytes = bytes(
|
|
104
|
+
[identifier.cylinder, identifier.head, identifier.record, identifier.size_code]
|
|
105
|
+
)
|
|
106
|
+
data = sector.data[: identifier.size(encoding)].ljust(identifier.size(encoding), b"\x00")
|
|
107
|
+
return id_bytes, data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _crc_bytes(crc: int, corrupt: bool) -> bytes:
|
|
111
|
+
return (crc ^ (0x5A5A if corrupt else 0)).to_bytes(2, "big")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _fm_track(sectors: Iterable[FormattedSector]) -> str:
|
|
115
|
+
layout = _FM_LAYOUT
|
|
116
|
+
gap = bytes([layout.gap_byte])
|
|
117
|
+
parts = [fm_encode(gap * layout.gap1)]
|
|
118
|
+
for sector in sectors:
|
|
119
|
+
id_bytes, data = _field(sector, Encoding.FM)
|
|
120
|
+
parts.append(fm_encode(b"\x00" * layout.sync))
|
|
121
|
+
parts.append(FM_ID_MARK)
|
|
122
|
+
id_crc = crc16(bytes([ID_MARK]) + id_bytes)
|
|
123
|
+
parts.append(fm_encode(id_bytes + _crc_bytes(id_crc, sector.bad_id_crc)))
|
|
124
|
+
parts.append(fm_encode(gap * layout.gap2))
|
|
125
|
+
if not sector.omit_data:
|
|
126
|
+
mark = DELETED_DATA_MARK if sector.deleted else DATA_MARK
|
|
127
|
+
parts.append(fm_encode(b"\x00" * layout.sync))
|
|
128
|
+
parts.append(FM_DELETED_DATA_MARK if sector.deleted else FM_DATA_MARK)
|
|
129
|
+
data_crc = crc16(bytes([mark]) + data)
|
|
130
|
+
parts.append(fm_encode(data + _crc_bytes(data_crc, sector.bad_data_crc)))
|
|
131
|
+
parts.append(fm_encode(gap * layout.gap3))
|
|
132
|
+
return "".join(parts)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _MFMWriter:
|
|
136
|
+
def __init__(self):
|
|
137
|
+
self._parts: list[str] = []
|
|
138
|
+
self._last_bit = 0
|
|
139
|
+
|
|
140
|
+
def bytes(self, data: bytes) -> None:
|
|
141
|
+
if data:
|
|
142
|
+
self._parts.append(mfm_encode(data, self._last_bit))
|
|
143
|
+
self._last_bit = data[-1] & 1
|
|
144
|
+
|
|
145
|
+
def sync(self) -> None:
|
|
146
|
+
self._parts.append(MFM_SYNC * 3)
|
|
147
|
+
self._last_bit = 1 # 0xA1 ends in a 1
|
|
148
|
+
|
|
149
|
+
def cells(self) -> str:
|
|
150
|
+
return "".join(self._parts)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _mfm_track(sectors: Iterable[FormattedSector]) -> str:
|
|
154
|
+
layout = _MFM_LAYOUT
|
|
155
|
+
gap = bytes([layout.gap_byte])
|
|
156
|
+
writer = _MFMWriter()
|
|
157
|
+
writer.bytes(gap * layout.gap1)
|
|
158
|
+
for sector in sectors:
|
|
159
|
+
id_bytes, data = _field(sector, Encoding.MFM)
|
|
160
|
+
writer.bytes(b"\x00" * layout.sync)
|
|
161
|
+
writer.sync()
|
|
162
|
+
id_crc = crc16(bytes([ID_MARK]) + id_bytes, MFM_SYNC_CRC)
|
|
163
|
+
writer.bytes(bytes([ID_MARK]) + id_bytes + _crc_bytes(id_crc, sector.bad_id_crc))
|
|
164
|
+
writer.bytes(gap * layout.gap2)
|
|
165
|
+
if not sector.omit_data:
|
|
166
|
+
mark = DELETED_DATA_MARK if sector.deleted else DATA_MARK
|
|
167
|
+
writer.bytes(b"\x00" * layout.sync)
|
|
168
|
+
writer.sync()
|
|
169
|
+
data_crc = crc16(bytes([mark]) + data, MFM_SYNC_CRC)
|
|
170
|
+
writer.bytes(bytes([mark]) + data + _crc_bytes(data_crc, sector.bad_data_crc))
|
|
171
|
+
writer.bytes(gap * layout.gap3)
|
|
172
|
+
return writer.cells()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def samples_per_half_cell(encoding: Encoding, bitrate_kbps: int = 250) -> int:
|
|
176
|
+
"""HFE samples per half-cell: an FM half-cell is 4 µs, an MFM one 2 µs.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ValueError: The bit rate does not give a whole number of samples.
|
|
180
|
+
"""
|
|
181
|
+
nominal = 125 if encoding is Encoding.FM else 250
|
|
182
|
+
count, remainder = divmod(bitrate_kbps, nominal)
|
|
183
|
+
if remainder or count == 0:
|
|
184
|
+
raise ValueError(f"{encoding.value} cannot be sampled at {bitrate_kbps} kbit/s")
|
|
185
|
+
return count
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def track_samples(half_cells: str, encoding: Encoding, bitrate_kbps: int = 250) -> str:
|
|
189
|
+
"""HFE samples for *half_cells*, each transition at the end of its cell."""
|
|
190
|
+
count = samples_per_half_cell(encoding, bitrate_kbps)
|
|
191
|
+
if count == 1:
|
|
192
|
+
return half_cells
|
|
193
|
+
pad = "0" * (count - 1)
|
|
194
|
+
return "".join(pad + cell for cell in half_cells)
|