oaknut.adfs

ADFS — the Acorn Advanced Disc Filing System used by the BBC Master, the Archimedes, and RISC OS machines. Unlike DFS it has a directory hierarchy and a free-space map; it spans small (S), medium (M), and large (L) floppy layouts as well as hard-disc images addressed through an explicit geometry.

Every name documented here is importable directly from oaknut.adfs.

The filesystem

class oaknut.adfs.ADFS(unified_disc, dir_format, fsm, geometry)

Handle to an open ADFS disc image.

The ADFS object provides disc-level metadata and serves as the factory for ADFSPath objects. File and directory operations are performed through ADFSPath.

Example:

with ADFS.from_file("games.adf") as adfs:
    games = adfs.root / "Games"
    for entry in games:
        print(entry.name, entry.stat().length)
    data = (games / "Elite").read_bytes()
Parameters:
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.

Return type:

None

static from_file(filepath)

Open an ADFS disc image file as a context manager.

For floppy images (.adf, .adl), auto-detects the format from the image size.

For hard disc images (.dat), requires a companion .dsc sidecar file alongside it containing SCSI disc geometry. Either the .dat or .dsc file may be specified — the companion is located by swapping the extension.

The image is opened writable when host filesystem permissions allow, read-only otherwise. Mutations attempted against a read-only-backed image raise from the mmap layer at the point of write.

Parameters:

filepath (str | PathLike) – Path to the disc image file.

Yields:

ADFS instance backed by the file.

Raises:
  • FileNotFoundError – If the file or its companion does not exist.

  • ADFSError – If the image is not a valid ADFS disc.

Return type:

Iterator[ADFS]

classmethod from_buffer(buffer)

Create ADFS from a buffer, auto-detecting format.

For known floppy sizes (160KB, 320KB, 640KB), uses the corresponding ADFS S/M/L format. For other sizes, treats the buffer as a flat hard disc image (single surface).

Parameters:

buffer (memoryview) – Disc image data.

Returns:

ADFS instance.

Raises:

ADFSError – If the image is not a valid ADFS disc.

Return type:

ADFS

classmethod create(adfs_format, *, title='', boot_option=0)

Create a new in-memory ADFS disc image with an empty root directory.

Parameters:
  • adfs_format (ADFSFormat) – ADFS format (ADFS_S, ADFS_M, or ADFS_L).

  • title (str) – Disc title (default empty).

  • boot_option (int) – Boot option 0–3 (default 0).

Returns:

ADFS instance backed by an in-memory buffer.

Return type:

ADFS

static create_file(filepath, adfs_format=None, *, capacity=None, cylinders=None, heads=4, sectors_per_track=33, title='', boot_option=0)

Create a new ADFS disc image file with an empty root directory.

For floppy images, pass an ADFSFormat:

with ADFS.create_file("disc.adl", ADFS_L, title="MyDisc") as adfs:
    ...

For hard disc images (.dat), specify either a capacity or explicit geometry. A companion .dsc sidecar file is written automatically:

# By capacity (str or int bytes). Geometry chosen automatically.
with ADFS.create_file("scsi0.dat", capacity="10MB") as adfs:
    ...
with ADFS.create_file("scsi0.dat", capacity=10 * 1024 * 1024) as adfs:
    ...

# By explicit geometry
with ADFS.create_file("scsi0.dat", cylinders=306, heads=4) as adfs:
    ...
Parameters:
  • filepath (str | PathLike) – Path for the new disc image file.

  • adfs_format (ADFSFormat) – Floppy format (ADFS_S, ADFS_M, or ADFS_L).

  • capacity (int | str | None) – Minimum hard disc capacity. int is bytes; str accepts "10MB", "40MiB", "1024kB" etc. — see oaknut.file.capacity.parse_capacity() for the full suffix table.

  • cylinders (int) – Number of cylinders (hard disc).

  • heads (int) – Number of heads (default 4, hard disc only).

  • sectors_per_track (int) – Sectors per track (default 33, hard disc only).

  • title (str) – Disc title (default empty).

  • boot_option (int) – Boot option 0–3 (default 0).

Yields:

