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 ($, AZ) that namespaces files of the same name. See DFSPath for 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() with block exits. Callers that build a DFS from a buffer directly can call close() to mark the lifecycle ended.

Return type:

None

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_format is optional — when omitted, the format is auto-detected from the file extension and size (see detect_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 (.ssd or .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:
Return type:

Iterator[DFS]

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:

DFS

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:

DFS

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_format is 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:

Iterator[DFS]

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 tags AZ. 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.root and oaknut.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.

Parameters:

path (str) – DFS path string, e.g. "$", "$.HELLO", "^.A.GAME", or "" for root.

Return type:

DFSPath

property title: str

Get disk title.

property boot_option: BootOption

Get boot option as a oaknut.file.BootOption enum.

property files: list[FileEntry]

List all files (*CAT).

property current_directory: str

Get current working directory.

change_directory(directory)

Change current working directory (*DIR).

Parameters:

directory (str) – Directory letter ($ or A-Z)

Raises:

ValueError – If directory invalid

Return type:

None

list_directory(directory=None)

List files in directory.

Parameters:

directory (str) – Directory to list (None = current)

Returns:

List of files in directory

Return type:

list[FileEntry]

property free_sectors: int

Get number of free 256-byte sectors available.

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:

order (Sequence[str])

Return type:

int

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. None suppresses metadata (data files only). See oaknut.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:

None

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 $, AZ. The directory character is a namespace, not a container: $.MYPROG and A.MYPROG are 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 of AZ. 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.root is 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 the DIR.NAME form:

dfs.root / "$.HELLO"        # default directory
dfs.root / "A.GAME"         # sibling directory A
dfs.root / "$" / "HELLO"    # also $.HELLO (two-step form)
Parameters:
property parent: DFSPath

$.HELLO$; $ → root; root → root.

Type:

Parent path

property name: str

$.HELLOHELLO; $$; root → "".

Type:

Final component

property parts: tuple[str, ...]

Path components as a tuple.

property path: str

Full path string.

exists()

Check whether this path exists on disc.

Return type:

bool

is_dir()

Check whether this path is a directory.

Return type:

bool

is_file()

Check whether this path is a file (not a directory).

Return type:

bool

stat()

Return metadata for this path.

Raises:

FileNotFoundError – If the path does not exist.

Return type:

DFSStat

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:

Iterator[DFSPath]

read_bytes()

Read file contents (*LOAD).

Raises:
Return type:

bytes

write_bytes(data, *, load_address=0, exec_address=0, access=None, date=None)

Write file contents (*SAVE).

access accepts the canonical oaknut.file.Access flags, a plain bool (True => locked, False => unlocked), or None to use the filesystem default (unlocked). DFS only stores the locked bit, so any other Access bits are silently dropped.

date is accepted for cross-filesystem signature uniformity but silently ignored — DFS does not store per-file dates.

Raises:

ValueError – If this path is a directory or filename is invalid.

Parameters:
Return type:

None

read_basic()

Read a BBC BASIC program and return its detokenised source.

Composes read_bytes() with oaknut.dfs.basic.detokenise(). Never compose a BASIC program with read_text() — tokenised BASIC is bytecode, not text, and decoding it through a character codec will produce garbage.

Raises:
Return type:

str

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() with write_bytes(). Defaults the load address to the BBC Micro’s canonical 0x1900; pass oaknut.dfs.basic.ELECTRON_BASIC_LOAD_ADDRESS for Electron programs.

Parameters:
  • source (str) – BBC BASIC source text.

  • load_address (int) – Load address (default 0x1900).

  • exec_address (int) – Execution address (default 0).

  • access (Access | None) – Access flags (see write_bytes()).

Raises:
Return type:

None

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_ok is True.

Parameters:
  • access (Access | None) – Access flags for the new file. Ignored on existing files (the access is not rewritten).

  • exist_ok (bool) – Default True matches pathlib. When False, an existing file or directory at the path raises.

Raises:
  • ValueError – If this path is a DFS directory letter.

  • FileExistsError – If something already exists at the path and exist_ok is False.

Return type:

None

rename(target)

Rename file, returns new DFSPath.

Raises:

FileNotFoundError – If this file doesn’t exist.

Parameters:

target (str | DFSPath)

Return type:

DFSPath

Delete file (*DELETE).

Raises:
Return type:

None

lock()

Lock file (*ACCESS +L).

Return type:

None

unlock()

Unlock file (*ACCESS -L).

Return type:

None

set_load_address(address)

Set the load address without rewriting the file data.

Parameters:

address (int)

Return type:

None

set_exec_address(address)

Set the exec address without rewriting the file data.

Parameters:

address (int)

Return type:

None

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 None to 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:

Path

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:

None

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.Stataccess is synthesised from locked plus DFS’s implicit always-WR base, and date is always None because DFS does not store per-file date stamps.

Parameters:
  • length (int)

  • load_address (int)

  • exec_address (int)

  • locked (bool)

  • start_sector (int)

  • is_directory (bool)

property access: Access

Canonical Access flags.

DFS only stores a single locked bit, so the result is always owner-read + owner-write, plus L if locked.

property date: None

DFS does not record per-file dates — always None.

class oaknut.dfs.DiscInfo(title, cycle_number, num_files, total_sectors, boot_option)

Disk catalog metadata.

Parameters:
  • title (str)

  • cycle_number (int)

  • num_files (int)

  • total_sectors (int)

  • boot_option (int)

Detecting and sizing images

oaknut.dfs.detect_dfs_format(filepath)

Auto-detect a DFS DiscFormat from the file extension and size.

Recognised pairs:

Extension

Size

Format

.ssd

100 KiB (102 400)

ACORN_DFS_40T_SINGLE_SIDED

.ssd

200 KiB (204 800)

ACORN_DFS_80T_SINGLE_SIDED

.dsd

200 KiB (204 800)

ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED

.dsd

400 KiB (409 600)

ACORN_DFS_80T_DOUBLE_SIDED_INTERLEAVED

The sequential-double-sided variants share their byte count with the interleaved forms, so detection cannot distinguish them — pass disc_format= explicitly for those.

Raises:
Parameters:

filepath (str | PathLike)

Return type:

DiscFormat

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:
  • filepath (str | PathLike) – Path to the disc image file.

  • disc_format (DiscFormat) – Target format whose image_size the 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:

int

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.