oaknut-dfs 4.0.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.
Files changed (66) hide show
  1. oaknut_dfs-4.0.0/LICENSE +21 -0
  2. oaknut_dfs-4.0.0/PKG-INFO +312 -0
  3. oaknut_dfs-4.0.0/README.md +287 -0
  4. oaknut_dfs-4.0.0/pyproject.toml +52 -0
  5. oaknut_dfs-4.0.0/setup.cfg +4 -0
  6. oaknut_dfs-4.0.0/src/oaknut/dfs/__init__.py +92 -0
  7. oaknut_dfs-4.0.0/src/oaknut/dfs/acorn_dfs_catalogue.py +726 -0
  8. oaknut_dfs-4.0.0/src/oaknut/dfs/acorn_encoding.py +241 -0
  9. oaknut_dfs-4.0.0/src/oaknut/dfs/adfs.py +2118 -0
  10. oaknut_dfs-4.0.0/src/oaknut/dfs/adfs_directory.py +445 -0
  11. oaknut_dfs-4.0.0/src/oaknut/dfs/adfs_free_space_map.py +355 -0
  12. oaknut_dfs-4.0.0/src/oaknut/dfs/basic.py +59 -0
  13. oaknut_dfs-4.0.0/src/oaknut/dfs/boot_option.py +15 -0
  14. oaknut_dfs-4.0.0/src/oaknut/dfs/catalogue.py +300 -0
  15. oaknut_dfs-4.0.0/src/oaknut/dfs/catalogued_surface.py +209 -0
  16. oaknut_dfs-4.0.0/src/oaknut/dfs/dfs.py +889 -0
  17. oaknut_dfs-4.0.0/src/oaknut/dfs/exceptions.py +165 -0
  18. oaknut_dfs-4.0.0/src/oaknut/dfs/formats.py +209 -0
  19. oaknut_dfs-4.0.0/src/oaknut/dfs/host_bridge.py +282 -0
  20. oaknut_dfs-4.0.0/src/oaknut/dfs/sectors_view.py +218 -0
  21. oaknut_dfs-4.0.0/src/oaknut/dfs/surface.py +278 -0
  22. oaknut_dfs-4.0.0/src/oaknut/dfs/unified_disc.py +116 -0
  23. oaknut_dfs-4.0.0/src/oaknut/dfs/watford_dfs_catalogue.py +973 -0
  24. oaknut_dfs-4.0.0/src/oaknut_dfs.egg-info/PKG-INFO +312 -0
  25. oaknut_dfs-4.0.0/src/oaknut_dfs.egg-info/SOURCES.txt +64 -0
  26. oaknut_dfs-4.0.0/src/oaknut_dfs.egg-info/dependency_links.txt +1 -0
  27. oaknut_dfs-4.0.0/src/oaknut_dfs.egg-info/requires.txt +2 -0
  28. oaknut_dfs-4.0.0/src/oaknut_dfs.egg-info/top_level.txt +1 -0
  29. oaknut_dfs-4.0.0/tests/test_acorn_dfs_catalogue.py +1373 -0
  30. oaknut_dfs-4.0.0/tests/test_acorn_encoding.py +379 -0
  31. oaknut_dfs-4.0.0/tests/test_adfs.py +500 -0
  32. oaknut_dfs-4.0.0/tests/test_adfs_access.py +293 -0
  33. oaknut_dfs-4.0.0/tests/test_adfs_compact.py +180 -0
  34. oaknut_dfs-4.0.0/tests/test_adfs_directory_serialize.py +236 -0
  35. oaknut_dfs-4.0.0/tests/test_adfs_directory_title.py +125 -0
  36. oaknut_dfs-4.0.0/tests/test_adfs_export_import.py +216 -0
  37. oaknut_dfs-4.0.0/tests/test_adfs_free_space_map_mutate.py +209 -0
  38. oaknut_dfs-4.0.0/tests/test_adfs_hard_disc.py +235 -0
  39. oaknut_dfs-4.0.0/tests/test_adfs_mkdir.py +116 -0
  40. oaknut_dfs-4.0.0/tests/test_adfs_properties.py +82 -0
  41. oaknut_dfs-4.0.0/tests/test_adfs_rename_cross_dir.py +115 -0
  42. oaknut_dfs-4.0.0/tests/test_adfs_rename_lock.py +174 -0
  43. oaknut_dfs-4.0.0/tests/test_adfs_rmdir.py +91 -0
  44. oaknut_dfs-4.0.0/tests/test_adfs_unlink.py +78 -0
  45. oaknut_dfs-4.0.0/tests/test_adfs_write.py +303 -0
  46. oaknut_dfs-4.0.0/tests/test_basic.py +66 -0
  47. oaknut_dfs-4.0.0/tests/test_beebem_images.py +400 -0
  48. oaknut_dfs-4.0.0/tests/test_catalogue_operations.py +408 -0
  49. oaknut_dfs-4.0.0/tests/test_catalogued_surface.py +355 -0
  50. oaknut_dfs-4.0.0/tests/test_create_adfs.py +131 -0
  51. oaknut_dfs-4.0.0/tests/test_create_adfs_hard_disc.py +210 -0
  52. oaknut_dfs-4.0.0/tests/test_create_dfs.py +208 -0
  53. oaknut_dfs-4.0.0/tests/test_dfs.py +955 -0
  54. oaknut_dfs-4.0.0/tests/test_dfs_advanced.py +342 -0
  55. oaknut_dfs-4.0.0/tests/test_dfs_export_import.py +363 -0
  56. oaknut_dfs-4.0.0/tests/test_dfs_path.py +339 -0
  57. oaknut_dfs-4.0.0/tests/test_dfs_path_new_methods.py +239 -0
  58. oaknut_dfs-4.0.0/tests/test_exceptions.py +254 -0
  59. oaknut_dfs-4.0.0/tests/test_game_images.py +207 -0
  60. oaknut_dfs-4.0.0/tests/test_host_bridge.py +244 -0
  61. oaknut_dfs-4.0.0/tests/test_reference_images_base.py +113 -0
  62. oaknut_dfs-4.0.0/tests/test_reference_integration.py +326 -0
  63. oaknut_dfs-4.0.0/tests/test_reference_metadata.py +80 -0
  64. oaknut_dfs-4.0.0/tests/test_surface.py +776 -0
  65. oaknut_dfs-4.0.0/tests/test_unified_disc.py +153 -0
  66. oaknut_dfs-4.0.0/tests/test_watford_dfs_catalogue.py +334 -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,312 @@
