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

11 KiB
Raw Blame History

← Home · AmigaDOS

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:

/* 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):

/* 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

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

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