oaknut.dfs¶
Acorn DFS, Watford DFS, and Opus DDOS — the flat-catalogue filesystems
of BBC Micro and Acorn Electron floppies, in .ssd / .dsd form.
A single catalogue holds up to 31 files (62 under Watford DFS), each
with a one-character directory, a seven-character name, and load/exec
addresses; there is no directory hierarchy.
Every name documented here is importable directly from oaknut.dfs.
The filesystem¶
- class oaknut.dfs.DFS(catalogued_surface)¶
High-level DFS filesystem operations.
DFS is the Acorn Disc Filing System used on BBC Micro and Electron floppies. The on-disc structure is a flat 31-entry catalogue, not a hierarchical tree: every entry carries a single-character “directory” tag (
$,A–Z) that namespaces files of the same name. SeeDFSPathfor the model and what this means for path construction.- Parameters:
catalogued_surface (CataloguedSurface)
- property closed: bool¶
Whether this handle has been closed.
Once closed, any I/O operation raises
oaknut.file.FilesystemClosedError. Pure path manipulation on path objects bound to this handle continues to work.
- close()¶
Mark this handle as closed; idempotent.
Normally invoked automatically when the
from_file()/create_file()withblock exits. Callers that build aDFSfrom a buffer directly can callclose()to mark the lifecycle ended.- Return type:
- static from_file(filepath, disc_format=None, *, side=0)¶
Open a disc image file as a context manager.
The image is opened writable when host filesystem permissions allow, read-only otherwise. Mutations against a read-only-backed image raise from the mmap layer at the point of write.
disc_formatis optional — when omitted, the format is auto-detected from the file extension and size (seedetect_dfs_format()). Pass it explicitly to override detection (necessary for the rare sequential-double-sided flavour, which shares its byte count with the interleaved form).A truncated image is padded in memory with zeros; the on-disc file is never grown by this call. Mutations to a truncated image therefore land in the in-memory pad and are not persisted — opening a file never modifies it.
- Parameters:
filepath (str | PathLike) – Path to the disc image file (
.ssdor.dsd).disc_format (DiscFormat | None) – DiscFormat specifying geometry and catalogue type; auto-detected when
None.side (int) – Which surface to use (0-based index, default 0).
- Yields:
DFS instance backed by the file
- Raises:
FileNotFoundError – If the file does not exist
IndexError – If side index is out of range for the format
ValueError – If auto-detection cannot identify the format
- Return type:
Examples
- with DFS.from_file(“Zalaga.ssd”) as dfs:
print(dfs.title) data = (dfs.root / “$” / “ZALAGA”).read_bytes()
- with DFS.from_file(“disc.ssd”, ACORN_DFS_40T_SINGLE_SIDED) as dfs:
(dfs.root / “$” / “HELLO”).write_bytes(b”Hello!”)
- classmethod from_buffer(buffer, disc_format, side=0)¶
Create DFS from buffer using specified disk format.
- Parameters:
buffer (memoryview) – Disk image buffer
disc_format (DiscFormat) – DiscFormat specifying geometry and catalogue type
side (int) – Which surface to use (0-based index, default 0)
- Returns:
DFS instance for the specified side
- Raises:
IndexError – If side index is out of range for the format
KeyError – If catalogue_name is not registered
ValueError – If buffer size doesn’t match format requirements
- Return type:
Examples
# Single-sided 40-track SSD dfs = DFS.from_buffer(buffer, ACORN_DFS_40T_SINGLE_SIDED)
# Double-sided 40-track DSD (side 0) dfs = DFS.from_buffer(buffer, ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED, side=0)
# Double-sided 40-track DSD (side 1) dfs = DFS.from_buffer(buffer, ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED, side=1)
# 80-track sequential DSD dfs = DFS.from_buffer(buffer, ACORN_DFS_80T_DOUBLE_SIDED_SEQUENTIAL, side=1)
- classmethod create(disc_format, *, side=0, title='', boot_option=0)¶
Create a new in-memory DFS disc image with an empty catalogue.
- Parameters:
disc_format (DiscFormat) – DiscFormat specifying geometry and catalogue type.
side (int) – Which surface to initialise (0-based, default 0).
title (str) – Disc title (default empty).
boot_option (int) – Boot option 0–3 (default 0).
- Returns:
DFS instance backed by an in-memory buffer.
- Return type:
- static create_file(filepath, disc_format=None, *, side=0, title='', boot_option=0)¶
Create a new DFS disc image file with an empty catalogue.
The file is created at filepath with the correct size and opened read-write via mmap. Changes are flushed on exit.
disc_formatis optional — when omitted it is picked from the filename extension (.ssd⇒ 80-track single-sided,.dsd⇒ 80-track double-sided interleaved). Pass it explicitly for the 40-track or sequential-double-sided variants.- Parameters:
filepath (str | PathLike) – Path for the new disc image file.
disc_format (DiscFormat | None) – DiscFormat specifying geometry and catalogue type; chosen from the extension when
None.side (int) – Which surface to initialise (0-based, default 0).
title (str) – Disc title (default empty).
boot_option (int) – Boot option 0–3 (default 0).
- Yields:
DFS instance backed by the file.
- Return type:
- property root: DFSPath¶
A path handle representing the whole catalogue.
DFS is a flat-catalogue filesystem (see
DFSPath); this handle is the empty-string entry point from which the/operator can build a path in any directory letter, including the default$and the sibling tagsA–Z. It is deliberately not$— making it$would suggest the other directories live inside it, which they do not.For the hierarchical-root equivalent on ADFS / AFS, see
oaknut.adfs.ADFS.rootandoaknut.afs.AFS.root.
- path(path)¶
Create a DFSPath from a path string.
Routes through
DFSPath.__truediv__()so the Acorn-shell^parent token (and consecutive^^) are interpreted the same way as in a slash-joined chain.
- property boot_option: BootOption¶
Get boot option as a
oaknut.file.BootOptionenum.
- change_directory(directory)¶
Change current working directory (*DIR).
- Parameters:
directory (str) – Directory letter ($ or A-Z)
- Raises:
ValueError – If directory invalid
- Return type:
- list_directory(directory=None)¶
List files in directory.
- property info: dict¶
Get comprehensive disk information.
- Returns:
title, num_files, total_sectors, free_sectors, boot_option
- Return type:
Dict with
- validate()¶
Validate disc-image integrity.
Delegates to the underlying catalogue, which checks file extents, overlaps, duplicate names, and so on. Returns a list of
oaknut.dfs.exceptions.DFSValidationError— empty when the image is clean.- Return type:
list[‘DFSValidationError’]
- compact(*, order=())¶
Compact disk by removing fragmentation.
Delegates to catalogue which works at the sector level to rebuild entries sequentially, consolidating all free space at the end.
order is a partial list of paths to lay down first, in the lowest sectors; unlisted files follow in their current order. Locked files are relocated like any other and stay locked — the lock is delete protection, not a placement constraint.
- Returns:
Number of files compacted
- Raises:
FileNotFoundError – If order names a file not on the disc
- Parameters:
- Return type:
- export_all(target_dirpath, *, meta_format=MetaFormat.INF_TRAD, owner=0)¶
Export all files in the catalogue to a host directory.
- Parameters:
target_dirpath (str | PathLike) – Directory to export into. Created if missing.
meta_format (MetaFormat | None) – Metadata encoding. Defaults to traditional INF.
Nonesuppresses metadata (data files only). Seeoaknut.file.host_bridge.export_with_metadata().owner (int) – Econet owner ID, used only by PiEconetBridge formats.
- Raises:
OSError – If directory cannot be created or files cannot be written.
- Return type:
- class oaknut.dfs.DFSPath(dfs, path)¶
A path within a DFS filesystem, inspired by pathlib.Path.
DFS is not a hierarchical filesystem. Its on-disc catalogue is a flat list of up to 31 entries, each tagged with a single-character “directory” — one of
$,A–Z. The directory character is a namespace, not a container:$.MYPROGandA.MYPROGare two independent files, and neither is “inside” the other. The Acorn DFS User Guide spells this out — quoting roughly, “Files of the same name can be created on the same disc with different directories” and “the current directory and drive number is always set to drive 0 and directory ``$`` … they will be assumed”.$is therefore the default directory DFS assumes when a path omits one; it is not a parent ofA–Z. Compare ADFS / AFS, which do have a tree rooted at$.DFSPath models the catalogue as a notional root holding the set of populated directory tags, each tag in turn enumerating the files that carry it:
(catalogue) path="" ├── $ path="$" (default directory) │ └── HELLO path="$.HELLO" └── A path="A" (sibling of $, not inside it) └── GAME path="A.GAME"dfs.rootis the empty-string catalogue handle, not$— walking/from it produces paths in any directory letter, not just the default one. Navigate with the/operator and theDIR.NAMEform:dfs.root / "$.HELLO" # default directory dfs.root / "A.GAME" # sibling directory A dfs.root / "$" / "HELLO" # also $.HELLO (two-step form)
- stat()¶
Return metadata for this path.
- Raises:
FileNotFoundError – If the path does not exist.
- Return type:
- iterdir()¶
Iterate over directory contents.
On root: yields a DFSPath for each directory letter that has files. On a directory: yields a DFSPath for each file in that directory.
- Raises:
ValueError – If this path is a file, not a directory.
- Return type:
- read_bytes()¶
Read file contents (*LOAD).
- Raises:
ValueError – If this path is a directory.
FileNotFoundError – If the file does not exist.
- Return type:
- write_bytes(data, *, load_address=0, exec_address=0, access=None, date=None)¶
Write file contents (*SAVE).
accessaccepts the canonicaloaknut.file.Accessflags, a plainbool(True=> locked,False=> unlocked), orNoneto use the filesystem default (unlocked). DFS only stores the locked bit, so any otherAccessbits are silently dropped.dateis accepted for cross-filesystem signature uniformity but silently ignored — DFS does not store per-file dates.
- read_basic()¶
Read a BBC BASIC program and return its detokenised source.
Composes
read_bytes()withoaknut.dfs.basic.detokenise(). Never compose a BASIC program withread_text()— tokenised BASIC is bytecode, not text, and decoding it through a character codec will produce garbage.- Raises:
ValueError – If this path is a directory.
FileNotFoundError – If the file does not exist.
NotImplementedError – Until the detokeniser is implemented.
- Return type:
- write_basic(source, *, load_address=6400, exec_address=0, access=None)¶
Write a BBC BASIC program, tokenising the source first.
Composes
oaknut.dfs.basic.tokenise()withwrite_bytes(). Defaults the load address to the BBC Micro’s canonical0x1900; passoaknut.dfs.basic.ELECTRON_BASIC_LOAD_ADDRESSfor Electron programs.- Parameters:
- Raises:
ValueError – If this path is a directory or filename is invalid.
NotImplementedError – Until the tokeniser is implemented.
- Return type:
- touch(*, access=None, exist_ok=True)¶
Create an empty file at this path, mirroring
pathlib.Path.touch().DFS catalogue entries carry no modification time, so touching an existing file is a no-op when
exist_okisTrue.- Parameters:
- Raises:
ValueError – If this path is a DFS directory letter.
FileExistsError – If something already exists at the path and
exist_okisFalse.
- Return type:
- rename(target)¶
Rename file, returns new DFSPath.
- Raises:
FileNotFoundError – If this file doesn’t exist.
- Parameters:
- Return type:
- unlink()¶
Delete file (*DELETE).
- Raises:
FileNotFoundError – If the file doesn’t exist.
PermissionError – If the file is locked.
- Return type:
- set_load_address(address)¶
Set the load address without rewriting the file data.
- set_exec_address(address)¶
Set the exec address without rewriting the file data.
- export_file(target_filepath, *, meta_format=MetaFormat.INF_TRAD, owner=0)¶
Export file to host filesystem, emitting Acorn metadata.
- Parameters:
target_filepath (str | PathLike) – Destination path on the host.
meta_format (MetaFormat | None) – How to encode metadata. Defaults to traditional INF sidecar. Pass
Noneto write only the data. Filename-encoded formats rewrite the target filename.owner (int) – Econet owner ID, used only by PiEconetBridge formats.
- Returns:
The actual path that was written. Equal to target_filepath except for filename-encoded formats.
- Return type:
- import_file(source_filepath, *, meta_formats=(MetaFormat.INF_TRAD, MetaFormat.XATTR_ACORN, MetaFormat.FILENAME_RISCOS))¶
Import a file from the host filesystem.
The DFS filename is taken from this
DFSPath, not from the source file or any sidecar. Metadata (load/exec addresses, locked flag) is resolved by trying meta_formats in order; the first reader to match wins. DFS only stores the locked attribute bit, so public/execute attributes from the source metadata are silently discarded.- Parameters:
source_filepath (str | PathLike) – Path to the source file on the host.
meta_formats (Sequence[MetaFormat]) – Ordered cascade of metadata schemes to try. Defaults to
DEFAULT_IMPORT_META_FORMATS.
- Return type:
- class oaknut.dfs.DFSStat(length, load_address, exec_address, locked, start_sector, is_directory)¶
DFS file/directory metadata, analogous to os.stat_result.
Conforms to
oaknut.file.Stat—accessis synthesised fromlockedplus DFS’s implicit always-WR base, anddateis alwaysNonebecause DFS does not store per-file date stamps.- Parameters:
Detecting and sizing images¶
- oaknut.dfs.detect_dfs_format(filepath)¶
Auto-detect a DFS
DiscFormatfrom the file extension and size.Recognised pairs:
Extension
Size
Format
.ssd100 KiB (102 400)
ACORN_DFS_40T_SINGLE_SIDED.ssd200 KiB (204 800)
ACORN_DFS_80T_SINGLE_SIDED.dsd200 KiB (204 800)
ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED.dsd400 KiB (409 600)
ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVEDThe sequential-double-sided variants share their byte count with the interleaved forms, so detection cannot distinguish them — pass
disc_format=explicitly for those.- Raises:
FileNotFoundError – If filepath does not exist.
ValueError – If extension or size is not in the table above.
- Parameters:
- Return type:
- oaknut.dfs.expand(filepath, disc_format)¶
Physically extend a truncated disc image file to its canonical format size.
Appends zero bytes to filepath until it reaches the size required by disc_format. The original data is preserved.
- Parameters:
disc_format (DiscFormat) – Target format whose
image_sizethe file should match after expansion.
- Returns:
The number of bytes appended (0 if the file was already the correct size).
- Raises:
FileNotFoundError – If the file does not exist.
ValueError – If the file is empty, not a whole number of sectors, or already larger than the canonical format size.
- Return type:
Disc formats¶
The format constants for the standard Acorn DFS geometries — 40- and
80-track, single- and double-sided (sequential or interleaved). Each is
a generic DiscFormat. Watford DFS catalogues
are detected at read time and need no separate format constant here.
- oaknut.dfs.ACORN_DFS_40T_SINGLE_SIDED = DiscFormat(surface_specs=[SurfaceSpec(num_tracks=40, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=2560)], catalogue_name='acorn-dfs')¶
Complete disk format specification including all surfaces and catalogue type.
- oaknut.dfs.ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED = DiscFormat(surface_specs=[SurfaceSpec(num_tracks=40, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=5120), SurfaceSpec(num_tracks=40, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=2560, track_stride_bytes=5120)], catalogue_name='acorn-dfs')¶
Complete disk format specification including all surfaces and catalogue type.
- oaknut.dfs.ACORN_DFS_40T_DOUBLE_SIDED_SEQUENTIAL = DiscFormat(surface_specs=[SurfaceSpec(num_tracks=40, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=2560), SurfaceSpec(num_tracks=40, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=102400, track_stride_bytes=2560)], catalogue_name='acorn-dfs')¶
Complete disk format specification including all surfaces and catalogue type.
- oaknut.dfs.ACORN_DFS_80T_SINGLE_SIDED = DiscFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=2560)], catalogue_name='acorn-dfs')¶
Complete disk format specification including all surfaces and catalogue type.
- oaknut.dfs.ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED = DiscFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=5120), SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=2560, track_stride_bytes=5120)], catalogue_name='acorn-dfs')¶
Complete disk format specification including all surfaces and catalogue type.
- oaknut.dfs.ACORN_DFS_80T_DOUBLE_SIDED_SEQUENTIAL = DiscFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=2560), SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=204800, track_stride_bytes=2560)], catalogue_name='acorn-dfs')¶
Complete disk format specification including all surfaces and catalogue type.
- oaknut.dfs.IMAGE_FORMAT_BY_EXTENSION = {'.dsd': DiscFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=5120), SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=2560, track_stride_bytes=5120)], catalogue_name='acorn-dfs'), '.ssd': DiscFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=10, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=2560)], catalogue_name='acorn-dfs')}¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s
(key, value) pairs
- dict(iterable) -> new dictionary initialized as if via:
d = {} for k, v in iterable:
d[k] = v
- dict(**kwargs) -> new dictionary initialized with the name=value pairs
in the keyword argument list. For example: dict(one=1, two=2)
See also¶
The shared metadata types that appear throughout the DFS API —
AcornMeta, the BootOption
and MetaFormat enums, the
FSError base, and the host-bridge import/export
helpers — belong to oaknut.file; the generic
DiscFormat belongs to
oaknut.discimage. Import them from the package that
owns them.