Skip to main content

Python library for handling Acorn DFS disc images (SSD/DSD format)

Project description

oaknut-dfs

PyPI version CI Python versions License: MIT

A Python library for reading and writing Acorn DFS (Disc Filing System) disc images in SSD and DSD formats, as used by the BBC Micro and Acorn Electron.

The problem

Software for the BBC Micro and related Acorn 8-bit computers is commonly distributed as disc images in SSD (single-sided) and DSD (double-sided) formats. These images contain a DFS catalogue structure that encodes filenames, load addresses, execution addresses, and file attributes in a format specific to the Acorn Disc Filing System.

Working with these images programmatically --- extracting files, inspecting metadata, creating new images, or modifying existing ones --- requires understanding the low-level catalogue format and sector layout. oaknut-dfs provides a Pythonic API that handles these details, letting you work with DFS disc images using familiar Python patterns.

Supported formats

  • Acorn DFS: 40-track and 80-track, single-sided (SSD) and double-sided (DSD)
  • Watford DFS: Extended catalogue supporting up to 62 files (format constants defined)
  • DSD interleaving: Both interleaved and sequential double-sided layouts
  • Acorn character encoding: Custom codec for the BBC Micro character set (£, ¦)

Prerequisites

oaknut-dfs requires only uv. uv handles Python installation, dependency resolution, and virtual environments automatically --- you do not need to install anything else by hand.

Installing uv

macOS (Homebrew):

brew install uv

Linux / macOS (standalone installer):

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

See the uv installation docs for other methods including pip, pipx, Cargo, Conda, Winget, and Scoop.

Installation

As a library dependency

uv add oaknut-dfs

For development

uv sync

Usage

Creating a DFS instance

from oaknut_dfs import DFS, ACORN_DFS_40T_SINGLE_SIDED

# Create a 40-track single-sided disc image (102,400 bytes)
buffer = bytearray(102400)
# ... initialise catalogue sectors ...

dfs = DFS.from_buffer(memoryview(buffer), ACORN_DFS_40T_SINGLE_SIDED)
print(dfs.title)   # 'DEMO'
print(repr(dfs))   # DFS(title='DEMO', files=0, free_sectors=398)

Saving and loading files

# Save files with load and execution addresses
dfs.save("$.HELLO", b"Hello, World!", load_address=0x1200, exec_address=0x1200)
dfs.save("$.README", b"oaknut-dfs demo disc")

# Load a file back
data = dfs.load("$.HELLO")
print(data)   # b'Hello, World!'

Listing files

for entry in dfs.files:
    lock = "L" if entry.locked else " "
    print(f"{lock} {entry.path:10s}  {entry.load_address:06X}  {entry.exec_address:06X}  {entry.length:06X}")
#   $.HELLO     001200  001200  00000D
#   $.README    000000  000000  000014
# L $.PROG      003000  003000  000200

File information

info = dfs.get_file_info("$.HELLO")
print(info.name)            # '$.HELLO'
print(hex(info.load_address))  # 0x1200
print(hex(info.exec_address))  # 0x1200
print(info.length)          # 13
print(info.locked)          # False
print(info.start_sector)    # 2
print(info.sectors)         # 1

Disc information

print(dfs.info)
# {
#     'title': 'DEMO',
#     'num_files': 1,
#     'total_sectors': 400,
#     'free_sectors': 397,
#     'boot_option': 0,
# }

Pythonic interface

# Check if a file exists
print("$.HELLO" in dfs)   # True
print("$.NOPE" in dfs)    # False

# Number of files
print(len(dfs))            # 2

# Iterate over files
for entry in dfs:
    print(entry.path)
# $.HELLO
# $.README

Double-sided discs (DSD)

from oaknut_dfs import ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED

# DSD images contain two independent sides, each with its own catalogue.
# This mirrors the BBC Micro, where double-sided discs were accessed as
# separate drives using *DRIVE 0 and *DRIVE 2.

buffer = bytearray(204800)  # 40-track double-sided
# ... initialise catalogue sectors for both sides ...

# Access each side independently
dfs0 = DFS.from_buffer(memoryview(buffer), ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED, side=0)
dfs1 = DFS.from_buffer(memoryview(buffer), ACORN_DFS_40T_DOUBLE_SIDED_INTERLEAVED, side=1)

# Each side has its own title, files, and catalogue
print(dfs0.title)   # 'SIDE ZERO'
print(dfs1.title)   # 'SIDE ONE'

# Files on one side are not visible from the other
print("$.FILE0" in dfs0)   # True
print("$.FILE0" in dfs1)   # False

Development

After cloning, install the pre-commit hooks:

uv run --group dev pre-commit install

Running the tests

uv run --group test pytest tests/ -v

Architecture

The library uses a layered architecture with dependencies flowing downward:

  1. Sector access (surface.py, sectors_view.py) --- operates on buffers to convert logical sector numbers to physical byte offsets. Handles disc geometry and interleaving schemes.

  2. Catalogue management (catalogue.py, acorn_dfs_catalogue.py, watford_dfs_catalogue.py) --- parses and manages the DFS catalogue structure in sectors 0--1. Supports Acorn DFS (31 files) and Watford DFS (62 files).

  3. DFS API (dfs.py) --- user-facing Pythonic interface mirroring BBC Micro DFS star commands. Supports file operations, disc metadata, iteration, and the in operator.

References

Format specifications

Related tools and projects

  • oaknut-zip --- Sister project for extracting ZIP files containing Acorn metadata.

Forum discussions

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oaknut_dfs-0.1.2-py3-none-any.whl (37.1 kB view details)

Uploaded Python 3

File details

Details for the file oaknut_dfs-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: oaknut_dfs-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 37.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for oaknut_dfs-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 746528cfb4986a4df9be89439f5d9bdf07a53e36c77a4de92061049e73ea6143
MD5 58dfeaa4175e20a19c535d39369fbd0e
BLAKE2b-256 1406c7b3cf556bdbfa746a560713c6962db7fbf4c6cce2710338b622bd5d93fd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page