ADFS instance backed by the file.

Return type:

Iterator[ADFS]

property root: ADFSPath

The root directory ($).

path(path)

Create an ADFSPath from a path string.

Routes through ADFSPath.__truediv__() so the Acorn-shell ^ parent token (and consecutive ^^) are interpreted the same way as in a slash-joined chain.

Parameters:

path (str) – ADFS path string, e.g. "$.Games.Elite", "$", or "$.Games.^.Docs.ReadMe".

Return type:

ADFSPath

property geometry: ADFSGeometry

Authoritative disc geometry (cylinders, heads, sectors per track).

property title: str

Disc title (from root directory title field).

property boot_option: BootOption

Boot option as a oaknut.file.BootOption enum.

property free_space: int

Free space in bytes.

property total_size: int

Total disc size in bytes.

property disc_name: str

Disc name from the free space map.

property has_afs_partition: bool

Whether this disc carries Level 3 File Server pointers.

True when an AFS partition is installed in the tail of this old-map ADFS disc (info-sector pointers at &F6 / &1F6 of the old map are non-zero and parse cleanly). Cheap to call — does not construct an AFS handle.

property afs_partition

Return the AFS partition handle for this disc.

Returns an oaknut.afs.AFS handle sharing this disc’s UnifiedDisc. Cached on first access so repeated reads return the same instance until that instance is closed.

The returned handle does not own the underlying file — keep this ADFS context manager alive for as long as the AFS handle is in use. The caller is responsible for the AFS handle’s lifecycle: either call AFS.close() explicitly or use open_afs_partition() which yields the same handle as a context manager.

Raises:

AFSNotPresentError – If the disc has no AFS pointers. Use has_afs_partition to test for presence without provoking the exception.

open_afs_partition()

Open the AFS partition as a context manager.

Yields the afs_partition handle and calls AFS.close() on clean exit or AFS.discard() if the body raises — so the in-flight writes survive only when the with block completes successfully. This is the preferred idiom for AFS operations that need a lifecycle bound to a scope.

Raises:

AFSNotPresentError – If the disc has no AFS partition.

validate()

Validate filesystem integrity.

Returns a list of oaknut.adfs.exceptions.ADFSValidationError instances — empty when the image is clean. Callers iterate the list to present every defect rather than aborting on the first.

Return type:

list[ADFSValidationError]

compact()

Defragment the disc by rebuilding with sequential sector allocation.

Reads all files and directories into memory, reinitialises the disc structures, and writes everything back with contiguous sector allocation. The result is a single free space region at the end of the disc.

Returns:

Number of objects (files and directories, excluding root) written.

Return type:

int

export_all(target_dirpath, *, preserve_metadata=True)

Export entire filesystem preserving directory structure.

Parameters:
  • target_dirpath (str | PathLike) – Host directory to export into.

  • preserve_metadata (bool) – If True, write .inf sidecar files.

Return type:

None

class oaknut.adfs.ADFSPath(adfs, path)
Parameters:
EntryExistsError

alias of ADFSEntryExistsError

DirectoryError

alias of ADFSPathError

supports_title = True

A path within an ADFS filesystem, inspired by pathlib.Path.

ADFSPath objects are lightweight handles that reference an ADFS filesystem and a normalised absolute path string. They do not cache directory contents, so they always reflect the current state of the disc image.

Navigation uses the / operator:

games = adfs.root / "Games"
elite = games / "Elite"

Iterate over directory contents:

for child in games:
    print(child.name)

Read file data:

data = elite.read_bytes()
property parent: ADFSPath

Parent directory.

property name: str

Final component of the path.

property parts: tuple[str, ...]

Path components as a tuple, e.g. ("$", "Games", "Elite").

property path: str

Full path string, e.g. "$.Games.Elite".

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:

ADFSPathError – If the path does not exist.

Return type:

ADFSStat

property title: str

Directory title (up to 19 characters).

Distinct from the directory name: the name is the structural component used in paths; the title is a human-readable label stored inside the directory block.

Raises:

ADFSPathError – If this path is a file, not a directory.

iterdir()

Iterate over directory contents.

Raises:

