Skip to main content

Disc image sector abstractions shared by Acorn filesystem packages

Project description

oaknut-discimage

Sector-level disc-image abstractions shared by the oaknut filesystem packages. This is the geometry layer: it turns a flat image file into addressable sectors, and is deliberately blind to what those sectors mean. The disc-based filing systems — oaknut-dfs, oaknut-adfs, and oaknut-afs — build the sector→files layer on top of it.

(The sequential filing systems are a different shape. oaknut-romfs, a Cassette-Filing-System block stream on a paged ROM, has no real sectors: it scans its byte stream directly and uses only SurfaceSpec to hand the framework a single trivial linear surface — see ROMFS, below.)

Part of the oaknut monorepo.

The model

A disc image is a flat run of bytes; a filing system wants numbered sectors. The mapping between the two is physical geometry — track count, sector size, sidedness, interleave — and it is exactly what this package models, in a small stack of abstractions that compose bottom-up:

raw bytes (mmap / bytearray)                  the image file
  ↓                                           described by
SurfaceSpec  ── one per surface ──            byte ⇄ sector geometry
  ↓                                           gathered into
DiscImage(buffer, [SurfaceSpec, …])           the whole image
  ↓  .surface(i)
Surface                                       one physical side
  ↓  .sector_range(start, count)
SectorsView                                   a read/write window of sectors

Above a Surface, a filing system reads and writes through the SectorsView it hands back. Crucially that view is a live window onto the backing buffer: writing through it lands in the mmap and thus on disc. The package never copies sector data.

The abstractions

SurfaceSpec — the geometry of one surface

A frozen dataclass describing how one surface's sectors are laid out in the image buffer:

Field Meaning
num_tracks tracks on the surface (e.g. 40 or 80)
sectors_per_track sectors per track (10 for Acorn DFS)
bytes_per_sector usually 256 (BYTES_PER_SECTOR)
track_zero_offset_bytes where this surface's track 0 begins in the buffer
track_stride_bytes byte distance from one of this surface's tracks to the next

The offset and stride are where interleaving lives. A single-sided image is contiguous: offset 0, stride one track. An interleaved double-sided image alternates the two sides track by track, so each side's stride is two tracks and side 1 starts one track in. A sequential double-sided image stores all of side 0 then all of side 1, so side 1's offset is the whole first half. The layers above never see any of this — they ask for logical sectors and the spec does the arithmetic.

DiscImage — the buffer plus its surfaces

DiscImage(buffer, surface_specs) pairs the raw memoryview with one or more SurfaceSpecs, validates that the surfaces fit and do not overlap, and vends a Surface per spec:

image = DiscImage(buffer, specs)
side0 = image.surface(0)        # a Surface
image.num_surfaces              # 1 for SSD, 2 for DSD

Surface — one physical side

A single side of the disc. Its job is to resolve a logical sector range to the underlying bytes:

view = side0.sector_range(start_sector=2, num_sectors=1)  # → SectorsView
side0.num_sectors, side0.num_tracks

SectorsView — a logical buffer over sectors

Presents one or more (possibly non-contiguous, thanks to track stride) memoryviews as a single addressable buffer. It indexes and slices like a bytearray, and writes pass straight through to the backing image:

view[0]                 # read a byte
view[0:8] = b"DISK    " # write — persists to the buffer (and the file)
view.tobytes()          # a plain bytes copy

This is the read/write surface every filing system actually operates on.

UnifiedDisc — many surfaces as one address space

Some filing systems treat a double-sided disc as a single logical disc with one continuous sector numbering (all of side 0, then all of side 1); others treat each side as an independent drive. UnifiedDisc is the former view — it maps a unified sector number to the right (surface, sector) pair and delegates to Surface:

disc = UnifiedDisc(image)
disc.sector_range(start_sector, num_sectors)   # spans both sides seamlessly

