Repository navigation
Filesystem
The allocator's exclusive limit is the smaller of the represented data extent
and 0xfff0. Cluster 0xffef remains usable; reserved numbers cannot satisfy
an exhausted volume. Lowest-free selection, one read per scanned FAT sector
and updating every FAT copy remain the allocation contract.
The maximum-volume regression occupies every usable entry and leaves reserved entries free. The allocator reports exhaustion after 256 reads and no writes; allocating the final usable cluster takes 258 reads and two writes. All fifteen methods pass through native and Cupid-built callers on Windows and Linux. The controlled HomeFS replacement retains its exact FAT bytes and makes 9,708 reads and 634 writes.
Commit a2e97f97 includes the source change, tests and embedded manual. Both normal build derivatives produce identical complete 200 MiB images and pass four strict four-CPU ls/SMP and feature 17 ISO sessions. See the evidence and retained failures.
kernel/fs/fat16.cc allocates the lowest free data cluster by reading each
examined FAT sector once and inspecting its little-endian entries in memory.
Reserved entries and unused tail padding are excluded. Invalid geometry or a
failed scan read returns allocation failure before marking an entry. The
existing marking helper updates each FAT copy and reports copy-write failures.
HomeFS still reserves its container, writes and syncs a replacement chain, publishes the directory entry, then releases the old chain. Sector scanning reduces read calls during this replacement without adding an allocation hint or shared cache. Native and Cupid-built tests cover ordinary allocation, storage faults and invalid geometry on both hosts. See ADR 0425.
cupid-os implements a Linux-style Virtual File System (VFS) that provides a unified file API across multiple filesystem types. The VFS enables a hierarchical directory structure (/home, /dev, /tmp, /bin) with three backend drivers: RamFS for in-memory storage, DevFS for device files, and a FAT16 wrapper for persistent disk storage.
┌──────────────────────────────────────────────────┐
│ Shell / Notepad / Applications │
│ (cd, ls, cat, mount, vls, vcat, exec, ...) │
├──────────────────────────────────────────────────┤
│ Virtual File System (VFS) │
│ vfs_open · vfs_read · vfs_write · vfs_close │
│ vfs_readdir · vfs_stat · vfs_mkdir · vfs_seek │
│ Mount table │ FD table │ Path resolution │
├────────────┬────────────┬────────────────────────┤
│ RamFS │ DevFS │ FAT16 VFS Wrapper │
│ (ramfs.c) │ (devfs.c) │ (fat16_vfs.c) │
│ │ ├────────────────────────┤
│ / │ /dev │ FAT16 Driver │
│ /bin │ │ (fat16.c) │
│ /tmp │ ├────────────────────────┤
│ │ │ Block Cache (LRU) │
│ │ │ (blockcache.c) │
│ │ ├────────────────────────┤
│ │ │ Block Device Layer │
│ │ │ (blockdev.c) │
│ │ ├────────────────────────┤
│ │ │ ATA/IDE PIO Driver │
│ │ │ (ata.c) │
└────────────┴────────────┴────────────────────────┘
The VFS (kernel/vfs.c/h) is the top-level abstraction providing a unified file API. All applications - shell, notepad, program loader - use VFS calls exclusively.
- Mount table - up to 16 simultaneous mount points
- File descriptor table - up to 64 open files
- Path resolution - longest-prefix match finds the correct filesystem
- Current working directory - shell tracks CWD for relative paths
-
Pluggable backends - any filesystem implementing
vfs_fs_ops_tcan be mounted
| Path | Filesystem | Purpose |
|---|---|---|
/ |
RamFS | Root filesystem, boot-time files |
/dev |
DevFS | Device special files |
/home |
FAT16 | User files on ATA disk |
/bin |
RamFS | System programs (subdirectory of root) |
/tmp |
RamFS | Temporary files (subdirectory of root) |
/ (RamFS root)
├── bin/ (system programs - future)
├── tmp/ (temporary files)
├── home/ (FAT16 - persistent user files on disk)
│ ├── HELLO.TXT
│ ├── SCRIPT.CUP
│ └── ...
└── dev/ (DevFS - device special files)
├── null (discard sink / EOF source)
├── zero (infinite zero bytes)
├── random (pseudo-random bytes)
└── serial (COM1 serial output)
| Function | Description |
|---|---|
vfs_init() |
Initialize VFS mount and FD tables |
vfs_register_fs(name, ops) |
Register a filesystem type |
vfs_mount(source, target, type) |
Mount a filesystem at a path |
vfs_open(path, flags) |
Open a file, returns fd |
vfs_close(fd) |
Close a file descriptor |
vfs_read(fd, buf, count) |
Read bytes from a file |
vfs_write(fd, buf, count) |
Write bytes to a file |
vfs_seek(fd, offset, whence) |
Seek within a file |
vfs_stat(path, stat) |
Get file/directory information |
vfs_readdir(fd, dirent) |
Read next directory entry |
vfs_mkdir(path) |
Create a directory |
vfs_unlink(path) |
Delete a file |
| Flag | Value | Description |
|---|---|---|
O_RDONLY |
0x0000 | Read only |
O_WRONLY |
0x0001 | Write only |
O_RDWR |
0x0002 | Read and write |
O_CREAT |
0x0100 | Create file if it doesn't exist |
O_TRUNC |
0x0200 | Truncate file to zero length |
O_APPEND |
0x0400 | Append to end of file |
When vfs_open("/home/README.TXT", O_RDONLY) is called:
-
Longest prefix match - scan mount table for best match
-
/homematches → FAT16 filesystem
-
-
Calculate relative path - strip mount prefix
-
/home/README.TXT→README.TXT
-
-
Call filesystem driver -
fat16_vfs_open(fs_data, "README.TXT", ...) - Allocate file descriptor - wrap in VFS fd and return to caller
| Code | Name | Meaning |
|---|---|---|
| 0 | VFS_OK |
Success |
| -2 | VFS_ENOENT |
No such file or directory |
| -5 | VFS_EIO |
I/O error |
| -12 | VFS_ENOMEM |
Out of memory |
| -17 | VFS_EEXIST |
File already exists |
| -20 | VFS_ENOTDIR |
Not a directory |
| -22 | VFS_EINVAL |
Invalid argument |
| -24 | VFS_EMFILE |
Too many open files |
| -28 | VFS_ENOSPC |
No space left |
| -38 | VFS_ENOSYS |
Operation not supported |
High-level convenience functions for common file I/O patterns. These wrap the low-level vfs_open / vfs_read / vfs_write / vfs_close sequence into single calls. Defined in kernel/vfs_helpers.c/h and available as CupidC bindings.
| Function | Description |
|---|---|
vfs_read_all(path, buffer, max_size) |
Read entire file into buffer. Returns bytes read, or negative VFS error. |
vfs_write_all(path, buffer, size) |
Write buffer to file (creates or truncates). Returns bytes written, or negative VFS error. |
vfs_read_text(path, buffer, max_size) |
Read file as null-terminated string. Reserves 1 byte for \0. Returns string length. |
vfs_write_text(path, text) |
Write null-terminated string to file. Returns bytes written (excluding null). |
Error handling: All functions return >= 0 on success, negative VFS error codes on failure (e.g., VFS_ENOENT, VFS_EIO, VFS_ENOSPC). File descriptors are always properly closed, even on error paths.
Example (CupidC):
void main() {
char buf[1024];
// Read a text file
int len = vfs_read_text("/home/notes.txt", buf, 1024);
if (len >= 0) {
print("Contents: ");
println(buf);
} else {
println("Read failed");
}
// Write a text file
int ret = vfs_write_text("/home/output.txt", "Hello from CupidC!");
if (ret >= 0) {
println("Written successfully");
}
}The RAM filesystem (kernel/ramfs.c/h) provides an in-memory directory tree used for the root filesystem and temporary storage.
- Full directory tree with parent/children pointers
- Dynamic file content allocation (up to 64KB per file)
- Create, read, write, delete files and directories
- Auto-creates parent directories via
ramfs_mkdirs() - Pre-populated at boot with built-in files (LICENSE.txt, MOTD.txt)
ramfs_node_t {
name[64] // Filename
data* // File contents (NULL for directories)
size // Current data size
capacity // Allocated buffer size
type // FILE or DIR
parent* // Parent directory
children* // First child (directories only)
next* // Next sibling
}-
Path lookup: Split path on
/, walk directory tree node by node -
File creation: Allocate
ramfs_node_t, link into parent's children list - File write: Grow data buffer if needed (allocate new, copy, free old)
-
Directory listing: Iterate children via
nextpointer
The device filesystem (kernel/devfs.c/h) exposes hardware and pseudo-devices as regular files under /dev.
| Device | Read behavior | Write behavior |
|---|---|---|
/dev/null |
Returns EOF (0 bytes) | Discards all data |
/dev/zero |
Returns zero bytes | Discards all data |
/dev/random |
Returns pseudo-random bytes | Ignored |
/dev/serial |
Not supported | Writes to COM1 serial |
> cat /dev/random # Outputs random bytes (limited to 64KB)
> vwrite /dev/null "test" # Writes are silently discarded
> vcat /dev/zero # Outputs zero bytes (limited to 64KB)
Note: Reading from infinite devices (/dev/zero, /dev/random) is capped at 64KB to prevent system freezes.
The FAT16 VFS wrapper (kernel/fat16_vfs.c/h) adapts the existing FAT16 driver to the VFS interface, making disk files accessible through the unified API.
-
fat16_vfs_open()wrapsfat16_open()for reading -
fat16_vfs_readdir()usesfat16_enumerate_root()to list directory entries -
fat16_vfs_unlink()wrapsfat16_delete_file() - Read operations wrap
fat16_read()with position tracking
| Feature | Status |
|---|---|
| Read files | ✅ via VFS |
| List directory | ✅ via VFS readdir |
| Delete files | ✅ via VFS unlink |
| Write files | fat16_write_file()
|
| Subdirectories | ❌ (root directory only) |
| Long filenames | ❌ (8.3 format only) |
- Maximum 8 characters for name, 3 for extension
- Case-insensitive (stored as uppercase)
- No spaces or special characters
- Examples:
HELLO.TXT,SCRIPT.CUP,DATA.BIN
The program loader (kernel/exec.c/h) loads and runs executables from the VFS. It supports two binary formats with automatic detection based on the first 4 bytes of the file.
| Format | Magic Bytes | Description |
|---|---|---|
| ELF32 |
7F 45 4C 46 (\x7FELF) |
Standard i386 ELF executables, compiled with GCC/Clang |
| CUPD |
43 55 50 44 (CUPD) |
CupidOS flat binary format (legacy) |
exec("/home/hello", "hello")
│
▼
┌─────────────────────┐
│ Read first 4 bytes │
└────┬────────────────┘
│
├─ 0x7F 'E' 'L' 'F' → elf_exec() (ELF32 loader)
│
├─ 0x43555044 (CUPD) → cupd_exec() (CUPD loader)
│
└─ anything else → VFS_EINVAL
ELF is the recommended format. Programs are compiled with a standard GCC cross-compiler and receive a syscall table pointer as their first argument. See the full ELF Programs wiki page for compiling, API reference, and examples.
Quick example:
#include "cupid.h"
void _start(cupid_syscall_table_t *sys) {
cupid_init(sys);
print("Hello from ELF!\n");
exit();
}Loading process (ELF):
- Open file via
vfs_open(path, O_RDONLY) - Read and validate 52-byte ELF header (architecture, class, endianness)
- Read program headers, find all
PT_LOADsegments - Compute virtual address range (
min_vaddr→max_vaddr) - Reserve physical pages via
pmm_reserve_region()(identity-mapped) - Zero the entire region, then load each segment to its
p_vaddr - Create a process with
process_create_with_arg(), passing the syscall table - Process runs as a normal scheduled kernel thread
CUPD is the original CupidOS flat binary format with a simple 20-byte header.
┌──────────────────────────┐ Offset 0
│ Header (20 bytes) │
│ ├─ magic: 0x43555044 │ "CUPD"
│ ├─ entry_offset │ Entry point offset from code start
│ ├─ code_size │ Size of code section
│ ├─ data_size │ Size of initialized data
│ └─ bss_size │ Size of uninitialized data
├──────────────────────────┤ Offset 20
│ Code Section │ Executable instructions
├──────────────────────────┤
│ Data Section │ Initialized global data
└──────────────────────────┘
Loading process (CUPD):
- Open file via
vfs_open(path, O_RDONLY) - Read and validate 20-byte header (check magic
0x43555044) - Allocate memory via
kmalloc()for code + data + BSS (max 256 KB) - Load code and data sections from file
- Zero-fill BSS section
- Create a new kernel process pointing to the entry function
- Process runs as a normal scheduled thread
> exec /home/hello
> exec /home/ls
> exec /bin/myprog
The VFS sits on top of the existing disk I/O stack, which remains unchanged.
The block device abstraction (kernel/blockdev.c) provides a uniform interface between the filesystem and disk driver.
| Function | Description |
|---|---|
blockdev_read(sector, buf) |
Read one 512-byte sector |
blockdev_write(sector, buf) |
Write one 512-byte sector |
blockdev_init() |
Initialize the block device |
An LRU (Least Recently Used) write-back cache (kernel/blockcache.c) sits between the block device layer and the ATA driver.
| Parameter | Value |
|---|---|
| Cache entries | 64 |
| Entry size | 512 bytes (1 sector) |
| Eviction policy | LRU |
| Write policy | Write-back (lazy) |
How it works:
- Read hit: Return cached sector immediately (no disk I/O)
- Read miss: Read from disk, store in cache, evict LRU if full
- Write: Mark cache entry as dirty, actual write deferred
- Sync: Flush all dirty entries to disk
- Eviction: When a dirty entry is evicted, it's written to disk first
The ATA driver (drivers/ata.c) implements PIO (Programmed I/O) mode for IDE disk access.
| Feature | Status |
|---|---|
| PIO read | ✅ |
| PIO write | ✅ |
| Identify device | ✅ |
| DMA | ❌ |
| ATAPI/CD | ❌ |
| Secondary channel | ❌ |
| Command | Usage | Description |
|---|---|---|
cd |
cd <path> |
Change current working directory |
pwd |
pwd |
Print current working directory |
ls |
ls [path] |
List files in current directory (or given path) |
cat |
cat <file> |
Display file contents (supports VFS paths) |
mount |
mount |
Show all mounted filesystems |
vls |
vls [path] |
List files at an absolute VFS path |
vcat |
vcat <path> |
Display file at an absolute VFS path |
vstat |
vstat <path> |
Show file/directory info (type, size) |
vmkdir |
vmkdir <path> |
Create a directory |
vrm |
vrm <path> |
Delete a file |
vwrite |
vwrite <path> <text> |
Write text to a file |
exec |
exec <path> |
Load and run a CUPD executable |
| Command | Description |
|---|---|
lsdisk |
List files on FAT16 disk directly |
catdisk <file> |
Read a file from FAT16 disk directly |
sync |
Flush block cache to disk |
cachestats |
Show block cache hit/miss statistics |
/home> ls
HELLO .TXT 1234
SCRIPT .CUP 256
/home> cat HELLO.TXT
Hello from CupidOS disk!
/home> cd /dev
/dev> ls
null
zero
random
serial
/dev> cd /
/> ls
bin/
tmp/
home/
dev/
LICENSE.txt
MOTD.txt
/> mount
Mounted filesystems:
/ ramfs
/dev devfs
/home fat16
/> vmkdir /tmp/test
/> vwrite /tmp/test/hello.txt Hello World
/> vcat /tmp/test/hello.txt
Hello World
The Notepad application uses VFS for its file dialog, open, and save operations:
-
File dialog browses VFS directories starting at
/home -
Directory navigation - click directories or press Enter to navigate into them;
..goes up - Double-click a file to open it, or a directory to enter it
-
Open reads file contents via
vfs_open()/vfs_read()/vfs_close() -
Save writes via
vfs_open(O_WRONLY | O_CREAT | O_TRUNC)/vfs_write()/vfs_close() - Saving to
/homepersists data to the ATA disk through the FAT16 VFS wrapper
To create a FAT16 disk image for testing with QEMU:
# Create a 32MB disk image
dd if=/dev/zero of=disk.img bs=1M count=32
# Partition with fdisk (create one primary partition, type 0x06 = FAT16)
echo -e "o\nn\np\n1\n\n\nt\n6\nw" | fdisk disk.img
# Set up a loop device with offset
LOOP=$(sudo losetup --show -fP disk.img)
# Format the partition as FAT16
sudo mkfs.fat -F 16 ${LOOP}p1
# Mount and add files
sudo mount ${LOOP}p1 /mnt
echo "Hello World" | sudo tee /mnt/HELLO.TXT
echo -e '#!/cupid\necho "Hello from CupidScript!"' | sudo tee /mnt/TEST.CUP
sudo umount /mnt
# Clean up
sudo losetup -d $LOOPThen attach it to QEMU:
qemu-system-i386 -hda disk.img -serial stdio ...- Architecture - How the filesystem fits into the system
-
CupidScript - Running
.cupscripts from disk - Shell Commands - Full command reference
- Debugging - Serial logging from filesystem operations
Getting Started
Shell & Scripting
Compilers
- CupidC Compiler
- CupidC Language Reference
- CupidC 2D Graphics Library
- CupidASM Assembler
- Toolchain Bootstrap
- Floating Point
Programs & Docs
GUI
Kernel
Filesystems
Hardware