oaknut.afs¶
The Acorn Level 3 File Server’s private on-disc filesystem, identified
by the AFS0 magic. An AFS region occupies the tail cylinders of an
old-map ADFS hard-disc image, coexisting with the ADFS partition at the
front of the same disc, so an AFS handle is opened through its host
ADFS (see oaknut.adfs).
Every name documented here is importable directly from oaknut.afs.
The filesystem¶
- class oaknut.afs.AFS(unified_disc, sec1, sec2, *, region_base=0, user='Syst', enforce_quota=True)¶
Open handle on an Acorn Level 3 File Server filesystem region.
Instances are normally obtained through
AFS.from_file()or viaADFS.afs_partition. The constructor is public but low level: callers must supply the two info-sector addresses from the host map themselves.- Parameters:
- static from_file(filepath)¶
Open a disc image and yield the AFS partition as a context manager.
Opens the image first as ADFS (reusing its sector-access and geometry detection), then reaches the AFS partition through
oaknut.adfs.ADFS.afs_partition. The image is opened writable when host filesystem permissions allow, read-only otherwise; mutations against a read-only-backed image raise from the mmap layer at the point of write.- Parameters:
filepath (str | PathLike) – Path to the ADFS hard-disc image carrying the AFS partition.
- Raises:
AFSNotPresentError – If the disc has no AFS pointers.
- Return type:
- static create_file(filepath, *, capacity=None, disc_name='', cylinders=None, compact_adfs=False, users=(), omit_users=(), emplacements=())¶
Create a new AFS image as a context manager.
Top-level orchestrator that composes
oaknut.adfs.ADFS.create_file()(#30 capacity strings) +oaknut.afs.wfsinit.initialise()+ zero or moreoaknut.afs.libraries.emplace_library()calls into a single named constructor — mirroring the symmetric shapeDFS.create_file()/ADFS.create_file()already provide for their filesystems.- Parameters:
filepath (Union[str, PathLike]) – Path for the new
.dathard-disc image. A companion.dscsidecar is written automatically.capacity (int | str | None) – Hard-disc capacity.
intis bytes;straccepts"10MB"/"40MiB"/ etc. (seeoaknut.file.capacity.parse_capacity()). DefaultNoneuses ADFS’s smallest hard-disc size.disc_name (str) – AFS disc-name string written into the info sector. Defaults to empty.
cylinders (int | None) – Number of cylinders the AFS partition should claim.
None(default) takes the existing free extent at the end of the ADFS partition — the same behaviour asdisc afs-initwithout--cylinders.compact_adfs (bool) – Run
ADFS.compact()before partitioning, consolidating ADFS data so AFS can claim the maximum possible tail extent.users (Sequence[UserSpec]) – Sequence of
UserSpecaccounts to create in addition to the built-inSyst,Boot, andWelcome.omit_users (Sequence[str]) – Names of built-in accounts to not create, e.g.
("Welcome",).SystandBootcannot be omitted.emplacements (Sequence[str | PathLike]) – Sequence of library names or paths passed to
oaknut.afs.emplace_library(). Names like"Library","Library1","ArthurLib"resolve to shipped images; anything else is treated as a path to an ADFS.adl.
- Yields:
The newly-initialised
AFSpartition handle.- Return type:
Iterator[AFS]
- property info_sector: InfoSector¶
The validated
InfoSectorfor this region.
- property title: str¶
The partition’s disc-level title — its info-sector disc name.
AFS directories have no individual title (unlike ADFS); the info-sector disc name is the only title-like field, so this is what
disc titlereads and writes for an AFS partition.
- property users: PasswordsFile¶
Parsed
$.Passwordsfile, read lazily and cached.
- add_user(name, *, password='', quota=0, system=False, privileges_locked=False, boot_option=None)¶
Add a user account to the AFS passwords file.
Equivalent to
disc afs-useraddon the CLI side. ComposesPasswordsFile.with_added()with the on-disc write so callers do not have to know the serialised passwords-file layout.quotaaccepts either an integer byte count or a capacity string ("2MB","512KiB", …) viaoaknut.file.capacity.parse_capacity().Raises
AFSUserExistsErrorif the name is taken.
- remove_user(name)¶
Remove
namefrom the AFS passwords file.Equivalent to
disc afs-userdelon the CLI side. The record is tombstoned in place so other users’ slots and directory references remain stable; subsequentadd_user()calls reuse the tombstoned slot if one is available.Raises
AFSUserNotFoundErrorifnameis not present.
- set_password(name, password)¶
Set
name’s login password in the AFS passwords file.Equivalent to
disc afs-passwdon the CLI side. The Level 3 File Server stores passwords as up to six cleartext ASCII characters (MAXPW) — there is no encryption, so the new password is written verbatim. Because the record stays the same 31 bytes this never grows the passwords file, unlikeadd_user().Raises
AFSUserNotFoundErrorif no active user has that name, andAFSPasswordErrorif the password is not ASCII or exceeds six characters.
- compact()¶
Defragment the AFS region, consolidating free space.
- Raises:
NotImplementedError – AFS compaction is not yet implemented.
- Return type:
- insert_into_directory(dir_sin, entry)¶
Insert
entryinto the directory atdir_sin.Reads the directory bytes, calls
insert_entry(), and writes the result back. If the directory’s free list is empty the underlying object is grown by one disc block (matchingCHZSZEatUade0E:1167) and the insert is retried. The grow step is capped atMAXDIR = 26sectors.Raises
AFSDirectoryFullErrorif the directory is already atMAXDIRand a grow would exceed the cap.- Parameters:
dir_sin (SystemInternalName)
entry (DirectoryEntry)
- Return type:
- flush()¶
Commit all buffered sector writes to the underlying disc.
The bitmap shadow is flushed first (which adds any dirty bitmap sectors to
_pending_writesvia the shadow’s writer callback), then every entry in the buffer is written to theUnifiedDiscin a single pass. After a successful flush the buffer is empty.- Return type:
- discard()¶
Drop all buffered writes without touching the disc.
Closes the handle. The caches are invalidated so they don’t carry stale state from the discarded session. Idempotent — a discard on an already-closed handle is a no-op.
Use this when an error path makes the in-flight changes invalid; the factory
from_file()/create_file()call this automatically when theirwithblock exits via an exception.- Return type:
- close()¶
Commit any pending writes and mark this handle closed.
Idempotent. Normally invoked automatically when the
from_file()/create_file()withblock exits normally; call manually if you build anAFSdirectly and need to control its lifecycle.- Return type:
- property closed: bool¶
Whether this handle has been closed via
close()ordiscard(). Pure path manipulation on bound paths still works after close; I/O raisesoaknut.file.FilesystemClosedError.
- class oaknut.afs.AFSPath(parts, afs=None)¶
An absolute path within an AFS directory tree.
Paths always start at the root
$and accumulate named components through/:root = AFSPath.root() library = root / "Library" fs_tool = library / "Fs" str(fs_tool) # "$.Library.Fs"
AFSPathvalues are treated as immutable:/returns a new path and the components are exposed through the read-onlyparts. It is a plain class (not a dataclass) because the inheritedAcornPathcarries properties —title,name,parent— and a frozen dataclass’s generated__setattr__masks inherited property setters, turningpath.title = ...into a confusingsuper()error.The
afshandle is bound at construction and excluded from equality and hashing — two paths denoting the same location are equal regardless of which (if any) handle they carry.- EntryExistsError¶
alias of
AFSDirectoryEntryExistsError
- DirectoryError¶
alias of
AFSPathError
- classmethod parse(text)¶
Parse a dot-separated path string into an
AFSPath.The string must start at the root
$; relative paths are not supported for this phase.
- property path: str¶
The canonical dot-separated path string (
$.A.B).Symmetrical with
DFSPath.pathandADFSPath.pathso tooling that walks heterogeneous trees can call.pathwithout type-discriminating.
- property parent: AFSPath¶
The path with the final component removed.
The parent of the root is the root itself.
- read_bytes()¶
Return the full contents of this file as bytes.
Raises
AFSPathErrorif the path is the root or a directory rather than a file.- Return type:
- stat()¶
Return the
AFSStatfor this path.Conforms to
oaknut.file.Stat— uniform across DFS, ADFS, and AFS. Usedirectory_entry()if you need the raw on-discDirectoryEntry(with itssinand AFSAccess-typedaccessbyte).The root directory has no parent entry and is a special case; asking for its
statraisesAFSPathError.- Return type:
AFSStat
- directory_entry()¶
Return the raw on-disc
DirectoryEntry.Lower-level companion to
stat(). Use this when you specifically need the on-discAFSAccessbyte layout, thesinsystem internal name, or other AFS-only fields. Most code should callstat()instead.- Return type:
DirectoryEntry
- iterdir()¶
Yield the children of this directory as bound ``AFSPath``s.
Raises
AFSPathErrorif this path is a file.
- write_bytes(data, *, load_address=0, exec_address=0, access=None, date=None)¶
Create or replace a file at this path with
data.If an object already exists at this path it is freed first, its directory entry is rewritten, and the new content is placed in freshly-allocated sectors. Allocator-level rollback on space exhaustion is handled by the lower layers.
accessaccepts:None: filesystem default — owner R+W, no public, unlocked.oaknut.file.Access(canonical wire form): translated to the AFS on-disc layout viaoaknut.afs.access.AFSAccess.from_acorn().Access.LWRis the canonical “locked owner R+W” combination.oaknut.afs.access.AFSAccess: used verbatim.int: read as a canonical wire-form access byte.
datedefaults to today’s date.
- touch(*, access=None, date=None, exist_ok=True)¶
Create an empty file at this path, mirroring
pathlib.Path.touch().AFS directory entries carry a date stamp. With the default
exist_ok=True, touching an existing file refreshes that date todate(defaulting to today).- Parameters:
access – Access flags for the file. On create, used for the new entry. On an existing file, ignored (pathlib’s touch does not rewrite mode either).
date – Acorn date stamp. Defaults to today.
exist_ok (bool) – Default
Truematches pathlib. WhenFalse, an existing file or directory at the path raises.
- Raises:
AFSPathError – If this path is the root, or already exists as a directory.
AFSDirectoryEntryExistsError – If a file already exists at the path and
exist_okisFalse.
- Return type:
- mkdir(*, parents=False, exist_ok=False, access=None, date=None)¶
Create an empty directory at this path.
A directory is an object whose data is a valid empty directory byte image (DRENTS=0, free list spanning every slot, trailing seq byte matching leading). Its access byte has the directory-type bit set.
Mirrors
pathlib.Path.mkdir().- Parameters:
parents (bool) – If
True, missing intermediate directories are created. DefaultFalseraises 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. DefaultFalse.access – Optional
AFSAccessfor the new directory; defaults toD/.date – Optional
AfsDate; defaults to today.
- Raises:
AFSPathError – If the path is the root, or already exists as a file, or the parent does not exist and
parentsisFalse.AFSDirectoryEntryExistsError – If the path already exists as a directory and
exist_okisFalse.
- Return type:
- unlink()¶
Delete this file (or empty directory) from its parent.
Frees the underlying object’s sectors and removes the directory entry. Refuses non-empty directories, matching
DELCHKatUade0D:1218.- Return type:
- chmod(access)¶
Set the access attributes of this file or directory.
accessaccepts anAFSAccess(used directly), a canonicaloaknut.file.Access, or anintread as that wire byte (matchingADFSPath.chmod()); the wireEbit, which has no AFS counterpart, is dropped.The
AFSAccess.DIRECTORYbit is always forced to match the object’s actual type;chmodcannot convert a file to a directory or vice versa.
- set_load_address(address)¶
Rewrite this entry’s load address without touching its data.
- set_exec_address(address)¶
Rewrite this entry’s exec address without touching its data.
- rename(target)¶
Rename or move this entry to
target, returning the new path.targetmust be an absolute path: a string starting with$or anAFSPathwith an absoluteparts. Moving across directories is supported — the underlying object’s data is not rewritten; only the directory entry changes parent.
- export_file(target_filepath, *, meta_format=MetaFormat.INF_TRAD, owner=0)¶
Export file to host filesystem, emitting Acorn metadata.
Mirrors
DFSPath.export_file()andADFSPath.export_file(). The AFS on-disc access byte is translated to the canonical wire-formoaknut.file.Accessbefore encoding so the resulting sidecar / xattrs use the same attribute representation as DFS and ADFS exports.- Parameters:
target_filepath (Union[str, PathLike]) – Destination path on the host.
meta_format (MetaFormat | None) – How to encode metadata. Defaults to traditional INF sidecar (
DEFAULT_EXPORT_META_FORMAT). PassNoneto write only the data, with no sidecar / xattr / filename rewrite.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.
Mirrors
DFSPath.import_file()andADFSPath.import_file(). The AFS filename is taken from thisAFSPath; metadata is resolved by trying meta_formats in order and the first reader to match wins. The full Acorn attribute byte (R/W/L/PR/PW — AFS does not have an execute bit) is applied viachmod()after the data is written so owner-write / public-read permissions round-trip losslessly viaMetaFormat.INF_PIEBor either xattr format.- Parameters:
source_filepath (Union[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:
- class oaknut.afs.AFSAccess(*values)¶
On-disc AFS access byte.
The integer value of any combination is the byte stored in the
DRACCSfield of a directory entry — it may be written straight into aSectorsViewwithout further translation:entry_bytes[20] = int(AFSAccess.from_string("LR/R"))
Composable with
|:AFSAccess.OWNER_READ | AFSAccess.OWNER_WRITE | AFSAccess.PUBLIC_READ
- classmethod from_byte(value)¶
Build an
AFSAccessfrom a raw access byte.Unknown bits (6 and 7) are silently ignored, matching the server’s tolerance for junk in the unused bits.
- to_string()¶
Format as the human-readable
"owner/public"string.Directory objects render as
"D/"or"DL/"— consistent with the server’s*EXlistings and with the Beebmaster PDF.- Return type:
- classmethod from_string(text)¶
Parse an
"owner/public"access string.Accepts the forms in the Beebmaster PDF’s “Example Access Strings” table:
"/"— no access at all (byte 0x00)"L/"— locked, otherwise no access"LR/R","LWR/WR","WR/R","R/"…"D/","DL/"— directories (the access bits are ignored for directories per the PDF; the server only honours locked)
Letters may appear in any order within each side, but the forward slash is mandatory. Upper/lower case is accepted; whitespace is not.
- classmethod from_acorn(access)¶
Translate a canonical
oaknut.file.Accessto AFS bits.Owner/public read-write and the locked bit map across. The wire
E(execute-only) bit has no AFS counterpart and is dropped. The directory-type bit is a property of the object, not its access, so it is never set here — the directory serialiser owns it.
Initialisation and partitioning¶
Partitioning an ADFS hard disc for AFS and initialising an empty
filesystem — the library analogue of WFSINIT. The spec objects
describe the desired partition size and initial user accounts.
- oaknut.afs.initialise(adfs, *, spec)¶
Initialise an AFS region on
adfsfromspec.Follows the WFSINIT.bas
PROCsetupflow (lines 2060-2350):Run
oaknut.afs.wfsinit.partition.plan()+applyunlessspec.repartitionis False.Initialise every AFS cylinder’s bitmap.
Allocate the root directory (map block + 2 data sectors).
Allocate a URD for each user (map block + 2 data sectors).
Allocate the passwords file (map block + 1 data sector).
Write info sectors (both copies).
Write cylinder bitmaps.
Write root directory map block + data, including entries for each user’s URD (access
&30) and the Passwords file (access&00).Write each user’s empty URD.
Write the passwords file with built-in accounts (Syst, Boot, Welcome) followed by user-specified accounts.
Emplace each library named in
spec.libraries.
- class oaknut.afs.InitSpec(disc_name, date=<factory>, size=<factory>, compact_adfs=False, addition_factor=0, default_quota=263172, users=(), libraries=(), repartition=True, omit_builtins=<factory>)¶
Caller-facing description of a freshly-initialised AFS disc.
Defaults match WFSINIT’s historical behaviour: the AFS region occupies the existing tail free extent without pre-compaction. Pass
compact_adfs=True(and optionallysize=AFSSizeSpec.max()) to compact the ADFS partition first and reclaim the maximum possible space.The default quota matches WFSINIT’s historical value of
0x40404(~256 KiB) — kept small because the L3FS address encoding caps a single drive at ~512 MB and real-period Winchesters were ~20 MB. Callers building discs for modern large images can raise this explicitly.Including a
UserSpecinuserswhosenamematches a built-in (Syst,Boot, orWelcome) overrides that built-in’s default quota / password / boot option. The spec’ssystemflag must match the built-in’s fixed value (TrueforSyst,Falsefor the others); an override does not create a URD. To instead reclaim a built-in’s name for a fresh regular user (with a URD), list the name inomit_builtins.
- class oaknut.afs.UserSpec(name, password='', quota=None, system=False, privileged=False, boot=BootOption.OFF)¶
Caller-facing description of one user account to create.
quotaaccepts either an integer byte count or a capacity string ("2MB","512KiB", etc.) — seeoaknut.file.capacity.parse_capacity()for the suffix table. Strings are parsed once at construction time and stored as bytes.
- class oaknut.afs.AFSSizeSpec(kind, value_a=0, value_b=0)¶
Tagged-union describing the caller’s size request.
Construct via the classmethods
max(),cylinders(),sectors(),bytes_(),ratio(), orexisting_free(). The dataclass fields are implementation details; do not construct directly.
- class oaknut.afs.RepartitionPlan(start_cylinder, afs_cylinders, new_adfs_cylinders, sec1, sec2, total_afs_sectors, will_compact)¶
Pure description of a repartition, produced by
plan().start_cylinder: where the AFS region will begin.afs_cylinders: cylinder count for the AFS region.new_adfs_cylinders: cylinder count retained for ADFS.sec1/sec2: absolute sector addresses of the two info sectors WFSINIT would install.total_afs_sectors: number of sectors in the AFS region.will_compact: whetherapply()will callADFS.compact()before shrinking.
- oaknut.afs.BUILTIN_ACCOUNT_NAMES = frozenset({'Boot', 'Syst', 'Welcome'})¶
Build an immutable unordered collection of unique elements.
Bulk operations¶
Composing files into an AFS region in bulk: merging another tree, importing a host directory tree, and emplacing one of the shipped library images.
- oaknut.afs.merge(target, source, *, source_path=None, target_path=None, conflict='error', exclude=None)¶
Copy a directory subtree from
sourcetotarget.source_pathdefaults tosource.root;target_pathdefaults totarget.root. Both must be directories.excludeis a set of entry names to skip during the merge. By default, thePasswordsfile is always excluded so that a library merge never overwrites the target disc’s user records.conflictcontrols what happens when a destination name already exists:"error"(default): refuse the whole merge withAFSMergeConflictErrorbefore writing anything."skip": leave the existing target entry alone."overwrite": replace the target entry (its old bytes are released back to the allocator).
- oaknut.afs.import_host_tree(target, *, source, target_path=None, on_collision='error')¶
Pull the host directory at
sourceintotarget_path.sourcemust be an existing directory on the host.target_pathdefaults to the AFS root; must be a directory (created viamkdirif it doesn’t yet exist).Per-file metadata is resolved through
oaknut.file.host_bridge.import_with_metadata()so INF sidecars and xattr-based schemes populate load/exec/access automatically.Names longer than 10 chars or containing forbidden chars (
.,:, space) are sanitised.
- oaknut.afs.emplace_library(target_afs, name, *, conflict='overwrite')¶
Emplace a library onto an AFS partition.
If name matches a shipped library (e.g.
"Library"), the bundled.adlis used. If name ends with.adl, it is treated as a path to a user-supplied ADFS image. In both cases, every file in the ADFS root is copied into$.{dirname}on the target AFS partition, where dirname is the stem of the image filename.The target directory is created if it does not already exist.
- oaknut.afs.SHIPPED_LIBRARIES = ('Library', 'Library1', 'ArthurLib', 'Utils')¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable’s items.
If the argument is a tuple, the return value is the same object.
Passwords¶
The file server’s Passwords account file: the per-user records and
the file that holds them.
- class oaknut.afs.PasswordsFile(records)¶
Read-only view over the parsed
$.Passwordsentries.Supports iteration (all slots, including the
!is_in_usetombstones from deleted users), length, integer indexing, and name-based lookup via__getitem__(str)orfind(). Only in-use entries are considered for name lookup; stale slots are skipped.Mutation arrives in phase 14. Until then this class intentionally exposes no write surface so that the read path can ship cleanly.
- Parameters:
records (Sequence[UserRecord])
- classmethod from_bytes(data)¶
Parse a passwords file image.
WFSINIT allocates the passwords file as a whole sector (
pssz% = &100) and storesBILB = 0, so the raw data may be 256 bytes — not a clean multiple of the 31-byte entry size. The L3FS server reads as many complete entries as fit and ignores any trailing fragment.- Parameters:
data (bytes)
- Return type:
- property active: tuple[UserRecord, ...]¶
All in-use records, in disc order.
- find(name)¶
Look up an in-use entry by its bare or
group.userID.Case-insensitive (the file server compares user IDs case-insensitively per
Uade06). RaisesKeyErrorif no active entry matches.- Parameters:
name (str)
- Return type:
- with_added(name, *, password='', quota=0, system=False, privileges_locked=False, boot_option=BootOption.OFF)¶
Return a new passwords file with
nameadded.Raises
KeyErrorif the user already exists. Reuses the first tombstoned slot if one is available, otherwise appends a fresh record at the end. Matches the USRMAN add-user flow semantically.- Parameters:
- Return type:
- class oaknut.afs.UserRecord(name, group, password, free_space, is_in_use, is_system, is_privileges_locked, boot_option)¶
A single parsed passwords file entry.
nameis the bare user name (the portion aftergroup.if the user belongs to a group) andgroupis the group prefix orNone. The two together form thePWUSIDfield.Phase 6 parses and exposes; phase 14 will introduce the mutating operations (add, remove, set_quota, set_password, etc.).
- Parameters:
Free-space allocation¶
- class oaknut.afs.Allocator(shadow, *, start_cylinder, sectors_per_cylinder)¶
Policy layer over
BitmapShadow.Given the AFS region’s
start_cylinder(absolute, as recorded in the info sector) and itssectors_per_cylinder, the allocator translates between cylinder-local bitmap coordinates and absolute disc sector numbers. All sector addresses exposed through the public API are absolute — the same values that go into aMapSector’s extents and aSystemInternalName.- allocate(num_sectors)¶
Allocate
num_sectorsdata sectors and return their extents.The returned extents cover exactly
num_sectorssectors in total. Policy:Cylinders are picked in descending free-count order (FNDCY).
Within a cylinder, contiguous free runs are taken from the lowest free sector upward (ALBLK first-fit).
If a single cylinder cannot satisfy the whole request, the allocator spills to successive cylinders by free-count (FLBLKS).
On failure (not enough total free space), every sub-allocation made during this call is rolled back before raising
AFSInsufficientSpaceError.
num_sectorsmust be positive.
- allocate_sector()¶
Allocate one sector and return its SIN.
Convenience for map-block allocation: a JesMap map block occupies exactly one sector, and its SIN is that sector’s absolute address. This is the
ALBLKequivalent used byMPCRSP(Uade10:168) and by the chain-link path inMKRLN(Uade12:187).- Return type:
SystemInternalName
- free_extent(extent)¶
Release
extentback to the bitmap shadow.The extent may span multiple cylinders; the allocator splits it into per-cylinder chunks before updating the shadow.
- Parameters:
extent (Extent)
- Return type:
- free_sector(sector)¶
Release a single sector (e.g. an obsolete map block).
Equivalent to
free_extent()with length 1.
Disc geometry and types¶
The geometry of the host hard disc and the small domain types used throughout the on-disc structures.
- class oaknut.afs.Geometry(cylinders, sectors_per_cylinder, total_sectors, bitmap_size_sectors=1)¶
Physical geometry of the disc containing an AFS region.
Attributes match the fields in the AFS info sector (see
docs/dev/afs-onwire.md§Info sector):cylinders: total cylinders on the physical disc (MPSZNC).sectors_per_cylinder:MPSZSC.total_sectors:MPSZNS— 24-bit, so capped at 2^24.bitmap_size_sectors:MPSZSB— almost always 1.
- Parameters:
- class oaknut.afs.AfsDate(date)¶
Packed 16-bit AFS creation date.
The on-disc format is the Acorn file-server “RISC OS / old-format” date, stored little-endian as two bytes. The bit layout is:
Bits
Field
0-4
Day (1..31)
5-7
High 3 bits of year delta
8-11
Month (1..12)
12-15
Low 4 bits of year delta
where
year_delta = year - 1981(so year 1981 has delta 0) and is a 7-bit value covering 1981..2108.This packing is equivalent to WFSINIT’s formula (
WFSINIT.basline 4890 ff.):encoded = ((year-81) * 4096) + (month * 256) + day + ((year-81) AND &F0) * 2 stored = encoded AND &FFFF
The high-nibble-shifted-left-1 term is how the low 4 bits of the year delta end up in bits 12-15 while the high 3 bits land in bits 5-7. WFSINIT lets the value overflow 16 bits; only the low 16 bits are actually written. We mask explicitly for clarity.
Verified against the Beebmaster PDF test disc:
8/8/2010round-trips to0xD828.This class holds a Python
datetime.dateand (de)serialises on demand — we do not store the packed form, so every operation sees a plain date.- Parameters:
date (date)
- oaknut.afs.Cylinder = oaknut.afs.types.Cylinder¶
NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage:
UserId = NewType('UserId', int) def name_by_id(user_id: UserId) -> str: ... UserId('user') # Fails type check name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK num = UserId(5) + 1 # type: int
- oaknut.afs.Sector = oaknut.afs.types.Sector¶
NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage:
UserId = NewType('UserId', int) def name_by_id(user_id: UserId) -> str: ... UserId('user') # Fails type check name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK num = UserId(5) + 1 # type: int
- oaknut.afs.SystemInternalName = oaknut.afs.types.SystemInternalName¶
NewType creates simple unique types with almost zero runtime overhead.
NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.
Usage:
UserId = NewType('UserId', int) def name_by_id(user_id: UserId) -> str: ... UserId('user') # Fails type check name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK num = UserId(5) + 1 # type: int
Errors¶
The AFS exception hierarchy. AFSError is the base; every other AFS
error inherits from it. The broader error model and the
handled_errors boundary are covered in Error handling.
- exception oaknut.afs.AFSError(*args, exit_code=None)¶
Base exception for all AFS errors.
- exception oaknut.afs.AFSNotPresentError(*args, exit_code=None)¶
Raised when a caller asks for AFS on a disc that has no AFS pointers.
- exception oaknut.afs.AFSAccessDeniedError(*args, exit_code=None)¶
Acting user lacks permission for the requested operation.
- exception oaknut.afs.AFSAlreadyPartitionedError(*args, exit_code=None)¶
Refused: the disc already contains AFS pointers at &F6/&1F6.
- exception oaknut.afs.AFSBrokenDirectoryError(*args, exit_code=None)¶
Master-sequence-number mismatch on a directory object.
The leading and trailing master-sequence bytes do not agree, which the file server reports as FS error &42 (“Broken Directory”). This typically means a write to the directory was interrupted.
- exception oaknut.afs.AFSBrokenMapError(*args, exit_code=None)¶
Invalid
JesMapmagic or sequence-number mismatch.
- exception oaknut.afs.AFSDirectoryEntryExistsError(*args, exit_code=None)¶
An entry with the same name already exists in the directory.
- exception oaknut.afs.AFSDirectoryEntryNotFoundError(*args, exit_code=None)¶
No entry with the requested name exists in the directory.
Corresponds to the server’s
DRERRCerror code returned byFNDTEXatUade0D:249when the walk reaches the end of the in-use list without a match.
- exception oaknut.afs.AFSDirectoryFullError(*args, exit_code=None)¶
Cannot insert into a directory whose free list is empty.
The Level 3 File Server handles this by growing the directory automatically via
CHZSZE(Uade0E:1198). The oaknut-afs write path currently raises this error when growth would be required — phase 10 will add automatic growth, matching the ROM.
- exception oaknut.afs.AFSDirectoryNotEmptyError(*args, exit_code=None)¶
Cannot remove a directory that still has entries.
Raised by
rmdir/unlinkon a non-empty sub-directory, matchingDELCHKatUade0D:1218+(DRERRJ).
- exception oaknut.afs.AFSDiscNameError(*args, exit_code=None)¶
The proposed AFS disc name is empty, too long, or contains non-printable / space characters (printable ASCII only, 1..16 chars).
- exception oaknut.afs.AFSDiscNotCompactedError(*args, exit_code=None)¶
Refused: the ADFS free list is fragmented and compaction is disabled.
Raised only when
partition.plan(..., compact_adfs=False). Passcompact_adfs=True(the default) to have the repartitioner runADFS.compact()first.
- exception oaknut.afs.AFSFileLockedError(*args, exit_code=None)¶
Operation refused because the object’s
Lbit is set.
- exception oaknut.afs.AFSFormatError(*args, exit_code=None)¶
Malformed on-disc AFS structure.
- exception oaknut.afs.AFSHostImportError(*args, exit_code=None)¶
import_host_treefailed to read or translate a host-side file.
- exception oaknut.afs.AFSInfoSectorError(*args, exit_code=None)¶
Invalid
AFS0magic or redundancy mismatch between info sectors.
- exception oaknut.afs.AFSInitSpecError(*args, exit_code=None)¶
Base for InitSpec / UserSpec validation failures.
- exception oaknut.afs.AFSInsufficientADFSSpaceError(*args, exit_code=None)¶
Refused: the requested AFS size would leave too little space for ADFS.
- exception oaknut.afs.AFSInsufficientSpaceError(*args, exit_code=None)¶
The allocator cannot satisfy a request from the free space pool.
- exception oaknut.afs.AFSMergeConflictError(*args, exit_code=None)¶
A target name already exists and the merge policy is
"error".
- exception oaknut.afs.AFSNewMapNotSupportedError(*args, exit_code=None)¶
Refused: the ADFS disc uses the new-map format.
v1 of
oaknut-afssupports old-map (S/M/L/D-style) ADFS hosts only. The new-map formats (E/E+/F/F+) use the reserved bytes differently and do not carry AFS pointers.
- exception oaknut.afs.AFSPasswordError(*args, exit_code=None)¶
A password is not ASCII or exceeds 6 characters.
- exception oaknut.afs.AFSPathError(*args, exit_code=None)¶
Path syntax error or non-existent object.
Covers invalid file titles, bad separators, paths that traverse through something that isn’t a directory, and
$.foowherefoodoes not exist.
- exception oaknut.afs.AFSQuotaError(*args, exit_code=None)¶
A quota (per-user or default) is outside 0..0xFFFFFFFF.
- exception oaknut.afs.AFSQuotaExceededError(*args, exit_code=None)¶
The acting user does not have enough quota for this operation.
- exception oaknut.afs.AFSRepartitionError(*args, exit_code=None)¶
Base for repartitioning failures.