oaknut-filesystem 12.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.
@@ -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,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: oaknut-filesystem
3
+ Version: 12.0.0
4
+ Summary: The pluggable filesystem contract for the oaknut family: detection, capabilities, geometry, partitions, and the identification coordinator.
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-filesystem
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: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: System :: Filesystems
19
+ Requires-Python: >=3.11
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-discimage>=10.0
25
+ Requires-Dist: oaknut-file>=10.0
26
+ Dynamic: license-file
27
+
28
+ # oaknut-filesystem
29
+
30
+ The pluggable **filesystem contract** for the oaknut family — the base every
31
+ Acorn (and, in principle, foreign) filesystem plugs into.
32
+
33
+ A *filesystem* is the unit of extension: Acorn DFS, Watford DFS, Opus DDOS,
34
+ ADFS, AFS, … are peers, registered on the `oaknut.filesystem` entry-point axis
35
+ (built on `oaknut-extension`). Each one:
36
+
37
+ - **probes** an image region and proposes what it is — with a confidence,
38
+ evidence, and a candidate *geometry*;
39
+ - **opens** a region into a mount exposing a small **core** (list / stat /
40
+ read / write / exists, and parsing its own path syntax) plus opt-in
41
+ **capability protocols** (`HierarchicalDirectories`, `AcornMetadata`,
42
+ `BootOption`, `UserDatabase`, `RegionHost`);
43
+ - declares its **geometry grammar** — the physical layouts it supports.
44
+
45
+ Two concerns are kept strictly apart:
46
+
47
+ - **Geometry** — the *physical* mapping of image bytes to logical sectors
48
+ (track count, sides, interleave, density, CHS). This is the discimage
49
+ `DiscFormat` layer, *beneath* the filesystem.
50
+ - **Filesystem** — the *logical* structure that turns sectors into files and
51
+ directories.
52
+
53
+ This package also hosts the **identification coordinator** (`identify()`): it
54
+ runs the registered filesystems over an image, ranks the candidates, and
55
+ recurses into reserved regions (an ADFS host's tail, where an AFS or other
56
+ filesystem may live) to produce a per-partition `Identification` tree.
57
+
58
+ It depends on **no** concrete filesystem package and imports none: filesystems
59
+ are discovered purely through entry points, so any subset can be installed and
60
+ the rest simply aren't handled.
61
+
62
+ See `docs/dev/filesystem-extensions.md` (architecture) and
63
+ `docs/dev/filesystem-extensions-plan.md` (implementation plan).
@@ -0,0 +1,36 @@
1
+ # oaknut-filesystem
2
+
3
+ The pluggable **filesystem contract** for the oaknut family — the base every
4
+ Acorn (and, in principle, foreign) filesystem plugs into.
5
+
6
+ A *filesystem* is the unit of extension: Acorn DFS, Watford DFS, Opus DDOS,
7
+ ADFS, AFS, … are peers, registered on the `oaknut.filesystem` entry-point axis
8
+ (built on `oaknut-extension`). Each one:
9
+
10
+ - **probes** an image region and proposes what it is — with a confidence,
11
+ evidence, and a candidate *geometry*;
12
+ - **opens** a region into a mount exposing a small **core** (list / stat /
13
+ read / write / exists, and parsing its own path syntax) plus opt-in
14
+ **capability protocols** (`HierarchicalDirectories`, `AcornMetadata`,
15
+ `BootOption`, `UserDatabase`, `RegionHost`);
16
+ - declares its **geometry grammar** — the physical layouts it supports.
17
+
18
+ Two concerns are kept strictly apart:
19
+
20
+ - **Geometry** — the *physical* mapping of image bytes to logical sectors
21
+ (track count, sides, interleave, density, CHS). This is the discimage
22
+ `DiscFormat` layer, *beneath* the filesystem.
23
+ - **Filesystem** — the *logical* structure that turns sectors into files and
24
+ directories.
25
+
26
+ This package also hosts the **identification coordinator** (`identify()`): it
27
+ runs the registered filesystems over an image, ranks the candidates, and
28
+ recurses into reserved regions (an ADFS host's tail, where an AFS or other
29
+ filesystem may live) to produce a per-partition `Identification` tree.
30
+
31
+ It depends on **no** concrete filesystem package and imports none: filesystems
32
+ are discovered purely through entry points, so any subset can be installed and
33
+ the rest simply aren't handled.
34
+
35
+ See `docs/dev/filesystem-extensions.md` (architecture) and
36
+ `docs/dev/filesystem-extensions-plan.md` (implementation plan).
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oaknut-filesystem"
7
+ dynamic = ["version"]
8
+ authors = [{ name = "Robert Smallshire", email = "robert@smallshire.org.uk" }]
9
+ description = "The pluggable filesystem contract for the oaknut family: detection, capabilities, geometry, partitions, and the identification coordinator."
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ requires-python = ">=3.11"
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: System :: Filesystems",
24
+ ]
25
+ dependencies = [
26
+ "oaknut-exception>=10.0",
27
+ "oaknut-extension>=10.0",
28
+ "oaknut-discimage>=10.0",
29
+ "oaknut-file>=10.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-filesystem"
34
+ Repository = "https://github.com/rob-smallshire/oaknut"
35
+ Issues = "https://github.com/rob-smallshire/oaknut/issues"
36
+
37
+ [dependency-groups]
38
+ test = [
39
+ "pytest>=8.0",
40
+ ]
41
+ dev = [
42
+ "bump-my-version>=0.28.0",
43
+ "pre-commit>=3.0",
44
+ {include-group = "test"},
45
+ ]
46
+
47
+ [tool.setuptools.dynamic]
48
+ version = { attr = "oaknut.filesystem.__version__" }
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,119 @@
1
+ """The pluggable filesystem contract for the oaknut family.
2
+
3
+ A *filesystem* (Acorn DFS, Watford DFS, ADFS, AFS, …) is the unit of
4
+ extension, registered on the ``oaknut.filesystem`` entry-point axis. Each
5
+ :class:`Filesystem` detects itself (:meth:`Filesystem.probe`), opens a
6
+ region into a :class:`Mount` exposing a small core plus opt-in capability
7
+ protocols, and declares its physical :class:`Geometry` grammar.
8
+
9
+ Geometry (the physical byte→sector layout) and the filesystem (the
10
+ logical structure) are kept strictly apart. :func:`identify` runs the
11
+ registered filesystems over an image, ranks the candidates, and recurses
12
+ into reserved regions to build a per-partition :class:`Identification`
13
+ tree — depending on, and importing, no concrete filesystem package.
14
+ """
15
+
16
+ from oaknut.filesystem.capabilities import (
17
+ AcornMetadata,
18
+ Bootable,
19
+ Compactable,
20
+ DirectoryTitled,
21
+ DiscGeometry,
22
+ Entry,
23
+ FreeMap,
24
+ FreeMapData,
25
+ FreeSpace,
26
+ HierarchicalDirectories,
27
+ Mount,
28
+ PhysicalGeometry,
29
+ RegionHost,
30
+ Sized,
31
+ Titled,
32
+ UserDatabase,
33
+ Validatable,
34
+ )
35
+ from oaknut.filesystem.coordinator import (
36
+ create_filesystem,
37
+ creating_filesystem,
38
+ describe_filesystem,
39
+ filesystem_names,
40
+ identify,
41
+ )
42
+ from oaknut.filesystem.exceptions import (
43
+ FilesystemError,
44
+ FilesystemExtensionError,
45
+ GeometryError,
46
+ ReadOnlyFilesystemError,
47
+ )
48
+ from oaknut.filesystem.filesystem import (
49
+ FILESYSTEM_KIND,
50
+ FILESYSTEM_NAMESPACE,
51
+ Filesystem,
52
+ )
53
+ from oaknut.filesystem.geometry import (
54
+ FLOPPY,
55
+ WINCHESTER,
56
+ Geometry,
57
+ GeometryGrammar,
58
+ floppy_geometry,
59
+ geometry_from_dsc,
60
+ region_reader,
61
+ winchester_geometry,
62
+ )
63
+ from oaknut.filesystem.identification import Confidence, Identification, Partition
64
+ from oaknut.filesystem.reader import ImageReader, ImageSource, reader_for
65
+
66
+ __version__ = "12.0.0"
67
+
68
+ __all__ = [
69
+ # contract
70
+ "Filesystem",
71
+ "FILESYSTEM_KIND",
72
+ "FILESYSTEM_NAMESPACE",
73
+ # capabilities
74
+ "Mount",
75
+ "Entry",
76
+ "HierarchicalDirectories",
77
+ "AcornMetadata",
78
+ "Titled",
79
+ "DirectoryTitled",
80
+ "Bootable",
81
+ "FreeSpace",
82
+ "FreeMap",
83
+ "FreeMapData",
84
+ "Sized",
85
+ "PhysicalGeometry",
86
+ "DiscGeometry",
87
+ "Compactable",
88
+ "Validatable",
89
+ "UserDatabase",
90
+ "RegionHost",
91
+ # geometry
92
+ "Geometry",
93
+ "GeometryGrammar",
94
+ "floppy_geometry",
95
+ "winchester_geometry",
96
+ "geometry_from_dsc",
97
+ "region_reader",
98
+ "FLOPPY",
99
+ "WINCHESTER",
100
+ # identification
101
+ "Confidence",
102
+ "Identification",
103
+ "Partition",
104
+ # coordinator
105
+ "identify",
106
+ "filesystem_names",
107
+ "describe_filesystem",
108
+ "create_filesystem",
109
+ "creating_filesystem",
110
+ # reader
111
+ "ImageReader",
112
+ "ImageSource",
113
+ "reader_for",
114
+ # exceptions
115
+ "FilesystemError",
116
+ "GeometryError",
117
+ "ReadOnlyFilesystemError",
118
+ "FilesystemExtensionError",
119
+ ]
@@ -0,0 +1,302 @@
1
+ """The mounted-filesystem interface: a small core plus opt-in capabilities.
2
+
3
+ Every mounted filesystem provides the :class:`Mount` core. Beyond that,
4
+ features differ wildly — a flat DFS catalogue, a hierarchical ADFS tree,
5
+ AFS user accounts, a foreign FAT with none of Acorn's metadata — so the
6
+ extras are **opt-in capability protocols** the CLI feature-detects with
7
+ ``isinstance`` (each is ``runtime_checkable``). A command is "available
8
+ when the mount provides capability X", never "when the filesystem is Y".
9
+
10
+ These signatures establish the *shape* of the contract; they will be
11
+ refined as the concrete filesystems are wrapped (Phase B).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Iterable
17
+ from dataclasses import dataclass
18
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
19
+
20
+ if TYPE_CHECKING:
21
+ from oaknut.file import AcornMeta, BootOption
22
+ from oaknut.filesystem.identification import Partition
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Entry:
27
+ """One directory entry, in filesystem-agnostic terms.
28
+
29
+ Acorn-specific metadata (load/exec/access) is reached through the
30
+ :class:`AcornMetadata` capability, not carried here, so a foreign
31
+ filesystem's entries need none of it.
32
+ """
33
+
34
+ name: str
35
+ is_dir: bool
36
+ length: int = 0
37
+ #: The entry's full in-partition path, so a caller can address it
38
+ #: (e.g. fetch its metadata) without re-joining in the filesystem's
39
+ #: own syntax. Empty only for a bare, unaddressed entry.
40
+ path: str = ""
41
+
42
+
43
+ @runtime_checkable
44
+ class Mount(Protocol):
45
+ """The core every mounted filesystem provides.
46
+
47
+ Paths are strings in the *filesystem's own* syntax (`$.DIR.FILE` for
48
+ Acorn, `\\DIR\\FILE` for FAT, `D.DIR.FILE` for a DDOS volume); the
49
+ filesystem parses them — the CLI never does.
50
+ """
51
+
52
+ def path_root(self) -> str:
53
+ """The root path string (e.g. ``"$"`` for Acorn filesystems)."""
54
+ ...
55
+
56
+ def stat(self, path: str) -> Entry:
57
+ """The :class:`Entry` for *path* (name, kind, length, full path)."""
58
+ ...
59
+
60
+ def join(self, parent: str, name: str) -> str:
61
+ """The path of child *name* under directory *parent*.
62
+
63
+ The filesystem owns its path syntax (``$.A`` for Acorn, the root
64
+ sometimes nameless), so the CLI builds new paths through this
65
+ rather than concatenating — needed when creating a path that does
66
+ not exist yet (e.g. a bulk import target).
67
+ """
68
+ ...
69
+
70
+ def iter_entries(self, path: str) -> Iterable[Entry]:
71
+ """Yield the entries of the directory at *path*."""
72
+ ...
73
+
74
+ def exists(self, path: str) -> bool:
75
+ """Whether anything exists at *path*."""
76
+ ...
77
+
78
+ def read_bytes(self, path: str) -> bytes:
79
+ """The contents of the file at *path*."""
80
+ ...
81
+
82
+ def write_bytes(self, path: str, data: bytes) -> None:
83
+ """Write *data* to *path*, creating or replacing the file."""
84
+ ...
85
+
86
+ def remove(self, path: str, *, force: bool = False) -> None:
87
+ """Delete the file or directory at *path*.
88
+
89
+ A directory is removed if the filesystem represents one (a flat
90
+ catalogue has none, so removing its notional directory is a
91
+ no-op). *force* overrides a lock — the filesystem unlocks the
92
+ entry first, owning its own locked-entry semantics so the access
93
+ byte's layout never leaks to the caller.
94
+ """
95
+ ...
96
+
97
+ def rename(self, old_path: str, new_path: str) -> None:
98
+ """Rename / move the entry at *old_path* to *new_path* in place."""
99
+ ...
100
+
101
+
102
+ @runtime_checkable
103
+ class HierarchicalDirectories(Protocol):
104
+ """The filesystem nests directories arbitrarily (ADFS, AFS, FAT, DDOS).
105
+
106
+ Its absence marks a flat catalogue (Acorn/Watford DFS: a single
107
+ top-level directory only).
108
+ """
109
+
110
+ def make_directory(
111
+ self,
112
+ path: str,
113
+ *,
114
+ parents: bool = False,
115
+ exist_ok: bool = False,
116
+ title: str | None = None,
117
+ ) -> None:
118
+ """Create a directory at *path*.
119
+
120
+ *parents* creates missing ancestors; *exist_ok* tolerates an
121
+ existing directory. *title* sets the new directory's title where
122
+ the filesystem supports per-directory titles (ADFS) and is
123
+ rejected (before anything is created) where it does not (AFS).
124
+ """
125
+ ...
126
+
127
+
128
+ @runtime_checkable
129
+ class AcornMetadata(Protocol):
130
+ """Files carry Acorn load/exec addresses and an access byte."""
131
+
132
+ def acorn_meta(self, path: str) -> "AcornMeta":
133
+ """The Acorn metadata of the file at *path*."""
134
+ ...
135
+
136
+ def set_acorn_meta(self, path: str, meta: "AcornMeta") -> None:
137
+ """Replace the Acorn metadata of the file at *path*."""
138
+ ...
139
+
140
+
141
+ @runtime_checkable
142
+ class Titled(Protocol):
143
+ """The disc carries a title / name (DFS, ADFS, AFS)."""
144
+
145
+ @property
146
+ def title(self) -> str:
147
+ """The disc title / name."""
148
+ ...
149
+
150
+ def set_title(self, title: str) -> None:
151
+ """Set the disc title / name."""
152
+ ...
153
+
154
+
155
+ @runtime_checkable
156
+ class DirectoryTitled(Protocol):
157
+ """Directories carry their own title, distinct from the disc's (ADFS).
158
+
159
+ Its absence marks a filesystem whose directories have no title field
160
+ (DFS, AFS) — setting one there is rejected.
161
+ """
162
+
163
+ def directory_title(self, path: str) -> str:
164
+ """The title of the directory at *path*."""
165
+ ...
166
+
167
+ def set_directory_title(self, path: str, title: str) -> None:
168
+ """Set the title of the directory at *path*."""
169
+ ...
170
+
171
+
172
+ @runtime_checkable
173
+ class Bootable(Protocol):
174
+ """The disc carries a ``*OPT 4`` boot option (DFS, ADFS)."""
175
+
176
+ @property
177
+ def boot_option(self) -> "BootOption":
178
+ """The disc's ``*OPT 4`` boot option."""
179
+ ...
180
+
181
+ def set_boot_option(self, option: "BootOption | int") -> None:
182
+ """Set the disc's ``*OPT 4`` boot option."""
183
+ ...
184
+
185
+
186
+ @runtime_checkable
187
+ class FreeSpace(Protocol):
188
+ """The filesystem reports its free space (ADFS, AFS)."""
189
+
190
+ def free_bytes(self) -> int:
191
+ """Free space remaining, in bytes."""
192
+ ...
193
+
194
+
195
+ @runtime_checkable
196
+ class Sized(Protocol):
197
+ """The filesystem reports its own occupied size (its partition's span)."""
198
+
199
+ def size_bytes(self) -> int:
200
+ """The size of this filesystem's partition, in bytes.
201
+
202
+ For a filesystem sharing a disc (ADFS with an AFS tail) this is
203
+ its slice, not the whole image — so partition sizes sum to the
204
+ disc.
205
+ """
206
+ ...
207
+
208
+
209
+ @dataclass(frozen=True)
210
+ class DiscGeometry:
211
+ """A disc's physical geometry, for the ``stat`` summary.
212
+
213
+ *label* is a human description in the filesystem's own vocabulary
214
+ (ADFS speaks cylinders/heads/track; AFS speaks cylinders/sectors-per-
215
+ cylinder). *sectors_per_cylinder* lets the caller place a partition's
216
+ logical-sector span into a cylinder range without parsing the label.
217
+ """
218
+
219
+ label: str
220
+ sectors_per_cylinder: int
221
+ total_sectors: int
222
+
223
+
224
+ @runtime_checkable
225
+ class PhysicalGeometry(Protocol):
226
+ """The filesystem knows the disc's physical geometry (ADFS, AFS).
227
+
228
+ A flat-catalogue floppy filesystem (DFS) records no geometry and does
229
+ not advertise this.
230
+ """
231
+
232
+ def disc_geometry(self) -> DiscGeometry:
233
+ """The disc's physical geometry."""
234
+ ...
235
+
236
+
237
+ @dataclass(frozen=True)
238
+ class FreeMapData:
239
+ """A filesystem's free space as partition-relative sector runs.
240
+
241
+ Carries no geometry: the renderer lays the *total_sectors* out as a
242
+ sector matrix sized to the terminal, marking those in *free_regions*
243
+ free and the rest used. Each region is ``(start_sector, length)``.
244
+ """
245
+
246
+ free_regions: tuple[tuple[int, int], ...]
247
+ total_sectors: int
248
+
249
+
250
+ @runtime_checkable
251
+ class FreeMap(Protocol):
252
+ """The filesystem can report which of its sectors are free."""
253
+
254
+ def free_map(self) -> FreeMapData:
255
+ """The free-space map as partition-relative sector runs."""
256
+ ...
257
+
258
+
259
+ @runtime_checkable
260
+ class Compactable(Protocol):
261
+ """The filesystem can defragment in place, consolidating free space."""
262
+
263
+ def compact(self) -> int:
264
+ """Defragment, returning a filesystem-defined measure of work done."""
265
+ ...
266
+
267
+
268
+ @runtime_checkable
269
+ class Validatable(Protocol):
270
+ """The filesystem can check its on-disc structure for defects."""
271
+
272
+ def validate(self) -> list:
273
+ """Return a list of structural defects (empty when clean).
274
+
275
+ The entries are the filesystem's own validation-error objects,
276
+ rendered by the CLI's error formatter; the caller treats them
277
+ opaquely.
278
+ """
279
+ ...
280
+
281
+
282
+ @runtime_checkable
283
+ class UserDatabase(Protocol):
284
+ """The filesystem has user accounts (AFS passwords / quota)."""
285
+
286
+ def user_names(self) -> tuple[str, ...]:
287
+ """The names of the registered users."""
288
+ ...
289
+
290
+
291
+ @runtime_checkable
292
+ class RegionHost(Protocol):
293
+ """The filesystem reserves regions another filesystem may occupy.
294
+
295
+ An ADFS host reserves tail cylinders that an AFS or (DRDOS) FAT
296
+ filesystem lives in; the coordinator recurses into these. The host
297
+ stays ignorant of *what* occupies them.
298
+ """
299
+
300
+ def reserved_regions(self) -> tuple["Partition", ...]:
301
+ """The regions reserved within this filesystem, for recursion."""
302
+ ...