Phase 1: enrich 07_dos and 10_devices (highest FPGA priority)

07_dos:
- file_io.md: 108→240+ lines — buffered I/O (FRead/FWrite/SetVBuf),
  access mode comparison, FileHandle struct with offsets, standard
  handles, Printf %ld warning, FileInfoBlock, practical patterns
  (copy file, get size, load to RAM), error code table
- filesystem.md: 91→270+ lines — full disk geometry (ADF/HDF),
  all 8 DOS\x filesystem IDs, root block byte-level layout, file
  header layout with reverse-order pointer quirk, OFS vs FFS data
  blocks with efficiency numbers, bitmap blocks, extension blocks,
  checksum algorithm, Python ADF reader
- locks_examine.md: 113→270+ lines — lock semantics diagram, FileLock
  struct with handler discovery, ExAll bulk scan, practical patterns
  (atomic write, path resolution, volume info), 4 antipatterns
  (leaked locks, exclusive too long, unchecked IoErr, DupLock),
  pattern matching

10_devices:
- audio.md: 73→240+ lines — hardware architecture diagram, channel
  registers with offsets, period/frequency table, priority allocation,
  double-buffering, audio interrupts, AM/PM modulation, direct HW
- timer.md: 80→230+ lines — CIA timer hardware, all 5 units with
  decision flowchart, non-blocking delays, signal-based waiting,
  time arithmetic, ReadEClock, periodic game loop pattern, pitfalls
- trackdisk.md: 82→210+ lines — MFM encoding, track format, disk
  geometry, read/write/motor, change notification, track caching,
  direct hardware access, FPGA timing implications
- keyboard.md: 58→220+ lines — CIA-A serial handshake protocol with
  sequence diagram, bit rotation quirk, complete key code map,
  key matrix bitmap, reset sequence, FPGA notes
This commit is contained in:
Ilia Sharin 2026-04-23 20:23:50 -04:00
parent aeaea88d75
commit da9e7d3b63
7 changed files with 1599 additions and 301 deletions

View file