ADFSPathError – If this path is not a directory or doesn’t exist.

Return type:

Iterator[ADFSPath]

read_bytes()

Read file contents.

Raises:

ADFSPathError – If the path doesn’t exist or is a directory.

Return type:

bytes

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:
  • ADFSPathError – If the path doesn’t exist or is a directory.

  • NotImplementedError – Until the detokeniser is implemented.

Return type:

str

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

Write file contents, creating or overwriting the file.

access accepts a oaknut.file.Access value, or None for the filesystem default (unlocked + owner R+W). Only the locked bit is honoured at the catalogue-write layer; richer flags (owner R/W/E, public R/W) must be applied via chmod() after the write.

date is accepted for cross-filesystem signature uniformity but silently ignored at this layer.

Parameters:
  • data (bytes) – File contents.

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

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

  • access (Access | None)

  • date (object)

Raises:
  • ADFSPathError – If this path is the root directory.

  • ADFSDiscFullError – If the disc has insufficient free space.

  • ADFSDirectoryFullError – If the parent directory is full.

Return type:

None

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:
  • ADFSPathError – If this path is the root directory.

  • ADFSDiscFullError – If the disc has insufficient free space.

  • ADFSDirectoryFullError – If the parent directory is full.

  • NotImplementedError – Until the tokeniser is implemented.

Return type:

None

Delete this file.

Raises:
  • ADFSPathError – If the path is root, doesn’t exist, or is a directory.

  • ADFSFileLockedError – If the file is locked.

Return type:

None

mkdir(*, parents=False, exist_ok=False)

Create a new directory at this path.

Mirrors pathlib.Path.mkdir().

Parameters:
  • parents (bool) – If True, missing intermediate directories are created. Default False raises when any ancestor is absent.

  • exist_ok (bool) – If True, do not raise when this path already resolves to a directory. A non-directory at the path still raises. Default False.

Raises:
  • ADFSPathError – If the path is root, parent is not found (and parents is False), or already exists as a file. Also when exist_ok is False and the path already exists.

  • ADFSDiscFullError – If the disc has insufficient free space.

  • ADFSDirectoryFullError – If the parent directory is full.

Return type:

None

rmdir()

Remove an empty directory.

Raises:
  • ADFSPathError – If the path is root, doesn’t exist, is not a directory, or is not empty.

  • ADFSFileLockedError – If the directory is locked.

Return type:

None

rename(target)

Rename this file or directory, returning the new path.

Moving across directories is supported.

Parameters:

target (str | ADFSPath) – New path (ADFSPath or string like "$.NewName").

Returns:

ADFSPath for the new location.

Raises:

ADFSPathError – If this path doesn’t exist, or target already exists.

Return type:

ADFSPath

lock()

Lock this file.

Raises:

ADFSPathError – If the path is root or doesn’t exist.

Return type:

None

unlock()

Unlock this file.

Raises:

ADFSPathError – If the path is root or doesn’t exist.

Return type:

None

chmod(access)

Set access attributes, replacing the current ones.

Uses the Access IntFlag enum:

from oaknut.adfs.directory import Access
path.chmod(Access.R | Access.W | Access.L)

Only the owner R, W, L, and E attributes are affected. Public/private NFS attributes are preserved.

Parameters:

access (int) – Combination of Access flags.

Raises:

ADFSPathError – If the path is root or doesn’t exist.

Return type:

None

set_load_address(address)

Set the load address without rewriting the file data.

Raises:

ADFSPathError – If the path doesn’t exist.

Parameters:

address (int)

Return type:

None

set_exec_address(address)

Set the exec address without rewriting the file data.

Raises:

ADFSPathError – If the path doesn’t exist.

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 ADFS filename is taken from this ADFSPath, not from the source file or any sidecar. Metadata is resolved by trying meta_formats in order; the first reader to match wins. The full Acorn attribute byte (R/W/E/L/PR/PW) is applied via chmod() after the data has been written, so owner-execute and public permissions round-trip losslessly via MetaFormat.INF_PIEB or either xattr format.

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.adfs.ADFSStat(length, load_address, exec_address, locked, owner_read, owner_write, owner_execute, public_read, public_write, public_execute, is_directory)

