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.
- oaknut_filesystem-12.0.0/LICENSE +21 -0
- oaknut_filesystem-12.0.0/PKG-INFO +63 -0
- oaknut_filesystem-12.0.0/README.md +36 -0
- oaknut_filesystem-12.0.0/pyproject.toml +51 -0
- oaknut_filesystem-12.0.0/setup.cfg +4 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/__init__.py +119 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/capabilities.py +302 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/coordinator.py +197 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/exceptions.py +35 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/filesystem.py +110 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/geometry.py +316 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/identification.py +94 -0
- oaknut_filesystem-12.0.0/src/oaknut/filesystem/reader.py +194 -0
- oaknut_filesystem-12.0.0/src/oaknut_filesystem.egg-info/PKG-INFO +63 -0
- oaknut_filesystem-12.0.0/src/oaknut_filesystem.egg-info/SOURCES.txt +20 -0
- oaknut_filesystem-12.0.0/src/oaknut_filesystem.egg-info/dependency_links.txt +1 -0
- oaknut_filesystem-12.0.0/src/oaknut_filesystem.egg-info/requires.txt +4 -0
- oaknut_filesystem-12.0.0/src/oaknut_filesystem.egg-info/top_level.txt +1 -0
- oaknut_filesystem-12.0.0/tests/test_coordinator.py +170 -0
- oaknut_filesystem-12.0.0/tests/test_geometry.py +139 -0
- oaknut_filesystem-12.0.0/tests/test_identification.py +30 -0
- oaknut_filesystem-12.0.0/tests/test_reader.py +127 -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,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,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
|
+
...
|