@ -1,10 +1,10 @@
[← Home](../README.md) · [AmigaDOS](README.md)
# File I/O — Open, Close, Read, Write, Seek
# File I/O — Open, Close, Read, Write, Seek, Async I/O
## Overview
AmigaDOS file I/O is synchronous from the caller's perspective. All functions use `BPTR` file handles and communicate errors via `IoErr()`.
AmigaDOS file I/O is synchronous from the caller's perspective — each call blocks until the filesystem handler completes the operation. All functions use `BPTR` file handles and communicate errors via `IoErr()`. Internally, every I/O call is translated into a DosPacket sent to the filesystem handler process (see [packet_system.md](packet_system.md)).
---
@ -17,6 +17,12 @@ AmigaDOS file I/O is synchronous from the caller's perspective. All functions us
| 42 | `Read(fh, buf, len)` | D1=handle, D2=buf, D3=len | D0=actual bytes (1=error) |
| 48 | `Write(fh, buf, len)` | D1=handle, D2=buf, D3=len | D0=actual bytes (1=error) |
| 66 | `Seek(fh, pos, mode)` | D1=handle, D2=pos, D3=mode | D0=old position (1=error) |
| 330 | `FRead(fh, buf, bsize, n)` | D1D4 | D0=blocks read (buffered) |
| 336 | `FWrite(fh, buf, bsize, n)` | D1D4 | D0=blocks written (buffered) |
| 348 | `FGets(fh, buf, len)` | D1D3 | D0=buf or NULL |
| 354 | `FPuts(fh, str)` | D1D2 | D0=0 or EOF |
| 360 | `Flush(fh)` | D1=handle | D0=BOOL |
| 366 | `SetVBuf(fh, buf, type, size)` | D1D4 | D0=0 or 1 |
---
@ -29,6 +35,12 @@ AmigaDOS file I/O is synchronous from the caller's perspective. All functions us
#define MODE_NEWFILE 1006 /* create new (truncate if exists) */
```
| Mode | If file exists | If file doesn't exist |
|---|---|---|
| `MODE_OLDFILE` | Opens for reading | Fails — `IoErr() = ERROR_OBJECT_NOT_FOUND` |
| `MODE_NEWFILE` | Truncates to 0, opens for writing | Creates new file |
| `MODE_READWRITE` | Opens at current size, read+write | Creates new file |
---
## Seek Modes
@ -39,49 +51,192 @@ AmigaDOS file I/O is synchronous from the caller's perspective. All functions us
#define OFFSET_END 1 /* from end of file */
```
> **Note**: `Seek()` returns the **old position** (before seeking), not the new one. To get file size: `Seek(fh, 0, OFFSET_END)` then `size = Seek(fh, 0, OFFSET_BEGINNING)`.
---
## File Handle (BPTR)
## File Handle Structure
The returned handle is a BPTR to a `struct FileHandle`:
```c
/* dos/dosextens.h */
struct FileHandle {
struct Message *fh_Link;
struct MsgPort *fh_Interactive; /* non-NULL if console */
struct MsgPort *fh_Type; /* handler process port */
BPTR fh_Buf; /* I/O buffer (BPTR) */
LONG fh_Pos; /* current position in buffer */
LONG fh_End; /* end of valid data in buffer */
LONG fh_Funcs; /* unused */
LONG fh_Func2; /* unused */
LONG fh_Func3; /* unused */
LONG fh_Args; /* packet args */
BPTR fh_Arg2;
struct Message *fh_Link; /* +$00: exec message for reply */
struct MsgPort *fh_Interactive; /* +$04: non-NULL if console device */
struct MsgPort *fh_Type; /* +$08: handler process MsgPort */
BPTR fh_Buf; /* +$0C: I/O buffer (BPTR) */
LONG fh_Pos; /* +$10: current position in buffer */
LONG fh_End; /* +$14: end of valid data in buffer */
LONG fh_Funcs; /* +$18: unused (was BCPL function) */
LONG fh_Func2; /* +$1C: unused */
LONG fh_Func3; /* +$20: unused */
LONG fh_Args; /* +$24: FH_Arg1 — passed to handler */
BPTR fh_Arg2; /* +$28: handler-specific */
};
/* sizeof(struct FileHandle) = 44 bytes ($2C) */
```
To access as a C pointer: `struct FileHandle *fh = BADDR(handle);`
```c
/* To access as a C pointer: */
struct FileHandle *fhp = BADDR(handle); /* BADDR = (ptr << 2) */
/* Check if interactive (console): */
if (IsInteractive(fh))
Printf("Connected to a console\n");
```
### I/O Buffering
AmigaDOS uses **per-handle buffering** (OS 2.0+). Default buffer is 512 bytes. Control with `SetVBuf()`:
```c
/* Make a file fully buffered with 8 KB buffer: */
SetVBuf(fh, NULL, BUF_FULL, 8192);
/* Line-buffered (flush on newline — useful for consoles): */
SetVBuf(fh, NULL, BUF_LINE, 1024);
/* Unbuffered (every write goes to handler immediately): */
SetVBuf(fh, NULL, BUF_NONE, 0);
/* Manually flush: */
Flush(fh);
```
| Buffer Type | Constant | Behaviour |
|---|---|---|
| `BUF_FULL` | 1 | Flush when buffer fills |
| `BUF_LINE` | 0 | Flush on newline or buffer full |
| `BUF_NONE` | 1 | No buffering — direct I/O |
---
## Usage Example
## Standard I/O Handles
```c
BPTR fh = Open("RAM:test.txt", MODE_NEWFILE);
if (fh) {
Write(fh, "Hello Amiga\n", 12);
Close(fh);
}
/* Get the current process's stdin/stdout/stderr: */
BPTR in = Input(); /* stdin — from CLI or WB */
BPTR out = Output(); /* stdout */
BPTR err = ErrorOutput(); /* stderr (OS 3.0+ only) */
fh = Open("RAM:test.txt", MODE_OLDFILE);
if (fh) {
UBYTE buf[64];
LONG n = Read(fh, buf, sizeof(buf));
if (n > 0) Write(Output(), buf, n); /* echo to stdout */
/* Write to stdout: */
FPuts(Output(), "Hello from AmigaDOS\n");
/* Printf — formatted output to stdout: */
Printf("Count: %ld, Name: %s\n", count, name);
/* Note: Printf uses %ld (not %d) — AmigaDOS always uses LONG */
```
> [!IMPORTANT]
> AmigaDOS `Printf` uses `%ld` for integers, not `%d`. The `%d` format is undefined. This is a common bug source when porting from Unix/ANSI C.
---
## File Information — ExamineFH, ExAll
```c
/* Get file metadata from a handle: */
struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL);
if (ExamineFH(fh, fib))
{
Printf("Name: %s\n", fib->fib_FileName);
Printf("Size: %ld\n", fib->fib_Size);
Printf("Type: %ld\n", fib->fib_DirEntryType);
/* > 0 = directory, < 0 = file */
}
FreeDosObject(DOS_FIB, fib);
```
### FileInfoBlock Structure
```c
struct FileInfoBlock {
LONG fib_DiskKey; /* block number on disk */
LONG fib_DirEntryType; /* >0=dir, <0=file */
char fib_FileName[108]; /* null-terminated name */
LONG fib_Protection; /* RWED protection bits */
LONG fib_EntryType; /* same as DirEntryType */
LONG fib_Size; /* file size in bytes */
LONG fib_NumBlocks; /* blocks consumed */
struct DateStamp fib_Date; /* modification date */
char fib_Comment[80]; /* file comment string */
UWORD fib_OwnerUID; /* owner (multiuser) */
UWORD fib_OwnerGID; /* group */
};
```
---
## Practical Patterns
### Copy a File
```c
BOOL CopyFile(CONST_STRPTR src, CONST_STRPTR dst)
{
BPTR in = Open(src, MODE_OLDFILE);
if (!in) return FALSE;
BPTR out = Open(dst, MODE_NEWFILE);
if (!out) { Close(in); return FALSE; }
UBYTE buf[4096];
LONG n;
while ((n = Read(in, buf, sizeof(buf))) > 0)
{
if (Write(out, buf, n) != n)
{
Close(in); Close(out);
return FALSE; /* write error */
}
}
Close(out);
Close(in);
return (n == 0); /* 0=EOF=success, -1=error */
}
```
### Determine File Size
```c
LONG GetFileSize(BPTR fh)
{
LONG oldpos = Seek(fh, 0, OFFSET_END); /* seek to end */
LONG size = Seek(fh, oldpos, OFFSET_BEGINNING); /* seek back */
return size;
}
/* Or with ExamineFH (no seeking needed): */
struct FileInfoBlock fib;
ExamineFH(fh, &fib);
LONG size = fib.fib_Size;
```
### Read Entire File into Memory
```c
APTR LoadFileToRAM(CONST_STRPTR path, ULONG *sizeOut)
{
BPTR fh = Open(path, MODE_OLDFILE);
if (!fh) return NULL;
/* Get size */
Seek(fh, 0, OFFSET_END);
LONG size = Seek(fh, 0, OFFSET_BEGINNING);
APTR buf = AllocVec(size, MEMF_ANY);
if (buf)
{
if (Read(fh, buf, size) != size)
{
FreeVec(buf);
buf = NULL;
}
}
Close(fh);
} else {
PrintFault(IoErr(), "Open failed");
*sizeOut = size;
return buf;
}
```
@ -90,18 +245,41 @@ if (fh) {
## Error Checking
```c
LONG err = IoErr(); /* LVO 66 — returns last DOS error code */
/* Common codes: */
#define ERROR_OBJECT_NOT_FOUND 205
#define ERROR_OBJECT_EXISTS 203
#define ERROR_DISK_FULL 221
#define ERROR_SEEK_ERROR 219
LONG err = IoErr(); /* returns last DOS error code */
/* Human-readable error message: */
PrintFault(err, "Operation failed");
/* Output: "Operation failed: object not found" */
/* Or get error string into buffer: */
Fault(err, "Error", buf, sizeof(buf));
```
### Common Error Codes
| Code | Constant | Meaning |
|---|---|---|
| 103 | `ERROR_NO_FREE_STORE` | Out of memory |
| 202 | `ERROR_OBJECT_IN_USE` | File is locked by another process |
| 203 | `ERROR_OBJECT_EXISTS` | File already exists |
| 204 | `ERROR_DIR_NOT_FOUND` | Path component not found |
| 205 | `ERROR_OBJECT_NOT_FOUND` | File not found |
| 209 | `ERROR_ACTION_NOT_KNOWN` | Handler doesn't support this action |
| 210 | `ERROR_INVALID_COMPONENT_NAME` | Bad filename character |
| 212 | `ERROR_OBJECT_WRONG_TYPE` | Expected file, got directory (or vice versa) |
| 214 | `ERROR_DISK_FULL` | No space on volume |
| 216 | `ERROR_DELETE_PROTECTED` | File has delete-protection bit |
| 218 | `ERROR_WRITE_PROTECTED` | Disk is write-protected |
| 219 | `ERROR_SEEK_ERROR` | Invalid seek position |
| 221 | `ERROR_DISK_FULL` | Volume full |
| 225 | `ERROR_NOT_A_DOS_DISK` | Unrecognised filesystem |
| 232 | `ERROR_NO_MORE_ENTRIES` | ExNext — end of directory |
---
## References
- NDK39: `dos/dos.h`, `dos/dosextens.h`
- NDK39: `dos/dos.h`, `dos/dosextens.h`, `dos/stdio.h`
- ADCD 2.1: `Open`, `Close`, `Read`, `Write`, `Seek`, `SetVBuf`, `FRead`
- [error_handling.md](error_handling.md) — full error code list
- ADCD 2.1: `Open`, `Close`, `Read`, `Write`, `Seek`
- [packet_system.md](packet_system.md) — how I/O translates to handler packets

View file

@ -1,84 +1,296 @@
[← Home](../README.md) · [AmigaDOS](README.md)
# Filesystem — FFS/OFS Block Structure
# Filesystem — FFS/OFS Block Structure and Disk Layout
## Overview
AmigaOS supports two native filesystem types: **OFS** (Old File System, OS 1.x) and **FFS** (Fast File System, OS 2.0+). Both use a block-based layout with 512-byte blocks. FFS differs by storing data blocks without headers, improving throughput.
AmigaOS supports two native filesystem types: **OFS** (Old File System, OS 1.x) and **FFS** (Fast File System, OS 2.0+). Both use a block-based layout with 512-byte blocks. FFS differs by storing data blocks without headers, improving throughput. Understanding the on-disk layout is essential for FPGA core developers implementing virtual filesystems, HDF support, and ADF image handling.
---
## Disk Geometry and Layout
### ADF (Amiga Disk File) — Floppy Disk
```
Total: 880 KB = 1,760 sectors = 80 tracks × 2 sides × 11 sectors
Block size: 512 bytes
Blocks 01: Boot block (1 KB)
Block 880: Root block (always at disk midpoint)
```
### HDF (Hard Disk File) — Hard Drive
```
Block 0: Reserved (or RDB — Rigid Disk Block)
Block 1+: RDB partition table (if present)
Partition: Starts at first block of partition
Root block at partition midpoint
```
---
## Block Types
| Block | Type ID | Description |
|---|---|---|
| Boot block | `"DOS\0"` / `"DOS\1"` | Blocks 01; OFS=`DOS\0`, FFS=`DOS\1` |
| Root block | `T_HEADER` (2) | Always at middle of partition; directory root |
| File header | `T_HEADER` (2) | Metadata for one file |
| Directory header | `T_HEADER` (2) | Metadata for one directory |
| Data block | `T_DATA` (8) | OFS: header + data; FFS: pure data |
| File extension | `T_LIST` (16) | Overflow pointer table for large files |
| Hash chain | — | Root/dir blocks have a 72-entry hash table |
| Type | ID | Constant | Purpose |
|---|---|---|---|
| Boot block | — | (bytes 01023) | Boot code + filesystem ID (`DOS\0` to `DOS\7`) |
| Root block | 2 | `T_HEADER` | Volume root directory — always at midpoint |
| File header | 2 | `T_HEADER` | File metadata + first 72 data block pointers |
| Directory | 2 | `T_HEADER` | Subdirectory — contains 72-slot hash table |
| Data block | 8 | `T_DATA` | OFS: 24-byte header + 488 data bytes |
| Extension | 16 | `T_LIST` | Overflow block pointers for files >72 blocks |
| Bitmap block | — | — | Free/allocated block tracking |
---
## Root Block Layout (Simplified)
## Boot Block (Blocks 01)
| Offset | Field | Description |
```
Offset Size Field
────── ──── ─────────────────────
$00 4 Filesystem ID — "DOS\0" to "DOS\7"
$04 4 Checksum
$08 4 Root block number (usually 880)
$0C 1012 Boot code (optional — loaded by ROM bootstrap)
```
### Filesystem ID Variants
| ID | Hex | Description |
|---|---|---|
| 0 | `type` | Always 2 (`T_HEADER`) |
| 4 | `header_key` | Own block number |
| 8 | `high_seq` | Number of data blocks in hash table |
| 12 | `ht_size` | Hash table size (usually 72) |
| 16 | `first_data` | Unused |
| 20 | `checksum` | Block checksum |
| 24312 | `ht[72]` | Hash table: block pointers for directory entries |
| 420 | `bm_flag` | Bitmap valid flag (`-1` = valid) |
| 424472 | `bm_pages[25]` | Pointers to bitmap blocks |
| 484 | `last_altered_days` | Modification date |
| 504 | `disk_name` | BSTR: volume name |
| `DOS\0` | $444F5300 | OFS (original) |
| `DOS\1` | $444F5301 | FFS (fast file system) |
| `DOS\2` | $444F5302 | OFS + International mode (case-insensitive) |
| `DOS\3` | $444F5303 | FFS + International mode |
| `DOS\4` | $444F5304 | FFS + Directory cache |
| `DOS\5` | $444F5305 | OFS + International + Directory cache |
| `DOS\6` | $444F5306 | OFS + Long filenames (OS 3.5+) |
| `DOS\7` | $444F5307 | FFS + Long filenames (OS 3.5+) |
---
## Root Block (Block 880 on Floppy)
The root block is the entry point for the entire filesystem. It always lives at the midpoint of the partition:
```
root_block_number = (total_blocks) / 2 /* e.g., 1760/2 = 880 */
```
### Root Block Layout (512 bytes)
```
Offset Size Field Description
────── ────── ────────────────────── ────────────────────────────────
$000 4 type Always 2 (T_HEADER)
$004 4 header_key Own block number
$008 4 high_seq 0 (unused for root)
$00C 4 ht_size Hash table size (72 for DD floppy)
$010 4 first_data 0 (unused)
$014 4 checksum Block checksum
$018 288 ht[72] Hash table: LONG[72] block pointers
for directory entries (0 = empty slot)
$138 4 bm_flag Bitmap valid flag (-1 = valid)
$13C 100 bm_pages[25] LONG[25] pointers to bitmap blocks
$1A0 4 bm_ext Pointer to extended bitmap block
$1A4 12 last_root_alteration DateStamp: root last modified
$1B0 32 disk_name BSTR: volume name (length-prefixed)
$1D0 4 (reserved)
$1D4 12 last_disk_alteration DateStamp: disk last modified
$1E0 12 creation_date DateStamp: volume creation date
$1EC 4 (reserved)
$1F0 4 extension 0 (unused for root)
$1F4 4 sec_type 1 = ST_ROOT
$1F8 4 (reserved)
$1FC ← end of 512-byte block
```
---
## Hash Function
File/directory names are hashed into the 72-slot table:
File and directory names are hashed into the hash table slots:
```c
ULONG hash_name(const char *name, int table_size) {
ULONG hash = strlen(name);
for (int i = 0; name[i]; i++) {
hash = hash * 13 + toupper(name[i]);
hash &= 0x7FF;
/* The canonical AmigaDOS hash function: */
ULONG HashName(const char *name, ULONG table_size)
{
ULONG hash = (ULONG)strlen(name);
for (int i = 0; name[i]; i++)
{
hash = (hash * 13 + toupper((unsigned char)name[i])) & 0x7FF;
}
return hash % table_size;
}
/* table_size = 72 for DD floppy, 128 for HD floppy */
```
Collisions are resolved by chaining: each file/dir header has a `hash_chain` pointer to the next entry in the same slot.
### Hash Collision Resolution
Collisions are resolved by **chaining**: each file/directory header has a `hash_chain` pointer (at offset `$1F0`) linking to the next entry that hashed to the same slot. The chain ends with 0.
```
Hash Table (root block):
Slot 0: → 0 (empty)
Slot 1: → Block 950 ("Startup-Sequence")
└→ Block 1100 ("System-Startup") ← hash_chain
└→ 0 (end)
Slot 2: → Block 882 ("Libs")
...
```
---
## OFS vs FFS
## File Header Block
| Feature | OFS (`DOS\0`) | FFS (`DOS\1`) |
```
Offset Size Field Description
────── ────── ────────────────────── ──────────────────────────────
$000 4 type 2 (T_HEADER)
$004 4 header_key Own block number
$008 4 high_seq Number of data block pointers stored here
$00C 4 data_size 0 (unused in file header)
$010 4 first_data First data block (OFS only — FFS uses table)
$014 4 checksum
$018 288 data_blocks[72] LONG[72] — block pointers to data blocks
Stored in REVERSE ORDER: [71]=first, [0]=last
$138 — ... (padding/reserved)
$144 4 protect Protection bits (RWED)
$148 4 byte_size File size in bytes
$14C 80 comment BSTR: file comment
$19C 12 date DateStamp: file modification date
$1A8 32 filename BSTR: file name (length-prefixed)
$1D0 4 real_entry For hard links: the real file header
$1D4 4 next_link For hard links: next link to same file
$1EC 4 hash_chain Next entry in same hash slot (0 = end)
$1F0 4 parent Block number of parent directory
$1F4 4 extension Block number of extension block (0 = none)
$1F8 4 sec_type -3 = ST_FILE
```
> **Reverse order**: Data block pointers in `data_blocks[]` are stored **last-to-first**. Index 71 points to the first data block, index 70 to the second, etc. This is a BCPL heritage quirk.
---
## OFS vs FFS — Data Block Differences
### OFS Data Block (T_DATA = 8)
```
$000 4 type 8 (T_DATA)
$004 4 header_key Pointer back to file header
$008 4 seq_num Sequence number (1-based)
$00C 4 data_size Bytes of valid data in this block
$010 4 next_data Next data block (0 = last)
$014 4 checksum
$018 488 data[488] Actual file data (488 usable bytes)
```
**Efficiency**: 488 / 512 = **95.3%** — 24 bytes wasted per block on headers.
### FFS Data Block
```
$000 512 data[512] Pure file data — no header overhead
```
**Efficiency**: 512 / 512 = **100%**
| Feature | OFS | FFS |
|---|---|---|
| Data blocks | 24-byte header + 488 bytes data | Pure 512 bytes data |
| Max filename | 30 chars | 30 chars |
| International | No | `DOS\2` (INTL OFS), `DOS\3` (INTL FFS) |
| Dir cache | No | `DOS\4` (FFS + dir cache) |
| Throughput | ~488/512 = 95% efficiency | 100% efficiency |
| Bytes per data block | 488 | 512 |
| Header overhead | 24 bytes/block | 0 |
| Self-describing blocks | Yes (can recover from corruption) | No |
| Max filename | 30 chars | 30 chars (107 with DOS\6/\7) |
| Throughput | ~5% slower | Baseline |
| International mode | DOS\2 | DOS\3 |
| Directory cache | No | DOS\4 |
| Min OS version | 1.0 | 2.0 |
---
## Bitmap Blocks — Free Space Tracking
The bitmap tracks which blocks are free (1) or allocated (0):
```c
/* Each bitmap block covers up to (512-4) × 8 = 4064 blocks */
struct BitmapBlock {
ULONG checksum; /* block checksum */
ULONG map[127]; /* bit 1 = free, bit 0 = allocated */
/* bit 0 of map[0] = block corresponding to this bitmap's range */
};
```
The root block's `bm_pages[25]` array can reference up to 25 bitmap blocks, covering 25 × 4064 = **101,600 blocks** (≈49 MB). Larger partitions need `bm_ext` extension blocks.
---
## Checksum Algorithm
```c
LONG compute_checksum(ULONG *block, int longs) {
LONG ComputeBlockChecksum(ULONG *block, LONG longs)
{
LONG sum = 0;
for (int i = 0; i < longs; i++) sum += block[i];
return -sum; /* stored in the checksum field to make total = 0 */
block[5] = 0; /* clear checksum field before computing */
for (int i = 0; i < longs; i++)
sum += block[i];
return -sum; /* store at block[5] so total = 0 */
}
/* Verify: if sum of all 128 longs (incl. checksum) = 0, block is valid */
```
---
## File Extension Blocks
Files larger than 72 data blocks need extension blocks to store additional pointers:
```
extension_block.data_blocks[72] → next 72 data block pointers
extension_block.extension → next extension block (or 0)
```
Each extension block adds 72 more data block pointers. With FFS (512 bytes/block):
- 72 blocks = 36 KB directly in file header
- +72 per extension = 36 KB more per extension
- Maximum chain depth is effectively unlimited
---
## Practical: Reading an ADF Image
```python
import struct
def read_adf(filename):
with open(filename, 'rb') as f:
data = f.read()
# Boot block — filesystem type
fs_type = data[0:4]
print(f"Filesystem: {fs_type}") # b'DOS\x00' = OFS, b'DOS\x01' = FFS
# Root block at block 880 (offset 880 * 512 = 450560)
root_off = 880 * 512
root = data[root_off:root_off + 512]
# Volume name (BSTR at offset $1B0)
name_len = root[0x1B0]
vol_name = root[0x1B1:0x1B1 + name_len].decode('ascii')
print(f"Volume: {vol_name}")
# Hash table: 72 entries starting at offset $018
ht = struct.unpack('>72I', root[0x18:0x18 + 72 * 4])
for i, blk in enumerate(ht):
if blk != 0:
# Read the file/dir header at that block
hdr_off = blk * 512
hdr = data[hdr_off:hdr_off + 512]
fname_len = hdr[0x1A8]
fname = hdr[0x1A9:0x1A9 + fname_len].decode('ascii')
sec_type = struct.unpack('>i', hdr[0x1F4:0x1F8])[0]
kind = "DIR" if sec_type == 2 else "FILE"
print(f" [{i:2d}] {kind} {fname} (block {blk})")
```
---
@ -87,4 +299,6 @@ LONG compute_checksum(ULONG *block, int longs) {
- NDK39: `dos/filehandler.h`
- Ralph Babel: *The AmigaDOS Manual* (3rd edition) — definitive FFS reference
- Laurent Clevy: *The Amiga Filesystem* — http://lclevy.free.fr/adflib/
- Laurent Clévy: *The Amiga Filesystem* — http://lclevy.free.fr/adflib/
- See also: [packet_system.md](packet_system.md) — filesystem handler protocol
- See also: [locks_examine.md](locks_examine.md) — lock/examine API layer

View file

@ -1,126 +1,382 @@
[← Home](../README.md) · [AmigaDOS](README.md)
# Locks and Examine — Lock, UnLock, Examine, ExNext, ExAll
# Locks, Examine, and Directory Scanning
## Overview
AmigaDOS uses **locks** to reference files and directories. A lock is a BPTR to a `FileLock` structure. Locks provide exclusive or shared access and are used for directory scanning, attribute reading, and path resolution.
A **Lock** is AmigaDOS's equivalent of a Unix file descriptor for directory operations. It represents a reference to a filesystem object (file or directory) and is the primary mechanism for navigating the filesystem, examining metadata, and preventing concurrent modifications. Understanding lock semantics — especially the shared/exclusive model — is critical for writing robust AmigaOS applications.
---
## Lock Types
```c
/* dos/dos.h */
#define SHARED_LOCK -2 /* read-only; multiple readers allowed */
#define ACCESS_READ SHARED_LOCK
#define EXCLUSIVE_LOCK -1 /* read/write; only one holder */
#define ACCESS_WRITE EXCLUSIVE_LOCK
```
---
## Core Functions
| LVO | Function | Registers | Returns |
| Mode | Constant | Dec | Meaning |
|---|---|---|---|
| 84 | `Lock(name, mode)` | D1=name, D2=mode | D0=lock BPTR (0=fail) |
| 90 | `UnLock(lock)` | D1=lock | — |
| 96 | `DupLock(lock)` | D1=lock | D0=new lock |
| 102 | `Examine(lock, fib)` | D1=lock, D2=fib | D0=BOOL |
| 108 | `ExNext(lock, fib)` | D1=lock, D2=fib | D0=BOOL |
| 78 | `CurrentDir(lock)` | D1=lock | D0=old lock |
| 654 | `ExAll(lock, buf, size, type, ctrl)` | D1D5 | D0=BOOL |
---
## struct FileInfoBlock
| Shared | `SHARED_LOCK` / `ACCESS_READ` | 2 | Multiple readers allowed simultaneously |
| Exclusive | `EXCLUSIVE_LOCK` / `ACCESS_WRITE` | 1 | Only one holder; blocks all other locks |
```c
/* dos/dos.h — NDK39 */
struct FileInfoBlock {
LONG fib_DiskKey; /* handler-private key */
LONG fib_DirEntryType; /* >0 = directory, <0 = file */
char fib_FileName[108]; /* null-terminated name */
LONG fib_Protection; /* rwed bits */
LONG fib_EntryType; /* same as DirEntryType */
LONG fib_Size; /* file size in bytes */
LONG fib_NumBlocks; /* blocks used */
struct DateStamp fib_Date; /* modification date */
char fib_Comment[80]; /* file comment string */
UWORD fib_OwnerUID;
UWORD fib_OwnerGID;
char fib_Reserved[32];
};
```
/* Obtain a shared lock: */
BPTR lock = Lock("SYS:Libs", SHARED_LOCK);
> [!IMPORTANT]
> `FileInfoBlock` must be longword-aligned. Use `AllocDosObject(DOS_FIB, NULL)` on OS 2.0+ or `AllocMem(sizeof(struct FileInfoBlock), MEMF_PUBLIC)`.
/* Obtain an exclusive lock: */
BPTR lock = Lock("RAM:temp.dat", EXCLUSIVE_LOCK);
---
## Protection Bits
```c
/* dos/dos.h */
#define FIBF_SCRIPT (1<<6) /* s script (executable script) */
#define FIBF_PURE (1<<5) /* p pure (re-entrant) */
#define FIBF_ARCHIVE (1<<4) /* a archived */
#define FIBF_READ (1<<3) /* r readable (0=allowed, 1=denied!) */
#define FIBF_WRITE (1<<2) /* w writable */
#define FIBF_EXECUTE (1<<1) /* e executable */
#define FIBF_DELETE (1<<0) /* d deletable */
```
> [!WARNING]
> Amiga protection bits are **inverted** from Unix: bit SET means access is **denied**.
---
## Directory Scanning
```c
BPTR lock = Lock("SYS:", SHARED_LOCK);
struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL);
if (Examine(lock, fib)) { /* read dir's own info */
while (ExNext(lock, fib)) { /* iterate entries */
Printf("%-30s %8ld %s\n",
fib->fib_FileName,
fib->fib_Size,
fib->fib_DirEntryType > 0 ? "(dir)" : "");
}
/* ExNext returns FALSE when done; IoErr() == ERROR_NO_MORE_ENTRIES */
}
FreeDosObject(DOS_FIB, fib);
/* Always unlock when done: */
UnLock(lock);
```
### Lock Semantics
```mermaid
graph TD
subgraph "Shared Lock Active"
S1["Process A: SHARED"] --> OK1["Process B: SHARED → OK"]
S1 --> FAIL1["Process C: EXCLUSIVE → FAILS"]
end
subgraph "Exclusive Lock Active"
E1["Process A: EXCLUSIVE"] --> FAIL2["Process B: SHARED → FAILS"]
E1 --> FAIL3["Process C: EXCLUSIVE → FAILS"]
end
```
| Scenario | Result | Error Code |
|---|---|---|
| Shared + Shared | Both succeed | — |
| Shared + Exclusive | Exclusive fails | `ERROR_OBJECT_IN_USE` (202) |
| Exclusive + Shared | Shared fails | `ERROR_OBJECT_IN_USE` (202) |
| Exclusive + Exclusive | Second fails | `ERROR_OBJECT_IN_USE` (202) |
> [!IMPORTANT]
> `Lock()` is **non-blocking** — it returns immediately with 0 and sets `IoErr()` if the lock cannot be obtained. There is no "wait for lock" mechanism in AmigaDOS. You must poll or retry.
---
## ExAll (OS 2.0+) — Bulk Scan
## Lock Internals — struct FileLock
```c
/* dos/dosextens.h */
struct FileLock {
BPTR fl_Link; /* BPTR to next lock (handler's list) */
LONG fl_Key; /* block number on disk (handler-specific) */
LONG fl_Access; /* SHARED_LOCK or EXCLUSIVE_LOCK */
struct MsgPort *fl_Task; /* handler's MsgPort — THIS IS HOW YOU
FIND THE FILE HANDLER */
BPTR fl_Volume; /* BPTR to DosList volume node */
};
```
### Finding a File's Handler Process
This is a common need — e.g., to send custom packets:
```c
/* Get the handler MsgPort from any lock: */
struct FileLock *fl = (struct FileLock *)BADDR(lock);
struct MsgPort *handler = fl->fl_Task;
/* Now you can PutMsg(handler, &packet) */
/* From a filename (without an existing lock): */
struct DevProc *dp = GetDeviceProc("DF0:myfile", NULL);
if (dp) {
struct MsgPort *handler = dp->dvp_Port;
/* ... send packets ... */
FreeDeviceProc(dp);
}
/* From a file handle: */
struct FileHandle *fhp = (struct FileHandle *)BADDR(filehandle);
struct MsgPort *handler = fhp->fh_Type;
```
---
## Examining Files and Directories
### Examine a Single Object
```c
struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL);
BPTR lock = Lock("SYS:C/Dir", SHARED_LOCK);
if (lock && Examine(lock, fib))
{
Printf("Name: %s\n", fib->fib_FileName);
Printf("Size: %ld bytes\n", fib->fib_Size);
Printf("Type: %s\n",
fib->fib_DirEntryType > 0 ? "Directory" : "File");
/* Protection bits: */
Printf("Protect: %08lx\n", fib->fib_Protection);
/* Bits are ACTIVE-LOW: bit set = permission DENIED */
/* FIBF_READ=8, FIBF_WRITE=4, FIBF_EXECUTE=2, FIBF_DELETE=1 */
/* Bits 4-7: hold/script/pure/archive (active-HIGH) */
}
UnLock(lock);
FreeDosObject(DOS_FIB, fib);
```
> [!WARNING]
> **Protection bits are inverted** for RWED: bit set means the permission is **denied**, not granted. This is opposite to Unix. `fib_Protection = 0` means "all permissions granted".
### Scan a Directory (ExNext Loop)
```c
/* Classic directory enumeration pattern: */
BPTR dirLock = Lock("SYS:Libs", SHARED_LOCK);
struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL);
if (dirLock && Examine(dirLock, fib)) /* examine the dir itself first */
{
while (ExNext(dirLock, fib))
{
Printf(" %s (%ld bytes)\n",
fib->fib_FileName, fib->fib_Size);
}
/* ExNext returns FALSE when done — check error: */
if (IoErr() != ERROR_NO_MORE_ENTRIES)
PrintFault(IoErr(), "ExNext error");
}
UnLock(dirLock);
FreeDosObject(DOS_FIB, fib);
```
### ExAll — Bulk Directory Scan (OS 2.0+)
`ExAll` is significantly faster than `ExNext` for large directories — it batches results:
```c
#define BUFSIZE 4096
APTR buf = AllocVec(BUFSIZE, MEMF_ANY);
struct ExAllControl *eac = AllocDosObject(DOS_EXALLCONTROL, NULL);
UBYTE buf[4096];
eac->eac_LastKey = 0;
BPTR dirLock = Lock("SYS:Libs", SHARED_LOCK);
BOOL more;
eac->eac_LastKey = 0;
do {
more = ExAll(lock, buf, sizeof(buf), ED_NAME, eac);
more = ExAll(dirLock, buf, BUFSIZE, ED_SIZE, eac);
if (!more && IoErr() != ERROR_NO_MORE_ENTRIES)
{
PrintFault(IoErr(), "ExAll error");
break;
}
struct ExAllData *ead = (struct ExAllData *)buf;
while (ead) {
Printf("%s\n", ead->ed_Name);
while (ead)
{
Printf(" %s (%ld bytes)\n", ead->ed_Name, ead->ed_Size);
ead = ead->ed_Next;
}
} while (more);
FreeDosObject(DOS_EXALLCONTROL, eac);
FreeVec(buf);
UnLock(dirLock);
```
---
## Practical Patterns
### Safe File Replacement (Atomic Write)
```c
/* Anti-pattern: overwriting directly — corruption on crash */
/* WRONG: */
fh = Open("config.prefs", MODE_NEWFILE); /* truncates original! */
Write(fh, data, size); /* crash here = lost config */
Close(fh);
/* Correct: write-to-temp then rename */
fh = Open("config.prefs.tmp", MODE_NEWFILE);
Write(fh, data, size);
Close(fh);
/* Atomic swap: */
DeleteFile("config.prefs.bak"); /* remove old backup */
Rename("config.prefs", "config.prefs.bak"); /* backup current */
Rename("config.prefs.tmp", "config.prefs"); /* install new */
```
### Check If File Exists (Without Opening)
```c
/* Use Lock — lighter than Open: */
BPTR lock = Lock("SYS:Libs/68040.library", SHARED_LOCK);
if (lock) {
UnLock(lock);
/* file exists */
} else {
/* file doesn't exist (or permission denied — check IoErr()) */
}
```
### Get Parent Directory
```c
BPTR fileLock = Lock("SYS:Libs/68040.library", SHARED_LOCK);
BPTR parentLock = ParentDir(fileLock);
if (parentLock) {
/* parentLock = lock on SYS:Libs */
NameFromLock(parentLock, buf, sizeof(buf));
Printf("Parent: %s\n", buf); /* "SYS:Libs" */
UnLock(parentLock);
}
UnLock(fileLock);
```
### Resolve Full Path from Lock
```c
/* Get the full filesystem path of any lock: */
BPTR lock = Lock("PROGDIR:data", SHARED_LOCK);
char fullpath[256];
if (NameFromLock(lock, fullpath, sizeof(fullpath)))
Printf("Full path: %s\n", fullpath);
/* e.g. "DH0:Games/MyGame/data" */
UnLock(lock);
```
### Determine Volume / Device Name
```c
/* Find which volume a lock belongs to: */
struct InfoData *id = AllocVec(sizeof(struct InfoData), MEMF_ANY);
BPTR lock = Lock("SYS:", SHARED_LOCK);
if (Info(lock, id))
{
struct DosList *vol = (struct DosList *)BADDR(id->id_VolumeNode);
UBYTE *bname = (UBYTE *)BADDR(vol->dol_Name);
Printf("Volume: %.*s\n", bname[0], &bname[1]); /* BSTR */
Printf("Disk type: $%08lx\n", id->id_DiskType); /* 'DOS\1'=FFS */
Printf("Used: %ld / %ld blocks\n",
id->id_NumBlocksUsed, id->id_NumBlocks);
}
UnLock(lock);
FreeVec(id);
```
---
## Common Antipatterns
### ❌ Leaking Locks
```c
/* WRONG: lock is never released → filesystem handler keeps reference
forever, preventing volume ejection */
void bad_function(void) {
BPTR lock = Lock("DF0:file", SHARED_LOCK);
if (!lock) return;
/* ... forgot UnLock(lock) ... */
}
/* CORRECT: always pair Lock/UnLock */
void good_function(void) {
BPTR lock = Lock("DF0:file", SHARED_LOCK);
if (!lock) return;
/* ... do work ... */
UnLock(lock); /* ALWAYS release */
}
```
> Leaked locks are the #1 cause of "please insert volume X" requesters that never go away. The filesystem handler keeps the volume "in use" because an outstanding lock exists.
### ❌ Holding Exclusive Lock Too Long
```c
/* WRONG: other processes blocked for entire read operation */
BPTR lock = Lock("shared_data.dat", EXCLUSIVE_LOCK);
fh = OpenFromLock(lock); /* lock consumed by OpenFromLock */
/* ... lengthy processing ... */
Close(fh);
/* BETTER: use shared lock, only exclusive for the write phase */
BPTR lock = Lock("shared_data.dat", SHARED_LOCK);
fh = OpenFromLock(lock);
Read(fh, buf, size); /* shared — others can read too */
Close(fh);
/* Now open exclusively just for writing: */
fh = Open("shared_data.dat", MODE_READWRITE);
Write(fh, newdata, newsize);
Close(fh);
```
### ❌ Not Checking IoErr() After Lock Failure
```c
/* WRONG: can't distinguish "file not found" from "disk error" */
BPTR lock = Lock("DF0:important", SHARED_LOCK);
if (!lock) Printf("Not found!\n"); /* might be disk error! */
/* CORRECT: */
BPTR lock = Lock("DF0:important", SHARED_LOCK);
if (!lock) {
LONG err = IoErr();
switch (err) {
case ERROR_OBJECT_NOT_FOUND: Printf("Not found\n"); break;
case ERROR_OBJECT_IN_USE: Printf("Locked by another process\n"); break;
case ERROR_DISK_NOT_VALIDATED: Printf("Disk not ready\n"); break;
default: PrintFault(err, "Lock failed"); break;
}
}
```
### ❌ Using DupLock on Exclusive Locks
```c
/* WRONG: DupLock on an exclusive lock creates a SHARED copy.
This breaks the exclusivity guarantee! */
BPTR excl = Lock("data.dat", EXCLUSIVE_LOCK);
BPTR dup = DupLock(excl); /* dup is SHARED — exclusivity lost */
/* DupLock always creates a shared lock, regardless of the source. */
```
---
## Pattern Matching (Directory Wildcards)
```c
/* Match files against AmigaDOS wildcard patterns: */
/* #? = any string (like Unix *) */
/* ? = any single char */
/* ~ = NOT */
/* | = OR */
/* ( ) = grouping */
char tokenBuf[256];
LONG tok = ParsePattern("#?.library", tokenBuf, sizeof(tokenBuf));
if (tok >= 0) {
/* tok > 0 means pattern contains wildcards */
/* tok = 0 means literal string (no wildcards) */
while (ExNext(dirLock, fib)) {
if (MatchPattern(tokenBuf, fib->fib_FileName))
Printf(" Match: %s\n", fib->fib_FileName);
}
}
```
| AmigaDOS Pattern | Unix Equivalent | Meaning |
|---|---|---|
| `#?` | `*` | Any string |
| `?` | `?` | Any single character |
| `#?.library` | `*.library` | All library files |
| `~(#?.info)` | (no equiv) | Everything except .info files |
| `(a|b)#?` | `{a,b}*` | Starting with a or b |
---
## References
- NDK39: `dos/dos.h`, `dos/dosextens.h`, `dos/exall.h`
- ADCD 2.1: `Lock`, `UnLock`, `Examine`, `ExNext`, `ExAll`
- ADCD 2.1: `Lock`, `UnLock`, `Examine`, `ExNext`, `ExAll`, `NameFromLock`
- Ralph Babel: *The AmigaDOS Manual* — lock semantics chapter
- See also: [packet_system.md](packet_system.md) — how locks translate to handler packets
- See also: [file_io.md](file_io.md) — file operations using handles