File/directory metadata, analogous to os.stat_result.

Conforms to oaknut.file.Stataccess is synthesised from the per-owner / per-public bits, and date is always None (ADFS dates live at the directory level and are not currently surfaced here).

Parameters:
  • length (int)

  • load_address (int)

  • exec_address (int)

  • locked (bool)

  • owner_read (bool)

  • owner_write (bool)

  • owner_execute (bool)

  • public_read (bool)

  • public_write (bool)

  • public_execute (bool)

  • is_directory (bool)

property access: Access

Access flags as an Access IntFlag, suitable for chmod().

property date: None

ADFS does not surface per-entry dates here — always None.

Disc formats

An ADFSFormat describes one ADFS layout. The three standard floppy formats are provided as constants, and IMAGE_FORMAT_BY_EXTENSION maps a filename extension to its format (None for the extensions whose size must be measured instead).

class oaknut.adfs.ADFSFormat(surface_specs, total_sectors, total_bytes, label)

ADFS disc format specification.

Parameters:
oaknut.adfs.ADFS_S = ADFSFormat(surface_specs=[SurfaceSpec(num_tracks=40, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=4096)], total_sectors=640, total_bytes=163840, label='S')

ADFS disc format specification.

oaknut.adfs.ADFS_M = ADFSFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=4096)], total_sectors=1280, total_bytes=327680, label='M')

ADFS disc format specification.

oaknut.adfs.ADFS_L = ADFSFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=8192), SurfaceSpec(num_tracks=80, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=4096, track_stride_bytes=8192)], total_sectors=2560, total_bytes=655360, label='L')

ADFS disc format specification.

oaknut.adfs.IMAGE_FORMAT_BY_EXTENSION = {'.adf': None, '.adl': ADFSFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=8192), SurfaceSpec(num_tracks=80, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=4096, track_stride_bytes=8192)], total_sectors=2560, total_bytes=655360, label='L'), '.adm': ADFSFormat(surface_specs=[SurfaceSpec(num_tracks=80, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=4096)], total_sectors=1280, total_bytes=327680, label='M'), '.ads': ADFSFormat(surface_specs=[SurfaceSpec(num_tracks=40, sectors_per_track=16, bytes_per_sector=256, track_zero_offset_bytes=0, track_stride_bytes=4096)], total_sectors=640, total_bytes=163840, label='S'), '.dat': None}

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)

Hard-disc geometry

Hard-disc images carry an explicit cylinders/heads/sectors geometry, held in an ADFSGeometry and persisted alongside the image in a 22-byte .dsc sidecar.

class oaknut.adfs.ADFSGeometry(cylinders, heads, sectors_per_track=33)

Authoritative disc geometry.

For hard disc images this comes from the .dsc sidecar file. For floppies it is derived from the format constants (ADFS_S/M/L).

Parameters:
  • cylinders (int)

  • heads (int)

  • sectors_per_track (int)

property sectors_per_cylinder: int

Sectors per cylinder (heads x sectors per track).

property total_sectors: int

Total sectors on the disc.

oaknut.adfs.geometry_for_capacity(capacity_bytes, *, heads=4, sectors_per_track=33)

Compute a disc geometry that meets or exceeds a requested capacity.

Returns a ADFSGeometry with the minimum number of cylinders needed to provide at least capacity_bytes of storage.

Parameters:
  • capacity_bytes (int) – Minimum disc capacity in bytes.

  • heads (int) – Number of heads (default 4).

  • sectors_per_track (int) – Sectors per track (default 33).

Returns:

Geometry with cylinders computed from the capacity.

Raises:

ValueError – If capacity_bytes is not positive.

Return type:

ADFSGeometry

oaknut.adfs.write_dsc(filepath, geometry)

Write a 22-byte .dsc sidecar file with SCSI disc geometry.

The sidecar carries cylinders/heads/sectors-per-track for a hard disc image so ADFS.from_file() can address sectors via CHS.

Parameters:
Return type:

None

See also

ADFS access bits use the shared Access enum, which belongs to oaknut.file (see also File metadata); import it from there.