amiga-bootcamp/07_dos/filesystem.md
Ilia Sharin da9e7d3b63 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
2026-04-23 20:23:50 -04:00

304 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[← Home](../README.md) · [AmigaDOS](README.md)
# 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. 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
| 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 |
---
## Boot Block (Blocks 01)
```
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 |
|---|---|---|
| `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 and directory names are hashed into the hash table slots:
```c
/* 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 */
```
### 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")
...
```
---
## File Header Block
```
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 |
|---|---|---|
| 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 ComputeBlockChecksum(ULONG *block, LONG longs)
{
LONG sum = 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})")
```
---
## References
- NDK39: `dos/filehandler.h`
- Ralph Babel: *The AmigaDOS Manual* (3rd edition) — definitive FFS reference
- 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