Python library for handling Acorn DFS disc images (SSD/DSD format)
Project description
oaknut-dfs
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:
-
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. -
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). -
DFS API (
dfs.py) --- user-facing Pythonic interface mirroring BBC Micro DFS star commands. Supports file operations, disc metadata, iteration, and theinoperator.
References
Format specifications
- Acorn DFS disc format --- BeebWiki specification for the Acorn DFS catalogue layout.
- Disc Filing System --- Wikipedia overview of DFS and its variants.
- INF file format ---
BeebWiki specification for the
.infsidecar metadata format.
Related tools and projects
- oaknut-zip --- Sister project for extracting ZIP files containing Acorn metadata.
Forum discussions
- Stardot forum: DFS format --- Community discussion of DFS disc image formats and variants.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
746528cfb4986a4df9be89439f5d9bdf07a53e36c77a4de92061049e73ea6143
|
|
| MD5 |
58dfeaa4175e20a19c535d39369fbd0e
|
|
| BLAKE2b-256 |
1406c7b3cf556bdbfa746a560713c6962db7fbf4c6cce2710338b622bd5d93fd
|