ADFS reads a double-sided floppy through a UnifiedDisc; DFS, where each side is a separate *DRIVE, talks to each Surface directly. For a single-sided disc a UnifiedDisc is a trivial pass-through.

DiscFormat — geometry plus filing system

DiscFormat(surface_specs, catalogue_name) is a complete format: the surface layout together with the name of the filing system that lives on it. Filesystem packages compose these into named constants and ship them alongside the filing system they describe — for example oaknut.dfs.formats.ACORN_DFS_80T_SINGLE_SIDED.

Geometry helpers

Building SurfaceSpec lists by hand is fiddly, so the common layouts have constructors:

  • single_sided_spec(num_tracks, sectors_per_track, bytes_per_sector=256)
  • interleaved_double_sided_specs(...)
  • sequential_double_sided_specs(...)

BYTES_PER_SECTOR (256) is the Acorn sector size, and open_image_mmap(path) opens an image with the right access — writable when the host file permits, read-only otherwise (mutations then raise at the mmap layer), so DFS, ADFS, and AFS all behave identically.

Putting it together

from oaknut.discimage import DiscImage, single_sided_spec

buffer = memoryview(bytearray(b"\x00" * (80 * 10 * 256)))  # 80T × 10 sectors
image = DiscImage(buffer, [single_sided_spec(num_tracks=80, sectors_per_track=10)])

surface = image.surface(0)
sector0 = surface.sector_range(0, 1)
sector0[0:8] = b"MYDISC  "          # write the catalogue title into sector 0
assert bytes(buffer[0:8]) == b"MYDISC  "   # it landed in the backing buffer

ROMFS: a sequential exception

Not every Acorn filing system is sector-addressed. The ROM Filing System (oaknut-romfs) stores files as a Cassette-Filing-System block stream on a paged ROM — a flat, sequential byte run with no tracks or sectors. It reads that stream directly and never calls sector_range. It depends on this package only to borrow SurfaceSpec and BYTES_PER_SECTOR for a single, trivial linear surface (num_tracks=1, the whole ROM as one "track"), purely so it can present a Geometry to the oaknut-filesystem framework. So while ROMFS lists this package as a dependency, it is not a consumer of the sector abstractions described above.

Relationship to the rest of oaknut

This package owns the physical mapping (bytes ⇄ sectors); a filing system owns the logical mapping (sectors ⇄ files and directories). That separation mirrors oaknut-filesystem, whose Geometry / Filesystem split sits one level up: a filing system reads logical sectors and is blind to interleave, sidedness, and on-disc byte offsets — all of which live here.

Licence

MIT.

Project details


Download files

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

Source Distribution

oaknut_discimage-12.8.0.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

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

oaknut_discimage-12.8.0-py3-none-any.whl (14.9 kB view details)

Uploaded Python 3

File details

Details for the file oaknut_discimage-12.8.0.tar.gz.

File metadata

  • Download URL: oaknut_discimage-12.8.0.tar.gz
  • Upload date:
  • Size: 20.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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_discimage-12.8.0.tar.gz
Algorithm Hash digest
SHA256 abe83b08291d4e0df066689d5db9b4ca380646f6350e5c3262e798f087cd585a
MD5 3c375a03f170644b2a6a11ad81c38cc8
BLAKE2b-256 7130c4b93df33d4ecb4bcaf4b72c927ce793687d699ea9bbad8bc6bcc9fe1589

See more details on using hashes here.

File details

Details for the file oaknut_discimage-12.8.0-py3-none-any.whl.

File metadata

  • Download URL: oaknut_discimage-12.8.0-py3-none-any.whl
  • Upload date:
  • Size: 14.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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_discimage-12.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7b766ecfc826b5e0102a29a058a3754a844be760894463bb97f95804de892154
MD5 e4495d89170e60f251ef8942471050dd
BLAKE2b-256 4406c568aa46bfb6462fdc5d1410ca1395a1e71391cbec6e7a94eccd3893dc58

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