The extension axis: working with any filesystem

The concrete filesystem classes — DFS, ADFS, AFS — are the natural choice when your code already knows which format it is reading or writing: they expose the richest, filesystem-specific surface.

When your code does not know in advance — a bulk-processing script that should work on any image you throw at it, a tool that adapts to whichever filesystem packages happen to be installed, or anything that mirrors what disc identify and the rest of the CLI do — work through the filesystem extension axis in oaknut.filesystem. The CLI itself is built on it; nothing it does is private.

The four workflows below cover almost every case. Each recipe lives in scripts/api-examples/ and runs as part of the test suite, so the code in this page is the same code the project exercises.

Identify an image by content

identify() reports the format(s) of an image by reading its bytes, not by trusting its extension. It returns a list of Identification candidates ranked best-first; each carries a Confidence, the evidence behind the match, the partition it describes, and any nested sub-partitions a host disc carries in contained.

def report_identification(filepath: Path) -> None:
    """Print the identification tree for *filepath*, best-first.

    A bare summary line per candidate plus an indented line per
    contained sub-partition. The recipe walks one level of nesting —
    enough for ADFS + AFS — but ``contained`` is a tree, so a deeper
    walk just recurses.

    Args:
        filepath: Any disc image. Returns silently with a "nothing
            recognised" message if no installed filesystem matches.
    """
    candidates = identify(filepath)
    if not candidates:
        print(f"{filepath.name}: nothing recognised")
        return
    for host in candidates:
        print(f"{host.confidence.name:8s} {host.filesystem}")
        for nested in host.contained:
            print(
                f"         └─ {nested.confidence.name:8s} "
                f"{nested.filesystem} ({nested.partition.selector})"
            )

On a combined ADFS + AFS hard disc the printed tree looks like

STRONG   adfs
         └─ CERTAIN  afs (afs)

For an image nothing recognises, identify returns an empty list.

Open any image generically

Once you know what an image is, open it through the coordinator and work against the resulting Mount. The coordinator never imports a concrete filesystem package; it discovers what is installed through the oaknut.filesystem entry points and hands you a uniform interface. The same loop walks a DFS floppy, an ADFS hard disc, an AFS partition, or a ZIP archive.

Capability protocols — AcornMetadata, HierarchicalDirectories, Bootable, Titled, FreeSpace, and others — let the same code opt into extra behaviour where the mount supports it and skip it cleanly where it does not. Test for the capability, not the filesystem type:

def walk_any_disc(filepath: Path) -> None:
    """Walk any recognised disc one directory deep, with disc-level details.

    The same function works against a DFS floppy, an ADFS hard disc,
    an AFS partition, or a ZIP archive — the mount's core is uniform
    and the capability checks adapt the rest.

    Args:
        filepath: Any disc image.
    """
    candidates = identify(filepath)
    if not candidates:
        print(f"{filepath.name}: nothing recognised")
        return
    choice = candidates[0]

    filesystem = create_filesystem(choice.filesystem)
    with reader_for(filepath) as reader:
        mount = filesystem.open(reader, choice.geometry)

        if isinstance(mount, Titled):
            print(f"Title: {mount.title}")
        if isinstance(mount, Bootable):
            print(f"Boot option: {mount.boot_option.name}")

        def _show(entry, indent: int) -> None:
            kind = "dir " if entry.is_dir else "file"
            line = f"{'  ' * (indent + 1)}{entry.name:14s} {kind}  size={entry.length}"
            if isinstance(mount, AcornMetadata) and not entry.is_dir:
                meta = mount.acorn_meta(entry.path)
                if meta.load_address is not None:
                    line += f"  load={meta.load_address:#010x}"
            print(line)

        for top in mount.iter_entries(mount.path_root()):
            _show(top, indent=0)
            if top.is_dir:
                for child in mount.iter_entries(top.path):
                    _show(child, indent=1)

The mount’s core methods — iter_entries(), stat(), read_bytes(), exists() — have the same shape on every filesystem. To reach a contained partition (the AFS tail of an ADFS disc, say), open the nested Identification rather than the host: its partition records the region, and region_reader() gives a windowed view to open the contained filesystem on.

Discover what is installed

For tools that adapt to the environment — a chooser UI, a creation front-end, an installer probe — enumerate the filesystems through the coordinator. The set grows automatically as filesystem packages are installed; no hand-wired registry.

def list_installed() -> None:
    """Print every recognised filesystem with its one-line description."""
    for name in sorted(filesystem_names()):
        print(f"  {name:14s}  {describe_filesystem(name, single_line=True)}")

To examine one filesystem’s full description (the same text disc describe-filesystem NAME prints), call describe_filesystem() without single_line.

When to use which surface

Goal

Use

“I’m writing a DFS-aware script.”

oaknut.dfs.DFS

“I’m writing an ADFS-aware script.”

oaknut.adfs.ADFS

“I have an image; I’m not sure what.”

identify()

“Open this and treat it uniformly.”

create_filesystem()

“Adapt to what this filesystem can do.”

isinstance(mount, …) capability checks

“What’s installed in this environment?”

filesystem_names()

The concrete-class API and the extension axis are not alternatives; they are layers. The CLI uses the extension axis to route to the right filesystem, then the concrete classes do the work underneath. Your code can sit at whichever layer fits its job.