1
+ Metadata-Version: 2.4
2
+ Name: oaknut-dfs
3
+ Version: 4.0.0
4
+ Summary: Python library for handling Acorn DFS disc images (SSD/DSD format) and ADFS 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
8
+ Project-URL: Repository, https://github.com/rob-smallshire/oaknut
9
+ Project-URL: Issues, https://github.com/rob-smallshire/oaknut/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: oaknut-file>=1.0
23
+ Requires-Dist: typename>=1.0.4
24
+ Dynamic: license-file
25
+
26
+ # oaknut-dfs
27
+
28
+ [![PyPI version](https://img.shields.io/pypi/v/oaknut-dfs.svg)](https://pypi.org/project/oaknut-dfs/)
29
+ [![CI](https://github.com/rob-smallshire/oaknut-dfs/actions/workflows/tests.yml/badge.svg)](https://github.com/rob-smallshire/oaknut-dfs/actions/workflows/tests.yml)
30
+ [![Python versions](https://img.shields.io/pypi/pyversions/oaknut-dfs.svg)](https://pypi.org/project/oaknut-dfs/)
31
+ [![License: MIT](https://img.shields.io/pypi/l/oaknut-dfs.svg)](https://github.com/rob-smallshire/oaknut-dfs/blob/master/LICENSE)
32
+
33
+ A Python library for reading, writing, and creating
34
+ [Acorn DFS](https://en.wikipedia.org/wiki/Disc_Filing_System) and
35
+ [ADFS](https://en.wikipedia.org/wiki/Advanced_Disc_Filing_System)
36
+ disc images, as used by the
37
+ [BBC Micro](https://en.wikipedia.org/wiki/BBC_Micro),
38
+ [Acorn Electron](https://en.wikipedia.org/wiki/Acorn_Electron),
39
+ and [BBC Master](https://en.wikipedia.org/wiki/BBC_Master).
40
+
41
+ With oaknut-dfs you can open DFS floppy images (SSD/DSD), ADFS floppy
42
+ images (ADF/ADL), and ADFS hard disc images (DAT/DSC) to browse
43
+ directories, read and write files, inspect metadata, and create new
44
+ formatted disc images --- all from Python, with a pathlib-inspired API.
45
+
46
+ ## Supported formats
47
+
48
+ ### DFS (Disc Filing System)
49
+
50
+ - **Acorn DFS**: 40-track and 80-track, single-sided (SSD) and double-sided (DSD)
51
+ - **Watford DFS**: Extended catalogue supporting up to 62 files
52
+ - **DSD interleaving**: Both interleaved and sequential double-sided layouts
53
+
54
+ ### ADFS (Advanced Disc Filing System)
55
+
56
+ - **ADFS S/M/L**: Single- and double-sided floppy images (ADF/ADL)
57
+ - **ADFS hard disc**: SCSI hard disc images (DAT + DSC sidecar pairs)
58
+ - **Hierarchical directories**: Full directory tree navigation with pathlib-inspired API
59
+ - **Old map format**: Free space map parsing and validation
60
+
61
+ ### Common
62
+
63
+ - **Acorn character encoding**: Custom codec for the BBC Micro character set (`£`, `¦`)
64
+
65
+ ## Prerequisites
66
+
67
+ oaknut-dfs is a standard Python package and can be installed with any Python
68
+ package manager, including `pip`. The instructions below use
69
+ [`uv`](https://docs.astral.sh/uv/), which handles Python installation,
70
+ dependency resolution, and virtual environments automatically.
71
+
72
+ ### Installing uv
73
+
74
+ **macOS (Homebrew):**
75
+
76
+ ```
77
+ brew install uv
78
+ ```
79
+
80
+ **Linux / macOS (standalone installer):**
81
+
82
+ ```
83
+ curl -LsSf https://astral.sh/uv/install.sh | sh
84
+ ```
85
+
86
+ **Windows:**
87
+
88
+ ```
89
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
90
+ ```
91
+
92
+ See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/)
93
+ for other methods including pip, pipx, Cargo, Conda, Winget, and Scoop.
94
+
95
+ ## Installation
96
+
97
+ ### As a library dependency
98
+
99
+ ```
100
+ uv add oaknut-dfs
101
+ ```
102
+
103
+ or with pip:
104
+
105
+ ```
106
+ pip install oaknut-dfs
107
+ ```
108
+
109
+ ### For development
110
+
111
+ ```
112
+ uv sync
113
+ ```
114
+
115
+ ## Usage
116
+
117
+ ### DFS disc images
118
+
119
+ #### Opening and reading files
120
+
121
+ ```python
122
+ from oaknut.dfs import DFS, ACORN_DFS_80T_SINGLE_SIDED
123
+
124
+ with DFS.from_file("Zalaga.ssd", ACORN_DFS_80T_SINGLE_SIDED) as dfs:
125
+ print(dfs.title) # 'ZALAG-L'
126
+
127
+ # Navigate with pathlib-inspired API
128
+ for entry in dfs.root / "$":
129
+ s = entry.stat()
130
+ print(f"{entry.name:10s} {s.length:6d} load={s.load_address:08X}")
131
+
132
+ # Read file data
133
+ data = (dfs.root / "$" / "ZALAGA").read_bytes()
134
+ ```
135
+
136
+ #### Creating a new DFS disc
137
+
138
+ ```python
139
+ from oaknut.dfs import DFS, ACORN_DFS_80T_SINGLE_SIDED
140
+
141
+ with DFS.create_file("demo.ssd", ACORN_DFS_80T_SINGLE_SIDED, title="DEMO") as dfs:
142
+ dfs.save("$.HELLO", b"Hello, World!", load_address=0x1900)
143
+ dfs.save("$.README", b"oaknut-dfs demo disc")
144
+ ```
145
+
146
+ #### Double-sided discs (DSD)
147
+
148
+ DSD images contain two independent sides, each with its own catalogue.
149
+ This mirrors the BBC Micro, where double-sided discs were accessed as
150
+ separate drives using `*DRIVE 0` and `*DRIVE 2`.
151
+
152
+ ```python
153
+ from oaknut.dfs import DFS, ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED
154
+
155
+ with DFS.from_file("game.dsd", ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED) as side0:
156
+ print(side0.title)
157
+
158
+ with DFS.from_file("game.dsd", ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED, side=1) as side1:
159
+ print(side1.title)
160
+ ```
161
+
162
+ #### Walking the disc
163
+
164
+ DFS directories (`$`, `A`--`Z`) appear as children of a virtual root:
165
+
166
+ ```python
167
+ with DFS.from_file("disc.ssd", ACORN_DFS_80T_SINGLE_SIDED) as dfs:
168
+ for dirpath, dirnames, filenames in dfs.root.walk():
169
+ for name in filenames:
170
+ print(dirpath / name)
171
+ ```
172
+
173
+ ### ADFS floppy disc images
174
+
175
+ #### Opening and navigating
176
+
177
+ ADFS supports hierarchical directories. The format is auto-detected from
178
+ the image size:
179
+
180
+ ```python
181
+ from oaknut.dfs import ADFS
182
+
183
+ with ADFS.from_file("MasterWelcome.adl") as adfs:
184
+ print(adfs.title) # '80T Welcome & Utils'
185
+
186
+ # Navigate with / operator
187
+ for entry in adfs.root / "LIBRARY":
188
+ print(entry.name, entry.stat().length)
189
+
190
+ # Read a file
191
+ data = (adfs.root / "HELP" / "aform").read_bytes()
192
+ ```
193
+
194
+ #### Walking the directory tree
195
+
196
+ ```python
197
+ with ADFS.from_file("disc.adl") as adfs:
198
+ for dirpath, dirnames, filenames in adfs.root.walk():
199
+ for name in filenames:
200
+ print(dirpath / name)
201
+ ```
202
+
203
+ #### Creating a new ADFS floppy
204
+
205
+ ```python
206
+ from oaknut.dfs import ADFS, ADFS_L
207
+
208
+ with ADFS.create_file("blank.adl", ADFS_L, title="My Disc") as adfs:
209
+ pass # empty formatted disc ready for use
210
+ ```
211
+
212
+ Available floppy formats: `ADFS_S` (160KB), `ADFS_M` (320KB), `ADFS_L` (640KB).
213
+
214
+ ### ADFS hard disc images
215
+
216
+ Hard disc images consist of a `.dat` file (raw sector data) and a `.dsc`
217
+ sidecar file (SCSI disc geometry). Pass either file to `from_file` ---
218
+ the companion is located automatically.
219
+
220
+ #### Opening a hard disc image
221
+
222
+ ```python
223
+ from oaknut.dfs import ADFS
224
+
225
+ with ADFS.from_file("scsi0.dat") as adfs:
226
+ print(adfs.title)
227
+ print(f"{adfs.total_size // 1024}KB, {adfs.free_space // 1024}KB free")
228
+
229
+ for dirpath, dirnames, filenames in adfs.root.walk():
230
+ for name in filenames:
231
+ p = dirpath / name
232
+ print(f"{p} {p.stat().length}")
233
+ ```
234
+
235
+ #### Creating a new hard disc image
236
+
237
+ Specify a capacity and the geometry is chosen automatically (4 heads,
238
+ 33 sectors/track --- the Acorn convention):
239
+
240
+ ```python
241
+ from oaknut.dfs import ADFS
242
+
243
+ # Create a 20MB hard disc image
244
+ with ADFS.create_file("scsi0.dat", capacity_bytes=20 * 1024 * 1024, title="Data") as adfs:
245
+ pass # creates both scsi0.dat and scsi0.dsc
246
+ ```
247
+
248
+ For explicit geometry control:
249
+
250
+ ```python
251
+ with ADFS.create_file("scsi0.dat", cylinders=306, heads=4) as adfs:
252
+ pass
253
+ ```
254
+
255
+ ## Development
256
+
257
+ After cloning, install the pre-commit hooks:
258
+
259
+ ```
260
+ uv run --group dev pre-commit install
261
+ ```
262
+
263
+ ### Running the tests
264
+
265
+ ```
266
+ uv run --group test pytest tests/ -v
267
+ ```
268
+
269
+ ## Architecture
270
+
271
+ The library uses a layered architecture with dependencies flowing downward:
272
+
273
+ 1. **Sector access** (`surface.py`, `sectors_view.py`, `unified_disc.py`) ---
274
+ operates on buffers to convert logical sector numbers to physical byte
275
+ offsets. Handles disc geometry, interleaving schemes, and multi-surface
276
+ aggregation.
277
+
278
+ 2. **Catalogue and directory management** --- two parallel implementations:
279
+ - **DFS** (`catalogue.py`, `acorn_dfs_catalogue.py`,
280
+ `watford_dfs_catalogue.py`) --- flat catalogue in sectors 0--1. Supports
281
+ Acorn DFS (31 files) and Watford DFS (62 files).
282
+ - **ADFS** (`adfs_directory.py`, `adfs_free_space_map.py`) --- hierarchical
283
+ directories stored as disc objects, with an explicit free space map.
284
+
285
+ 3. **Filesystem API** --- user-facing interfaces with pathlib-inspired navigation:
286
+ - **DFS** (`dfs.py`) --- `DFS`, `DFSPath`, `DFSStat`
287
+ - **ADFS** (`adfs.py`) --- `ADFS`, `ADFSPath`, `ADFSStat`
288
+
289
+ ## References
290
+
291
+ ### Format specifications
292
+
293
+ - [Acorn DFS disc format](https://beebwiki.mdfs.net/Acorn_DFS_disc_format) ---
294
+ BeebWiki specification for the Acorn DFS catalogue layout.
295
+ - [Disc Filing System](https://en.wikipedia.org/wiki/Disc_Filing_System) ---
296
+ Wikipedia overview of DFS and its variants.
297
+ - [Advanced Disc Filing System](https://en.wikipedia.org/wiki/Advanced_Disc_Filing_System) ---
298
+ Wikipedia overview of ADFS and its evolution.
299
+ - [Guide to Disc Formats](https://github.com/geraldholdsworth/DiscImageManager) ---
300
+ Gerald Holdsworth's detailed technical reference for DFS, ADFS, and other formats.
301
+ - [INF file format](https://beebwiki.mdfs.net/INF_file_format) ---
302
+ BeebWiki specification for the `.inf` sidecar metadata format.
303
+
304
+ ### Related tools and projects
305
+
306
+ - [oaknut-zip](https://github.com/rob-smallshire/oaknut-zip) ---
307
+ Sister project for extracting ZIP files containing Acorn metadata.
308
+
309
+ ### Forum discussions
310
+
311
+ - [Stardot forum: DFS format](https://stardot.org.uk/forums/viewtopic.php?t=4714) ---
312
+ Community discussion of DFS disc image formats and variants.
@@ -0,0 +1,287 @@
1
+ # oaknut-dfs
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/oaknut-dfs.svg)](https://pypi.org/project/oaknut-dfs/)
4
+ [![CI](https://github.com/rob-smallshire/oaknut-dfs/actions/workflows/tests.yml/badge.svg)](https://github.com/rob-smallshire/oaknut-dfs/actions/workflows/tests.yml)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/oaknut-dfs.svg)](https://pypi.org/project/oaknut-dfs/)
6
+ [![License: MIT](https://img.shields.io/pypi/l/oaknut-dfs.svg)](https://github.com/rob-smallshire/oaknut-dfs/blob/master/LICENSE)
7
+
8
+ A Python library for reading, writing, and creating
9
+ [Acorn DFS](https://en.wikipedia.org/wiki/Disc_Filing_System) and
10
+ [ADFS](https://en.wikipedia.org/wiki/Advanced_Disc_Filing_System)
11
+ disc images, as used by the
12
+ [BBC Micro](https://en.wikipedia.org/wiki/BBC_Micro),
13
+ [Acorn Electron](https://en.wikipedia.org/wiki/Acorn_Electron),
14
+ and [BBC Master](https://en.wikipedia.org/wiki/BBC_Master).
15
+
16
+ With oaknut-dfs you can open DFS floppy images (SSD/DSD), ADFS floppy
17
+ images (ADF/ADL), and ADFS hard disc images (DAT/DSC) to browse
18
+ directories, read and write files, inspect metadata, and create new
19
+ formatted disc images --- all from Python, with a pathlib-inspired API.
20
+
21
+ ## Supported formats
22
+
23
+ ### DFS (Disc Filing System)
24
+
25
+ - **Acorn DFS**: 40-track and 80-track, single-sided (SSD) and double-sided (DSD)
26
+ - **Watford DFS**: Extended catalogue supporting up to 62 files
27
+ - **DSD interleaving**: Both interleaved and sequential double-sided layouts
28
+
29
+ ### ADFS (Advanced Disc Filing System)
30
+
31
+ - **ADFS S/M/L**: Single- and double-sided floppy images (ADF/ADL)
32
+ - **ADFS hard disc**: SCSI hard disc images (DAT + DSC sidecar pairs)
33
+ - **Hierarchical directories**: Full directory tree navigation with pathlib-inspired API
34
+ - **Old map format**: Free space map parsing and validation
35
+
36
+ ### Common
37
+
38
+ - **Acorn character encoding**: Custom codec for the BBC Micro character set (`£`, `¦`)
39
+
40
+ ## Prerequisites
41
+
42
+ oaknut-dfs is a standard Python package and can be installed with any Python
43
+ package manager, including `pip`. The instructions below use
44
+ [`uv`](https://docs.astral.sh/uv/), which handles Python installation,
45
+ dependency resolution, and virtual environments automatically.
46
+
47
+ ### Installing uv
48
+
49
+ **macOS (Homebrew):**
50
+
51
+ ```
52
+ brew install uv
53
+ ```
54
+
55
+ **Linux / macOS (standalone installer):**
56
+
57
+ ```
58
+ curl -LsSf https://astral.sh/uv/install.sh | sh
59
+ ```
60
+
61
+ **Windows:**
62
+
63
+ ```
64
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
65
+ ```
66
+
67
+ See the [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/)
68
+ for other methods including pip, pipx, Cargo, Conda, Winget, and Scoop.
69
+
70
+ ## Installation
71
+
72
+ ### As a library dependency
73
+
74
+ ```
75
+ uv add oaknut-dfs
76
+ ```
77
+
78
+ or with pip:
79
+
80
+ ```
81
+ pip install oaknut-dfs
82
+ ```
83
+
84
+ ### For development
85
+
86
+ ```
87
+ uv sync
88
+ ```
89
+
90
+ ## Usage
91
+
92
+ ### DFS disc images
93
+
94
+ #### Opening and reading files
95
+
96
+ ```python
97
+ from oaknut.dfs import DFS, ACORN_DFS_80T_SINGLE_SIDED
98
+
99
+ with DFS.from_file("Zalaga.ssd", ACORN_DFS_80T_SINGLE_SIDED) as dfs:
100
+ print(dfs.title) # 'ZALAG-L'
101
+
102
+ # Navigate with pathlib-inspired API
103
+ for entry in dfs.root / "$":
104
+ s = entry.stat()
105
+ print(f"{entry.name:10s} {s.length:6d} load={s.load_address:08X}")
106
+
107
+ # Read file data
108
+ data = (dfs.root / "$" / "ZALAGA").read_bytes()
109
+ ```
110
+
111
+ #### Creating a new DFS disc
112
+
113
+ ```python
114
+ from oaknut.dfs import DFS, ACORN_DFS_80T_SINGLE_SIDED
115
+
116
+ with DFS.create_file("demo.ssd", ACORN_DFS_80T_SINGLE_SIDED, title="DEMO") as dfs:
117
+ dfs.save("$.HELLO", b"Hello, World!", load_address=0x1900)
118
+ dfs.save("$.README", b"oaknut-dfs demo disc")
119
+ ```
120
+
121
+ #### Double-sided discs (DSD)
122
+
123
+ DSD images contain two independent sides, each with its own catalogue.
124
+ This mirrors the BBC Micro, where double-sided discs were accessed as
125
+ separate drives using `*DRIVE 0` and `*DRIVE 2`.
126
+
127
+ ```python
128
+ from oaknut.dfs import DFS, ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED
129
+
130
+ with DFS.from_file("game.dsd", ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED) as side0:
131
+ print(side0.title)
132
+
133
+ with DFS.from_file("game.dsd", ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED, side=1) as side1:
134
+ print(side1.title)
135
+ ```
136
+
137
+ #### Walking the disc
138
+
139
+ DFS directories (`$`, `A`--`Z`) appear as children of a virtual root:
140
+
141
+ ```python
142
+ with DFS.from_file("disc.ssd", ACORN_DFS_80T_SINGLE_SIDED) as dfs:
143
+ for dirpath, dirnames, filenames in dfs.root.walk():
144
+ for name in filenames:
145
+ print(dirpath / name)
146
+ ```
147
+
148
+ ### ADFS floppy disc images
149
+
150
+ #### Opening and navigating
151
+
152
+ ADFS supports hierarchical directories. The format is auto-detected from
153
+ the image size:
154
+
155
+ ```python
156
+ from oaknut.dfs import ADFS
157
+
158
+ with ADFS.from_file("MasterWelcome.adl") as adfs:
159
+ print(adfs.title) # '80T Welcome & Utils'
160
+
161
+ # Navigate with / operator
162
+ for entry in adfs.root / "LIBRARY":
163
+ print(entry.name, entry.stat().length)
164
+
165
+ # Read a file
166
+ data = (adfs.root / "HELP" / "aform").read_bytes()
167
+ ```
168
+
169
+ #### Walking the directory tree
170
+
171
+ ```python
172
+ with ADFS.from_file("disc.adl") as adfs:
173
+ for dirpath, dirnames, filenames in adfs.root.walk():
174
+ for name in filenames:
175
+ print(dirpath / name)
176
+ ```
177
+
178
+ #### Creating a new ADFS floppy
179
+
180
+ ```python
181
+ from oaknut.dfs import ADFS, ADFS_L
182
+
183
+ with ADFS.create_file("blank.adl", ADFS_L, title="My Disc") as adfs:
184
+ pass # empty formatted disc ready for use
185
+ ```
186
+
187
+ Available floppy formats: `ADFS_S` (160KB), `ADFS_M` (320KB), `ADFS_L` (640KB).
188
+
189
+ ### ADFS hard disc images
190
+
191
+ Hard disc images consist of a `.dat` file (raw sector data) and a `.dsc`
192
+ sidecar file (SCSI disc geometry). Pass either file to `from_file` ---
193
+ the companion is located automatically.
194
+
195
+ #### Opening a hard disc image
196
+
197
+ ```python
198
+ from oaknut.dfs import ADFS
199
+
200
+ with ADFS.from_file("scsi0.dat") as adfs:
201
+ print(adfs.title)
202
+ print(f"{adfs.total_size // 1024}KB, {adfs.free_space // 1024}KB free")
203
+
204
+ for dirpath, dirnames, filenames in adfs.root.walk():
205
+ for name in filenames:
206
+ p = dirpath / name
207
+ print(f"{p} {p.stat().length}")
208
+ ```
209
+
210
+ #### Creating a new hard disc image
211
+
212
+ Specify a capacity and the geometry is chosen automatically (4 heads,
213
+ 33 sectors/track --- the Acorn convention):
214
+
215
+ ```python
216
+ from oaknut.dfs import ADFS
217
+
218
+ # Create a 20MB hard disc image
219
+ with ADFS.create_file("scsi0.dat", capacity_bytes=20 * 1024 * 1024, title="Data") as adfs:
220
+ pass # creates both scsi0.dat and scsi0.dsc
221
+ ```
222
+
223
+ For explicit geometry control:
224
+
225
+ ```python
226
+ with ADFS.create_file("scsi0.dat", cylinders=306, heads=4) as adfs:
227
+ pass
228
+ ```
229
+
230
+ ## Development
231
+
232
+ After cloning, install the pre-commit hooks:
233
+
234
+ ```
235
+ uv run --group dev pre-commit install
236
+ ```
237
+
238
+ ### Running the tests
239
+
240
+ ```
241
+ uv run --group test pytest tests/ -v
242
+ ```
243
+
244
+ ## Architecture
245
+
246
+ The library uses a layered architecture with dependencies flowing downward:
247
+
248
+ 1. **Sector access** (`surface.py`, `sectors_view.py`, `unified_disc.py`) ---
249
+ operates on buffers to convert logical sector numbers to physical byte
250
+ offsets. Handles disc geometry, interleaving schemes, and multi-surface
251
+ aggregation.
252
+
253
+ 2. **Catalogue and directory management** --- two parallel implementations:
254
+ - **DFS** (`catalogue.py`, `acorn_dfs_catalogue.py`,
255
+ `watford_dfs_catalogue.py`) --- flat catalogue in sectors 0--1. Supports
256
+ Acorn DFS (31 files) and Watford DFS (62 files).
257
+ - **ADFS** (`adfs_directory.py`, `adfs_free_space_map.py`) --- hierarchical
258
+ directories stored as disc objects, with an explicit free space map.
259
+
260
+ 3. **Filesystem API** --- user-facing interfaces with pathlib-inspired navigation:
261
+ - **DFS** (`dfs.py`) --- `DFS`, `DFSPath`, `DFSStat`
262
+ - **ADFS** (`adfs.py`) --- `ADFS`, `ADFSPath`, `ADFSStat`
263
+
264
+ ## References
265
+
266
+ ### Format specifications
267
+
268
+ - [Acorn DFS disc format](https://beebwiki.mdfs.net/Acorn_DFS_disc_format) ---
269
+ BeebWiki specification for the Acorn DFS catalogue layout.
270
+ - [Disc Filing System](https://en.wikipedia.org/wiki/Disc_Filing_System) ---
271
+ Wikipedia overview of DFS and its variants.
272
+ - [Advanced Disc Filing System](https://en.wikipedia.org/wiki/Advanced_Disc_Filing_System) ---
273
+ Wikipedia overview of ADFS and its evolution.
274
+ - [Guide to Disc Formats](https://github.com/geraldholdsworth/DiscImageManager) ---
275
+ Gerald Holdsworth's detailed technical reference for DFS, ADFS, and other formats.
276
+ - [INF file format](https://beebwiki.mdfs.net/INF_file_format) ---
277
+ BeebWiki specification for the `.inf` sidecar metadata format.
278
+
279
+ ### Related tools and projects
280
+
281
+ - [oaknut-zip](https://github.com/rob-smallshire/oaknut-zip) ---
282
+ Sister project for extracting ZIP files containing Acorn metadata.
283
+
284
+ ### Forum discussions
285
+
286
+ - [Stardot forum: DFS format](https://stardot.org.uk/forums/viewtopic.php?t=4714) ---
287
+ Community discussion of DFS disc image formats and variants.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oaknut-dfs"
7
+ dynamic = ["version"]
8
+ authors = [{ name = "Robert Smallshire", email = "robert@smallshire.org.uk" }]
9
+ description = "Python library for handling Acorn DFS disc images (SSD/DSD format) and ADFS disc images"
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ requires-python = ">=3.11"
14
+ classifiers = [
15
+ "Development Status :: 3 - 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>=1.0",
27
+ "typename>=1.0.4",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/rob-smallshire/oaknut"
32
+ Repository = "https://github.com/rob-smallshire/oaknut"
33
+ Issues = "https://github.com/rob-smallshire/oaknut/issues"
34
+
35
+ [tool.setuptools.dynamic]
36
+ version = { attr = "oaknut.dfs.__version__" }
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.bumpversion]
42
+ current_version = "4.0.0"
43
+ parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
44
+ serialize = ["{major}.{minor}.{patch}"]
45
+ tag = true
46
+ commit = true
47
+ message = "Bump version: {current_version} → {new_version}"
48
+ tag_name = "oaknut-dfs-v{new_version}"
49
+ tag_message = "Bump version: {current_version} → {new_version}"
50
+ files = [
51
+ { filename = "src/oaknut/dfs/__init__.py" },
52
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+