Initial commit: Schunix as it stands before the SchOS rename
An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
This commit is contained in:
commit
d0f7acf9eb
275 changed files with 51417 additions and 0 deletions
433
tools/bootcheck.ps1
Normal file
433
tools/bootcheck.ps1
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
# Headless boot check: run Schunix under QEMU, type commands at the shell, and
|
||||
# assert on what actually comes back.
|
||||
#
|
||||
# Booting to a prompt proves nothing on its own -- a filesystem can mount and
|
||||
# still fail on the first real command, and exactly that has happened here
|
||||
# before. So this drives the serial console and greps the transcript for the
|
||||
# answers those commands should produce.
|
||||
#
|
||||
# The console is stdio with a chardev logfile: QEMU writes every byte of the
|
||||
# session to a file itself, so this script only ever has to *write* to stdin.
|
||||
# Reading QEMU's stdout from PowerShell needs a reader that cannot block the
|
||||
# writer, and two attempts at that (a background task, then a duplex named
|
||||
# pipe) both deadlocked. Letting QEMU do the logging removes the problem.
|
||||
#
|
||||
# QEMU's serial line has no flow control: a burst of characters gets dropped,
|
||||
# and anything typed before the shell is listening is discarded outright. Hence
|
||||
# the pacing below -- not superstition, just the difference between a reliable
|
||||
# check and a flaky one.
|
||||
#
|
||||
# tools\bootcheck.ps1
|
||||
# tools\bootcheck.ps1 -Tty -Ps
|
||||
|
||||
param(
|
||||
[switch]$Fb,
|
||||
# Boot schunix.iso from an emulated optical drive with NO hard disk
|
||||
# attached, which is the only way to prove the ISO is self-contained.
|
||||
# Needs `make iso` first.
|
||||
[switch]$Iso,
|
||||
# Terminal behaviour: pipes, isatty, SIGPIPE, and Ctrl-C. None of these
|
||||
# can be checked by looking at a screenshot, and all of them were broken
|
||||
# in ways that looked like broken *commands*.
|
||||
[switch]$Tty,
|
||||
# Attach an xHCI controller with a keyboard, a mouse and a mass storage
|
||||
# device on it. Nothing on the default machine has a USB port, so without
|
||||
# this the USB driver is never asked to do anything and a broken one looks
|
||||
# exactly like a working one.
|
||||
[switch]$Usb,
|
||||
# Unix domain sockets. Nothing in the default command set opens one, and a
|
||||
# socket layer that accepted every call and dropped every byte would look
|
||||
# identical from the outside -- socktest has one process write and another
|
||||
# read, which is the only check that can tell the difference.
|
||||
[switch]$Sock,
|
||||
# Raw input events. Needs the QEMU monitor for the same reason the USB run
|
||||
# does: a key has to be *pressed*, and characters typed at the serial port
|
||||
# never touch the keyboard controller at all.
|
||||
[switch]$Evdev,
|
||||
# Process enumeration. /proc is a synthetic filesystem now, and the thing
|
||||
# it exists for -- one directory per process -- is exactly what nothing
|
||||
# else in this suite touches.
|
||||
[switch]$Ps,
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$hda = Join-Path $root 'schunix.img'
|
||||
# The root filesystem. SchFS is the only root there is, so this is not a
|
||||
# choice -- a kernel handed anything else on device 1 panics in forkret().
|
||||
$hdb = Join-Path $root 'build\schfs.img'
|
||||
$log = Join-Path $root 'build\bootcheck.log'
|
||||
|
||||
Remove-Item -Force $log -ErrorAction SilentlyContinue
|
||||
|
||||
# The default run exercises the *root* filesystem: create a file, read it back,
|
||||
# grow it, list a directory, seek around it. Booting to a prompt proves nothing
|
||||
# on its own, and every one of these has broken at some point while the boot
|
||||
# still looked perfectly healthy.
|
||||
$commands = @(
|
||||
'echo hello-from-schunix > /tmp/bc.txt'
|
||||
'cat /tmp/bc.txt'
|
||||
'echo second-line >> /tmp/bc.txt'
|
||||
'wc -c /tmp/bc.txt'
|
||||
'mkdir /tmp/bcdir'
|
||||
'cp /tmp/bc.txt /tmp/bcdir/copy.txt'
|
||||
'cat /tmp/bcdir/copy.txt'
|
||||
'ls /tmp/bcdir'
|
||||
'rm /tmp/bcdir/copy.txt'
|
||||
'rmdir /tmp/bcdir'
|
||||
'ls /bin | wc -l'
|
||||
'cat /proc/pci'
|
||||
)
|
||||
# Every command above already went to SchFS. What is left is to show that
|
||||
# it is SchFS they went to, and that the volume noticed: the generation
|
||||
# counter must advance across the writes, which is the one thing a
|
||||
# read-only mount or a silently-discarded write could not fake.
|
||||
$commands += 'cat /proc/schfs'
|
||||
# Mounting the root device a second time has to fail. It is the check that
|
||||
# tells "the volume is genuinely mounted as /" apart from "it happens to be
|
||||
# readable", and getting EBUSY here is the correct answer, not a defect.
|
||||
$commands += 'mkdir /mnt'
|
||||
$commands += 'mount -t schfs /dev/hdb /mnt'
|
||||
# A deep path built and torn down entirely on the root volume.
|
||||
$commands += 'mkdir /tmp/rootdir'
|
||||
$commands += 'mkdir /tmp/rootdir/sub'
|
||||
$commands += 'echo schfs-is-root > /tmp/rootdir/sub/deep.txt'
|
||||
$commands += 'cat /tmp/rootdir/sub/deep.txt'
|
||||
$commands += 'ls /tmp/rootdir/sub'
|
||||
$commands += 'rm /tmp/rootdir/sub/deep.txt'
|
||||
$commands += 'rmdir /tmp/rootdir/sub'
|
||||
$commands += 'rmdir /tmp/rootdir'
|
||||
# A binary loaded off the root volume, not a shell builtin: exec() reading
|
||||
# an ELF through schfs_ops is the path that carried init itself.
|
||||
$commands += 'wc -l /etc/motd'
|
||||
# The second dump. The generation counter is compared against the first
|
||||
# one: on a copy-on-write volume it moves on every committed transaction,
|
||||
# so a root that silently discarded its writes would still pass every read
|
||||
# above and fail here.
|
||||
$commands += 'cat /proc/schfs'
|
||||
|
||||
if ($Fb) {
|
||||
# /dev/fb0. The geometry has to come back from the mode the bootloader
|
||||
# actually programmed -- three places in the kernel used to assume
|
||||
# 0xfd000000 at 800x600x32, and nothing would have said when that stopped
|
||||
# being true. `bars` goes through the device's write(), `fill` through
|
||||
# mmap, so a failure names which of the two paths broke.
|
||||
$commands += 'fbinfo'
|
||||
$commands += 'fbinfo bars'
|
||||
$commands += 'fbinfo fill'
|
||||
}
|
||||
if ($Tty) {
|
||||
# `ls` piped into `wc -l` reported 48 rather than 240 because ioctl
|
||||
# answered TIOCGWINSZ on a *pipe* with success, so ls formatted in
|
||||
# columns. `yes` failed outright because writev capped iovcnt at 16 and
|
||||
# toybox builds 256 of them. `cat | head` killed the shell, because
|
||||
# SIG_IGN is handler value 1 and signal delivery jumped to address 1.
|
||||
# Three different-looking bugs, none of them in the commands.
|
||||
$commands += 'ls /bin | wc -l'
|
||||
$commands += 'yes | head -2'
|
||||
# 4 bytes, not 3: an ELF file starts with 0x7F 'E' 'L' 'F', so three bytes
|
||||
# stops at "EL" and the invisible leading byte makes that easy to misread.
|
||||
$commands += 'cat /bin/toybox | head -c 4; echo'
|
||||
$commands += 'printf ''caf\303\251-\303\261-\303\266\n'''
|
||||
$commands += 'echo TTY-SURVIVED'
|
||||
# Shared memory and file-backed mmap: the two things a display server
|
||||
# cannot be written without, and the two whose failure mode is a
|
||||
# successful-looking return value.
|
||||
$commands += 'shmtest'
|
||||
}
|
||||
if ($Usb) {
|
||||
# /proc/interrupts is how you tell a registered handler from one that
|
||||
# merely compiled. A line with a zero count is a device that was found and
|
||||
# never heard from.
|
||||
$commands += 'cat /proc/interrupts'
|
||||
# Read a real block off the USB stick, through the buffer cache. Answering
|
||||
# INQUIRY proves a device is there; only this proves it can read.
|
||||
$commands += 'echo s > /proc/sysrq'
|
||||
}
|
||||
if ($Sock) {
|
||||
$commands += 'socktest'
|
||||
}
|
||||
if ($Ps) {
|
||||
$commands += 'ls /proc'
|
||||
$commands += 'cat /proc/1/stat'
|
||||
$commands += 'ps'
|
||||
$commands += 'ps -A -o pid,ppid,comm'
|
||||
$commands += 'top -b -n 1 | head -6'
|
||||
}
|
||||
$commands += 'echo MARKER-DONE'
|
||||
|
||||
# The ISO run attaches no hard disk at all. That is the whole point: a kernel
|
||||
# that still quietly needs one would boot happily here otherwise, and only
|
||||
# fail on someone else's machine.
|
||||
$isoPath = Join-Path $root 'schunix.iso'
|
||||
if ($Iso -and -not (Test-Path $isoPath)) { throw "no $isoPath -- run 'make iso' first" }
|
||||
|
||||
$drives = if ($Iso) {
|
||||
@("-cdrom `"$isoPath`"", '-boot d')
|
||||
} else {
|
||||
@("-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk")
|
||||
}
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$usbDevices = if ($Usb) {
|
||||
# One device of each speed class QEMU can produce: usb-storage negotiates
|
||||
# SuperSpeed on qemu-xhci, the HID devices come up high speed. They take
|
||||
# different paths through enumeration -- a USB3 port arrives already
|
||||
# enabled and a USB2 port has to be reset first -- so one of each is the
|
||||
# difference between testing the driver and testing half of it.
|
||||
@('-device qemu-xhci,id=xhci'
|
||||
'-device usb-kbd,bus=xhci.0'
|
||||
# The mouse goes behind a hub, so the tree is a tree and not a row of
|
||||
# root ports. That exercises the route string, the hub's own slot
|
||||
# context, and the transaction translator -- none of which a device
|
||||
# plugged straight into the controller touches at all.
|
||||
# QEMU places a device under a hub by port *path*, not by a child bus:
|
||||
# the hub sits on port 2 and the mouse on port 2.1 of the same bus.
|
||||
'-device usb-hub,id=hub,bus=xhci.0,port=2'
|
||||
'-device usb-mouse,bus=xhci.0,port=2.1'
|
||||
# Always the v1 image, never $hdb. The checks below read a v1 superblock
|
||||
# off this stick and assert on its sector count and size field, so the
|
||||
# stick has to be a v1 image whatever the root filesystem is -- under
|
||||
# the root $hdb is a SchFS volume and those fields are simply not
|
||||
# there, which failed three USB checks that had nothing wrong with USB.
|
||||
# It also stops the same file being attached twice, once as the root
|
||||
# disk and once as a USB device.
|
||||
"-drive id=usbstick,file=`"$(Join-Path $root 'build\fs.img')`",format=raw,if=none"
|
||||
'-device usb-storage,bus=xhci.0,drive=usbstick')
|
||||
} else { @() }
|
||||
|
||||
# The USB run needs the QEMU monitor, because `sendkey` is the only way to
|
||||
# press a key on an emulated USB keyboard -- typing on stdin goes to the
|
||||
# serial port and proves nothing about USB.
|
||||
# Ask the OS for a free port rather than hardcoding one. A run that dies
|
||||
# before its `finally` leaves QEMU holding the old number, and every later run
|
||||
# then fails with "Failed to find an available port" -- which looks like a
|
||||
# bug in whatever is being tested rather than in the harness.
|
||||
$monPort = if ($Usb -or $Evdev) {
|
||||
$l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
||||
$l.Start(); $p = $l.LocalEndpoint.Port; $l.Stop(); $p
|
||||
} else { 0 }
|
||||
$monitor = if ($Usb -or $Evdev) { @("-monitor tcp:127.0.0.1:$monPort,server,nowait") } else { @() }
|
||||
|
||||
$psi.Arguments = ($drives + $usbDevices + $monitor + @(
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
)) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
$what = if ($Iso) { 'schunix.iso, no hard disk' } else { "hdb = $(Split-Path $hdb -Leaf)" }
|
||||
Write-Host "==> booting ($what)" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds # wait for the shell prompt
|
||||
|
||||
if ($Usb) {
|
||||
# Type a whole command on the *USB* keyboard. QEMU routes sendkey to
|
||||
# the most recently attached keyboard, so with usb-kbd present these
|
||||
# keystrokes go through xHCI, the interrupt endpoint, the HID report
|
||||
# decoder and the console -- none of which the serial path touches.
|
||||
# If any link in that chain is broken the characters simply vanish,
|
||||
# which is exactly how the missing interrupt showed up.
|
||||
Write-Host ' > (usb keyboard) echo usbworks' -ForegroundColor DarkGray
|
||||
$mon = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$monw = New-Object System.IO.StreamWriter($mon.GetStream())
|
||||
$monw.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 400
|
||||
foreach ($k in @('e','c','h','o','spc','u','s','b','w','o','r','k','s','ret')) {
|
||||
$monw.WriteLine("sendkey $k")
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
$mon.Close()
|
||||
}
|
||||
|
||||
# Wait until the shell has finished the last command and printed a fresh
|
||||
# prompt, rather than sleeping a fixed interval and hoping.
|
||||
#
|
||||
# A flat 4-second wait was wrong in both directions: too long for the
|
||||
# dozens of commands that answer instantly, and too short for `ls /bin |
|
||||
# wc -l`, which reliably overran it. When it overran, the next command was
|
||||
# typed while the shell was still busy -- the tty echoed it but no reader
|
||||
# took it, and it ended up concatenated onto the following one. The
|
||||
# transcript showed `cat /proc/pcicat /proc/schfs` and three PCI checks
|
||||
# failed, which looks exactly like a broken PCI driver and is not one.
|
||||
#
|
||||
# This cannot read QEMU's stdout -- see the header; two attempts at that
|
||||
# deadlocked. It reads the logfile QEMU is writing instead, which is the
|
||||
# same bytes with none of the plumbing.
|
||||
function Wait-Prompt([int]$TimeoutMs = 20000) {
|
||||
$deadline = (Get-Date).AddMilliseconds($TimeoutMs)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 150
|
||||
$tail = ''
|
||||
try { $tail = Get-Content $log -Raw -ErrorAction Stop } catch { continue }
|
||||
if ($null -eq $tail) { continue }
|
||||
# A prompt at the very end, with nothing typed after it.
|
||||
if ($tail -match '(?s)#[ ]?$') { return $true }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($cmd in $commands) {
|
||||
Write-Host " > $cmd" -ForegroundColor DarkGray
|
||||
foreach ($ch in ($cmd + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # no flow control: pace the input
|
||||
}
|
||||
if (-not (Wait-Prompt)) {
|
||||
Write-Host " (no prompt after '$cmd' -- continuing)" -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
}
|
||||
|
||||
if ($Evdev) {
|
||||
# Start a reader on /dev/input/event0 and then press keys at it. The
|
||||
# program blocks in read() until events arrive, so if the keyboard
|
||||
# driver is not feeding the input layer this hangs and prints nothing
|
||||
# -- which is a failure that says exactly which half is broken.
|
||||
Write-Host ' > inputtest -n 6, then press keys' -ForegroundColor DarkGray
|
||||
foreach ($ch in "inputtest -n 6`n".ToCharArray()) {
|
||||
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$mon = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$monw = New-Object System.IO.StreamWriter($mon.GetStream())
|
||||
$monw.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 400
|
||||
# 'a' is keycode 30 and shift is 42, chosen because both numbers are
|
||||
# unmistakable in the output and neither is a scancode by accident.
|
||||
# Each sendkey is a press *and* a release, which is the whole point:
|
||||
# the console would report one character and nothing else.
|
||||
foreach ($k in @('a', 'shift', 'a', 'b')) {
|
||||
$monw.WriteLine("sendkey $k")
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
$mon.Close()
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# Those keypresses reached the console as well as the input device --
|
||||
# that is the design, not a leak -- so the shell's input line now holds
|
||||
# "aab". Clear it before typing the next command, or the shell tries to
|
||||
# run `aabecho`.
|
||||
$p.StandardInput.Write("`n"); $p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
foreach ($ch in "echo INPUT-DONE`n".ToCharArray()) {
|
||||
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30
|
||||
}
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
|
||||
if ($Tty) {
|
||||
# Ctrl-C has to reach the *foreground process group*. The old handler
|
||||
# set killed=1 on whatever process happened to be running when the
|
||||
# keyboard interrupt fired, which is almost never the one on screen.
|
||||
Write-Host ' > sleep 30, then Ctrl-C' -ForegroundColor DarkGray
|
||||
foreach ($ch in "sleep 30`n".ToCharArray()) {
|
||||
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
$p.StandardInput.Write([char]3) # ^C
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Seconds 2
|
||||
foreach ($ch in "echo INTERRUPTED-OK`n".ToCharArray()) {
|
||||
$p.StandardInput.Write($ch); $p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30
|
||||
}
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (-not (Test-Path $log)) { throw "no serial log produced at $log" }
|
||||
$text = Get-Content $log -Raw
|
||||
if ($null -eq $text) { $text = '' }
|
||||
|
||||
Write-Host "==> serial transcript ($($text.Length) bytes) -> $log" -ForegroundColor Cyan
|
||||
Write-Host $text
|
||||
|
||||
$script:fail = $false
|
||||
function Expect($pattern, $what) {
|
||||
if ($script:text -match $pattern) {
|
||||
Write-Host " [ok] $what" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " FAIL: $what (no match for /$pattern/)" -ForegroundColor Red
|
||||
$script:fail = $true
|
||||
}
|
||||
}
|
||||
|
||||
Expect 'MARKER-DONE' 'shell stayed alive through every command'
|
||||
Expect 'hello-from-schunix' 'a file written on the root filesystem reads back'
|
||||
Expect 'second-line' 'appending to that file works'
|
||||
# 19 bytes of "hello-from-schunix\n" plus 12 of "second-line\n". The exact
|
||||
# number is the point: an append that silently truncated still produced a file,
|
||||
# and only the byte count said which one.
|
||||
Expect '(?m)^\s*31\s' 'append extended the file rather than replacing it'
|
||||
Expect '(?m)^copy\.txt' 'a copied file appears in a new directory'
|
||||
Expect '(?ms)copy\.txt.*rmdir' 'rmdir removed the directory without an error'
|
||||
|
||||
# PCI. The point of these is that the numbers come off the bus rather than out
|
||||
# of a header: the host bridge identifies the chipset QEMU is emulating, and
|
||||
# the display controller's aperture is the same address VBE reported, checked
|
||||
# by two completely independent paths.
|
||||
Expect 'pci: \d+ devices' 'the PCI bus enumerated'
|
||||
Expect '00:00\.0 8086:1237' 'the host bridge is where a host bridge belongs'
|
||||
Expect '00:01\.1 8086:7010 IDE' 'the IDE controller was identified by class'
|
||||
Expect '\[vga\]' 'a driver bound itself to the display controller'
|
||||
Expect 'vga: display controller' 'the display driver found its aperture'
|
||||
|
||||
Expect 'schfs: mounted' 'the kernel mounted SchFS as the root filesystem'
|
||||
Expect 'init: ready' 'init came up on a SchFS root'
|
||||
Expect 'label:\s*schunix' '/proc/schfs reports the volume label'
|
||||
Expect 'root_objectid:\s*16' '/proc/schfs reports the root object id'
|
||||
Expect 'dir\s+bin' 'the root directory really contains bin/'
|
||||
# / is the mounted volume, so mounting that same device again must be
|
||||
# refused. Without this a kernel that had quietly fallen back to some other
|
||||
# root would still pass everything else here.
|
||||
Expect 'Resource busy' 'mounting the root device twice is refused'
|
||||
Expect 'schfs-is-root' 'a file three directories deep reads back'
|
||||
Expect '(?m)^deep\.txt' 'that file is listed by its full name'
|
||||
Expect '(?m)^\s*1\s+/etc/motd' 'a binary loaded off the SchFS root runs'
|
||||
# Two /proc/schfs dumps, before and after the writes. Same volume, so the
|
||||
# generation must have moved -- if it has not, nothing was committed.
|
||||
$gens = [regex]::Matches($text, 'generation:\s*(\d+)')
|
||||
if ($gens.Count -ge 2 -and
|
||||
[int]$gens[$gens.Count-1].Groups[1].Value -gt [int]$gens[0].Groups[1].Value) {
|
||||
Write-Host " [ok] the volume generation advanced across the writes" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " FAIL: the volume generation did not advance -- writes were not committed" -ForegroundColor Red
|
||||
$script:fail = $true
|
||||
}
|
||||
|
||||
if ($Fb) {
|
||||
Expect 'id\s+schunixfb' 'FBIOGET_FSCREENINFO identifies the device'
|
||||
# 800x600 is what boot/stage2.S asks VBE for. Asserting the numbers rather
|
||||
# than just "the ioctl returned" is the point: the old code path returned
|
||||
# success with an untouched buffer, i.e. a 0x0 screen.
|
||||
Expect '(?m)^xres\s+800' 'the reported width is the mode actually set'
|
||||
Expect '(?m)^yres\s+600' 'the reported height is the mode actually set'
|
||||
Expect '(?m)^bits_per_pixel\s+32' 'the reported depth is the mode actually set'
|
||||
Expect '(?m)^smem_start\s+0x' 'the framebuffer reports a physical address'
|
||||
Expect '(?m)^line_length\s+3200' 'the stride comes from VBE, not from xres*bpp'
|
||||
Expect 'transp.*absent' 'the absent alpha channel is reported as length 0'
|
||||
Expect 'colour bars drawn' 'writing pixels through write() reaches the screen'
|
||||
Expect 'gradient drawn' 'mmap of /dev/fb0 maps the real framebuffer'
|
||||
}
|
||||
|
||||
if ($script:fail) { exit 1 }
|
||||
Write-Host 'boot check passed' -ForegroundColor Green
|
||||
85
tools/crashqr.py
Normal file
85
tools/crashqr.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Read the QR code back off a captured crash screen.
|
||||
|
||||
This is the acceptance test for the crash channel, and it is deliberately
|
||||
end-to-end: it does not ask the encoder what it produced, it reads the pixels
|
||||
that were actually painted on the framebuffer. Everything in between -- payload
|
||||
construction in panic context, the encoder, the scaling into modules, the card
|
||||
and quiet zone -- is covered by whether this comes back as text.
|
||||
|
||||
pwsh -File tools/crashshot.ps1 # crash on purpose, screendump it
|
||||
PYTHONPATH=<dir> python tools/crashqr.py
|
||||
|
||||
Needs opencv-python-headless and numpy; see tools/qrcheck.py for the install.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import cv2
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
sys.exit("crashqr: needs opencv-python-headless and numpy")
|
||||
|
||||
PPM = sys.argv[1] if len(sys.argv) > 1 else "build/crashscreen.ppm"
|
||||
|
||||
try:
|
||||
data = open(PPM, "rb").read()
|
||||
except OSError as e:
|
||||
sys.exit(f"crashqr: {e} -- run tools/crashshot.ps1 first")
|
||||
|
||||
# PPM P6 header: three whitespace-separated fields after the magic. Parsed by
|
||||
# scanning rather than assuming a fixed length, because QEMU's is compact but
|
||||
# nothing guarantees that.
|
||||
pos, fields = 2, []
|
||||
while len(fields) < 3:
|
||||
while data[pos:pos + 1].isspace():
|
||||
pos += 1
|
||||
start = pos
|
||||
while not data[pos:pos + 1].isspace():
|
||||
pos += 1
|
||||
fields.append(int(data[start:pos]))
|
||||
pos += 1
|
||||
|
||||
w, h = fields[0], fields[1]
|
||||
img = np.frombuffer(data[pos:pos + w * h * 3], np.uint8).reshape(h, w, 3)
|
||||
grey = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
||||
|
||||
# The symbol sits on the only light panel on an otherwise purple screen, so a
|
||||
# brightness threshold finds it without needing to know the layout. The white
|
||||
# report text passes the same threshold, which does not matter -- the detector
|
||||
# locates the symbol by its finder patterns, and handing it extra screen
|
||||
# costs nothing.
|
||||
mask = (grey > 240).astype(np.uint8)
|
||||
ys, xs = np.nonzero(mask)
|
||||
if len(xs) == 0:
|
||||
sys.exit("crashqr: no light region found -- was a crash screen captured?")
|
||||
crop = grey[ys.min():ys.max() + 1, xs.min():xs.max() + 1]
|
||||
|
||||
# OpenCV's detector is sensitive to scale in ways unrelated to the encoding,
|
||||
# so try a few before concluding anything. See the note in tools/qrcheck.py.
|
||||
text = ""
|
||||
for scale in (1, 2, 3, 4):
|
||||
up = cv2.resize(crop, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST)
|
||||
up = cv2.copyMakeBorder(up, 40, 40, 40, 40, cv2.BORDER_CONSTANT, value=255)
|
||||
text, _, _ = cv2.QRCodeDetector().detectAndDecode(up)
|
||||
if text:
|
||||
break
|
||||
|
||||
if not text:
|
||||
print("crashqr: FAILED -- the symbol on the crash screen did not decode")
|
||||
sys.exit(1)
|
||||
|
||||
print("crashqr: decoded the crash screen's QR\n")
|
||||
for line in text.split("\n"):
|
||||
print(" ", line)
|
||||
|
||||
# The payload is a contract with whatever reads these later, so check its shape
|
||||
# rather than only that something came back.
|
||||
lines = text.split("\n")
|
||||
ok = lines[0] == "SXPSOGM1"
|
||||
print()
|
||||
if not ok:
|
||||
print(f"crashqr: FAILED -- expected magic 'SXPSOGM1', got {lines[0]!r}")
|
||||
sys.exit(1)
|
||||
print(f"crashqr: passed ({len(text)} bytes, {len(lines)} fields)")
|
||||
115
tools/crashshot.ps1
Normal file
115
tools/crashshot.ps1
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Boot Schunix, crash it on purpose, and capture the crash screen as an image.
|
||||
#
|
||||
# The crash screen is the one part of the system you cannot look at by using
|
||||
# the system, so it is the one part that quietly rots. This drives it through
|
||||
# /proc/sysrq and takes a picture, which turns "does the panic screen still
|
||||
# render" into something answerable in thirty seconds.
|
||||
#
|
||||
# QEMU's monitor is on its own socket so the serial console stays free for the
|
||||
# shell. screendump writes a PPM; the PNG conversion is optional and skipped if
|
||||
# ImageMagick is not around.
|
||||
#
|
||||
# tools\crashshot.ps1 crash the ramdisk-root build
|
||||
# tools\crashshot.ps1 -SchFS with a SchFS volume attached
|
||||
|
||||
param(
|
||||
[switch]$SchFS,
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Out = "build\crashscreen.ppm",
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$hda = Join-Path $root 'schunix.img'
|
||||
$hdb = if ($SchFS) { Join-Path $root 'build\schfs.img' } else { Join-Path $root 'build\fs.img' }
|
||||
$log = Join-Path $root 'build\crashshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$mon = Join-Path $root 'build\qemu-mon.sock'
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
# The monitor goes to a TCP port rather than a unix socket: PowerShell can open
|
||||
# a TCP client without any extra machinery, and QEMU on Windows has no unix
|
||||
# sockets anyway.
|
||||
$monPort = 55555
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
|
||||
# Put something on the display first. The whole design of the crash screen
|
||||
# is that it dims what was there rather than clearing it, so a blank screen
|
||||
# underneath would prove nothing.
|
||||
Write-Host " > filling the screen with normal output" -ForegroundColor DarkGray
|
||||
Send-Line 'ls /bin'
|
||||
Start-Sleep -Seconds 3
|
||||
Send-Line 'cat /proc/cpuinfo'
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-Host " > echo c > /proc/sysrq" -ForegroundColor DarkGray
|
||||
Send-Line 'echo c > /proc/sysrq'
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$stream = $client.GetStream()
|
||||
$writer = New-Object System.IO.StreamWriter($stream)
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
# QEMU on Windows wants a native path here, not the one PowerShell shows.
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
Write-Host "==> serial transcript" -ForegroundColor Cyan
|
||||
# Only the crash report, not the whole boot.
|
||||
$idx = $text.IndexOf('Schunix stopped')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) } else { Write-Host $text }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
$size = (Get-Item $ppm).Length
|
||||
Write-Host "==> $ppm ($size bytes)" -ForegroundColor Green
|
||||
$png = [System.IO.Path]::ChangeExtension($ppm, '.png')
|
||||
$magick = Get-Command magick -ErrorAction SilentlyContinue
|
||||
if ($magick) {
|
||||
& magick $ppm $png
|
||||
Write-Host "==> $png" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "FAIL: no screendump was produced" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
116
tools/dispshot.ps1
Normal file
116
tools/dispshot.ps1
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Boot Schunix, run the display self-test, and capture the result as an image.
|
||||
#
|
||||
# The display class cannot be checked without a display: the assertion is "did
|
||||
# the right pixels reach the screen", and the only instrument for that is a
|
||||
# screendump. This drives it through `echo d > /proc/sysrq` and takes a picture.
|
||||
#
|
||||
# What the pattern proves: four solid quadrants in theme colours (a stride error
|
||||
# would slant the bands, a bpp error would change the colours), a white frame one
|
||||
# pixel inside the edge (an off-by-one in the present rectangle would clip it),
|
||||
# and the fact that every pixel was drawn into RAM before a single byte reached
|
||||
# the framebuffer -- which is what double buffering means.
|
||||
#
|
||||
# QEMU's monitor is on its own socket so the serial console stays free for the
|
||||
# shell. screendump writes a PPM; the PNG conversion is optional and skipped if
|
||||
# ImageMagick is not around.
|
||||
#
|
||||
# tools\crashshot.ps1 crash the ramdisk-root build
|
||||
# tools\crashshot.ps1 -SchFS with a SchFS volume attached
|
||||
|
||||
param(
|
||||
[switch]$SchFS,
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Out = "build\dispshot.ppm",
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$hda = Join-Path $root 'schunix.img'
|
||||
$hdb = if ($SchFS) { Join-Path $root 'build\schfs.img' } else { Join-Path $root 'build\fs.img' }
|
||||
$log = Join-Path $root 'build\crashshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$mon = Join-Path $root 'build\qemu-mon.sock'
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
# The monitor goes to a TCP port rather than a unix socket: PowerShell can open
|
||||
# a TCP client without any extra machinery, and QEMU on Windows has no unix
|
||||
# sockets anyway.
|
||||
$monPort = 55555
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
|
||||
# Show the device list first, so the transcript records what was bound.
|
||||
Write-Host " > listing devices" -ForegroundColor DarkGray
|
||||
Send-Line 'cat /proc/devices'
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
Write-Host " > echo d > /proc/sysrq" -ForegroundColor DarkGray
|
||||
Send-Line 'echo d > /proc/sysrq'
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$stream = $client.GetStream()
|
||||
$writer = New-Object System.IO.StreamWriter($stream)
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
# QEMU on Windows wants a native path here, not the one PowerShell shows.
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
Write-Host "==> serial transcript" -ForegroundColor Cyan
|
||||
# Only the crash report, not the whole boot.
|
||||
$idx = $text.IndexOf('display: presented')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) } else { Write-Host $text }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
$size = (Get-Item $ppm).Length
|
||||
Write-Host "==> $ppm ($size bytes)" -ForegroundColor Green
|
||||
$png = [System.IO.Path]::ChangeExtension($ppm, '.png')
|
||||
$magick = Get-Command magick -ErrorAction SilentlyContinue
|
||||
if ($magick) {
|
||||
& magick $ppm $png
|
||||
Write-Host "==> $png" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "FAIL: no screendump was produced" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
87
tools/fbshot.ps1
Normal file
87
tools/fbshot.ps1
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Draw on /dev/fb0 and photograph the result.
|
||||
#
|
||||
# bootcheck -Fb proves fbinfo *ran* and that the numbers it read back are the
|
||||
# ones the bootloader programmed. It cannot prove the pixels landed where they
|
||||
# were meant to: a framebuffer written with the wrong stride still reports
|
||||
# every byte accepted, and the only symptom is a picture that shears
|
||||
# progressively down the screen. That is a thing you have to look at.
|
||||
#
|
||||
# tools\fbshot.ps1 gradient through mmap
|
||||
# tools\fbshot.ps1 -Mode bars colour bars through write()
|
||||
|
||||
param(
|
||||
[ValidateSet('fill', 'bars')]
|
||||
[string]$Mode = 'fill',
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$hda = Join-Path $root 'schunix.img'
|
||||
$hdb = Join-Path $root 'build\fs.img'
|
||||
$log = Join-Path $root 'build\fbshot.log'
|
||||
$ppm = Join-Path $root "build\fb-$Mode.ppm"
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
$monPort = 55556 # not crashshot's 55555, so the two can run back to back
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
Write-Host " > fbinfo $Mode" -ForegroundColor DarkGray
|
||||
Send-Line "fbinfo $Mode"
|
||||
Start-Sleep -Seconds 8 # 1.9 MB a pixel at a time is not fast
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$writer = New-Object System.IO.StreamWriter($client.GetStream())
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
$idx = $text.IndexOf('fbinfo')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
Write-Host "==> $ppm ($((Get-Item $ppm).Length) bytes)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host 'FAIL: no screendump was produced' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
200
tools/loc.ps1
Normal file
200
tools/loc.ps1
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# How much of Schunix is actually Schunix.
|
||||
#
|
||||
# `wc -l` over the whole tree answers a question nobody asked: it counts musl,
|
||||
# toybox, mksh and a NetBSD checkout, which between them are
|
||||
# two orders of magnitude larger than this project and none of them ours. This
|
||||
# counts only first-party code, and separates the code from the comments so the
|
||||
# number means something.
|
||||
#
|
||||
# tools\loc.ps1 the summary table
|
||||
# tools\loc.ps1 -PerFile every file, largest first
|
||||
# tools\loc.ps1 -All include third-party trees, for comparison
|
||||
#
|
||||
# Comment detection is deliberately simple: it handles // and /* */ for C and
|
||||
# # for scripts, and does not try to understand a // inside a string literal.
|
||||
# That is worth being honest about -- the comment ratio is indicative, not
|
||||
# exact. The line and byte counts are exact.
|
||||
|
||||
param(
|
||||
[switch]$PerFile,
|
||||
[switch]$All,
|
||||
[int]$Top = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
|
||||
# What counts as ours. Everything else in the tree is either somebody else's
|
||||
# source or generated output, and mixing either in makes the total meaningless.
|
||||
$areas = [ordered]@{
|
||||
'kernel' = 'kernel'
|
||||
'include' = 'include'
|
||||
'boot' = 'boot'
|
||||
'lib' = 'lib'
|
||||
'cmd' = 'cmd'
|
||||
'tools' = 'tools'
|
||||
'docs' = 'docs'
|
||||
'projects' = 'projects'
|
||||
}
|
||||
if ($All) {
|
||||
$areas['ports (3rd party)'] = 'ports'
|
||||
$areas['src-trunk (NetBSD)'] = 'src-trunk'
|
||||
}
|
||||
|
||||
# Source only. Images, fonts-as-data and upstream tarballs are not code.
|
||||
$exts = @('.c', '.h', '.S', '.s', '.ld', '.inc', '.ps1', '.sh', '.pl', '.py', '.md', '.mk')
|
||||
|
||||
function Measure-Area([string]$rel) {
|
||||
$dir = Join-Path $root $rel
|
||||
$res = [PSCustomObject]@{
|
||||
Area = $rel; Files = 0; Lines = 0; Code = 0; Comment = 0; Blank = 0
|
||||
Bytes = 0; NonWs = 0; Alpha = 0; Digit = 0; Files_ = @()
|
||||
}
|
||||
if (-not (Test-Path $dir)) { return $res }
|
||||
|
||||
Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$f = $_
|
||||
if ($exts -notcontains $f.Extension.ToLower()) { return }
|
||||
# Makefiles have no extension; picked up separately below.
|
||||
$text = [System.IO.File]::ReadAllText($f.FullName)
|
||||
$lines = $text -split "`r?`n"
|
||||
# A trailing newline produces one empty element; do not count it.
|
||||
if ($lines.Length -gt 0 -and $lines[-1] -eq '') { $lines = $lines[0..($lines.Length-2)] }
|
||||
|
||||
$code = 0; $comment = 0; $blank = 0
|
||||
$inBlock = $false
|
||||
$isC = @('.c','.h','.s','.ld','.inc') -contains $f.Extension.ToLower()
|
||||
foreach ($l in $lines) {
|
||||
$t = $l.Trim()
|
||||
if ($t -eq '') { $blank++; continue }
|
||||
if ($inBlock) {
|
||||
$comment++
|
||||
if ($t -match '\*/') { $inBlock = $false }
|
||||
continue
|
||||
}
|
||||
if ($isC) {
|
||||
if ($t.StartsWith('//')) { $comment++; continue }
|
||||
if ($t.StartsWith('/*')) {
|
||||
$comment++
|
||||
if ($t -notmatch '\*/') { $inBlock = $true }
|
||||
continue
|
||||
}
|
||||
} elseif ($t.StartsWith('#') -and $f.Extension -ne '.md') {
|
||||
$comment++; continue
|
||||
}
|
||||
$code++
|
||||
}
|
||||
|
||||
$nonWs = ($text.ToCharArray() | Where-Object { -not [char]::IsWhiteSpace($_) }).Count
|
||||
$alpha = ($text.ToCharArray() | Where-Object { [char]::IsLetter($_) }).Count
|
||||
$digit = ($text.ToCharArray() | Where-Object { [char]::IsDigit($_) }).Count
|
||||
|
||||
$res.Files++
|
||||
$res.Lines += $lines.Length
|
||||
$res.Code += $code
|
||||
$res.Comment += $comment
|
||||
$res.Blank += $blank
|
||||
$res.Bytes += $f.Length
|
||||
$res.NonWs += $nonWs
|
||||
$res.Alpha += $alpha
|
||||
$res.Digit += $digit
|
||||
$res.Files_ += [PSCustomObject]@{
|
||||
Path = $f.FullName.Substring($root.Length + 1)
|
||||
Lines = $lines.Length; Code = $code; Comment = $comment
|
||||
}
|
||||
}
|
||||
return $res
|
||||
}
|
||||
|
||||
$results = @()
|
||||
foreach ($k in $areas.Keys) { $results += (Measure-Area $areas[$k]) }
|
||||
|
||||
# The top-level Makefile has no extension, so it is added by hand rather than
|
||||
# widening the extension list and dragging in every stray file in the tree.
|
||||
$mk = Join-Path $root 'Makefile'
|
||||
if (Test-Path $mk) {
|
||||
$mkLines = ([System.IO.File]::ReadAllText($mk) -split "`r?`n")
|
||||
$r = $results | Where-Object { $_.Area -eq 'tools' } | Select-Object -First 1
|
||||
if ($r) {
|
||||
$r.Files++; $r.Lines += $mkLines.Length; $r.Bytes += (Get-Item $mk).Length
|
||||
$r.Code += ($mkLines | Where-Object { $_.Trim() -ne '' -and -not $_.Trim().StartsWith('#') }).Count
|
||||
$r.Comment += ($mkLines | Where-Object { $_.Trim().StartsWith('#') }).Count
|
||||
$r.Blank += ($mkLines | Where-Object { $_.Trim() -eq '' }).Count
|
||||
}
|
||||
}
|
||||
|
||||
$fmt = @(
|
||||
@{ N='Area'; E={$_.Area} }
|
||||
@{ N='Files'; E={$_.Files}; A='right' }
|
||||
@{ N='Lines'; E={'{0:N0}' -f $_.Lines}; A='right' }
|
||||
@{ N='Code'; E={'{0:N0}' -f $_.Code}; A='right' }
|
||||
@{ N='Comment'; E={'{0:N0}' -f $_.Comment}; A='right' }
|
||||
@{ N='Blank'; E={'{0:N0}' -f $_.Blank}; A='right' }
|
||||
@{ N='Cmt%'; E={ if ($_.Code + $_.Comment) { '{0:N0}%' -f (100 * $_.Comment / ($_.Code + $_.Comment)) } else { '-' } }; A='right' }
|
||||
@{ N='Ln/File'; E={ if ($_.Files) { '{0:N0}' -f ($_.Lines / $_.Files) } else { '-' } }; A='right' }
|
||||
@{ N='KiB'; E={'{0:N0}' -f ($_.Bytes / 1KB)}; A='right' }
|
||||
)
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' Schunix — first-party source' -ForegroundColor Cyan
|
||||
Write-Host ''
|
||||
Write-Host (($results | Where-Object { $_.Files -gt 0 } | Sort-Object Lines -Descending | Format-Table $fmt -AutoSize | Out-String))
|
||||
|
||||
# Two totals, because they answer different questions and conflating them
|
||||
# flatters the number. "The OS" is what boots and runs on the machine; docs,
|
||||
# host-side tools and the projects/ stubs are first-party but are not Schunix
|
||||
# running on hardware. Linux 0.01's 10,239 lines were kernel, headers and boot
|
||||
# -- no documentation and no build tooling -- so only the first total is a fair
|
||||
# comparison.
|
||||
$osAreas = @('kernel','include','boot','lib','cmd')
|
||||
$own = $results | Where-Object { $_.Area -notin @('ports','src-trunk') }
|
||||
$os = $results | Where-Object { $_.Area -in $osAreas }
|
||||
$T = ($own | Measure-Object -Property Lines -Sum).Sum
|
||||
$C = ($own | Measure-Object -Property Code -Sum).Sum
|
||||
$M = ($own | Measure-Object -Property Comment -Sum).Sum
|
||||
$B = ($own | Measure-Object -Property Blank -Sum).Sum
|
||||
$F = ($own | Measure-Object -Property Files -Sum).Sum
|
||||
$By = ($own | Measure-Object -Property Bytes -Sum).Sum
|
||||
$NW = ($own | Measure-Object -Property NonWs -Sum).Sum
|
||||
$AL = ($own | Measure-Object -Property Alpha -Sum).Sum
|
||||
$DG = ($own | Measure-Object -Property Digit -Sum).Sum
|
||||
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Files', $F)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Lines total', $T)
|
||||
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which code', $C, (100 * $C / [Math]::Max($T,1)))
|
||||
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which comment', $M, (100 * $M / [Math]::Max($T,1)))
|
||||
Write-Host (' {0,-22} {1,12:N0} {2:N0}%' -f ' of which blank', $B, (100 * $B / [Math]::Max($T,1)))
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Lines per file', ($T / [Math]::Max($F,1)))
|
||||
Write-Host ''
|
||||
$osT = ($os | Measure-Object -Property Lines -Sum).Sum
|
||||
$osC = ($os | Measure-Object -Property Code -Sum).Sum
|
||||
$osF = ($os | Measure-Object -Property Files -Sum).Sum
|
||||
Write-Host (' {0,-22} {1,12:N0} ({2:N0} code, {3} files)' -f 'THE OS ITSELF', $osT, $osC, $osF) -ForegroundColor Green
|
||||
Write-Host (' {0,-22}' -f ' kernel+include+boot+lib+cmd') -ForegroundColor DarkGray
|
||||
Write-Host ''
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Bytes', $By)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f 'Non-whitespace chars', $NW)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f ' letters', $AL)
|
||||
Write-Host (' {0,-22} {1,12:N0}' -f ' digits', $DG)
|
||||
Write-Host ''
|
||||
|
||||
# For scale. Linux 0.01 (September 1991) is 10,239 lines by the same crude
|
||||
# measure -- every line of every file in the tarball, comments included.
|
||||
$lin = 10239
|
||||
Write-Host (' For scale: Linux 0.01 was {0:N0} lines (kernel, headers, boot -- the' -f $lin) -ForegroundColor DarkGray
|
||||
Write-Host (' same things counted above as THE OS ITSELF). Schunix is {0:N1}x that.' -f ($osT / $lin)) -ForegroundColor DarkGray
|
||||
Write-Host ''
|
||||
|
||||
if ($PerFile) {
|
||||
Write-Host ' Largest files' -ForegroundColor Cyan
|
||||
# Out-String, not a bare Format-Table: two Format-Table calls writing to
|
||||
# the same output stream interleave their grouping records and PowerShell
|
||||
# rejects the second one outright.
|
||||
$rows = ($own | ForEach-Object { $_.Files_ }) |
|
||||
Sort-Object Lines -Descending | Select-Object -First $Top |
|
||||
Format-Table @{N='Lines';E={'{0:N0}' -f $_.Lines};A='right'},
|
||||
@{N='Code'; E={'{0:N0}' -f $_.Code};A='right'},
|
||||
@{N='Cmt'; E={'{0:N0}' -f $_.Comment};A='right'},
|
||||
Path -AutoSize | Out-String
|
||||
Write-Host $rows
|
||||
}
|
||||
153
tools/makeiso.ps1
Normal file
153
tools/makeiso.ps1
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Build a bootable Schunix ISO, ready for VMware or a real machine.
|
||||
#
|
||||
# Wraps `make iso`, which builds the ramdisk-root variant and wraps it in an
|
||||
# El Torito image. The ramdisk root is the point: the ISO carries a complete
|
||||
# userland inside the kernel, so it boots with no hard disk attached at all.
|
||||
#
|
||||
# tools\makeiso.ps1 build it
|
||||
# tools\makeiso.ps1 -Run build it, then boot it under QEMU to check
|
||||
# tools\makeiso.ps1 -Clean force a full rebuild first
|
||||
#
|
||||
# Afterwards the ISO is schunix.iso in the repository root; point a VM's optical
|
||||
# drive at it and boot from CD.
|
||||
|
||||
param(
|
||||
[switch]$Run,
|
||||
[switch]$Clean,
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
Push-Location $root
|
||||
|
||||
function Fail($msg) {
|
||||
Write-Host "FAIL: $msg" -ForegroundColor Red
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Git Bash, found by full path and never as bare `bash`.
|
||||
#
|
||||
# From PowerShell, `bash` resolves to C:\...\WindowsApps\bash.exe -- the *WSL*
|
||||
# launcher -- long before anything on PATH. On this machine WSL2 cannot start at
|
||||
# all, because it needs the Hyper-V hypervisor underneath the whole system and
|
||||
# that is deliberately off. So calling `bash` here does not run a different
|
||||
# shell, it fails with a wall of virtualisation advice and an exit code that
|
||||
# looks like the build broke.
|
||||
$BashCandidates = @(
|
||||
"$env:ProgramFiles\Git\bin\bash.exe"
|
||||
"${env:ProgramFiles(x86)}\Git\bin\bash.exe"
|
||||
"$env:USERPROFILE\scoop\apps\git\current\bin\bash.exe"
|
||||
"$env:LOCALAPPDATA\Programs\Git\bin\bash.exe"
|
||||
)
|
||||
$Bash = $BashCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $Bash) { Fail "Git Bash not found; looked in:`n $($BashCandidates -join "`n ")" }
|
||||
|
||||
function Invoke-Make([string]$cmd) {
|
||||
# -c, not -lc: a login shell sources the profile for no benefit here.
|
||||
# stderr folded in inside bash, because $ErrorActionPreference = 'Stop'
|
||||
# turns a native program's stderr into a terminating error, and mkisofs
|
||||
# writes a harmless warning there on every single run.
|
||||
& $Bash -c "cd '$($root -replace '\\','/')' && $cmd 2>&1"
|
||||
}
|
||||
|
||||
# An ISO needs one of these to assemble the filesystem. There is no xorriso
|
||||
# package for Windows in scoop; cdrtools provides mkisofs, and tools/mkiso.sh
|
||||
# accepts either -- their El Torito arguments are compatible.
|
||||
$haveIso = (Get-Command xorriso -ErrorAction SilentlyContinue) -or
|
||||
(Get-Command mkisofs -ErrorAction SilentlyContinue)
|
||||
if (-not $haveIso) {
|
||||
Write-Host "Need xorriso or mkisofs on PATH." -ForegroundColor Yellow
|
||||
Write-Host " Windows: scoop install cdrtools"
|
||||
Write-Host " Debian/Ubuntu: apt install xorriso"
|
||||
Fail "no ISO builder"
|
||||
}
|
||||
|
||||
if ($Clean) {
|
||||
Write-Host "==> cleaning" -ForegroundColor Cyan
|
||||
# Not `make clean`: that would drop the ports too, and those take an
|
||||
# afternoon to rebuild on a host where they are not rebuilt at all.
|
||||
Invoke-Make "rm -rf build/kernel build/lib build/cmd" | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "==> building (this makes the ramdisk-root kernel, which is larger)" -ForegroundColor Cyan
|
||||
|
||||
# Output captured whole rather than piped through a filter. Piping a native
|
||||
# command into ForEach-Object leaves $LASTEXITCODE reflecting the pipeline
|
||||
# rather than the command, which reported a perfectly successful `make iso` as
|
||||
# a failure. Redirect stderr inside bash too: with $ErrorActionPreference set to
|
||||
# Stop, PowerShell turns a native program's stderr into terminating errors, and
|
||||
# mkisofs writes a harmless warning there every single run.
|
||||
$out = Invoke-Make "make iso"
|
||||
$code = $LASTEXITCODE
|
||||
$out | Where-Object { $_ -match '^==>|mkiso:|nmeta|rror' } |
|
||||
ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||
if ($code -ne 0) {
|
||||
$out | Select-Object -Last 20 | ForEach-Object { Write-Host $_ }
|
||||
Fail "make iso returned $code"
|
||||
}
|
||||
|
||||
$iso = Join-Path $root 'schunix.iso'
|
||||
if (-not (Test-Path $iso)) { Fail "schunix.iso was not produced" }
|
||||
|
||||
# `make iso` leaves the tree holding objects compiled with -DRAMDISK_ROOT. A
|
||||
# following plain `make` would then fail to link, on a missing ramdisk symbol,
|
||||
# for reasons that look nothing like their cause. Put it back.
|
||||
Write-Host "==> restoring the normal (disk-root) build" -ForegroundColor Cyan
|
||||
Invoke-Make "rm -rf build/kernel build/lib build/cmd && make >/dev/null" | Out-Null
|
||||
|
||||
$mb = [Math]::Round((Get-Item $iso).Length / 1MB, 1)
|
||||
Write-Host ""
|
||||
Write-Host "==> $iso ($mb MB)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "VMware:" -ForegroundColor Cyan
|
||||
Write-Host " New VM -> I will install the OS later -> Other / Other 64-bit"
|
||||
Write-Host " CD/DVD -> Use ISO image file -> $iso, and tick Connect at power on"
|
||||
Write-Host " Boot order: CD-ROM first (F2 at power on for the firmware setup)"
|
||||
Write-Host ""
|
||||
Write-Host " Set the VM to BIOS, not UEFI. Schunix's loader is 16-bit real"
|
||||
Write-Host " mode and there is no EFI stub yet, so a UEFI-only VM will not boot."
|
||||
Write-Host ""
|
||||
Write-Host " Processors: as many as you like. SMP works as of 2026-08-08;"
|
||||
Write-Host " check with 'cat /proc/cpuinfo', which lists the online ones."
|
||||
Write-Host ""
|
||||
Write-Host " RAM: 64 MB is plenty. The old 224 MB floor is gone."
|
||||
|
||||
if ($Run) {
|
||||
Write-Host ""
|
||||
Write-Host "==> booting the ISO under QEMU, no disk attached" -ForegroundColor Cyan
|
||||
$log = Join-Path $root 'build\isoboot.log'
|
||||
if (Test-Path $log) { Clear-Content $log }
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-cdrom `"$iso`" -boot d"
|
||||
'-m 256M -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
'-no-reboot'
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
Start-Sleep -Seconds 25
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 600
|
||||
|
||||
$text = if (Test-Path $log) { Get-Content $log -Raw } else { "" }
|
||||
if ($text -match 'init: ready') {
|
||||
Write-Host "==> the ISO boots to a shell" -ForegroundColor Green
|
||||
($text -split "`n" | Select-Object -First 12) | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||
} else {
|
||||
Write-Host "==> the ISO did NOT reach a shell" -ForegroundColor Red
|
||||
Write-Host $text
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
1244
tools/mkfs.c
Normal file
1244
tools/mkfs.c
Normal file
File diff suppressed because it is too large
Load diff
91
tools/mkiso.sh
Normal file
91
tools/mkiso.sh
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/bin/sh
|
||||
# Build a bootable Schunix ISO.
|
||||
#
|
||||
# tools/mkiso.sh [output.iso]
|
||||
#
|
||||
# The disc boots through El Torito's "no emulation" mode, which is the only
|
||||
# one of the three that fits how Schunix actually loads itself.
|
||||
#
|
||||
# floppy emulation caps the image at 2.88 MB; the ramdisk-root kernel
|
||||
# alone is bigger than that.
|
||||
# hard disk emulation presents the image through the BIOS's INT 13h, and
|
||||
# Schunix's loader does not use INT 13h -- it drives
|
||||
# the IDE controller's registers directly, which
|
||||
# cannot reach a BIOS-emulated drive.
|
||||
# no emulation the BIOS loads a chosen run of sectors at 0x7C00
|
||||
# and gets out of the way. That is this.
|
||||
#
|
||||
# So the layout below is deliberate: -boot-load-size is 1 + STAGE2_SECTORS,
|
||||
# which makes the BIOS place both boot/cdboot and the whole of stage2 in memory
|
||||
# before anything of ours runs. That puts stage2 at 0x7E00 -- byte for byte the
|
||||
# arrangement bootasm.S produces on a hard disk. From there the only difference
|
||||
# is that stage2main.c reads the kernel back with ATAPI instead of ATA.
|
||||
#
|
||||
# STAGE2_SECTORS comes from the environment, set by the Makefile, because this
|
||||
# same number also lives in boot/bootasm.S, boot/stage2main.c and the dd offsets
|
||||
# in schunix.img's recipe. It was previously written out by hand in three
|
||||
# separate spots in this file alone, and raising it for the boot logo is what
|
||||
# turned that from a latent hazard into a broken image.
|
||||
#
|
||||
# NOT using -boot-info-table on purpose: it patches 56 bytes over offset 8 of
|
||||
# the boot image, which is the middle of cdboot's code. stage2main.c finds
|
||||
# where it was loaded from by reading the El Torito boot catalog instead.
|
||||
|
||||
set -e
|
||||
|
||||
BUILD=${BUILD:-build}
|
||||
OUT=${1:-schunix.iso}
|
||||
STAGE2_SECTORS=${STAGE2_SECTORS:-48}
|
||||
STAGE2_BYTES=$((STAGE2_SECTORS * 512))
|
||||
LOAD_SIZE=$((1 + STAGE2_SECTORS))
|
||||
STAGING="$BUILD/iso"
|
||||
|
||||
CDBOOT="$BUILD/boot/cdboot"
|
||||
STAGE2="$BUILD/boot/stage2"
|
||||
KERNEL="$BUILD/kernel/kernel"
|
||||
|
||||
for f in "$CDBOOT" "$STAGE2" "$KERNEL"; do
|
||||
[ -f "$f" ] || { echo "mkiso: missing $f -- run make first" >&2; exit 1; }
|
||||
done
|
||||
|
||||
# The three pieces must be contiguous and at these exact sizes: stage2main.c's
|
||||
# readseg() finds the kernel at sector (1 + STAGE2_SECTORS) of the boot image,
|
||||
# so a short stage2 would silently shift the kernel out from under it.
|
||||
cdboot_sz=$(wc -c < "$CDBOOT")
|
||||
stage2_sz=$(wc -c < "$STAGE2")
|
||||
[ "$cdboot_sz" -eq 512 ] || { echo "mkiso: cdboot is $cdboot_sz bytes, expected 512" >&2; exit 1; }
|
||||
[ "$stage2_sz" -eq "$STAGE2_BYTES" ] || { echo "mkiso: stage2 is $stage2_sz bytes, expected $STAGE2_BYTES (STAGE2_SECTORS=$STAGE2_SECTORS)" >&2; exit 1; }
|
||||
|
||||
rm -rf "$STAGING"
|
||||
mkdir -p "$STAGING/boot"
|
||||
|
||||
cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schunix.bin"
|
||||
|
||||
# xorriso if it is here, mkisofs otherwise. Both accept the El Torito flags
|
||||
# below unchanged -- xorriso's -as mkisofs mode exists precisely to be
|
||||
# argument-compatible with it. The fallback is not academic: there is no xorriso
|
||||
# package for Windows in scoop, where this is developed, but cdrtools provides
|
||||
# mkisofs.exe.
|
||||
if command -v xorriso >/dev/null 2>&1; then
|
||||
MKISOFS="xorriso -as mkisofs"
|
||||
elif command -v mkisofs >/dev/null 2>&1; then
|
||||
MKISOFS="mkisofs"
|
||||
else
|
||||
echo "mkiso: need xorriso or mkisofs on PATH" >&2
|
||||
echo " Windows: scoop install cdrtools" >&2
|
||||
echo " Debian/Ubuntu: apt install xorriso" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$MKISOFS \
|
||||
-quiet \
|
||||
-V SCHUNIX \
|
||||
-A "Schunix" \
|
||||
-b boot/schunix.bin \
|
||||
-no-emul-boot \
|
||||
-boot-load-size "$LOAD_SIZE" \
|
||||
-o "$OUT" \
|
||||
"$STAGING"
|
||||
|
||||
echo "==> $OUT ($(wc -c < "$OUT") bytes)"
|
||||
echo " qemu-system-x86_64 -cdrom $OUT -boot d"
|
||||
184
tools/mklogo.py
Normal file
184
tools/mklogo.py
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Turn Assets/*.png into a C header the kernel can draw.
|
||||
|
||||
Why this exists rather than a PNG decoder in the kernel: the boot logo is one
|
||||
fixed image that changes about never, so decoding it at runtime would mean
|
||||
carrying an inflate implementation in the kernel forever to answer a question
|
||||
that is already known at build time. Precedent is include/font.h, which is the
|
||||
same trade for the same reason.
|
||||
|
||||
Output is an 8-bit coverage mask, not colour. The logo is a single-colour
|
||||
silhouette (a monogram in a ring), so what matters is *how much* of each pixel
|
||||
is ink -- which lets the kernel composite it in whatever the theme's accent
|
||||
happens to be, anti-aliased, at one byte per pixel. Storing RGB would be four
|
||||
times the size and would hardcode a colour into the kernel image.
|
||||
|
||||
python3 tools/mklogo.py "Assets/Schunix Small Logo.png" include/logo.h 128
|
||||
"""
|
||||
|
||||
import sys, zlib, struct
|
||||
|
||||
|
||||
def read_png(path):
|
||||
d = open(path, 'rb').read()
|
||||
if d[:8] != b'\x89PNG\r\n\x1a\n':
|
||||
raise SystemExit(f'{path}: not a PNG')
|
||||
pos, idat, w, h, bd, ct = 8, b'', 0, 0, 0, 0
|
||||
while pos < len(d):
|
||||
ln, typ = struct.unpack('>I4s', d[pos:pos + 8])
|
||||
body = d[pos + 8:pos + 8 + ln]
|
||||
if typ == b'IHDR':
|
||||
w, h, bd, ct = struct.unpack('>IIBB', body[:10])
|
||||
elif typ == b'IDAT':
|
||||
idat += body
|
||||
elif typ == b'IEND':
|
||||
break
|
||||
pos += 12 + ln
|
||||
if bd != 8 or ct not in (2, 6):
|
||||
raise SystemExit(f'{path}: need 8-bit RGB or RGBA, got depth={bd} type={ct}')
|
||||
nch = 4 if ct == 6 else 3
|
||||
raw = zlib.decompress(idat)
|
||||
|
||||
# Undo PNG's per-scanline filters. Each row is prefixed with a filter byte
|
||||
# and predicted from the row above and the pixel to the left; without this
|
||||
# step the data is not an image at all.
|
||||
stride = w * nch
|
||||
out = bytearray(h * stride)
|
||||
prev = bytearray(stride)
|
||||
p = 0
|
||||
for y in range(h):
|
||||
f = raw[p]; p += 1
|
||||
cur = bytearray(raw[p:p + stride]); p += stride
|
||||
if f == 1:
|
||||
for i in range(nch, stride):
|
||||
cur[i] = (cur[i] + cur[i - nch]) & 0xFF
|
||||
elif f == 2:
|
||||
for i in range(stride):
|
||||
cur[i] = (cur[i] + prev[i]) & 0xFF
|
||||
elif f == 3:
|
||||
for i in range(stride):
|
||||
a = cur[i - nch] if i >= nch else 0
|
||||
cur[i] = (cur[i] + ((a + prev[i]) >> 1)) & 0xFF
|
||||
elif f == 4:
|
||||
for i in range(stride):
|
||||
a = cur[i - nch] if i >= nch else 0
|
||||
b = prev[i]
|
||||
c = prev[i - nch] if i >= nch else 0
|
||||
pa, pb, pc = abs(b - c), abs(a - c), abs(a + b - 2 * c)
|
||||
pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
|
||||
cur[i] = (cur[i] + pr) & 0xFF
|
||||
elif f != 0:
|
||||
raise SystemExit(f'unknown PNG filter {f}')
|
||||
out[y * stride:(y + 1) * stride] = cur
|
||||
prev = cur
|
||||
return w, h, nch, out
|
||||
|
||||
|
||||
def ink(px, nch, x, y, w):
|
||||
"""How much ink is at (x, y), 0..255.
|
||||
|
||||
The source is a dark mark on white with an alpha channel. Ink is therefore
|
||||
'opaque and not white': darkness weighted by alpha. Using luminance alone
|
||||
would make the transparent margin count as black.
|
||||
"""
|
||||
i = (y * w + x) * nch
|
||||
r, g, b = px[i], px[i + 1], px[i + 2]
|
||||
a = px[i + 3] if nch == 4 else 255
|
||||
lum = (r * 299 + g * 587 + b * 114) // 1000
|
||||
return ((255 - lum) * a) // 255
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
raise SystemExit(__doc__)
|
||||
src, dst = sys.argv[1], sys.argv[2]
|
||||
size = int(sys.argv[3]) if len(sys.argv) > 3 else 128
|
||||
|
||||
w, h, nch, px = read_png(src)
|
||||
|
||||
# Crop to the mark. These files are 1920x1080 with the logo floating in a
|
||||
# large empty field; scaling that whole canvas down would waste most of the
|
||||
# output on margin and leave the mark tiny.
|
||||
x0, y0, x1, y1 = w, h, -1, -1
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if ink(px, nch, x, y, w) > 24:
|
||||
if x < x0: x0 = x
|
||||
if y < y0: y0 = y
|
||||
if x > x1: x1 = x
|
||||
if y > y1: y1 = y
|
||||
if x1 < 0:
|
||||
raise SystemExit(f'{src}: no non-white pixels found')
|
||||
|
||||
# Square the crop so the aspect ratio survives the scale.
|
||||
cw, ch = x1 - x0 + 1, y1 - y0 + 1
|
||||
side = max(cw, ch)
|
||||
x0 -= (side - cw) // 2
|
||||
y0 -= (side - ch) // 2
|
||||
|
||||
# Box filter. Averaging every source pixel that lands in an output cell is
|
||||
# what keeps the ring's edge smooth; nearest-neighbour at this reduction
|
||||
# ratio (roughly 8:1) would break it into dashes.
|
||||
mask = bytearray(size * size)
|
||||
for oy in range(size):
|
||||
sy0 = y0 + oy * side // size
|
||||
sy1 = max(sy0 + 1, y0 + (oy + 1) * side // size)
|
||||
for ox in range(size):
|
||||
sx0 = x0 + ox * side // size
|
||||
sx1 = max(sx0 + 1, x0 + (ox + 1) * side // size)
|
||||
tot = n = 0
|
||||
for sy in range(sy0, sy1):
|
||||
if sy < 0 or sy >= h: continue
|
||||
for sx in range(sx0, sx1):
|
||||
if sx < 0 or sx >= w: continue
|
||||
tot += ink(px, nch, sx, sy, w); n += 1
|
||||
mask[oy * size + ox] = tot // n if n else 0
|
||||
|
||||
# Normalise to full strength. The source art is a mid-grey gradient on
|
||||
# white, so its darkest pixel is only about 60% "ink" -- composited as-is
|
||||
# against a near-black background the mark comes out muddy. Scaling by the
|
||||
# peak keeps the gradient's internal shape (the relative values are
|
||||
# unchanged) while letting the brightest part of the logo actually reach the
|
||||
# accent colour.
|
||||
peak = max(mask)
|
||||
if peak:
|
||||
for i, v in enumerate(mask):
|
||||
mask[i] = min(255, v * 255 // peak)
|
||||
|
||||
# Pack two pixels per byte. This header is included by the *bootloader* as
|
||||
# well as the kernel, and stage2 has to fit between 0x7E00 and the 0x10000
|
||||
# kernel-search buffer -- so the mask's size is a boot-time constraint, not
|
||||
# just a matter of taste. 16 grey levels is visually indistinguishable from
|
||||
# 256 for a single-colour mark, and halves the cost.
|
||||
packed = bytearray(len(mask) // 2)
|
||||
for i in range(0, len(mask), 2):
|
||||
packed[i // 2] = ((mask[i] // 17) << 4) | (mask[i + 1] // 17)
|
||||
|
||||
with open(dst, 'w', newline='\n') as f:
|
||||
f.write('#pragma once\n\n')
|
||||
f.write('// GENERATED by tools/mklogo.py -- do not edit. Run `make logo`.\n')
|
||||
f.write(f'// source: {src}\n')
|
||||
f.write('//\n')
|
||||
f.write('// An ink-coverage mask, not colour: 0 is background and full scale is\n')
|
||||
f.write('// solid logo. Whoever draws it picks the colour, so the palette stays in\n')
|
||||
f.write('// include/theme.h and can change without regenerating anything.\n')
|
||||
f.write('//\n')
|
||||
f.write('// Two pixels per byte, high nibble first. Included by boot/stage2main.c\n')
|
||||
f.write('// as well as kernel/dev/vga.c, and stage2 must fit below the 0x10000\n')
|
||||
f.write('// kernel-search buffer -- hence 4 bits rather than 8.\n\n')
|
||||
f.write(f'#define LOGO_W {size}\n#define LOGO_H {size}\n\n')
|
||||
f.write('// Ink at linear pixel index i, 0..255.\n')
|
||||
f.write('#define LOGO_INK(i) ((unsigned char)((((i) & 1) '
|
||||
'? (logo_mask[(i) >> 1] & 0xF) \\\n')
|
||||
f.write(' : (logo_mask[(i) >> 1] >> 4)) * 17))\n\n')
|
||||
f.write('static const unsigned char logo_mask[LOGO_W * LOGO_H / 2] = {\n')
|
||||
for i in range(0, len(packed), 16):
|
||||
f.write(' ' + ','.join('0x%02x' % b for b in packed[i:i + 16]) + ',\n')
|
||||
f.write('};\n')
|
||||
|
||||
nz = sum(1 for b in mask if b)
|
||||
print(f'{dst}: {size}x{size}, cropped from {cw}x{ch} at ({x0},{y0}), '
|
||||
f'{nz} inked pixels, {len(packed)} bytes packed (4bpp)')
|
||||
|
||||
|
||||
main()
|
||||
650
tools/mkschfs.c
Normal file
650
tools/mkschfs.c
Normal file
|
|
@ -0,0 +1,650 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// mkschfs — build a SchFS image from a host directory tree.
|
||||
//
|
||||
// This is the tool that makes the on-disk format real: everything the kernel
|
||||
// mounts in phase 2 is produced here. It deliberately shares the actual
|
||||
// filesystem code (btree.c, vol.c, obj.c, objw.c) rather than reimplementing
|
||||
// the layout, so a disagreement between writer and reader is impossible by
|
||||
// construction — there is only one implementation, exercised on the host where
|
||||
// a mistake costs a second instead of a boot.
|
||||
//
|
||||
// mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]
|
||||
//
|
||||
// Two behaviours worth knowing about:
|
||||
//
|
||||
// * Identical file contents are stored once and hard-linked. sysroot/bin
|
||||
// holds a full copy of the toybox binary per applet, so this is the
|
||||
// difference between a 190 MB image and a 6 MB one.
|
||||
// * Output is reproducible. Directory entries are sorted, the UUID is
|
||||
// derived rather than random, and timestamps come from the source files,
|
||||
// so building the same tree twice gives byte-identical images and a diff
|
||||
// means something actually changed.
|
||||
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
#define SCHFS_HOSTED 1
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
/* major()/minor() for device nodes. glibc puts them here; mingw has no such
|
||||
header and no device numbers at all, because Windows has no mknod. On that
|
||||
host the macros are stubbed: a device node cannot exist in the source tree
|
||||
being imaged, so the values are never anything but zero, and stubbing is
|
||||
honest where pretending to read st_rdev would not be. */
|
||||
#ifdef __MINGW32__
|
||||
#define major(x) 0
|
||||
#define minor(x) 0
|
||||
/* Symlinks, likewise. Windows has no lstat/readlink and mingw exposes neither,
|
||||
so on this host a symlink cannot be encountered while walking the tree --
|
||||
lstat degrades to stat, nothing is ever S_ISLNK, and readlink can only be
|
||||
reached through a branch that is now unreachable. Verified rather than
|
||||
assumed: `find sysroot -type l` returns nothing.
|
||||
|
||||
If symlinks are ever staged (procfs wants /proc/<pid>/exe to be one), this
|
||||
host stops being able to build a faithful image and the stub must become a
|
||||
real Win32 reparse-point read. Failing loudly then is the point of the
|
||||
abort() below. */
|
||||
#define lstat(p, b) stat((p), (b))
|
||||
#define S_ISLNK(m) 0
|
||||
static long readlink(const char *p, char *b, unsigned long n)
|
||||
{ (void)p; (void)b; (void)n; return -1; }
|
||||
#else
|
||||
#include <sys/sysmacros.h>
|
||||
#endif
|
||||
|
||||
#include "../kernel/fs/schfs/btree.c"
|
||||
#include "../kernel/fs/schfs/space.c"
|
||||
#include "../kernel/fs/schfs/vol.c"
|
||||
#include "../kernel/fs/schfs/obj.c"
|
||||
#include "../kernel/fs/schfs/objw.c"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* File-backed block device */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
struct imgdev {
|
||||
FILE *f;
|
||||
struct schfs_vol *vol;
|
||||
uint64_t nblocks;
|
||||
long reads, writes;
|
||||
};
|
||||
|
||||
static struct imgdev IMG;
|
||||
|
||||
static int
|
||||
img_read(void *ctx, uint64_t b, void *buf)
|
||||
{
|
||||
struct imgdev *d = ctx;
|
||||
if (b >= d->nblocks)
|
||||
return -1;
|
||||
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
|
||||
return -1;
|
||||
if (fread(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
|
||||
return -1;
|
||||
d->reads++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
img_write(void *ctx, uint64_t b, const void *buf)
|
||||
{
|
||||
struct imgdev *d = ctx;
|
||||
if (b >= d->nblocks)
|
||||
return -1;
|
||||
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
|
||||
return -1;
|
||||
if (fwrite(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
|
||||
return -1;
|
||||
d->writes++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Allocation */
|
||||
/* */
|
||||
/* Straight through to the volume's space manager. An earlier version */
|
||||
/* kept a build-time free list of its own, because the bump allocator it */
|
||||
/* replaced could only ever take back the block it handed out last and */
|
||||
/* a build otherwise leaked one root-to-leaf path per insert. Two things */
|
||||
/* made that unnecessary: the space manager reuses any freed block, and */
|
||||
/* the tree now rewrites its own nodes in place within a transaction */
|
||||
/* group, so a build barely frees anything in the first place. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static uint64_t
|
||||
img_alloc(void *ctx)
|
||||
{
|
||||
return schfs_vol_alloc(((struct imgdev *)ctx)->vol);
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
img_alloc_run(void *ctx, uint64_t n)
|
||||
{
|
||||
return schfs_vol_alloc_run(((struct imgdev *)ctx)->vol, n);
|
||||
}
|
||||
|
||||
static void
|
||||
img_free(void *ctx, uint64_t b)
|
||||
{
|
||||
schfs_vol_free(((struct imgdev *)ctx)->vol, b);
|
||||
}
|
||||
|
||||
// Pages for the allocation bitmap. calloc rather than malloc so a bug that
|
||||
// forgets to initialise one shows up as "everything is free" consistently
|
||||
// rather than as whatever the heap happened to contain.
|
||||
static void *
|
||||
img_palloc(void)
|
||||
{
|
||||
return calloc(1, SCHFS_BLKSIZE);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Content dedup */
|
||||
/* */
|
||||
/* sysroot/bin contains one independent ~800 KB copy of toybox per */
|
||||
/* applet name. Storing each separately is the difference between an */
|
||||
/* image that fits in RAM and one that does not, so identical content */
|
||||
/* becomes one object with several names -- a plain hard link, which */
|
||||
/* the format already supports through nlink. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
struct dedup_ent {
|
||||
char *path;
|
||||
dev_t hdev; /* host device+inode: when these match, */
|
||||
ino_t hino; /* it is literally the same file */
|
||||
off_t size;
|
||||
uint32_t crc;
|
||||
uint64_t ino;
|
||||
};
|
||||
|
||||
static struct dedup_ent *dedup_tab;
|
||||
static int dedup_n, dedup_cap;
|
||||
|
||||
/* A file the host already told us is the same file.
|
||||
*
|
||||
* ports/toybox/Makefile hard-links its 236 applet names to one binary, so this
|
||||
* settles all but the first of them without reading anything: the content path
|
||||
* below reads the candidate to CRC it and then re-reads both files to confirm,
|
||||
* which is half a gigabyte of I/O for an answer fstat already has. */
|
||||
static uint64_t
|
||||
dedup_lookup_ino(dev_t hdev, ino_t hino)
|
||||
{
|
||||
if (hino == 0)
|
||||
return 0;
|
||||
for (int i = 0; i < dedup_n; i++)
|
||||
if (dedup_tab[i].hdev == hdev && dedup_tab[i].hino == hino)
|
||||
return dedup_tab[i].ino;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
files_identical(const char *a, const char *b)
|
||||
{
|
||||
FILE *fa = fopen(a, "rb"), *fb = fopen(b, "rb");
|
||||
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return 0; }
|
||||
int same = 1;
|
||||
for (;;) {
|
||||
char ba[8192], bb[8192];
|
||||
size_t na = fread(ba, 1, sizeof(ba), fa);
|
||||
size_t nb = fread(bb, 1, sizeof(bb), fb);
|
||||
if (na != nb || memcmp(ba, bb, na) != 0) { same = 0; break; }
|
||||
if (na == 0) break;
|
||||
}
|
||||
fclose(fa); fclose(fb);
|
||||
return same;
|
||||
}
|
||||
|
||||
// Size and CRC agreeing is not proof, so a candidate is confirmed byte by byte.
|
||||
// Silently hard-linking two files that merely collide would corrupt the image
|
||||
// in a way nothing downstream could detect.
|
||||
static uint64_t
|
||||
dedup_lookup(const char *path, off_t size, uint32_t crc)
|
||||
{
|
||||
for (int i = 0; i < dedup_n; i++)
|
||||
if (dedup_tab[i].size == size && dedup_tab[i].crc == crc &&
|
||||
files_identical(dedup_tab[i].path, path))
|
||||
return dedup_tab[i].ino;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
dedup_remember(const char *path, const struct stat *st,
|
||||
off_t size, uint32_t crc, uint64_t ino)
|
||||
{
|
||||
dev_t dedup_hdev = st->st_nlink > 1 ? st->st_dev : 0;
|
||||
ino_t dedup_hino = st->st_nlink > 1 ? st->st_ino : 0;
|
||||
|
||||
if (dedup_n == dedup_cap) {
|
||||
dedup_cap = dedup_cap ? dedup_cap * 2 : 256;
|
||||
dedup_tab = realloc(dedup_tab, (size_t)dedup_cap * sizeof(*dedup_tab));
|
||||
if (!dedup_tab) { perror("realloc"); exit(1); }
|
||||
}
|
||||
dedup_tab[dedup_n].path = strdup(path);
|
||||
dedup_tab[dedup_n].hdev = dedup_hdev;
|
||||
dedup_tab[dedup_n].hino = dedup_hino;
|
||||
dedup_tab[dedup_n].size = size;
|
||||
dedup_tab[dedup_n].crc = crc;
|
||||
dedup_tab[dedup_n].ino = ino;
|
||||
dedup_n++;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static struct schfs_vol VOL;
|
||||
static char DATABLK[SCHFS_BLKSIZE];
|
||||
static char CURBLK[SCHFS_BLKSIZE];
|
||||
|
||||
// The volume's three scratch blocks. Statics rather than locals because the
|
||||
// same code runs in the kernel, where a 4096-byte local is the whole stack —
|
||||
// keeping the shapes identical is what makes the host the place bugs are found.
|
||||
static char CTX_A[SCHFS_BLKSIZE];
|
||||
static char CTX_B[SCHFS_BLKSIZE];
|
||||
static char CTX_C[SCHFS_BLKSIZE];
|
||||
static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C };
|
||||
|
||||
static long nfiles, ndirs, nlinks, nsymlinks, ndevs;
|
||||
static long long nbytes, ndeduped;
|
||||
|
||||
static void
|
||||
die(const char *what, int r)
|
||||
{
|
||||
fprintf(stderr, "mkschfs: %s failed (%d)%s\n", what, r,
|
||||
r == -3 ? " -- image is full, try a larger --size" : "");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static void
|
||||
inode_init(struct schfs_inode *in, const struct stat *st, uint16_t type,
|
||||
uint64_t parent)
|
||||
{
|
||||
memset(in, 0, sizeof(*in));
|
||||
in->type = type;
|
||||
in->parent = parent;
|
||||
in->nlink = 1;
|
||||
in->mode = st ? (uint32_t)(st->st_mode & 07777)
|
||||
: (type == SCHFS_T_DIR ? 0755u : 0644u);
|
||||
in->uid = 0;
|
||||
in->gid = 0;
|
||||
if (st) {
|
||||
// All three come from mtime, deliberately.
|
||||
//
|
||||
// Copying the host's atime would make the output depend on when the
|
||||
// source files were last *read* -- and mkschfs reads every one of them,
|
||||
// so building the same tree twice produced two different images. ctime
|
||||
// is similar: it tracks host inode changes that mean nothing inside the
|
||||
// image. mtime is the only one that describes the content, which is the
|
||||
// only thing being copied.
|
||||
in->atime = (uint64_t)st->st_mtime;
|
||||
in->mtime = (uint64_t)st->st_mtime;
|
||||
in->ctime = (uint64_t)st->st_mtime;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
bump_nlink(uint64_t ino)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
int r = schfs_inode_get(&VOL, &c, ino, &in);
|
||||
if (r != 0)
|
||||
die("re-reading an inode for a hard link", r);
|
||||
in.nlink++;
|
||||
// Under v1 this was the bug that shipped broken images: nlink was bumped
|
||||
// but the inode's checksum was not recomputed, so every deduped file failed
|
||||
// its own integrity check on first open. Here the checksum belongs to the
|
||||
// tree node and is recomputed by the insert, so it cannot get out of step.
|
||||
r = schfs_inode_put(&VOL, ino, &in);
|
||||
if (r != 0)
|
||||
die("writing back a hard link count", r);
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
add_file(uint64_t parent, const char *host, const char *name,
|
||||
const struct stat *st)
|
||||
{
|
||||
// A hard link is the same inode by definition, so ask the host before
|
||||
// reading anything.
|
||||
if (st->st_nlink > 1) {
|
||||
uint64_t hit = dedup_lookup_ino(st->st_dev, st->st_ino);
|
||||
if (hit) {
|
||||
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
||||
hit, SCHFS_T_FILE);
|
||||
if (r != 0)
|
||||
die("linking a deduplicated file", r);
|
||||
bump_nlink(hit);
|
||||
nlinks++;
|
||||
ndeduped += st->st_size;
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
|
||||
// CRC the content: it is both the dedup key and a cheap way to avoid
|
||||
// reading candidates that cannot possibly match.
|
||||
FILE *f = fopen(host, "rb");
|
||||
if (!f) { perror(host); exit(1); }
|
||||
uint32_t crc = 0;
|
||||
for (;;) {
|
||||
char buf[8192];
|
||||
size_t n = fread(buf, 1, sizeof(buf), f);
|
||||
if (n == 0) break;
|
||||
crc = schfs_crc32c(crc, buf, (unsigned)n);
|
||||
}
|
||||
|
||||
uint64_t existing = dedup_lookup(host, st->st_size, crc);
|
||||
if (existing) {
|
||||
fclose(f);
|
||||
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
||||
existing, SCHFS_T_FILE);
|
||||
if (r != 0)
|
||||
die("linking a deduplicated file", r);
|
||||
bump_nlink(existing);
|
||||
nlinks++;
|
||||
ndeduped += st->st_size;
|
||||
return existing;
|
||||
}
|
||||
|
||||
// The final size is known before a byte is written, so the block count goes
|
||||
// into the inode on its first and only write, and the whole run of data
|
||||
// blocks is claimed up front. That is what collapses a multi-megabyte file
|
||||
// into a single extent record instead of one per 4 KB block.
|
||||
uint64_t need = ((uint64_t)st->st_size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
|
||||
|
||||
uint64_t ino = schfs_alloc_objectid(&VOL);
|
||||
struct schfs_inode in;
|
||||
inode_init(&in, st, SCHFS_T_FILE, parent);
|
||||
in.blocks = need;
|
||||
int r = schfs_inode_put(&VOL, ino, &in);
|
||||
if (r != 0) die("writing a file inode", r);
|
||||
|
||||
r = schfs_file_reserve(&VOL, ino, need);
|
||||
if (r != 0) die("reserving data blocks", r);
|
||||
|
||||
rewind(f);
|
||||
for (;;) {
|
||||
// Large chunks on purpose: each append call re-reads and rewrites the
|
||||
// inode, and every tree write copies its path, so appending a big file
|
||||
// 8 KB at a time spends more space on metadata churn than on data.
|
||||
static char buf[256 * 1024];
|
||||
size_t n = fread(buf, 1, sizeof(buf), f);
|
||||
if (n == 0) break;
|
||||
r = schfs_file_append(&VOL, ino, buf, (unsigned)n, DATABLK);
|
||||
if (r != 0) die("writing file data", r);
|
||||
nbytes += (long long)n;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
||||
ino, SCHFS_T_FILE);
|
||||
if (r != 0) die("adding a directory entry", r);
|
||||
|
||||
dedup_remember(host, st, st->st_size, crc, ino);
|
||||
nfiles++;
|
||||
return ino;
|
||||
}
|
||||
|
||||
static void
|
||||
add_symlink(uint64_t parent, const char *host, const char *name,
|
||||
const struct stat *st)
|
||||
{
|
||||
char target[4096];
|
||||
ssize_t n = readlink(host, target, sizeof(target) - 1);
|
||||
if (n < 0) { perror(host); exit(1); }
|
||||
target[n] = '\0';
|
||||
|
||||
uint64_t need = ((uint64_t)n + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
|
||||
uint64_t ino = schfs_alloc_objectid(&VOL);
|
||||
struct schfs_inode in;
|
||||
inode_init(&in, st, SCHFS_T_SYMLINK, parent);
|
||||
in.blocks = need;
|
||||
int r = schfs_inode_put(&VOL, ino, &in);
|
||||
if (r != 0) die("writing a symlink inode", r);
|
||||
|
||||
// The target is stored as the object's data, exactly like a file's
|
||||
// contents, so nothing special is needed to read it back.
|
||||
r = schfs_file_reserve(&VOL, ino, need);
|
||||
if (r != 0) die("reserving a symlink block", r);
|
||||
r = schfs_file_append(&VOL, ino, target, (unsigned)n, DATABLK);
|
||||
if (r != 0) die("writing a symlink target", r);
|
||||
|
||||
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
||||
ino, SCHFS_T_SYMLINK);
|
||||
if (r != 0) die("adding a symlink entry", r);
|
||||
nsymlinks++;
|
||||
}
|
||||
|
||||
static void
|
||||
add_dev(uint64_t parent, const char *name, const struct stat *st)
|
||||
{
|
||||
uint64_t ino = schfs_alloc_objectid(&VOL);
|
||||
struct schfs_inode in;
|
||||
inode_init(&in, st, SCHFS_T_DEV, parent);
|
||||
in.rdev_major = (uint16_t)major(st->st_rdev);
|
||||
in.rdev_minor = (uint16_t)minor(st->st_rdev);
|
||||
int r = schfs_inode_put(&VOL, ino, &in);
|
||||
if (r != 0) die("writing a device inode", r);
|
||||
|
||||
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
|
||||
ino, SCHFS_T_DEV);
|
||||
if (r != 0) die("adding a device entry", r);
|
||||
ndevs++;
|
||||
}
|
||||
|
||||
/* Sorted directory listing, so images are reproducible. */
|
||||
struct namelist { char **v; int n, cap; };
|
||||
|
||||
static void
|
||||
nl_add(struct namelist *l, const char *s)
|
||||
{
|
||||
if (l->n == l->cap) {
|
||||
l->cap = l->cap ? l->cap * 2 : 64;
|
||||
l->v = realloc(l->v, (size_t)l->cap * sizeof(*l->v));
|
||||
if (!l->v) { perror("realloc"); exit(1); }
|
||||
}
|
||||
l->v[l->n++] = strdup(s);
|
||||
}
|
||||
|
||||
static int
|
||||
nl_cmp(const void *a, const void *b)
|
||||
{
|
||||
return strcmp(*(char *const *)a, *(char *const *)b);
|
||||
}
|
||||
|
||||
static void add_tree(uint64_t dirino, const char *hostdir);
|
||||
|
||||
// Returns 1 if the entry was a directory, so the caller can total up the link
|
||||
// count without walking the directory a second time.
|
||||
static int
|
||||
add_entry(uint64_t dirino, const char *hostdir, const char *name)
|
||||
{
|
||||
char path[8192];
|
||||
snprintf(path, sizeof(path), "%s/%s", hostdir, name);
|
||||
|
||||
struct stat st;
|
||||
// lstat, not stat: a symlink must be stored as a symlink rather than
|
||||
// silently turned into a second copy of whatever it points at.
|
||||
if (lstat(path, &st) != 0) { perror(path); exit(1); }
|
||||
|
||||
if (strlen(name) > SCHFS_NAME_MAX) {
|
||||
fprintf(stderr, "mkschfs: name too long (max %d): %s\n",
|
||||
SCHFS_NAME_MAX, path);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
uint64_t ino = schfs_alloc_objectid(&VOL);
|
||||
struct schfs_inode in;
|
||||
inode_init(&in, &st, SCHFS_T_DIR, dirino);
|
||||
in.nlink = 2; /* itself and its own "." */
|
||||
int r = schfs_inode_put(&VOL, ino, &in);
|
||||
if (r != 0) die("writing a directory inode", r);
|
||||
r = schfs_dir_add(&VOL, dirino, name, (unsigned)strlen(name),
|
||||
ino, SCHFS_T_DIR);
|
||||
if (r != 0) die("adding a subdirectory entry", r);
|
||||
ndirs++;
|
||||
add_tree(ino, path);
|
||||
return 1;
|
||||
}
|
||||
if (S_ISLNK(st.st_mode))
|
||||
add_symlink(dirino, path, name, &st);
|
||||
else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
|
||||
add_dev(dirino, name, &st);
|
||||
else if (S_ISREG(st.st_mode))
|
||||
add_file(dirino, path, name, &st);
|
||||
else
|
||||
fprintf(stderr, "mkschfs: skipping %s (unsupported file type)\n", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
add_tree(uint64_t dirino, const char *hostdir)
|
||||
{
|
||||
DIR *d = opendir(hostdir);
|
||||
if (!d) { perror(hostdir); exit(1); }
|
||||
|
||||
struct namelist l = { 0, 0, 0 };
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
|
||||
continue;
|
||||
nl_add(&l, e->d_name);
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
qsort(l.v, l.n, sizeof(*l.v), nl_cmp);
|
||||
long subdirs = 0;
|
||||
for (int i = 0; i < l.n; i++) {
|
||||
subdirs += add_entry(dirino, hostdir, l.v[i]);
|
||||
free(l.v[i]);
|
||||
}
|
||||
free(l.v);
|
||||
|
||||
// A directory's link count is 2 (itself, and its own ".") plus one per
|
||||
// subdirectory, because each subdirectory's ".." points back here. ".." is
|
||||
// synthetic in SchFS, but the count still has to match what POSIX tools
|
||||
// expect, or "find -type d" prunes real directories and du walks the tree
|
||||
// wrongly. Written last because the count is not known until the children
|
||||
// have been added.
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, dirino, &in) != 0)
|
||||
die("re-reading a directory inode", -2);
|
||||
in.nlink = (uint32_t)(2 + subdirs);
|
||||
if (schfs_inode_put(&VOL, dirino, &in) != 0)
|
||||
die("writing a directory link count", -2);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
const char *out = NULL, *sysroot = NULL, *label = "schunix";
|
||||
uint64_t nblocks = 6000;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--sysroot") == 0 && i + 1 < argc)
|
||||
sysroot = argv[++i];
|
||||
else if (strcmp(argv[i], "--size") == 0 && i + 1 < argc)
|
||||
nblocks = strtoull(argv[++i], NULL, 0);
|
||||
else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc)
|
||||
label = argv[++i];
|
||||
else if (strcmp(argv[i], "--hash-bits") == 0 && i + 1 < argc) {
|
||||
// Testing only: shrink the name hash so collisions are the norm
|
||||
// rather than a once-in-a-lifetime event. schfs_fsck takes the
|
||||
// same flag and must be given the same value to read the image.
|
||||
int bits = atoi(argv[++i]);
|
||||
schfs_hash_mask = bits >= 64 ? ~0ULL : ((1ULL << bits) - 1);
|
||||
}
|
||||
else if (argv[i][0] == '-') {
|
||||
fprintf(stderr, "mkschfs: unknown option %s\n", argv[i]);
|
||||
return 1;
|
||||
} else if (!out)
|
||||
out = argv[i];
|
||||
else {
|
||||
fprintf(stderr, "mkschfs: too many arguments\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!out || !sysroot) {
|
||||
fprintf(stderr,
|
||||
"usage: mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]\n"
|
||||
" BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n",
|
||||
SCHFS_BLKSIZE, (int)(6000 * SCHFS_BLKSIZE / (1024 * 1024)));
|
||||
return 1;
|
||||
}
|
||||
if (nblocks <= SCHFS_FIRST_BLOCK) {
|
||||
fprintf(stderr, "mkschfs: --size must exceed %d blocks (boot block "
|
||||
"plus the uberblock ring)\n", SCHFS_FIRST_BLOCK);
|
||||
return 1;
|
||||
}
|
||||
|
||||
FILE *f = fopen(out, "w+b");
|
||||
if (!f) { perror(out); return 1; }
|
||||
// Pre-extend so every block is readable. The bump allocator hands out
|
||||
// blocks in order, but the tree reads a block back the moment it is
|
||||
// written, and a short file would fail that read rather than the write.
|
||||
if (ftruncate(fileno(f), (off_t)nblocks * SCHFS_BLKSIZE) != 0) {
|
||||
perror("ftruncate");
|
||||
return 1;
|
||||
}
|
||||
|
||||
IMG.f = f;
|
||||
IMG.vol = &VOL;
|
||||
IMG.nblocks = nblocks;
|
||||
|
||||
struct schfs_bdev bd;
|
||||
bd.ctx = &IMG;
|
||||
bd.bread = img_read;
|
||||
bd.bwrite = img_write;
|
||||
bd.balloc = img_alloc;
|
||||
bd.bfree = img_free;
|
||||
bd.balloc_run = img_alloc_run;
|
||||
|
||||
int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc);
|
||||
if (r != 0) die("formatting", r);
|
||||
|
||||
// The root directory is an object like any other; nothing points at it
|
||||
// except the uberblock's root_objectid.
|
||||
struct schfs_inode root;
|
||||
struct stat rst;
|
||||
int have_rst = (lstat(sysroot, &rst) == 0);
|
||||
inode_init(&root, have_rst ? &rst : NULL, SCHFS_T_DIR, 0);
|
||||
root.mode = 0755;
|
||||
root.nlink = 2;
|
||||
r = schfs_inode_put(&VOL, VOL.root_objectid, &root);
|
||||
if (r != 0) die("writing the root inode", r);
|
||||
ndirs++;
|
||||
|
||||
add_tree(VOL.root_objectid, sysroot);
|
||||
|
||||
// Publish. Everything above this line is invisible to a reader; if the tool
|
||||
// died half way through, the image would simply have no valid uberblock and
|
||||
// be rejected as "not a SchFS volume" rather than mounted half-built.
|
||||
r = schfs_vol_commit(&VOL);
|
||||
if (r != 0) die("committing the uberblock", r);
|
||||
|
||||
if (fflush(f) != 0 || fclose(f) != 0) { perror(out); return 1; }
|
||||
|
||||
uint64_t used = nblocks - VOL.free_blocks - SCHFS_FIRST_BLOCK;
|
||||
printf("mkschfs: %s\n", out);
|
||||
printf(" %ld dirs, %ld files, %ld hard links, %ld symlinks, %ld devices\n",
|
||||
ndirs, nfiles, nlinks, nsymlinks, ndevs);
|
||||
printf(" %lld bytes of data written, %lld bytes saved by dedup\n",
|
||||
nbytes, ndeduped);
|
||||
printf(" %llu of %llu blocks used (%llu KB of %llu KB), generation %llu\n",
|
||||
(unsigned long long)used, (unsigned long long)nblocks,
|
||||
(unsigned long long)used * SCHFS_BLKSIZE / 1024,
|
||||
(unsigned long long)nblocks * SCHFS_BLKSIZE / 1024,
|
||||
(unsigned long long)VOL.generation);
|
||||
return 0;
|
||||
}
|
||||
38
tools/mktree.sh
Normal file
38
tools/mktree.sh
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/sh
|
||||
# Build the synthetic test tree used by schfs_roundtrip.sh into $1.
|
||||
# Kept separate so the tree can be recreated for debugging without running
|
||||
# the whole round-trip.
|
||||
set -e
|
||||
TREE=${1:?usage: mktree.sh <dir>}
|
||||
rm -rf "$TREE"
|
||||
mkdir -p "$TREE/bin" "$TREE/etc/conf.d" "$TREE/empty-dir" "$TREE/a/b/c/d/e/f/g"
|
||||
|
||||
: > "$TREE/etc/empty-file"
|
||||
echo "hello schfs" > "$TREE/etc/motd"
|
||||
printf 'x' > "$TREE/etc/one-byte"
|
||||
|
||||
dd if=/dev/zero bs=4096 count=1 2>/dev/null | tr '\0' 'A' > "$TREE/etc/exactly-4096"
|
||||
cp "$TREE/etc/exactly-4096" "$TREE/etc/block-plus-one"
|
||||
printf 'B' >> "$TREE/etc/block-plus-one"
|
||||
|
||||
dd if=/dev/zero bs=1000 count=37 2>/dev/null | tr '\0' 'C' > "$TREE/bin/multiblock"
|
||||
cp "$TREE/bin/multiblock" "$TREE/bin/multiblock-twin"
|
||||
dd if=/dev/zero bs=1000 count=37 2>/dev/null | tr '\0' 'D' > "$TREE/bin/decoy"
|
||||
|
||||
ln -s ../etc/motd "$TREE/bin/motd-link"
|
||||
ln -s /nowhere "$TREE/bin/dangling-link"
|
||||
|
||||
echo deep > "$TREE/a/b/c/d/e/f/g/deep.txt"
|
||||
echo one > "$TREE/etc/conf.d/one"
|
||||
|
||||
echo spaced > "$TREE/etc/a file with spaces"
|
||||
echo dotted > "$TREE/etc/.hidden"
|
||||
LONG=$(printf 'n%.0s' $(seq 1 255))
|
||||
echo long > "$TREE/etc/$LONG"
|
||||
|
||||
mkdir -p "$TREE/many"
|
||||
i=0
|
||||
while [ $i -lt 300 ]; do
|
||||
echo "file $i" > "$TREE/many/f$i"
|
||||
i=$((i + 1))
|
||||
done
|
||||
136
tools/mutate.sh
Normal file
136
tools/mutate.sh
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#!/bin/sh
|
||||
# Mutation-test the SchFS suites: inject a bug, confirm the tests report it,
|
||||
# restore. A test that cannot fail is worse than no test, because it is
|
||||
# believed. Run by hand; not part of the normal build.
|
||||
#
|
||||
# Every mutation is verified to have actually changed the file before the
|
||||
# suite runs. A sed pattern that silently stops matching -- after a refactor,
|
||||
# say -- would otherwise look exactly like a passing mutation test, which is
|
||||
# the same failure this script exists to catch.
|
||||
set -e
|
||||
cd /mnt/d/DEV/Schunix
|
||||
|
||||
OBJ=kernel/fs/schfs/obj.c
|
||||
OBJW=kernel/fs/schfs/objw.c
|
||||
SPACE=kernel/fs/schfs/space.c
|
||||
MK=tools/mkschfs.c
|
||||
cp $OBJ /tmp/obj.orig
|
||||
cp $OBJW /tmp/objw.orig
|
||||
cp $SPACE /tmp/space.orig
|
||||
cp $MK /tmp/mk.orig
|
||||
|
||||
restore() {
|
||||
cp /tmp/obj.orig $OBJ
|
||||
cp /tmp/objw.orig $OBJW
|
||||
cp /tmp/space.orig $SPACE
|
||||
cp /tmp/mk.orig $MK
|
||||
}
|
||||
trap restore EXIT
|
||||
|
||||
FAILED=0
|
||||
|
||||
# try <description> <file> <sed-expression> [suite]
|
||||
#
|
||||
# suite is "image" (the default: build an image and diff it against the source
|
||||
# tree) or "obj" (the in-memory object-layer test). They catch different things:
|
||||
# the image round-trip cannot see a bug that only shows up after a delete,
|
||||
# because it never deletes anything.
|
||||
try() {
|
||||
desc=$1; file=$2; expr=$3; suite=${4:-image}
|
||||
restore
|
||||
before=$(cksum < "$file")
|
||||
sed -i "$expr" "$file"
|
||||
after=$(cksum < "$file")
|
||||
|
||||
echo "mutation: $desc"
|
||||
if [ "$before" = "$after" ]; then
|
||||
echo " BROKEN MUTATION -- the pattern no longer matches anything"
|
||||
FAILED=1
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$suite" = obj ]; then
|
||||
if ! make build/schfs_objtest > /tmp/mk.log 2>&1; then
|
||||
echo " rejected at compile time"
|
||||
return
|
||||
fi
|
||||
if ./build/schfs_objtest > /tmp/rt.log 2>&1; then
|
||||
echo " NOT CAUGHT -- the suite passed with this bug present"
|
||||
FAILED=1
|
||||
else
|
||||
echo " caught: $(grep -aE 'FAIL' /tmp/rt.log | head -1 | sed 's/^ *//')"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if ! make build/mkschfs build/schfs_fsck > /tmp/mk.log 2>&1; then
|
||||
echo " rejected at compile time"
|
||||
return
|
||||
fi
|
||||
if sh tools/schfs_roundtrip.sh ./build/mkschfs ./build/schfs_fsck \
|
||||
> /tmp/rt.log 2>&1; then
|
||||
echo " NOT CAUGHT -- the suite passed with this bug present"
|
||||
FAILED=1
|
||||
else
|
||||
echo " caught: $(grep -aE 'FAIL|Only in|differ|loop' /tmp/rt.log | head -1 | sed 's/^ *//')"
|
||||
fi
|
||||
}
|
||||
|
||||
# Reads run past end-of-file, so every file comes back padded.
|
||||
try "file_read ignores the end-of-file clamp" $OBJ \
|
||||
's|if (off + n > in.size)|if (0)|'
|
||||
|
||||
# The dirent walk skips forward too far, losing entries in any packed list.
|
||||
try "dirent_walk advances past the next entry" $OBJ \
|
||||
's| off += step;| off += step + 8;|'
|
||||
|
||||
# Directory lookup returns the neighbouring object.
|
||||
try "dir_lookup returns the wrong objectid" $OBJ \
|
||||
's| f->ino = d->objectid;| f->ino = d->objectid + 1;|'
|
||||
|
||||
# Every extent points one block past its data.
|
||||
#
|
||||
# Note this replaced an earlier mutation that shortened the extent by a block.
|
||||
# That one was NOT caught, and correctly so: an extent covering too little just
|
||||
# leaves the tail unmapped, and schfs_file_append then allocates it separately,
|
||||
# so the file still reads back byte for byte. The design absorbs it. Pointing
|
||||
# at the wrong block cannot be absorbed.
|
||||
try "extents point one block past their data" $OBJW \
|
||||
's| e.disk_block = first;| e.disk_block = first + 1;|'
|
||||
|
||||
# Copy-on-write splits an extent into head, replacement and tail. Losing the
|
||||
# tail leaves everything after the rewritten block unmapped, so it silently
|
||||
# reads back as zeros -- data loss that looks exactly like a sparse file.
|
||||
try "an overwrite drops the tail of the extent it split" $OBJW \
|
||||
's| if (fblk + 1 < start + e.nblocks) {| if (0) {|' obj
|
||||
|
||||
# Truncating down and growing again must read zeros. Skipping the zeroing of
|
||||
# the partial last block hands back whatever the file used to hold there --
|
||||
# which, across a delete and a reallocation, is somebody else's data.
|
||||
try "truncate leaves stale bytes in the last partial block" $OBJW \
|
||||
's| memset((char \*)dblk + tail, 0, SCHFS_BLKSIZE - tail);| /* mutated */;|' obj
|
||||
|
||||
# Deferred frees never rejoin the free map, so every delete leaks. Nothing that
|
||||
# only reads or only writes can see this; it takes a test that deletes
|
||||
# everything and then checks the free count.
|
||||
try "committed frees are never returned to the free map" $SPACE \
|
||||
's| inuse\[w\] &= ~pend\[w\];| /* mutated */;|' obj
|
||||
|
||||
# Hard links are not counted, so dedup is indistinguishable from duplication
|
||||
# and only the explicit nlink assertion can tell the difference.
|
||||
try "hard links do not bump nlink" $MK \
|
||||
's|^ in.nlink++;| /* mutated */;|'
|
||||
|
||||
# Data checksums cover only the head of a block, so tail corruption is missed.
|
||||
try "data checksum covers only the first 16 bytes" $OBJW \
|
||||
's|uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);|uint32_t c = schfs_crc32c(0, data, 16);|'
|
||||
|
||||
restore
|
||||
make build/mkschfs build/schfs_fsck > /dev/null 2>&1
|
||||
echo
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
echo "all mutations were caught"
|
||||
else
|
||||
echo "SOME MUTATIONS WERE NOT CAUGHT -- the suite has a blind spot"
|
||||
exit 1
|
||||
fi
|
||||
24
tools/padstage2.pl
Normal file
24
tools/padstage2.pl
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/perl
|
||||
# Zero-pads the stage2 bootloader binary out to a fixed size (STAGE2_SECTORS
|
||||
# * 512 bytes), so the kernel's on-disk starting sector is predictable
|
||||
# regardless of stage2's actual compiled size. Errors out if stage2 has
|
||||
# grown past that budget instead of silently truncating it.
|
||||
|
||||
my $size = $ARGV[1] || die "usage: padstage2.pl <file> <size-in-bytes>\n";
|
||||
|
||||
open(F, $ARGV[0]) || die "open $ARGV[0]: $!";
|
||||
my $n = sysread(F, my $buf, $size + 1);
|
||||
close(F);
|
||||
|
||||
if ($n > $size) {
|
||||
print STDERR "stage2 too large: $n bytes (max $size)\n";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
print STDERR "stage2 is $n bytes (max $size)\n";
|
||||
|
||||
$buf .= "\0" x ($size - $n);
|
||||
|
||||
open(F, ">$ARGV[0]") || die "open >$ARGV[0]: $!";
|
||||
print F $buf;
|
||||
close(F);
|
||||
36
tools/pr.pl
Normal file
36
tools/pr.pl
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use POSIX qw(strftime);
|
||||
|
||||
if($ARGV[0] eq "-h"){
|
||||
shift @ARGV;
|
||||
$h = $ARGV[0];
|
||||
shift @ARGV;
|
||||
}else{
|
||||
$h = $ARGV[0];
|
||||
}
|
||||
|
||||
$page = 0;
|
||||
$now = strftime "%b %e %H:%M %Y", localtime;
|
||||
|
||||
@lines = <>;
|
||||
for($i=0; $i<@lines; $i+=50){
|
||||
print "\n\n";
|
||||
++$page;
|
||||
print "$now $h Page $page\n";
|
||||
print "\n\n";
|
||||
for($j=$i; $j<@lines && $j<$i +50; $j++){
|
||||
$lines[$j] =~ s!//DOC.*!!;
|
||||
print $lines[$j];
|
||||
}
|
||||
for(; $j<$i+50; $j++){
|
||||
print "\n";
|
||||
}
|
||||
$sheet = "";
|
||||
if($lines[$i] =~ /^([0-9][0-9])[0-9][0-9] /){
|
||||
$sheet = "Sheet $1";
|
||||
}
|
||||
print "\n\n";
|
||||
print "$sheet\n";
|
||||
print "\n\n";
|
||||
}
|
||||
185
tools/qrcheck.py
Normal file
185
tools/qrcheck.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Cross-check kernel/core/qr.c against independent implementations.
|
||||
|
||||
The self-checks inside tools/qrtest.c verify what has a local notion of
|
||||
correctness -- table arithmetic, the Reed-Solomon generator polynomials, whether
|
||||
a finder pattern is where it should be. They cannot verify the parts that only
|
||||
mean something to a decoder: zigzag placement order, mask selection, and block
|
||||
interleaving. All three have the same failure mode, which is a symbol that looks
|
||||
exactly like a QR code and is noise.
|
||||
|
||||
Two oracles, in order of authority:
|
||||
|
||||
1. **A real decoder** (OpenCV). Does the symbol read back as the exact bytes
|
||||
that went in? This is the only test that matches what the feature is for --
|
||||
somebody pointing a phone at a crashed machine -- and it is the one that
|
||||
decides pass or fail.
|
||||
|
||||
2. **A second encoder** (segno), matrix compared module for module. Stricter
|
||||
than decoding when it agrees, because it pins down the exact mask and
|
||||
placement rather than merely something readable. Applied only to payloads
|
||||
that exactly fill their version, for the reason below.
|
||||
|
||||
segno adds a spurious zero pad codeword to every byte-mode symbol. Its
|
||||
write_padding_bits() does `buff.extend([0] * (8 - (length % 8)))`, which
|
||||
emits a whole byte when the stream is already on a codeword boundary -- and
|
||||
in byte mode the stream is always 8L+16 bits after the terminator, so it
|
||||
always is. ISO/IEC 18004 makes that conditional ("if the bit stream length is
|
||||
such that it does not end at a codeword boundary"), and segno quotes exactly
|
||||
that sentence above the line. It is harmless -- decoders read `count` bytes
|
||||
and ignore padding -- but it means the two encoders cannot agree bit-for-bit
|
||||
on any payload that needs padding, so those are compared by decoding only.
|
||||
|
||||
pip install --target <dir> opencv-python-headless numpy segno
|
||||
PYTHONPATH=<dir> python tools/qrcheck.py
|
||||
|
||||
Requires build/tools/qrtest (make qrtest). Both libraries are optional; whatever
|
||||
is installed gets used.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Normalised and absolute: Windows CreateProcess rejects a relative path written
|
||||
# with forward slashes, even when the file is plainly there.
|
||||
QRTEST = os.path.abspath(os.path.join("build", "tools",
|
||||
"qrtest.exe" if os.name == "nt" else "qrtest"))
|
||||
|
||||
try:
|
||||
import cv2
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
cv2 = None
|
||||
try:
|
||||
import segno
|
||||
except ImportError:
|
||||
segno = None
|
||||
|
||||
if cv2 is None and segno is None:
|
||||
sys.exit("qrcheck: needs opencv-python-headless and/or segno")
|
||||
if not os.path.exists(QRTEST):
|
||||
sys.exit(f"qrcheck: {QRTEST} not found -- run `make qrtest`")
|
||||
|
||||
|
||||
def ours(text):
|
||||
"""Our encoder's matrix, as a list of strings of '0'/'1'."""
|
||||
out = subprocess.run([QRTEST, "-m", text], capture_output=True, text=True)
|
||||
if out.returncode != 0:
|
||||
raise RuntimeError(f"qrtest failed on {text!r}")
|
||||
return out.stdout.split()
|
||||
|
||||
|
||||
def our_mask(m):
|
||||
"""The mask our encoder chose, read back out of the format bits.
|
||||
|
||||
Read from the symbol rather than reported by it, because the point is to
|
||||
check what was actually written.
|
||||
"""
|
||||
bits = 0
|
||||
for i in range(15):
|
||||
if i <= 5: b = int(m[i][8])
|
||||
elif i == 6: b = int(m[7][8])
|
||||
elif i == 7: b = int(m[8][8])
|
||||
elif i == 8: b = int(m[8][7])
|
||||
else: b = int(m[8][14 - i])
|
||||
bits |= b << i
|
||||
return ((bits ^ 0x5412) >> 10) & 7
|
||||
|
||||
|
||||
def render(m, scale, border):
|
||||
n = len(m)
|
||||
side = (n + 2 * border) * scale
|
||||
img = np.full((side, side), 255, np.uint8)
|
||||
for y in range(n):
|
||||
for x in range(n):
|
||||
if m[y][x] == "1":
|
||||
img[(y + border) * scale:(y + border + 1) * scale,
|
||||
(x + border) * scale:(x + border + 1) * scale] = 0
|
||||
return img
|
||||
|
||||
|
||||
# OpenCV's detector is not a reference implementation and does fail on symbols
|
||||
# that are perfectly valid -- sensitive to scale and quiet-zone width in ways
|
||||
# that have nothing to do with the encoding. Several renderings are tried before
|
||||
# concluding anything, and a payload that still will not decode is re-tested
|
||||
# against segno's symbol for the same input: if that fails too, the detector is
|
||||
# what is at fault, not the encoder. Without that second step this check reports
|
||||
# false failures, which is worse than not running it.
|
||||
RENDERINGS = [(8, 4), (6, 6), (8, 6), (4, 4), (12, 6), (16, 4)]
|
||||
|
||||
|
||||
def decode(m):
|
||||
for scale, border in RENDERINGS:
|
||||
text, _, _ = cv2.QRCodeDetector().detectAndDecode(render(m, scale, border))
|
||||
if text:
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
# Byte capacity per version: total - ecc*blocks - 2. Mirrors the table in qr.c;
|
||||
# the exact-fill cases below are built from it, and a disagreement would show up
|
||||
# as a version mismatch rather than passing quietly.
|
||||
CAPACITY = {1: 17, 2: 32, 3: 53, 4: 78, 5: 106, 6: 134}
|
||||
|
||||
alpha = lambda n: "".join(chr(65 + i % 26) for i in range(n))
|
||||
|
||||
CASES = [
|
||||
"HELLO",
|
||||
"SCHUNIX",
|
||||
"a",
|
||||
"0123456789",
|
||||
"SCHUNIX PANIC\ntrap 14 err 4 cpu 0\nrip ffffffff80104e2a",
|
||||
bytes(range(1, 40)).decode("latin-1"), # non-printable payload
|
||||
]
|
||||
# One payload that exactly fills each version, and one just under, so both the
|
||||
# padded and unpadded paths are covered at every version -- including 6, which
|
||||
# is the only one with more than one Reed-Solomon block.
|
||||
for v, cap in CAPACITY.items():
|
||||
CASES.append(alpha(cap))
|
||||
CASES.append(alpha(cap - 1))
|
||||
|
||||
fails = 0
|
||||
print("decode (OpenCV)" if cv2 else "decode -- skipped, no opencv")
|
||||
for text in CASES:
|
||||
if not cv2:
|
||||
break
|
||||
m = ours(text)
|
||||
v = (len(m) - 17) // 4
|
||||
got = decode(m)
|
||||
if got == text:
|
||||
print(f" [ok] v{v} {len(text):3d}B")
|
||||
continue
|
||||
|
||||
# Before blaming the encoder, check whether an independently generated
|
||||
# symbol for the same payload decodes either.
|
||||
if segno and not decode([
|
||||
"".join(str(x) for x in row)
|
||||
for row in segno.make(text.encode("latin-1"), error="l", version=v,
|
||||
boost_error=False, mode="byte").matrix]):
|
||||
print(f" [detector] v{v} {len(text):3d}B -- segno's symbol does not "
|
||||
f"decode either; OpenCV's limitation, not ours")
|
||||
continue
|
||||
|
||||
fails += 1
|
||||
print(f" [FAIL] v{v} {len(text):3d}B decoded {got[:40]!r}")
|
||||
|
||||
print("\nmatrix vs segno (exact-fill payloads only)" if segno else "\nsegno -- skipped")
|
||||
for v, cap in CAPACITY.items():
|
||||
if not segno:
|
||||
break
|
||||
text = alpha(cap)
|
||||
m = ours(text)
|
||||
mask = our_mask(m)
|
||||
q = segno.make(text.encode("latin-1"), error="l", version=v,
|
||||
mask=mask, boost_error=False, mode="byte")
|
||||
ref = ["".join(str(x) for x in row) for row in q.matrix]
|
||||
ok = m == ref
|
||||
fails += not ok
|
||||
n = len(m)
|
||||
diff = "" if ok else f" {sum(1 for a, b in zip(''.join(m), ''.join(ref)) if a != b)}/{n*n} differ"
|
||||
print(f" [{'ok' if ok else 'FAIL'}] v{v} mask {mask} {cap:3d}B{diff}")
|
||||
|
||||
print()
|
||||
print("qrcheck: FAILED" if fails else "qrcheck: passed")
|
||||
sys.exit(1 if fails else 0)
|
||||
354
tools/qrtest.c
Normal file
354
tools/qrtest.c
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
// Host harness for kernel/core/qr.c.
|
||||
//
|
||||
// The QR encoder cannot be unit-tested against itself in any meaningful way: a
|
||||
// symbol with a mistyped table entry is still a plausible-looking grid of black
|
||||
// squares. The only real test is whether a decoder reads it back, so this builds
|
||||
// the same encoder on the host and writes a PNG to scan.
|
||||
//
|
||||
// make qrtest a sample crash report
|
||||
// build/tools/qrtest out.png "text" arbitrary payload
|
||||
//
|
||||
// It also self-checks the parts that *can* be checked mechanically -- the
|
||||
// version table's internal consistency, and the Reed-Solomon remainder against
|
||||
// a known vector -- so a transcription error is caught before anyone reaches
|
||||
// for a phone.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define QR_HOST
|
||||
#include "../kernel/core/qr.c"
|
||||
|
||||
// --- Minimal PNG writer ----------------------------------------------------
|
||||
//
|
||||
// Uncompressed (stored) deflate blocks, so there is no zlib dependency for a
|
||||
// tool whose entire job is to produce one small image. A stored block costs
|
||||
// five bytes of header per 65535 bytes of data, which for a 41-module symbol is
|
||||
// irrelevant.
|
||||
|
||||
static unsigned long crc_table[256];
|
||||
|
||||
static void
|
||||
crc_init(void)
|
||||
{
|
||||
for (int n = 0; n < 256; n++) {
|
||||
unsigned long c = (unsigned long)n;
|
||||
for (int k = 0; k < 8; k++)
|
||||
c = (c & 1) ? 0xEDB88320UL ^ (c >> 1) : c >> 1;
|
||||
crc_table[n] = c;
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned long
|
||||
crc(const unsigned char *buf, int len)
|
||||
{
|
||||
unsigned long c = 0xFFFFFFFFUL;
|
||||
for (int n = 0; n < len; n++)
|
||||
c = crc_table[(c ^ buf[n]) & 0xFF] ^ (c >> 8);
|
||||
return c ^ 0xFFFFFFFFUL;
|
||||
}
|
||||
|
||||
static void
|
||||
be32(unsigned char *p, unsigned long v)
|
||||
{
|
||||
p[0] = (unsigned char)(v >> 24); p[1] = (unsigned char)(v >> 16);
|
||||
p[2] = (unsigned char)(v >> 8); p[3] = (unsigned char)v;
|
||||
}
|
||||
|
||||
static void
|
||||
put_chunk(FILE *f, const char *type, const unsigned char *data, int len)
|
||||
{
|
||||
unsigned char hdr[4];
|
||||
be32(hdr, (unsigned long)len);
|
||||
fwrite(hdr, 1, 4, f);
|
||||
|
||||
unsigned char *buf = malloc((size_t)len + 4);
|
||||
memcpy(buf, type, 4);
|
||||
if (len)
|
||||
memcpy(buf + 4, data, (size_t)len);
|
||||
fwrite(buf, 1, (size_t)len + 4, f);
|
||||
|
||||
be32(hdr, crc(buf, len + 4));
|
||||
fwrite(hdr, 1, 4, f);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
// Write the symbol as a PNG, `scale` pixels per module, with the 4-module quiet
|
||||
// zone the standard requires. The quiet zone is not decoration: without it many
|
||||
// decoders will not find the symbol at all.
|
||||
static int
|
||||
write_png(const char *path, const struct qr *q, int scale, int quiet)
|
||||
{
|
||||
int mods = q->size + 2 * quiet;
|
||||
int w = mods * scale, h = w;
|
||||
|
||||
// Greyscale, 8-bit, one filter byte per row.
|
||||
int stride = w + 1;
|
||||
unsigned char *raw = malloc((size_t)stride * h);
|
||||
for (int y = 0; y < h; y++) {
|
||||
unsigned char *row = raw + (size_t)y * stride;
|
||||
row[0] = 0; // filter: none
|
||||
int my = y / scale - quiet;
|
||||
for (int x = 0; x < w; x++) {
|
||||
int mx = x / scale - quiet;
|
||||
int dark = (mx >= 0 && my >= 0 && mx < q->size && my < q->size)
|
||||
? q->mod[my][mx] : 0;
|
||||
row[1 + x] = dark ? 0x00 : 0xFF;
|
||||
}
|
||||
}
|
||||
int rawlen = stride * h;
|
||||
|
||||
// zlib stream: header, stored deflate blocks, adler32.
|
||||
int cap = rawlen + (rawlen / 65535 + 1) * 5 + 6;
|
||||
unsigned char *z = malloc((size_t)cap);
|
||||
int zn = 0;
|
||||
z[zn++] = 0x78; z[zn++] = 0x01;
|
||||
int off = 0;
|
||||
while (off < rawlen) {
|
||||
int n = rawlen - off;
|
||||
if (n > 65535) n = 65535;
|
||||
z[zn++] = (off + n >= rawlen) ? 1 : 0;
|
||||
z[zn++] = (unsigned char)(n & 0xFF);
|
||||
z[zn++] = (unsigned char)(n >> 8);
|
||||
z[zn++] = (unsigned char)(~n & 0xFF);
|
||||
z[zn++] = (unsigned char)((~n >> 8) & 0xFF);
|
||||
memcpy(z + zn, raw + off, (size_t)n);
|
||||
zn += n;
|
||||
off += n;
|
||||
}
|
||||
unsigned long a = 1, b = 0;
|
||||
for (int i = 0; i < rawlen; i++) {
|
||||
a = (a + raw[i]) % 65521;
|
||||
b = (b + a) % 65521;
|
||||
}
|
||||
be32(z + zn, (b << 16) | a);
|
||||
zn += 4;
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { perror(path); return -1; }
|
||||
fwrite("\x89PNG\r\n\x1a\n", 1, 8, f);
|
||||
|
||||
unsigned char ihdr[13];
|
||||
be32(ihdr, (unsigned long)w);
|
||||
be32(ihdr + 4, (unsigned long)h);
|
||||
ihdr[8] = 8; ihdr[9] = 0; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
||||
put_chunk(f, "IHDR", ihdr, 13);
|
||||
put_chunk(f, "IDAT", z, zn);
|
||||
put_chunk(f, "IEND", NULL, 0);
|
||||
fclose(f);
|
||||
|
||||
free(raw);
|
||||
free(z);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Self-checks -----------------------------------------------------------
|
||||
|
||||
static int failures;
|
||||
|
||||
static void
|
||||
check(int ok, const char *what)
|
||||
{
|
||||
printf(" [%s] %s\n", ok ? "ok" : "FAIL", what);
|
||||
if (!ok) failures++;
|
||||
}
|
||||
|
||||
// The version table is the one part of qr.c that is transcription rather than
|
||||
// reasoning, so check what can be checked: block sizes must divide evenly, and
|
||||
// the symbol must have room for exactly the codewords claimed.
|
||||
static void
|
||||
check_tables(void)
|
||||
{
|
||||
printf("version table\n");
|
||||
for (int v = 1; v <= QR_MAX_VERSION; v++) {
|
||||
int total = vinfo[v].total;
|
||||
int data = total - vinfo[v].ecc * vinfo[v].blocks;
|
||||
char msg[128];
|
||||
|
||||
snprintf(msg, sizeof msg, "v%d: %d data codewords divide into %d blocks",
|
||||
v, data, vinfo[v].blocks);
|
||||
check(data > 0 && data % vinfo[v].blocks == 0, msg);
|
||||
|
||||
// Count the modules a symbol of this version actually leaves for data,
|
||||
// by building one and asking. If the table's codeword count disagrees
|
||||
// with the geometry, one of the two is wrong.
|
||||
struct qr q;
|
||||
memset(&q, 0, sizeof q);
|
||||
q.size = 17 + 4 * v;
|
||||
draw_function_patterns(&q);
|
||||
int free_modules = 0;
|
||||
for (int y = 0; y < q.size; y++)
|
||||
for (int x = 0; x < q.size; x++)
|
||||
if (!q.fn[y][x]) free_modules++;
|
||||
|
||||
snprintf(msg, sizeof msg,
|
||||
"v%d: %d free modules holds %d codewords (%d spare bits)",
|
||||
v, free_modules, total, free_modules - total * 8);
|
||||
check(free_modules >= total * 8 && free_modules - total * 8 < 8, msg);
|
||||
}
|
||||
}
|
||||
|
||||
// A known Reed-Solomon vector. "HELLO WORLD" as a version 1-M byte-mode symbol
|
||||
// has data codewords 0x40 0xB3 0x42 ... ; rather than depend on remembering the
|
||||
// whole thing, check the standard's own worked example: the generator
|
||||
// polynomial for degree 10 begins with the coefficients below.
|
||||
static void
|
||||
check_rs(void)
|
||||
{
|
||||
printf("reed-solomon\n");
|
||||
unsigned char g[30];
|
||||
|
||||
rs_divisor(10, g);
|
||||
// Generator for 10 EC codewords, highest power first, leading 1 implicit.
|
||||
static const unsigned char want10[10] = {
|
||||
216, 194, 159, 111, 199, 94, 95, 113, 157, 193
|
||||
};
|
||||
check(memcmp(g, want10, 10) == 0, "degree-10 generator polynomial");
|
||||
|
||||
rs_divisor(7, g);
|
||||
static const unsigned char want7[7] = { 127, 122, 154, 164, 11, 68, 117 };
|
||||
check(memcmp(g, want7, 7) == 0, "degree-7 generator polynomial");
|
||||
|
||||
// Multiplication is the primitive everything else rests on.
|
||||
check(gmul(0, 5) == 0 && gmul(1, 5) == 5, "gf(256) identity and zero");
|
||||
check(gmul(0x80, 0x02) == 0x1D, "gf(256) reduction by 0x11D");
|
||||
}
|
||||
|
||||
static void
|
||||
check_encode(void)
|
||||
{
|
||||
printf("encode\n");
|
||||
struct qr q;
|
||||
|
||||
check(qr_encode(&q, (const unsigned char *)"HELLO", 5) == 0, "short payload encodes");
|
||||
check(q.size == 21, "and picks version 1");
|
||||
|
||||
unsigned char big[QR_MAX_BYTES];
|
||||
memset(big, 'A', sizeof big);
|
||||
check(qr_encode(&q, big, QR_MAX_BYTES) == 0, "maximum payload encodes");
|
||||
check(q.size == 41, "and picks version 6");
|
||||
check(qr_encode(&q, big, QR_MAX_BYTES + 1) == -1, "one byte over is refused");
|
||||
|
||||
// Every function module must still be a function module after masking --
|
||||
// i.e. masking must not have touched the finders.
|
||||
qr_encode(&q, (const unsigned char *)"test", 4);
|
||||
check(q.mod[0][0] && q.mod[6][6] && !q.mod[5][5], "finder pattern intact");
|
||||
check(q.mod[q.size - 8][8] == 1, "dark module set");
|
||||
// Row 6 alternates with the *even* coordinates dark, so the first timing
|
||||
// module clear of the separator (x=8) is dark and x=9 is light. Asserting
|
||||
// this the other way round was the one self-check that failed on the first
|
||||
// run, and the test was what was wrong -- worth keeping the orientation
|
||||
// written down.
|
||||
check(q.mod[6][8] == 1 && q.mod[6][9] == 0, "horizontal timing pattern");
|
||||
check(q.mod[8][6] == 1 && q.mod[9][6] == 0, "vertical timing pattern");
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
crc_init();
|
||||
|
||||
// -q suppresses the terminal rendering. It is on by default because a
|
||||
// phone can read the symbol straight off the terminal without anyone
|
||||
// opening a file, but it is 40 lines of escape codes and sometimes in the
|
||||
// way.
|
||||
int quiet = 0;
|
||||
if (argc > 1 && strcmp(argv[1], "-q") == 0) {
|
||||
quiet = 1;
|
||||
argc--; argv++;
|
||||
}
|
||||
|
||||
// -m dumps the raw module matrix and nothing else, for comparison against
|
||||
// an independent encoder. This is the check that actually matters: a symbol
|
||||
// can pass every self-check above and still be unreadable if the zigzag,
|
||||
// the masking or the block interleave is subtly wrong, and none of those
|
||||
// have a plausible-looking failure mode -- they all just produce noise that
|
||||
// resembles a QR code. tools/qrcheck.py drives this.
|
||||
// -f dumps the function-pattern map alongside the modules, so a checker can
|
||||
// run the zigzag backwards and recover the codewords. Knowing *which*
|
||||
// codeword two encoders disagree about is the difference between a bug you
|
||||
// can see and one you can only stare at.
|
||||
if (argc > 2 && strcmp(argv[1], "-f") == 0) {
|
||||
struct qr q;
|
||||
int n = (int)strlen(argv[2]);
|
||||
if (n > QR_MAX_BYTES || qr_encode(&q, (const unsigned char *)argv[2], n) != 0)
|
||||
return 1;
|
||||
for (int y = 0; y < q.size; y++) {
|
||||
for (int x = 0; x < q.size; x++)
|
||||
putchar(q.fn[y][x] ? '1' : '0');
|
||||
putchar('\n');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (argc > 2 && strcmp(argv[1], "-m") == 0) {
|
||||
struct qr q;
|
||||
int n = (int)strlen(argv[2]);
|
||||
if (n > QR_MAX_BYTES || qr_encode(&q, (const unsigned char *)argv[2], n) != 0)
|
||||
return 1;
|
||||
for (int y = 0; y < q.size; y++) {
|
||||
for (int x = 0; x < q.size; x++)
|
||||
putchar(q.mod[y][x] ? '1' : '0');
|
||||
putchar('\n');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *out = argc > 1 ? argv[1] : "build/qr.png";
|
||||
const char *text = argc > 2 ? argv[2] :
|
||||
"SCHUNIX PANIC\n"
|
||||
"build 8f2a91c4\n"
|
||||
"trap 14 err 4 cpu 0\n"
|
||||
"rip ffffffff80104e2a\n"
|
||||
"cr2 0000000000000018\n"
|
||||
"pid 7 schinit";
|
||||
|
||||
check_tables();
|
||||
check_rs();
|
||||
check_encode();
|
||||
|
||||
struct qr q;
|
||||
int len = (int)strlen(text);
|
||||
if (len > QR_MAX_BYTES) {
|
||||
fprintf(stderr, "qrtest: payload is %d bytes, max %d\n", len, QR_MAX_BYTES);
|
||||
return 1;
|
||||
}
|
||||
if (qr_encode(&q, (const unsigned char *)text, len) != 0) {
|
||||
fprintf(stderr, "qrtest: encode failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (write_png(out, &q, 8, 4) != 0)
|
||||
return 1;
|
||||
|
||||
printf("\n%d bytes -> version %d (%dx%d modules) -> %s\n",
|
||||
len, (q.size - 17) / 4, q.size, q.size, out);
|
||||
|
||||
// Also dump it to the terminal, where a phone can read it directly off the
|
||||
// screen without opening the file. Two spaces per module because a terminal
|
||||
// cell is about twice as tall as it is wide, and a squashed symbol will not
|
||||
// scan.
|
||||
if (!quiet) {
|
||||
printf("\n");
|
||||
for (int i = 0; i < 2; i++) {
|
||||
printf(" ");
|
||||
for (int x = 0; x < q.size + 8; x++) printf("\033[107m \033[0m");
|
||||
printf("\n");
|
||||
}
|
||||
for (int y = 0; y < q.size; y++) {
|
||||
printf(" \033[107m \033[0m");
|
||||
for (int x = 0; x < q.size; x++)
|
||||
printf("%s \033[0m", q.mod[y][x] ? "\033[40m" : "\033[107m");
|
||||
printf("\033[107m \033[0m\n");
|
||||
}
|
||||
for (int i = 0; i < 2; i++) {
|
||||
printf(" ");
|
||||
for (int x = 0; x < q.size + 8; x++) printf("\033[107m \033[0m");
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n%s\n", failures ? "SELF-CHECKS FAILED" : "self-checks passed");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
720
tools/schfs_fsck.c
Normal file
720
tools/schfs_fsck.c
Normal file
|
|
@ -0,0 +1,720 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// schfs_fsck — inspect, verify and extract a SchFS image.
|
||||
//
|
||||
// "fsck" is a slight misnomer: copy-on-write means there is nothing to repair.
|
||||
// A crash leaves either the old tree or the new one, so a volume is either
|
||||
// consistent or it has no valid uberblock at all. What this does is *check* --
|
||||
// and, just as usefully, read an image back out so its contents can be compared
|
||||
// against whatever produced it.
|
||||
//
|
||||
// schfs_fsck img --info uberblock ring and volume summary
|
||||
// schfs_fsck img --check full structural verification
|
||||
// schfs_fsck img --ls PATH list a directory
|
||||
// schfs_fsck img --stat PATH one object's metadata
|
||||
// schfs_fsck img --cat PATH a file's contents to stdout
|
||||
// schfs_fsck img --extract DIR the whole tree onto the host
|
||||
// schfs_fsck img --tree every path in the volume
|
||||
//
|
||||
// --extract is what makes the round-trip test possible, and the reason this
|
||||
// reads the image through the same code the kernel will use: if that code is
|
||||
// wrong, the extracted tree differs from the source tree and diff says so.
|
||||
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
#define SCHFS_HOSTED 1
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "../kernel/fs/schfs/btree.c"
|
||||
#include "../kernel/fs/schfs/space.c"
|
||||
#include "../kernel/fs/schfs/vol.c"
|
||||
#include "../kernel/fs/schfs/obj.c"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
struct imgdev {
|
||||
FILE *f;
|
||||
uint64_t nblocks;
|
||||
};
|
||||
|
||||
static struct imgdev IMG;
|
||||
|
||||
static int
|
||||
img_read(void *ctx, uint64_t b, void *buf)
|
||||
{
|
||||
struct imgdev *d = ctx;
|
||||
if (b >= d->nblocks)
|
||||
return -1;
|
||||
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
|
||||
return -1;
|
||||
if (fread(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
img_write(void *ctx, uint64_t b, const void *buf)
|
||||
{
|
||||
(void)ctx; (void)b; (void)buf;
|
||||
return -1; /* this tool never writes */
|
||||
}
|
||||
|
||||
static uint64_t img_alloc(void *ctx) { (void)ctx; return 0; }
|
||||
static void img_nofree(void *ctx, uint64_t b) { (void)ctx; (void)b; }
|
||||
|
||||
// Pages for the allocation map --check derives. Nothing is written back; this
|
||||
// is a check on the map, not a use of it.
|
||||
static void *img_palloc(void) { return calloc(1, SCHFS_BLKSIZE); }
|
||||
|
||||
static struct schfs_vol VOL;
|
||||
static char CURBLK[SCHFS_BLKSIZE];
|
||||
static char DATABLK[SCHFS_BLKSIZE];
|
||||
|
||||
// Read-only, so only the first of these is ever touched — but a volume owns all
|
||||
// three for its whole life, and handing it two null pointers would turn a
|
||||
// mistake into a crash rather than a compile error.
|
||||
static char CTX_A[SCHFS_BLKSIZE];
|
||||
static char CTX_B[SCHFS_BLKSIZE];
|
||||
static char CTX_C[SCHFS_BLKSIZE];
|
||||
static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C };
|
||||
|
||||
static const char *
|
||||
typename(uint16_t t)
|
||||
{
|
||||
switch (t) {
|
||||
case SCHFS_T_FILE: return "file";
|
||||
case SCHFS_T_DIR: return "dir";
|
||||
case SCHFS_T_DEV: return "dev";
|
||||
case SCHFS_T_SYMLINK: return "symlink";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* --info */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
do_info(void)
|
||||
{
|
||||
printf("uberblock ring (%d slots from block %d)\n",
|
||||
SCHFS_UBER_COUNT, SCHFS_UBER_START);
|
||||
|
||||
int valid = 0, best = -1;
|
||||
uint64_t bestgen = 0;
|
||||
for (int i = 0; i < SCHFS_UBER_COUNT; i++) {
|
||||
struct schfs_uberblock *u = (struct schfs_uberblock *)CTX_A;
|
||||
if (img_read(&IMG, SCHFS_UBER_START + i, u) != 0)
|
||||
continue;
|
||||
if (u->magic != SCHFS_MAGIC)
|
||||
continue;
|
||||
uint32_t stored = u->crc;
|
||||
u->crc = 0;
|
||||
uint32_t calc = schfs_crc32c(0, u, sizeof(*u));
|
||||
u->crc = stored;
|
||||
if (calc != stored) {
|
||||
printf(" slot %3d: magic ok, CHECKSUM BAD (generation %llu)\n",
|
||||
i, (unsigned long long)u->generation);
|
||||
continue;
|
||||
}
|
||||
valid++;
|
||||
if (best < 0 || u->generation > bestgen) {
|
||||
bestgen = u->generation;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
printf(" %d slot%s valid; newest is slot %d at generation %llu\n\n",
|
||||
valid, valid == 1 ? "" : "s", best, (unsigned long long)bestgen);
|
||||
|
||||
printf("volume\n");
|
||||
printf(" label %s\n", VOL.label);
|
||||
printf(" uuid ");
|
||||
for (int i = 0; i < 16; i++)
|
||||
printf("%02x%s", VOL.uuid[i], (i == 3 || i == 5 || i == 7 || i == 9) ? "-" : "");
|
||||
printf("\n");
|
||||
printf(" block size %d bytes\n", SCHFS_BLKSIZE);
|
||||
printf(" total blocks %llu (%llu KB)\n",
|
||||
(unsigned long long)VOL.total_blocks,
|
||||
(unsigned long long)VOL.total_blocks * SCHFS_BLKSIZE / 1024);
|
||||
printf(" free blocks %llu (%llu KB)\n",
|
||||
(unsigned long long)VOL.free_blocks,
|
||||
(unsigned long long)VOL.free_blocks * SCHFS_BLKSIZE / 1024);
|
||||
printf(" tree root block %llu, generation %llu\n",
|
||||
(unsigned long long)VOL.tree.root,
|
||||
(unsigned long long)VOL.tree.generation);
|
||||
printf(" next object id %llu\n", (unsigned long long)VOL.next_objectid);
|
||||
printf(" root object id %llu\n", (unsigned long long)VOL.root_objectid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* --check */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
struct tally {
|
||||
long inodes, dirents, extents, csums, other;
|
||||
long long bytes;
|
||||
int bad;
|
||||
};
|
||||
|
||||
static int
|
||||
count_rec(const struct schfs_key *k, const void *val, unsigned len, void *arg)
|
||||
{
|
||||
struct tally *t = arg;
|
||||
(void)val;
|
||||
switch (k->type) {
|
||||
case SCHFS_KEY_INODE:
|
||||
t->inodes++;
|
||||
if (len != sizeof(struct schfs_inode)) {
|
||||
fprintf(stderr, " object %llu: inode record is %u bytes, expected %zu\n",
|
||||
(unsigned long long)k->objectid, len,
|
||||
sizeof(struct schfs_inode));
|
||||
t->bad++;
|
||||
} else {
|
||||
t->bytes += ((const struct schfs_inode *)val)->size;
|
||||
}
|
||||
break;
|
||||
case SCHFS_KEY_DIRENT: t->dirents++; break;
|
||||
case SCHFS_KEY_EXTENT:
|
||||
t->extents++;
|
||||
if (len != sizeof(struct schfs_extent)) {
|
||||
fprintf(stderr, " object %llu: extent record is %u bytes\n",
|
||||
(unsigned long long)k->objectid, len);
|
||||
t->bad++;
|
||||
}
|
||||
break;
|
||||
case SCHFS_KEY_CSUM: t->csums++; break;
|
||||
default: t->other++; break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Verify every data block against its stored checksum, by reading every file
|
||||
in full. This is the part a metadata-only check would miss entirely. */
|
||||
static long files_checked, blocks_checked;
|
||||
|
||||
// Chain of object ids from the root down to whatever is being checked.
|
||||
//
|
||||
// A directory that is its own ancestor makes any recursive walk run until the
|
||||
// stack runs out, and a segfault is a terrible way to be told the filesystem
|
||||
// has a loop. Directories cannot be hard-linked, so a repeat in this chain is
|
||||
// always a fault -- and the first version of mkschfs produced exactly one, by
|
||||
// handing a subdirectory the root's object id.
|
||||
#define MAX_DEPTH 64
|
||||
static uint64_t chain[MAX_DEPTH];
|
||||
static int chain_n;
|
||||
|
||||
static int
|
||||
chain_push(uint64_t ino, const char *path)
|
||||
{
|
||||
for (int i = 0; i < chain_n; i++)
|
||||
if (chain[i] == ino) {
|
||||
fprintf(stderr, " %s: directory loop -- object %llu is its own "
|
||||
"ancestor\n", path, (unsigned long long)ino);
|
||||
return -1;
|
||||
}
|
||||
if (chain_n >= MAX_DEPTH) {
|
||||
fprintf(stderr, " %s: directory nesting deeper than %d\n",
|
||||
path, MAX_DEPTH);
|
||||
return -1;
|
||||
}
|
||||
chain[chain_n++] = ino;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void chain_pop(void) { if (chain_n) chain_n--; }
|
||||
|
||||
static int check_dir(uint64_t ino, const char *path);
|
||||
|
||||
static int
|
||||
check_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
|
||||
void *arg)
|
||||
{
|
||||
const char *parent = arg;
|
||||
char path[8192];
|
||||
snprintf(path, sizeof(path), "%s%s%.*s",
|
||||
parent, strcmp(parent, "/") == 0 ? "" : "/", (int)namelen, name);
|
||||
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
|
||||
fprintf(stderr, " %s: entry points at object %llu, which has no inode\n",
|
||||
path, (unsigned long long)ino);
|
||||
return -1;
|
||||
}
|
||||
if (in.type != type)
|
||||
fprintf(stderr, " %s: dirent says %s, inode says %s\n",
|
||||
path, typename(type), typename(in.type));
|
||||
|
||||
if (in.type == SCHFS_T_DIR)
|
||||
return check_dir(ino, path);
|
||||
|
||||
uint64_t off = 0;
|
||||
while (off < in.size) {
|
||||
// A full block is requested every time, even at the tail. Clamping
|
||||
// here instead would mean nothing ever tests that schfs_file_read
|
||||
// stops at end-of-file -- and the kernel's read(2) will depend on
|
||||
// exactly that, since a real program just asks for a bufferful.
|
||||
char buf[SCHFS_BLKSIZE];
|
||||
unsigned want = sizeof(buf);
|
||||
long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK);
|
||||
if (got <= 0) {
|
||||
fprintf(stderr, " %s: read failed at offset %llu (%ld)\n",
|
||||
path, (unsigned long long)off, got);
|
||||
return -1;
|
||||
}
|
||||
off += (uint64_t)got;
|
||||
blocks_checked++;
|
||||
}
|
||||
files_checked++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
check_dir(uint64_t ino, const char *path)
|
||||
{
|
||||
if (chain_push(ino, path) != 0)
|
||||
return -1;
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
int r = schfs_dir_iterate(&VOL, &c, ino, check_entry, (void *)path);
|
||||
chain_pop();
|
||||
return r;
|
||||
}
|
||||
|
||||
static int
|
||||
do_check(void)
|
||||
{
|
||||
printf("checking %s\n", VOL.label);
|
||||
|
||||
if (schfs_btree_check(&VOL.tree) != 0) {
|
||||
fprintf(stderr, " FAIL: tree structure is invalid\n");
|
||||
return 1;
|
||||
}
|
||||
printf(" [ok] tree structure, checksums and key ordering\n");
|
||||
|
||||
struct tally t;
|
||||
memset(&t, 0, sizeof(t));
|
||||
if (schfs_btree_iterate(&VOL.tree, count_rec, &t) != 0) {
|
||||
fprintf(stderr, " FAIL: could not walk every record\n");
|
||||
return 1;
|
||||
}
|
||||
if (t.bad) {
|
||||
fprintf(stderr, " FAIL: %d malformed records\n", t.bad);
|
||||
return 1;
|
||||
}
|
||||
printf(" [ok] %ld inodes, %ld dirent items, %ld extents, %ld data checksums\n",
|
||||
t.inodes, t.dirents, t.extents, t.csums);
|
||||
|
||||
if (check_dir(VOL.root_objectid, "/") != 0) {
|
||||
fprintf(stderr, " FAIL: walking the directory tree found a problem\n");
|
||||
return 1;
|
||||
}
|
||||
printf(" [ok] %ld files read end to end, every data block matched its checksum\n",
|
||||
files_checked);
|
||||
|
||||
// Rebuild the allocation map the way a mount would, and hold it against the
|
||||
// count the writer left in the uberblock. The map is derived rather than
|
||||
// stored, so nothing else would ever notice if the scan and the writer
|
||||
// disagreed — right up until a mounted volume handed out a block that was
|
||||
// still in use.
|
||||
if (schfs_space_init(&VOL.space, VOL.total_blocks, img_palloc) != 0) {
|
||||
fprintf(stderr, " FAIL: could not build an allocation map\n");
|
||||
return 1;
|
||||
}
|
||||
if (schfs_space_scan(&VOL) != 0) {
|
||||
fprintf(stderr, " FAIL: could not walk the tree to derive free space\n");
|
||||
return 1;
|
||||
}
|
||||
if (VOL.space.free_blocks != VOL.free_blocks) {
|
||||
fprintf(stderr, " FAIL: uberblock says %llu blocks free, walking the "
|
||||
"tree says %llu\n",
|
||||
(unsigned long long)VOL.free_blocks,
|
||||
(unsigned long long)VOL.space.free_blocks);
|
||||
return 1;
|
||||
}
|
||||
printf(" [ok] free space derived from the tree matches the uberblock "
|
||||
"(%llu of %llu blocks in use)\n",
|
||||
(unsigned long long)(VOL.total_blocks - VOL.space.free_blocks),
|
||||
(unsigned long long)VOL.total_blocks);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* --ls / --stat / --cat / --tree */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
print_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
|
||||
void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
|
||||
printf(" ? %.*s\n", (int)namelen, name);
|
||||
return 0;
|
||||
}
|
||||
printf(" %-7s %6llu %4o %2u links %.*s\n",
|
||||
typename(type), (unsigned long long)in.size, in.mode & 07777,
|
||||
in.nlink, (int)namelen, name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
resolve(const char *path)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
uint64_t ino = schfs_path_lookup(&VOL, &c, path);
|
||||
if (ino == 0)
|
||||
fprintf(stderr, "schfs_fsck: %s: no such file or directory\n", path);
|
||||
return ino;
|
||||
}
|
||||
|
||||
static int
|
||||
do_ls(const char *path)
|
||||
{
|
||||
uint64_t ino = resolve(path);
|
||||
if (!ino)
|
||||
return 1;
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
printf("%s\n", path);
|
||||
return schfs_dir_iterate(&VOL, &c, ino, print_entry, NULL) != 0;
|
||||
}
|
||||
|
||||
static int
|
||||
do_stat(const char *path)
|
||||
{
|
||||
uint64_t ino = resolve(path);
|
||||
if (!ino)
|
||||
return 1;
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
|
||||
fprintf(stderr, "schfs_fsck: %s: inode unreadable\n", path);
|
||||
return 1;
|
||||
}
|
||||
printf("path: %s\n", path);
|
||||
printf("objectid: %llu\n", (unsigned long long)ino);
|
||||
printf("type: %s\n", typename(in.type));
|
||||
printf("size: %llu\n", (unsigned long long)in.size);
|
||||
printf("blocks: %llu\n", (unsigned long long)in.blocks);
|
||||
printf("mode: %04o\n", in.mode & 07777);
|
||||
printf("nlink: %u\n", in.nlink);
|
||||
printf("uid: %u\n", in.uid);
|
||||
printf("gid: %u\n", in.gid);
|
||||
printf("mtime: %llu\n", (unsigned long long)in.mtime);
|
||||
printf("parent: %llu\n", (unsigned long long)in.parent);
|
||||
if (in.type == SCHFS_T_DEV)
|
||||
printf("rdev: %u:%u\n", in.rdev_major, in.rdev_minor);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
do_cat(const char *path)
|
||||
{
|
||||
uint64_t ino = resolve(path);
|
||||
if (!ino)
|
||||
return 1;
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0)
|
||||
return 1;
|
||||
|
||||
uint64_t off = 0;
|
||||
while (off < in.size) {
|
||||
// A full block is requested every time, even at the tail. Clamping
|
||||
// here instead would mean nothing ever tests that schfs_file_read
|
||||
// stops at end-of-file -- and the kernel's read(2) will depend on
|
||||
// exactly that, since a real program just asks for a bufferful.
|
||||
char buf[SCHFS_BLKSIZE];
|
||||
unsigned want = sizeof(buf);
|
||||
long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK);
|
||||
if (got <= 0) {
|
||||
fprintf(stderr, "schfs_fsck: %s: read error at %llu\n",
|
||||
path, (unsigned long long)off);
|
||||
return 1;
|
||||
}
|
||||
fwrite(buf, 1, (size_t)got, stdout);
|
||||
off += (uint64_t)got;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int tree_walk(uint64_t ino, const char *path);
|
||||
|
||||
static int
|
||||
tree_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
|
||||
void *arg)
|
||||
{
|
||||
const char *parent = arg;
|
||||
char path[8192];
|
||||
snprintf(path, sizeof(path), "%s%s%.*s",
|
||||
parent, strcmp(parent, "/") == 0 ? "" : "/", (int)namelen, name);
|
||||
printf("%-8s %s\n", typename(type), path);
|
||||
if (type == SCHFS_T_DIR)
|
||||
return tree_walk(ino, path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
tree_walk(uint64_t ino, const char *path)
|
||||
{
|
||||
if (chain_push(ino, path) != 0)
|
||||
return -1;
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
int r = schfs_dir_iterate(&VOL, &c, ino, tree_entry, (void *)path);
|
||||
chain_pop();
|
||||
return r;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* --extract */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Objects already written out, so a hard link on the image becomes a hard link
|
||||
// on the host rather than a second copy. Without this the round-trip test's
|
||||
// link-count check could not distinguish dedup from duplication.
|
||||
struct seen_ent { uint64_t ino; char *path; };
|
||||
static struct seen_ent *seen;
|
||||
static int seen_n, seen_cap;
|
||||
|
||||
static const char *
|
||||
seen_lookup(uint64_t ino)
|
||||
{
|
||||
for (int i = 0; i < seen_n; i++)
|
||||
if (seen[i].ino == ino)
|
||||
return seen[i].path;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
seen_add(uint64_t ino, const char *path)
|
||||
{
|
||||
if (seen_n == seen_cap) {
|
||||
seen_cap = seen_cap ? seen_cap * 2 : 128;
|
||||
seen = realloc(seen, (size_t)seen_cap * sizeof(*seen));
|
||||
if (!seen) { perror("realloc"); exit(1); }
|
||||
}
|
||||
seen[seen_n].ino = ino;
|
||||
seen[seen_n].path = strdup(path);
|
||||
seen_n++;
|
||||
}
|
||||
|
||||
static long xfiles, xdirs, xlinks, xsymlinks;
|
||||
|
||||
static int extract_dir(uint64_t ino, const char *hostpath);
|
||||
|
||||
static int
|
||||
extract_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
|
||||
void *arg)
|
||||
{
|
||||
const char *parent = arg;
|
||||
char host[8192];
|
||||
(void)type; /* the inode is authoritative, not the dirent */
|
||||
snprintf(host, sizeof(host), "%s/%.*s", parent, (int)namelen, name);
|
||||
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0) {
|
||||
fprintf(stderr, "schfs_fsck: %s: inode %llu unreadable\n",
|
||||
host, (unsigned long long)ino);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (in.type == SCHFS_T_DIR) {
|
||||
if (mkdir(host, in.mode & 07777) != 0 && errno != EEXIST) {
|
||||
perror(host);
|
||||
return -1;
|
||||
}
|
||||
xdirs++;
|
||||
return extract_dir(ino, host);
|
||||
}
|
||||
|
||||
if (in.type == SCHFS_T_SYMLINK) {
|
||||
char target[4096];
|
||||
if (in.size >= sizeof(target)) {
|
||||
fprintf(stderr, "schfs_fsck: %s: symlink target too long\n", host);
|
||||
return -1;
|
||||
}
|
||||
long got = schfs_file_read(&VOL, &c, ino, 0, target,
|
||||
(unsigned)in.size, DATABLK);
|
||||
if (got < 0 || (uint64_t)got != in.size) {
|
||||
fprintf(stderr, "schfs_fsck: %s: could not read symlink target\n", host);
|
||||
return -1;
|
||||
}
|
||||
target[in.size] = '\0';
|
||||
unlink(host);
|
||||
if (symlink(target, host) != 0) { perror(host); return -1; }
|
||||
xsymlinks++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (in.type == SCHFS_T_DEV) {
|
||||
// Creating a real device node needs privileges the build does not have,
|
||||
// so record it as an empty placeholder. The round-trip tree contains no
|
||||
// device nodes; --check is what validates them.
|
||||
FILE *f = fopen(host, "wb");
|
||||
if (!f) { perror(host); return -1; }
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A second name for an object already written: make it a real hard link so
|
||||
// the extracted tree has the same link structure as the image.
|
||||
const char *first = seen_lookup(ino);
|
||||
if (first) {
|
||||
unlink(host);
|
||||
if (link(first, host) != 0) { perror(host); return -1; }
|
||||
xlinks++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *f = fopen(host, "wb");
|
||||
if (!f) { perror(host); return -1; }
|
||||
uint64_t off = 0;
|
||||
while (off < in.size) {
|
||||
// A full block is requested every time, even at the tail. Clamping
|
||||
// here instead would mean nothing ever tests that schfs_file_read
|
||||
// stops at end-of-file -- and the kernel's read(2) will depend on
|
||||
// exactly that, since a real program just asks for a bufferful.
|
||||
char buf[SCHFS_BLKSIZE];
|
||||
unsigned want = sizeof(buf);
|
||||
long got = schfs_file_read(&VOL, &c, ino, off, buf, want, DATABLK);
|
||||
if (got <= 0) {
|
||||
fprintf(stderr, "schfs_fsck: %s: read error at %llu\n",
|
||||
host, (unsigned long long)off);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
fwrite(buf, 1, (size_t)got, f);
|
||||
off += (uint64_t)got;
|
||||
}
|
||||
fclose(f);
|
||||
if (chmod(host, in.mode & 07777) != 0)
|
||||
perror(host);
|
||||
seen_add(ino, host);
|
||||
xfiles++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
extract_dir(uint64_t ino, const char *hostpath)
|
||||
{
|
||||
if (chain_push(ino, hostpath) != 0)
|
||||
return -1;
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
int r = schfs_dir_iterate(&VOL, &c, ino, extract_entry, (void *)hostpath);
|
||||
chain_pop();
|
||||
return r;
|
||||
}
|
||||
|
||||
static int
|
||||
do_extract(const char *dir)
|
||||
{
|
||||
if (mkdir(dir, 0755) != 0 && errno != EEXIST) {
|
||||
perror(dir);
|
||||
return 1;
|
||||
}
|
||||
if (extract_dir(VOL.root_objectid, dir) != 0)
|
||||
return 1;
|
||||
printf("extracted %ld dirs, %ld files, %ld hard links, %ld symlinks into %s\n",
|
||||
xdirs, xfiles, xlinks, xsymlinks, dir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 3) {
|
||||
fprintf(stderr,
|
||||
"usage: schfs_fsck img.schfs <action>\n"
|
||||
" --info uberblock ring and volume summary\n"
|
||||
" --check full structural and data verification\n"
|
||||
" --ls PATH list a directory\n"
|
||||
" --stat PATH one object's metadata\n"
|
||||
" --cat PATH a file's contents\n"
|
||||
" --tree every path in the volume\n"
|
||||
" --extract DIR write the whole tree onto the host\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char *imgpath = argv[1];
|
||||
const char *action = argv[2];
|
||||
const char *arg = argc > 3 ? argv[3] : NULL;
|
||||
|
||||
// Must match whatever mkschfs used, or every directory lookup misses.
|
||||
for (int i = 3; i + 1 < argc; i++)
|
||||
if (strcmp(argv[i], "--hash-bits") == 0) {
|
||||
int bits = atoi(argv[i + 1]);
|
||||
schfs_hash_mask = bits >= 64 ? ~0ULL : ((1ULL << bits) - 1);
|
||||
if (arg == argv[i])
|
||||
arg = NULL;
|
||||
}
|
||||
|
||||
FILE *f = fopen(imgpath, "rb");
|
||||
if (!f) { perror(imgpath); return 1; }
|
||||
if (fseeko(f, 0, SEEK_END) != 0) { perror(imgpath); return 1; }
|
||||
off_t sz = ftello(f);
|
||||
|
||||
IMG.f = f;
|
||||
IMG.nblocks = (uint64_t)sz / SCHFS_BLKSIZE;
|
||||
|
||||
struct schfs_bdev bd;
|
||||
bd.ctx = &IMG;
|
||||
bd.bread = img_read;
|
||||
bd.bwrite = img_write;
|
||||
bd.balloc = img_alloc;
|
||||
bd.bfree = img_nofree;
|
||||
bd.balloc_run = NULL; /* read-only: nothing is ever allocated */
|
||||
|
||||
// No page allocator: this tool never writes, and a volume without an
|
||||
// allocation map cannot allocate even by accident.
|
||||
int r = schfs_vol_open(&VOL, &bd, &CTX, NULL);
|
||||
if (r == -1) {
|
||||
fprintf(stderr, "schfs_fsck: %s: no valid uberblock -- not a SchFS "
|
||||
"volume, or never successfully written\n", imgpath);
|
||||
return 1;
|
||||
}
|
||||
if (r != 0) {
|
||||
fprintf(stderr, "schfs_fsck: %s: newest uberblock names an unreadable "
|
||||
"tree root\n", imgpath);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strcmp(action, "--info") == 0)
|
||||
return do_info();
|
||||
if (strcmp(action, "--check") == 0)
|
||||
return do_check();
|
||||
if (strcmp(action, "--tree") == 0)
|
||||
return tree_walk(VOL.root_objectid, "");
|
||||
if (strcmp(action, "--ls") == 0 && arg)
|
||||
return do_ls(arg);
|
||||
if (strcmp(action, "--stat") == 0 && arg)
|
||||
return do_stat(arg);
|
||||
if (strcmp(action, "--cat") == 0 && arg)
|
||||
return do_cat(arg);
|
||||
if (strcmp(action, "--extract") == 0 && arg)
|
||||
return do_extract(arg);
|
||||
|
||||
fprintf(stderr, "schfs_fsck: unknown or incomplete action '%s'\n", action);
|
||||
return 2;
|
||||
}
|
||||
568
tools/schfs_objtest.c
Normal file
568
tools/schfs_objtest.c
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
// SchFS object-layer test: files, directories and space accounting.
|
||||
//
|
||||
// schfs_test covers the B-tree. This covers everything built on it — create,
|
||||
// write at an offset, truncate, unlink, rename, hard links — and, most
|
||||
// importantly, that space comes back. A filesystem that loses a block on every
|
||||
// delete passes every functional test ever written and is still useless.
|
||||
//
|
||||
// Everything runs against a memory-backed volume, so a full create/write/
|
||||
// delete/remount cycle takes milliseconds. The kernel gets this code only once
|
||||
// it passes here, because the same failure inside a VM costs a rebuild and a
|
||||
// boot to see once.
|
||||
//
|
||||
// Build and run: make schfs-objtest
|
||||
|
||||
#define SCHFS_HOSTED 1
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../kernel/fs/schfs/btree.c"
|
||||
#include "../kernel/fs/schfs/space.c"
|
||||
#include "../kernel/fs/schfs/vol.c"
|
||||
#include "../kernel/fs/schfs/obj.c"
|
||||
#include "../kernel/fs/schfs/objw.c"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Memory-backed device */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define NBLOCKS 4096
|
||||
|
||||
static unsigned char *DISK;
|
||||
static struct schfs_vol VOL;
|
||||
|
||||
static char CTX_A[SCHFS_BLKSIZE];
|
||||
static char CTX_B[SCHFS_BLKSIZE];
|
||||
static char CTX_C[SCHFS_BLKSIZE];
|
||||
static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C };
|
||||
|
||||
static char CURBLK[SCHFS_BLKSIZE];
|
||||
static char DATABLK[SCHFS_BLKSIZE];
|
||||
|
||||
static int
|
||||
md_read(void *ctx, uint64_t b, void *buf)
|
||||
{
|
||||
(void)ctx;
|
||||
if (b >= NBLOCKS) return -1;
|
||||
memcpy(buf, DISK + b * SCHFS_BLKSIZE, SCHFS_BLKSIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
md_write(void *ctx, uint64_t b, const void *buf)
|
||||
{
|
||||
(void)ctx;
|
||||
if (b >= NBLOCKS) return -1;
|
||||
memcpy(DISK + b * SCHFS_BLKSIZE, buf, SCHFS_BLKSIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t md_alloc(void *ctx) { (void)ctx; return schfs_vol_alloc(&VOL); }
|
||||
static uint64_t md_run(void *ctx, uint64_t n) { (void)ctx; return schfs_vol_alloc_run(&VOL, n); }
|
||||
static void md_free(void *ctx, uint64_t b) { (void)ctx; schfs_vol_free(&VOL, b); }
|
||||
static void *md_palloc(void) { return calloc(1, SCHFS_BLKSIZE); }
|
||||
|
||||
static void
|
||||
mkbdev(struct schfs_bdev *bd)
|
||||
{
|
||||
bd->ctx = 0;
|
||||
bd->bread = md_read;
|
||||
bd->bwrite = md_write;
|
||||
bd->balloc = md_alloc;
|
||||
bd->bfree = md_free;
|
||||
bd->balloc_run = md_run;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int failures;
|
||||
|
||||
static void
|
||||
ok(int cond, const char *what)
|
||||
{
|
||||
if (cond) {
|
||||
printf(" [ok] %s\n", what);
|
||||
} else {
|
||||
printf(" FAIL: %s\n", what);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t rng = 987654321;
|
||||
static uint32_t
|
||||
rnd(void)
|
||||
{
|
||||
rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
|
||||
return rng;
|
||||
}
|
||||
|
||||
// Read a whole file into buf, returning its length or -1.
|
||||
static long
|
||||
readall(uint64_t ino, char *buf, unsigned cap)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0)
|
||||
return -1;
|
||||
if (in.size > cap)
|
||||
return -1;
|
||||
|
||||
unsigned off = 0;
|
||||
while (off < in.size) {
|
||||
long got = schfs_file_read(&VOL, &c, ino, off, buf + off,
|
||||
(unsigned)(in.size - off), DATABLK);
|
||||
if (got <= 0)
|
||||
return -1;
|
||||
off += (unsigned)got;
|
||||
}
|
||||
return (long)off;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
lookup(const char *path)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
return schfs_path_lookup(&VOL, &c, path);
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
nlink_of(uint64_t ino)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0)
|
||||
return 0;
|
||||
return in.nlink;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
size_of(uint64_t ino)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(&VOL, &c, ino, &in) != 0)
|
||||
return ~0ULL;
|
||||
return in.size;
|
||||
}
|
||||
|
||||
static int
|
||||
count_extents_of(uint64_t ino)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &VOL.tree, CURBLK);
|
||||
struct schfs_key start;
|
||||
schfs_mkkey(&start, ino, SCHFS_KEY_EXTENT, 0);
|
||||
if (schfs_cursor_seek(&c, &start) != 0)
|
||||
return 0;
|
||||
int n = 0;
|
||||
for (;;) {
|
||||
const struct schfs_key *k = schfs_cursor_key(&c);
|
||||
if (!k || k->objectid != ino || k->type != SCHFS_KEY_EXTENT)
|
||||
break;
|
||||
n++;
|
||||
if (schfs_cursor_next(&c) != 0)
|
||||
break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Publish, then reopen the volume from its uberblock — which rebuilds the
|
||||
// allocation map by walking the tree. Returns the free count the writer had, so
|
||||
// a caller can hold it against the one the mount derived: a bitmap that has
|
||||
// drifted from its own counter shows up here and nowhere else.
|
||||
//
|
||||
// Reopening also ages every block into a previous transaction group, which is
|
||||
// what makes the copy-on-write paths run at all. Within one group the tree and
|
||||
// the data both rewrite in place, so a test that never commits exercises the
|
||||
// cheap half of the filesystem and none of the careful half.
|
||||
static uint64_t
|
||||
remount(void)
|
||||
{
|
||||
if (schfs_vol_commit(&VOL) != 0) {
|
||||
printf(" FAIL: commit before remount\n");
|
||||
failures++;
|
||||
return ~0ULL;
|
||||
}
|
||||
|
||||
uint64_t before = VOL.free_blocks;
|
||||
struct schfs_bdev bd2;
|
||||
mkbdev(&bd2);
|
||||
|
||||
// Straight back into VOL, not into a local that is then copied over it: a
|
||||
// schfs_vol points at its own dev field, so a copy of one leaves the tree
|
||||
// talking to whichever struct it was built in.
|
||||
memset(&VOL, 0, sizeof(VOL));
|
||||
if (schfs_vol_open(&VOL, &bd2, &CTX, md_palloc) != 0) {
|
||||
printf(" FAIL: remount\n");
|
||||
failures++;
|
||||
return ~0ULL;
|
||||
}
|
||||
return before;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define ROOT (VOL.root_objectid)
|
||||
|
||||
static uint64_t
|
||||
mkfile(uint64_t dir, const char *name)
|
||||
{
|
||||
uint64_t ino = 0;
|
||||
int r = schfs_create(&VOL, dir, name, (unsigned)strlen(name),
|
||||
SCHFS_T_FILE, 0644, 0, 0, 1000, &ino);
|
||||
if (r != 0) {
|
||||
printf(" FAIL: create %s -> %d\n", name, r);
|
||||
failures++;
|
||||
return 0;
|
||||
}
|
||||
return ino;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
mkdirent(uint64_t dir, const char *name)
|
||||
{
|
||||
uint64_t ino = 0;
|
||||
int r = schfs_create(&VOL, dir, name, (unsigned)strlen(name),
|
||||
SCHFS_T_DIR, 0755, 0, 0, 1000, &ino);
|
||||
if (r != 0) {
|
||||
printf(" FAIL: mkdir %s -> %d\n", name, r);
|
||||
failures++;
|
||||
return 0;
|
||||
}
|
||||
return ino;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
struct schfs_bdev bd;
|
||||
|
||||
DISK = calloc(NBLOCKS, SCHFS_BLKSIZE);
|
||||
if (!DISK) { perror("calloc"); return 1; }
|
||||
mkbdev(&bd);
|
||||
|
||||
printf("SchFS object-layer test\n\n");
|
||||
|
||||
if (schfs_vol_format(&VOL, &bd, NBLOCKS, "objtest", &CTX, md_palloc) != 0) {
|
||||
fprintf(stderr, "FAIL: format\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The root directory is an object like any other; nothing points at it
|
||||
// except the uberblock's root_objectid.
|
||||
{
|
||||
struct schfs_inode root;
|
||||
memset(&root, 0, sizeof(root));
|
||||
root.type = SCHFS_T_DIR;
|
||||
root.mode = 0755;
|
||||
root.nlink = 2;
|
||||
if (schfs_inode_put(&VOL, ROOT, &root) != 0) {
|
||||
fprintf(stderr, "FAIL: root inode\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (schfs_vol_commit(&VOL) != 0) { fprintf(stderr, "FAIL: commit\n"); return 1; }
|
||||
|
||||
uint64_t baseline_free = VOL.free_blocks;
|
||||
|
||||
/* ---- 1. create, write, read back ---- */
|
||||
{
|
||||
uint64_t d = mkdirent(ROOT, "dir");
|
||||
uint64_t f = mkfile(d, "hello");
|
||||
|
||||
const char *msg = "the quick brown fox";
|
||||
long w = schfs_file_write(&VOL, f, 0, msg, (unsigned)strlen(msg), DATABLK);
|
||||
ok(w == (long)strlen(msg), "write returns the byte count");
|
||||
|
||||
char buf[128];
|
||||
long got = readall(f, buf, sizeof(buf));
|
||||
ok(got == (long)strlen(msg) && memcmp(buf, msg, (size_t)got) == 0,
|
||||
"a short file reads back exactly");
|
||||
ok(lookup("/dir/hello") == f, "path lookup finds it");
|
||||
ok(nlink_of(d) == 2, "a new directory has nlink 2");
|
||||
ok(nlink_of(ROOT) == 3, "creating a subdirectory bumps the parent");
|
||||
}
|
||||
|
||||
/* ---- 2. a large file becomes few extents, not one per block ---- */
|
||||
{
|
||||
uint64_t f = mkfile(ROOT, "big");
|
||||
static char src[64 * 1024];
|
||||
for (unsigned i = 0; i < sizeof(src); i++)
|
||||
src[i] = (char)(i * 7 + (i >> 8));
|
||||
|
||||
// Written in small pieces on purpose: this is the shape that used to
|
||||
// produce one extent record per block, and one copy-on-write per call.
|
||||
unsigned off = 0;
|
||||
while (off < sizeof(src)) {
|
||||
unsigned n = 100 + rnd() % 900;
|
||||
if (off + n > sizeof(src)) n = (unsigned)sizeof(src) - off;
|
||||
long w = schfs_file_write(&VOL, f, off, src + off, n, DATABLK);
|
||||
if (w != (long)n) { printf(" FAIL: partial write %ld\n", w); failures++; break; }
|
||||
off += n;
|
||||
}
|
||||
|
||||
static char back[64 * 1024];
|
||||
long got = readall(f, back, sizeof(back));
|
||||
ok(got == (long)sizeof(src) && memcmp(back, src, sizeof(src)) == 0,
|
||||
"64 KB written in small pieces reads back identical");
|
||||
|
||||
int ne = count_extents_of(f);
|
||||
ok(ne <= 4, "sequential writes merge into a handful of extents");
|
||||
if (ne > 4)
|
||||
printf(" (%d extents for 16 blocks)\n", ne);
|
||||
}
|
||||
|
||||
/* ---- 3. overwriting the middle, and holes ---- */
|
||||
{
|
||||
uint64_t f = mkfile(ROOT, "patch");
|
||||
static char src[16 * 1024];
|
||||
memset(src, 'a', sizeof(src));
|
||||
schfs_file_write(&VOL, f, 0, src, sizeof(src), DATABLK);
|
||||
|
||||
// Straddles a block boundary, so it exercises the split path on both
|
||||
// sides of the extent it lands in.
|
||||
char patch[600];
|
||||
memset(patch, 'Z', sizeof(patch));
|
||||
schfs_file_write(&VOL, f, 4000, patch, sizeof(patch), DATABLK);
|
||||
memcpy(src + 4000, patch, sizeof(patch));
|
||||
|
||||
static char back[16 * 1024];
|
||||
long got = readall(f, back, sizeof(back));
|
||||
ok(got == (long)sizeof(src) && memcmp(back, src, sizeof(src)) == 0,
|
||||
"an overwrite across a block boundary lands exactly");
|
||||
|
||||
// Now the same thing across a commit. Everything above happened inside
|
||||
// one transaction group, where a data block may be rewritten in place;
|
||||
// once it is committed it belongs to a tree a crash can still land on,
|
||||
// so an overwrite has to copy the block and split the extent record
|
||||
// around it. That is the path that actually risks losing data, and only
|
||||
// a commit makes it run.
|
||||
if (schfs_vol_commit(&VOL) != 0) { printf(" FAIL: commit\n"); failures++; }
|
||||
|
||||
char patch2[3000];
|
||||
memset(patch2, 'K', sizeof(patch2));
|
||||
long w = schfs_file_write(&VOL, f, 9000, patch2, sizeof(patch2), DATABLK);
|
||||
ok(w == (long)sizeof(patch2), "an overwrite after a commit succeeds");
|
||||
memcpy(src + 9000, patch2, sizeof(patch2));
|
||||
|
||||
got = readall(f, back, sizeof(back));
|
||||
ok(got == (long)sizeof(src) && memcmp(back, src, sizeof(src)) == 0,
|
||||
"copy-on-write of a committed block keeps the rest of the run intact");
|
||||
|
||||
// And the block it replaced must really be gone, not still in the map.
|
||||
uint64_t was = remount();
|
||||
ok(was == VOL.free_blocks,
|
||||
"free space after a copy-on-write overwrite survives a remount");
|
||||
|
||||
got = readall(lookup("/patch"), back, sizeof(back));
|
||||
ok(got == (long)sizeof(src) && memcmp(back, src, sizeof(src)) == 0,
|
||||
"and the patched file still reads correctly after remounting");
|
||||
|
||||
uint64_t h = mkfile(ROOT, "sparse");
|
||||
schfs_file_write(&VOL, h, 40000, "end", 3, DATABLK);
|
||||
ok(size_of(h) == 40003, "writing past the end extends the file");
|
||||
|
||||
static char hb[64 * 1024];
|
||||
got = readall(h, hb, sizeof(hb));
|
||||
int zeros = (got == 40003);
|
||||
for (long i = 0; i < 40000 && zeros; i++)
|
||||
if (hb[i] != 0) zeros = 0;
|
||||
ok(zeros && memcmp(hb + 40000, "end", 3) == 0,
|
||||
"the gap left behind is a hole and reads as zeros");
|
||||
}
|
||||
|
||||
/* ---- 4. truncate ---- */
|
||||
{
|
||||
uint64_t f = mkfile(ROOT, "trunc");
|
||||
static char src[20 * 1024];
|
||||
memset(src, 'q', sizeof(src));
|
||||
schfs_file_write(&VOL, f, 0, src, sizeof(src), DATABLK);
|
||||
|
||||
ok(schfs_file_truncate(&VOL, f, 100, DATABLK) == 0, "truncate succeeds");
|
||||
ok(size_of(f) == 100, "truncate sets the size");
|
||||
|
||||
char back[256];
|
||||
long got = readall(f, back, sizeof(back));
|
||||
int good = (got == 100);
|
||||
for (int i = 0; i < 100 && good; i++)
|
||||
if (back[i] != 'q') good = 0;
|
||||
ok(good, "what survives a truncate is the original head");
|
||||
|
||||
// Growing again must read as zeros, not as whatever the released
|
||||
// blocks used to hold.
|
||||
ok(schfs_file_truncate(&VOL, f, 8192, DATABLK) == 0, "truncate can grow");
|
||||
static char big[8192];
|
||||
got = readall(f, big, sizeof(big));
|
||||
int zeroed = (got == 8192);
|
||||
for (int i = 100; i < 8192 && zeroed; i++)
|
||||
if (big[i] != 0) zeroed = 0;
|
||||
ok(zeroed, "growing past the old end reads as zeros, not stale data");
|
||||
}
|
||||
|
||||
/* ---- 5. links, unlink, rename, rmdir ---- */
|
||||
{
|
||||
uint64_t f = mkfile(ROOT, "orig");
|
||||
schfs_file_write(&VOL, f, 0, "content", 7, DATABLK);
|
||||
|
||||
ok(schfs_link(&VOL, ROOT, "alias", 5, f) == 0, "hard link succeeds");
|
||||
ok(nlink_of(f) == 2, "hard link bumps nlink");
|
||||
ok(lookup("/alias") == f, "both names reach the same object");
|
||||
|
||||
ok(schfs_unlink(&VOL, ROOT, "orig", 4) == 0, "unlink one name");
|
||||
ok(nlink_of(f) == 1, "nlink drops but the object survives");
|
||||
char buf[32];
|
||||
ok(readall(f, buf, sizeof(buf)) == 7, "the surviving name still reads");
|
||||
|
||||
ok(schfs_rename(&VOL, ROOT, "alias", 5, ROOT, "renamed", 7) == 0,
|
||||
"rename within a directory");
|
||||
ok(lookup("/alias") == 0 && lookup("/renamed") == f,
|
||||
"the old name is gone and the new one resolves");
|
||||
|
||||
uint64_t d = lookup("/dir");
|
||||
ok(schfs_unlink(&VOL, ROOT, "dir", 3) == -5,
|
||||
"rmdir refuses a directory that still has entries");
|
||||
ok(schfs_unlink(&VOL, d, "hello", 5) == 0, "emptying it first");
|
||||
ok(schfs_unlink(&VOL, ROOT, "dir", 3) == 0, "then rmdir succeeds");
|
||||
ok(nlink_of(ROOT) == 2, "removing a subdirectory drops the parent's count");
|
||||
|
||||
// Moving a directory under itself would detach the whole subtree.
|
||||
uint64_t a = mkdirent(ROOT, "a");
|
||||
uint64_t b = mkdirent(a, "b");
|
||||
(void)b;
|
||||
ok(schfs_rename(&VOL, ROOT, "a", 1, lookup("/a/b"), "loop", 4) != 0,
|
||||
"renaming a directory into its own subtree is refused");
|
||||
}
|
||||
|
||||
/* ---- 6. it all survives a commit and a remount ---- */
|
||||
{
|
||||
if (schfs_vol_commit(&VOL) != 0) { printf(" FAIL: commit\n"); failures++; }
|
||||
|
||||
static char back_before[64 * 1024];
|
||||
long before = readall(lookup("/big"), back_before, sizeof(back_before));
|
||||
|
||||
uint64_t was_free = remount();
|
||||
ok(was_free != ~0ULL, "the volume reopens from its uberblock");
|
||||
|
||||
static char back_after[64 * 1024];
|
||||
long after = readall(lookup("/big"), back_after, sizeof(back_after));
|
||||
ok(before == after && before > 0 &&
|
||||
memcmp(back_before, back_after, (size_t)before) == 0,
|
||||
"file contents are identical after a remount");
|
||||
ok(lookup("/renamed") != 0 && lookup("/dir") == 0,
|
||||
"the namespace is exactly as it was left");
|
||||
ok(VOL.free_blocks == was_free,
|
||||
"free space derived at mount matches what the writer had");
|
||||
if (VOL.free_blocks != was_free)
|
||||
printf(" (mount says %llu, writer said %llu)\n",
|
||||
(unsigned long long)VOL.free_blocks,
|
||||
(unsigned long long)was_free);
|
||||
}
|
||||
|
||||
/* ---- 7. deleting everything gives the space back ----
|
||||
*
|
||||
* The one test that catches a leak. Every earlier check would still pass on
|
||||
* a filesystem that never reused a block; this one does not.
|
||||
*/
|
||||
{
|
||||
const char *names[] = { "big", "patch", "sparse", "trunc", "renamed" };
|
||||
for (unsigned i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
|
||||
int r = schfs_unlink(&VOL, ROOT, names[i], (unsigned)strlen(names[i]));
|
||||
if (r != 0) { printf(" FAIL: unlink %s -> %d\n", names[i], r); failures++; }
|
||||
}
|
||||
int r = schfs_unlink(&VOL, lookup("/a"), "b", 1);
|
||||
if (r == 0) r = schfs_unlink(&VOL, ROOT, "a", 1);
|
||||
if (r != 0) { printf(" FAIL: rmdir a/b -> %d\n", r); failures++; }
|
||||
|
||||
if (schfs_vol_commit(&VOL) != 0) { printf(" FAIL: commit\n"); failures++; }
|
||||
|
||||
ok(VOL.free_blocks == baseline_free,
|
||||
"an emptied filesystem is back to its starting free-space count");
|
||||
if (VOL.free_blocks != baseline_free)
|
||||
printf(" (%llu free now, %llu when empty: %lld blocks lost)\n",
|
||||
(unsigned long long)VOL.free_blocks,
|
||||
(unsigned long long)baseline_free,
|
||||
(long long)baseline_free - (long long)VOL.free_blocks);
|
||||
|
||||
ok(schfs_btree_check(&VOL.tree) == 0,
|
||||
"the tree is still structurally sound after all of that");
|
||||
}
|
||||
|
||||
/* ---- 8. fill, empty, repeat: the space has to be genuinely reusable ----
|
||||
*
|
||||
* The free *count* coming back is not the same as the free *map* coming
|
||||
* back, and a remount hides the difference -- it throws the map away and
|
||||
* derives a correct one from the tree. Only spending the space again, on
|
||||
* the same mount, holds the two against each other. A map that leaks a
|
||||
* little on every delete stores less on each round until nothing fits.
|
||||
*/
|
||||
{
|
||||
static char chunk[32 * 1024];
|
||||
memset(chunk, 'x', sizeof(chunk));
|
||||
|
||||
uint64_t first_round = 0;
|
||||
int good = 1;
|
||||
|
||||
for (int round = 0; round < 3 && good; round++) {
|
||||
uint64_t f = mkfile(ROOT, "hog");
|
||||
uint64_t off = 0;
|
||||
int saw_enospc = 0;
|
||||
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
long w = schfs_file_write(&VOL, f, off, chunk,
|
||||
sizeof(chunk), DATABLK);
|
||||
if (w == -3) { saw_enospc = 1; break; }
|
||||
if (w != (long)sizeof(chunk)) {
|
||||
printf(" FAIL: write returned %ld\n", w);
|
||||
good = 0;
|
||||
break;
|
||||
}
|
||||
off += sizeof(chunk);
|
||||
}
|
||||
|
||||
if (round == 0) {
|
||||
ok(saw_enospc,
|
||||
"filling the volume reports out of space rather than corrupting");
|
||||
first_round = off;
|
||||
} else if (off + 128 * 1024 < first_round) {
|
||||
printf(" FAIL: round %d stored %llu KB, round 0 stored %llu KB\n",
|
||||
round, (unsigned long long)(off / 1024),
|
||||
(unsigned long long)(first_round / 1024));
|
||||
good = 0;
|
||||
}
|
||||
|
||||
int r = schfs_unlink(&VOL, ROOT, "hog", 3);
|
||||
if (r != 0) {
|
||||
printf(" FAIL: could not delete on a full volume (%d)\n", r);
|
||||
good = 0;
|
||||
}
|
||||
if (schfs_vol_commit(&VOL) != 0) {
|
||||
printf(" FAIL: commit\n");
|
||||
good = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ok(good, "filling and emptying three times reuses the same space");
|
||||
ok(schfs_btree_check(&VOL.tree) == 0,
|
||||
"the tree survives being filled to the brim");
|
||||
}
|
||||
|
||||
/* ---- 9. the map and its counter still agree ---- */
|
||||
{
|
||||
uint64_t claimed = remount();
|
||||
ok(claimed == VOL.free_blocks,
|
||||
"free space counted in memory matches free space derived from the tree");
|
||||
if (claimed != VOL.free_blocks)
|
||||
printf(" (counter said %llu, the tree says %llu)\n",
|
||||
(unsigned long long)claimed,
|
||||
(unsigned long long)VOL.free_blocks);
|
||||
}
|
||||
|
||||
printf("\n%s\n", failures ? "FAILED" : "all checks passed");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
120
tools/schfs_roundtrip.sh
Normal file
120
tools/schfs_roundtrip.sh
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# SchFS round-trip test: build an image from a directory tree, read it back
|
||||
# with a completely separate program, and diff the result against the original.
|
||||
#
|
||||
# The point is the oracle. schfs_test checks the B-tree against a model written
|
||||
# for the purpose; this checks the *filesystem* against a tree produced by
|
||||
# something with no connection to SchFS at all -- the host's own filesystem.
|
||||
# If mkschfs and schfs_fsck ever agree on something wrong, the source tree still
|
||||
# disagrees, and diff says so.
|
||||
#
|
||||
# tools/schfs_roundtrip.sh <mkschfs> <schfs_fsck> [tree]
|
||||
#
|
||||
# With no tree, a synthetic one is generated covering the cases that have
|
||||
# historically broken image builders: empty files, empty directories, files
|
||||
# spanning several blocks, files ending exactly on a block boundary, duplicate
|
||||
# content that must be hard-linked, symlinks, deep nesting and awkward names.
|
||||
|
||||
set -e
|
||||
|
||||
# Every tool here is piped into sed for indenting, and a shell only checks the
|
||||
# *last* command of a pipeline. Without this, schfs_fsck could exit non-zero --
|
||||
# or crash outright -- and the script would sail past it, which is exactly what
|
||||
# happened the first time this ran.
|
||||
if (set -o pipefail) 2>/dev/null; then set -o pipefail; fi
|
||||
|
||||
MKSCHFS=${1:?usage: schfs_roundtrip.sh <mkschfs> <schfs_fsck> [tree]}
|
||||
FSCK=${2:?usage: schfs_roundtrip.sh <mkschfs> <schfs_fsck> [tree]}
|
||||
TREE=$3
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
if [ -z "$TREE" ]; then
|
||||
# Generated by a separate script so the same tree can be rebuilt by hand
|
||||
# when debugging a failure, without running the whole round-trip.
|
||||
TREE="$WORK/tree"
|
||||
sh "$(dirname "$0")/mktree.sh" "$TREE"
|
||||
fi
|
||||
|
||||
IMG="$WORK/out.img"
|
||||
OUT="$WORK/extracted"
|
||||
|
||||
echo "SchFS round-trip"
|
||||
echo " source tree: $TREE"
|
||||
|
||||
"$MKSCHFS" "$IMG" --size 4096 --label roundtrip --sysroot "$TREE" | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
"$FSCK" "$IMG" --check | sed 's/^/ /'
|
||||
|
||||
mkdir -p "$OUT"
|
||||
"$FSCK" "$IMG" --extract "$OUT" | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
# --no-dereference so a symlink is compared as a symlink rather than followed;
|
||||
# without it a broken link that was stored correctly still fails the diff.
|
||||
if diff -r --no-dereference "$TREE" "$OUT"; then
|
||||
echo " [ok] extracted tree is identical to the source"
|
||||
else
|
||||
echo " FAIL: extracted tree differs from the source"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Hard links are the one thing diff cannot see: it compares contents, and two
|
||||
# separate copies with identical contents pass. Check the link count directly,
|
||||
# or dedup could silently degrade to duplication and nothing would notice.
|
||||
LINKS=$("$FSCK" "$IMG" --stat /bin/multiblock | sed -n 's/^nlink: *//p')
|
||||
if [ "$LINKS" != "2" ]; then
|
||||
echo " FAIL: /bin/multiblock has nlink=$LINKS, expected 2 (dedup did not hard-link)"
|
||||
exit 1
|
||||
fi
|
||||
echo " [ok] duplicate content stored once, with nlink=2"
|
||||
|
||||
# ...and the converse: same size, different bytes must stay separate.
|
||||
DLINKS=$("$FSCK" "$IMG" --stat /bin/decoy | sed -n 's/^nlink: *//p')
|
||||
if [ "$DLINKS" != "1" ]; then
|
||||
echo " FAIL: /bin/decoy has nlink=$DLINKS, expected 1 (dedup merged unequal files)"
|
||||
exit 1
|
||||
fi
|
||||
echo " [ok] same-size different-content files kept separate"
|
||||
|
||||
# Building the same tree twice must produce the same bytes. A difference means
|
||||
# something non-deterministic crept in, which makes every future image diff
|
||||
# useless as a signal.
|
||||
IMG2="$WORK/out2.img"
|
||||
"$MKSCHFS" "$IMG2" --size 4096 --label roundtrip --sysroot "$TREE" > /dev/null
|
||||
if cmp -s "$IMG" "$IMG2"; then
|
||||
echo " [ok] rebuilding the same tree produces an identical image"
|
||||
else
|
||||
echo " FAIL: image build is not reproducible"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Second pass with a deliberately crippled name hash.
|
||||
#
|
||||
# With the real 64-bit hash, two names in one directory never collide, so the
|
||||
# packed-dirent list -- the fiddliest code in the object layer -- never runs at
|
||||
# all. Six bits means 64 buckets for ~330 names, so every bucket holds several
|
||||
# entries and lookup, readdir and the append-on-collision path all get properly
|
||||
# exercised. Both tools must agree on the mask or nothing resolves.
|
||||
echo
|
||||
echo " forcing hash collisions (6-bit name hash)"
|
||||
IMG3="$WORK/collide.img"
|
||||
OUT3="$WORK/collide-out"
|
||||
"$MKSCHFS" "$IMG3" --size 4096 --hash-bits 6 --sysroot "$TREE" | sed 's/^/ /'
|
||||
"$FSCK" "$IMG3" --check --hash-bits 6 | sed 's/^/ /'
|
||||
mkdir -p "$OUT3"
|
||||
"$FSCK" "$IMG3" --extract "$OUT3" --hash-bits 6 | sed 's/^/ /'
|
||||
if diff -r --no-dereference "$TREE" "$OUT3" > /dev/null; then
|
||||
echo " [ok] identical tree survives a directory hash that collides constantly"
|
||||
else
|
||||
echo " FAIL: colliding-hash image does not round-trip"
|
||||
diff -r --no-dereference "$TREE" "$OUT3" | head -10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "round-trip passed"
|
||||
617
tools/schfs_test.c
Normal file
617
tools/schfs_test.c
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
// SchFS B-tree torture harness.
|
||||
//
|
||||
// The B-tree is the one component of SchFS where a subtle bug is unfixable
|
||||
// later: every other record type is built on it, so a corruption here is a
|
||||
// corruption in everything. Testing it inside the kernel means rebuilding an
|
||||
// image and driving a shell for each attempt. Testing it here means a fast
|
||||
// loop against a reference model, so this runs first and the kernel gets the
|
||||
// code only once it passes.
|
||||
//
|
||||
// What it checks:
|
||||
// 1. Against a reference model — a plain sorted array — after every batch of
|
||||
// random inserts, replaces and deletes: same keys, same values, in order.
|
||||
// 2. That the tree is structurally sound (magic, self-pointer, checksum,
|
||||
// level, key ordering) after every batch.
|
||||
// 3. Crash safety: writes made after a commit are thrown away and the tree
|
||||
// is re-opened from the last committed root. Copy-on-write means it must
|
||||
// come back exactly as it was at that commit, never a mixture.
|
||||
// 4. That a deliberately corrupted block is *detected* rather than silently
|
||||
// returning bad data.
|
||||
//
|
||||
// Build and run: make schfs-test
|
||||
|
||||
#define SCHFS_HOSTED 1
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "../kernel/fs/schfs/btree.c"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Memory-backed block device with commit/rollback */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define NBLOCKS 8192
|
||||
|
||||
struct memdev {
|
||||
unsigned char blk[NBLOCKS][SCHFS_BLKSIZE];
|
||||
unsigned char committed[NBLOCKS][SCHFS_BLKSIZE];
|
||||
int allocated[NBLOCKS]; // live in the working tree
|
||||
int committed_alloc[NBLOCKS];
|
||||
uint64_t next;
|
||||
long writes;
|
||||
int fail_after; // simulate a crash: -1 = never
|
||||
};
|
||||
|
||||
static int
|
||||
md_read(void *ctx, uint64_t b, void *buf)
|
||||
{
|
||||
struct memdev *d = ctx;
|
||||
if (b == 0 || b >= NBLOCKS) return -1;
|
||||
memcpy(buf, d->blk[b], SCHFS_BLKSIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
md_write(void *ctx, uint64_t b, const void *buf)
|
||||
{
|
||||
struct memdev *d = ctx;
|
||||
if (b == 0 || b >= NBLOCKS) return -1;
|
||||
if (d->fail_after >= 0 && d->writes >= d->fail_after)
|
||||
return -1; // the "crash"
|
||||
memcpy(d->blk[b], buf, SCHFS_BLKSIZE);
|
||||
d->writes++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
md_alloc(void *ctx)
|
||||
{
|
||||
struct memdev *d = ctx;
|
||||
for (uint64_t i = d->next; i < NBLOCKS; i++) {
|
||||
// Never hand back a block the committed tree still references --
|
||||
// that is the core copy-on-write safety rule.
|
||||
if (!d->allocated[i] && !d->committed_alloc[i]) {
|
||||
d->allocated[i] = 1;
|
||||
d->next = i + 1;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
for (uint64_t i = SCHFS_UBER_START + 1; i < d->next; i++) {
|
||||
if (!d->allocated[i] && !d->committed_alloc[i]) {
|
||||
d->allocated[i] = 1;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // out of space
|
||||
}
|
||||
|
||||
static void
|
||||
md_free(void *ctx, uint64_t b)
|
||||
{
|
||||
struct memdev *d = ctx;
|
||||
if (b > 0 && b < NBLOCKS)
|
||||
d->allocated[b] = 0; // reusable only once committed
|
||||
}
|
||||
|
||||
// Publish everything written so far and open a new transaction group.
|
||||
//
|
||||
// Advancing the tree's generation is not bookkeeping — it is the rule the tree
|
||||
// uses to decide whether a node may be rewritten in place. Without it every
|
||||
// block would look like part of the open transaction and the tree would happily
|
||||
// overwrite the very blocks this snapshot is supposed to preserve, so the
|
||||
// rollback test below is what keeps that rule honest.
|
||||
static void
|
||||
md_commit(struct memdev *d, struct schfs_btree *t)
|
||||
{
|
||||
memcpy(d->committed, d->blk, sizeof(d->blk));
|
||||
memcpy(d->committed_alloc, d->allocated, sizeof(d->allocated));
|
||||
t->generation++;
|
||||
}
|
||||
|
||||
static void
|
||||
md_rollback(struct memdev *d)
|
||||
{
|
||||
memcpy(d->blk, d->committed, sizeof(d->blk));
|
||||
memcpy(d->allocated, d->committed_alloc, sizeof(d->allocated));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Reference model */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define MAXREF 4000
|
||||
|
||||
struct ref_ent {
|
||||
struct schfs_key key;
|
||||
unsigned char val[64];
|
||||
unsigned len;
|
||||
int live;
|
||||
};
|
||||
|
||||
static struct ref_ent ref[MAXREF];
|
||||
static int nref;
|
||||
|
||||
// Deliberately does NOT call schfs_key_cmp. The reference model is the oracle,
|
||||
// so it must not share the code under test: when the model used the real
|
||||
// comparator, a deliberate bug in schfs_key_cmp broke the tree and the oracle
|
||||
// in exactly the same way and every check still passed. Written out longhand
|
||||
// so a bug in the real comparator surfaces as a disagreement.
|
||||
static int
|
||||
ref_key_cmp(const struct schfs_key *a, const struct schfs_key *b)
|
||||
{
|
||||
if (a->objectid != b->objectid) return a->objectid < b->objectid ? -1 : 1;
|
||||
if (a->type != b->type) return a->type < b->type ? -1 : 1;
|
||||
if (a->offset != b->offset) return a->offset < b->offset ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
ref_find(const struct schfs_key *k)
|
||||
{
|
||||
for (int i = 0; i < nref; i++)
|
||||
if (ref[i].live && ref_key_cmp(&ref[i].key, k) == 0)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
ref_cmp(const void *a, const void *b)
|
||||
{
|
||||
return ref_key_cmp(&((const struct ref_ent *)a)->key,
|
||||
&((const struct ref_ent *)b)->key);
|
||||
}
|
||||
|
||||
/* Compare a full tree walk against the reference model. */
|
||||
struct cmp_ctx {
|
||||
struct ref_ent *sorted;
|
||||
int n;
|
||||
int i;
|
||||
int bad;
|
||||
};
|
||||
|
||||
static int
|
||||
cmp_one(const struct schfs_key *k, const void *v, unsigned len, void *arg)
|
||||
{
|
||||
struct cmp_ctx *c = arg;
|
||||
if (c->i >= c->n) {
|
||||
fprintf(stderr, " tree has extra key (%llu,%u,%llu)\n",
|
||||
(unsigned long long)k->objectid, k->type,
|
||||
(unsigned long long)k->offset);
|
||||
c->bad = 1;
|
||||
return 1;
|
||||
}
|
||||
struct ref_ent *e = &c->sorted[c->i++];
|
||||
if (ref_key_cmp(k, &e->key) != 0) {
|
||||
fprintf(stderr, " key mismatch at %d: tree (%llu,%u,%llu) "
|
||||
"vs ref (%llu,%u,%llu)\n", c->i - 1,
|
||||
(unsigned long long)k->objectid, k->type,
|
||||
(unsigned long long)k->offset,
|
||||
(unsigned long long)e->key.objectid, e->key.type,
|
||||
(unsigned long long)e->key.offset);
|
||||
c->bad = 1;
|
||||
return 1;
|
||||
}
|
||||
if (len != e->len || memcmp(v, e->val, len) != 0) {
|
||||
fprintf(stderr, " value mismatch for key (%llu,%u,%llu)\n",
|
||||
(unsigned long long)k->objectid, k->type,
|
||||
(unsigned long long)k->offset);
|
||||
c->bad = 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
verify(struct schfs_btree *t, const char *what)
|
||||
{
|
||||
static struct ref_ent sorted[MAXREF];
|
||||
int n = 0;
|
||||
for (int i = 0; i < nref; i++)
|
||||
if (ref[i].live)
|
||||
sorted[n++] = ref[i];
|
||||
qsort(sorted, n, sizeof(sorted[0]), ref_cmp);
|
||||
|
||||
if (schfs_btree_check(t) != 0) {
|
||||
fprintf(stderr, "FAIL [%s]: structural check failed\n", what);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct cmp_ctx c = { sorted, n, 0, 0 };
|
||||
schfs_btree_iterate(t, cmp_one, &c);
|
||||
if (c.bad) {
|
||||
fprintf(stderr, "FAIL [%s]: contents differ from reference\n", what);
|
||||
return 1;
|
||||
}
|
||||
if (c.i != n) {
|
||||
fprintf(stderr, "FAIL [%s]: tree has %d records, reference has %d\n",
|
||||
what, c.i, n);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static uint32_t rng_state = 12345;
|
||||
static uint32_t
|
||||
rnd(void)
|
||||
{
|
||||
rng_state ^= rng_state << 13;
|
||||
rng_state ^= rng_state >> 17;
|
||||
rng_state ^= rng_state << 5;
|
||||
return rng_state;
|
||||
}
|
||||
|
||||
static struct memdev dev_storage;
|
||||
|
||||
// The three scratch blocks every mutation borrows. Statics, not locals, because
|
||||
// this is exactly the shape the kernel has to use: a block is 4096 bytes and so
|
||||
// is the whole kernel stack.
|
||||
static char CTX_A[SCHFS_BLKSIZE];
|
||||
static char CTX_B[SCHFS_BLKSIZE];
|
||||
static char CTX_C[SCHFS_BLKSIZE];
|
||||
static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C };
|
||||
|
||||
static void
|
||||
mkdev(struct schfs_bdev *bd, struct memdev *d)
|
||||
{
|
||||
memset(d, 0, sizeof(*d));
|
||||
d->next = SCHFS_UBER_START + 1;
|
||||
d->fail_after = -1;
|
||||
bd->ctx = d;
|
||||
bd->bread = md_read;
|
||||
bd->bwrite = md_write;
|
||||
bd->balloc = md_alloc;
|
||||
bd->bfree = md_free;
|
||||
bd->balloc_run = 0; /* the B-tree never asks for a run */
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
struct schfs_bdev bd;
|
||||
struct schfs_btree t;
|
||||
int failures = 0;
|
||||
|
||||
printf("SchFS B-tree torture test\n");
|
||||
printf(" block size %d, max inline value %d, fanout %d/%d (kptr/item)\n\n",
|
||||
SCHFS_BLKSIZE, SCHFS_MAX_INLINE, SCHFS_MAX_KPTR, SCHFS_MAX_ITEMS);
|
||||
|
||||
/* ---- 1. random insert / replace / delete against the model ---- */
|
||||
mkdev(&bd, &dev_storage);
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.generation = 1; /* the first transaction group */
|
||||
if (schfs_btree_create(&t, &bd, &CTX) != 0) {
|
||||
fprintf(stderr, "FAIL: create\n");
|
||||
return 1;
|
||||
}
|
||||
nref = 0;
|
||||
|
||||
// Committing part way through matters: between commits the tree rewrites
|
||||
// its own nodes in place, and only after one does the copy-on-write path
|
||||
// run at all. A test that never commits exercises half the code.
|
||||
const int ROUNDS = 60, OPS = 60, COMMIT_EVERY = 20;
|
||||
for (int round = 0; round < ROUNDS; round++) {
|
||||
if (round && round % COMMIT_EVERY == 0)
|
||||
md_commit(&dev_storage, &t);
|
||||
for (int op = 0; op < OPS; op++) {
|
||||
struct schfs_key k;
|
||||
memset(&k, 0, sizeof(k));
|
||||
k.objectid = rnd() % 200;
|
||||
k.type = (uint8_t)(1 + rnd() % 3);
|
||||
k.offset = rnd() % 50;
|
||||
|
||||
int action = rnd() % 10;
|
||||
if (action < 7) { /* insert or replace */
|
||||
unsigned char val[64];
|
||||
unsigned len = 1 + rnd() % 48;
|
||||
for (unsigned i = 0; i < len; i++)
|
||||
val[i] = (unsigned char)(rnd() & 0xff);
|
||||
|
||||
int r = schfs_btree_insert(&t, &CTX, &k, val, len);
|
||||
if (r == -3)
|
||||
continue; /* out of blocks: fine */
|
||||
if (r != 0) {
|
||||
fprintf(stderr, "FAIL: insert returned %d\n", r);
|
||||
return 1;
|
||||
}
|
||||
int idx = ref_find(&k);
|
||||
if (idx < 0) {
|
||||
if (nref >= MAXREF) continue;
|
||||
idx = nref++;
|
||||
ref[idx].key = k;
|
||||
ref[idx].live = 1;
|
||||
}
|
||||
memcpy(ref[idx].val, val, len);
|
||||
ref[idx].len = len;
|
||||
} else { /* delete */
|
||||
int r = schfs_btree_delete(&t, &CTX, &k);
|
||||
int idx = ref_find(&k);
|
||||
if (idx >= 0) {
|
||||
if (r != 0) {
|
||||
fprintf(stderr, "FAIL: delete of present key -> %d\n", r);
|
||||
return 1;
|
||||
}
|
||||
ref[idx].live = 0;
|
||||
} else if (r != -1) {
|
||||
fprintf(stderr, "FAIL: delete of absent key -> %d "
|
||||
"(expected -1)\n", r);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (verify(&t, "random ops")) {
|
||||
failures++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!failures)
|
||||
printf(" [ok] %d rounds x %d random ops match the reference model\n",
|
||||
ROUNDS, OPS);
|
||||
|
||||
/* ---- 2. lookup agrees with the model for every live key ---- */
|
||||
{
|
||||
int checked = 0, bad = 0;
|
||||
for (int i = 0; i < nref; i++) {
|
||||
if (!ref[i].live) continue;
|
||||
unsigned char buf[64];
|
||||
unsigned len = sizeof(buf);
|
||||
if (schfs_btree_lookup(&t, &CTX, &ref[i].key, buf, &len) != 0) {
|
||||
bad++;
|
||||
continue;
|
||||
}
|
||||
if (len != ref[i].len || memcmp(buf, ref[i].val, len) != 0)
|
||||
bad++;
|
||||
checked++;
|
||||
}
|
||||
if (bad) {
|
||||
fprintf(stderr, "FAIL: %d of %d lookups wrong\n", bad, checked + bad);
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] %d lookups all return the right value\n", checked);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 3. the cursor: forward, backward, range and predecessor ----
|
||||
*
|
||||
* The cursor is what the kernel actually reads through, because it holds
|
||||
* one caller-supplied block buffer instead of putting 4 KB on a 4 KB
|
||||
* kernel stack per level. It re-reads a parent when climbing, so it has
|
||||
* genuinely different failure modes from the recursive walker and is
|
||||
* tested against the same independent model rather than against iterate.
|
||||
*/
|
||||
{
|
||||
static struct ref_ent sorted[MAXREF];
|
||||
int n = 0;
|
||||
for (int i = 0; i < nref; i++)
|
||||
if (ref[i].live)
|
||||
sorted[n++] = ref[i];
|
||||
qsort(sorted, n, sizeof(sorted[0]), ref_cmp);
|
||||
|
||||
static char curblk[SCHFS_BLKSIZE];
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &t, curblk);
|
||||
|
||||
/* (a) forward from the start */
|
||||
int bad = 0, seen = 0;
|
||||
if (schfs_cursor_seek(&c, NULL) != 0 && n > 0)
|
||||
bad = 1;
|
||||
while (!bad && seen < n) {
|
||||
const struct schfs_key *k = schfs_cursor_key(&c);
|
||||
unsigned vlen;
|
||||
const void *v = schfs_cursor_val(&c, &vlen);
|
||||
if (!k || !v) { bad = 1; break; }
|
||||
if (ref_key_cmp(k, &sorted[seen].key) != 0 ||
|
||||
vlen != sorted[seen].len ||
|
||||
memcmp(v, sorted[seen].val, vlen) != 0) {
|
||||
fprintf(stderr, " cursor diverges at record %d\n", seen);
|
||||
bad = 1;
|
||||
break;
|
||||
}
|
||||
seen++;
|
||||
int r = schfs_cursor_next(&c);
|
||||
if (r == -1) break;
|
||||
if (r != 0) { bad = 1; break; }
|
||||
}
|
||||
if (bad || seen != n) {
|
||||
fprintf(stderr, "FAIL: forward cursor walked %d of %d records\n",
|
||||
seen, n);
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] cursor walks all %d records forward in key order\n", n);
|
||||
}
|
||||
|
||||
/* (b) backward from the end. Getting to the end uses lookup_le with a
|
||||
* key past every real one, which also exercises the "greater than
|
||||
* everything" branch. */
|
||||
struct schfs_key maxk;
|
||||
memset(&maxk, 0, sizeof(maxk));
|
||||
maxk.objectid = ~0ULL; maxk.type = 255; maxk.offset = ~0ULL;
|
||||
|
||||
bad = 0; seen = 0;
|
||||
if (n > 0 && schfs_btree_lookup_le(&c, &maxk) != 0)
|
||||
bad = 1;
|
||||
while (!bad && seen < n) {
|
||||
const struct schfs_key *k = schfs_cursor_key(&c);
|
||||
if (!k) { bad = 1; break; }
|
||||
if (ref_key_cmp(k, &sorted[n - 1 - seen].key) != 0) {
|
||||
fprintf(stderr, " reverse cursor diverges at record %d\n", seen);
|
||||
bad = 1;
|
||||
break;
|
||||
}
|
||||
seen++;
|
||||
int r = schfs_cursor_prev(&c);
|
||||
if (r == -1) break;
|
||||
if (r != 0) { bad = 1; break; }
|
||||
}
|
||||
if (bad || seen != n) {
|
||||
fprintf(stderr, "FAIL: reverse cursor walked %d of %d records\n",
|
||||
seen, n);
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] cursor walks all %d records backward\n", n);
|
||||
}
|
||||
|
||||
/* (c) range scans from arbitrary start keys must yield exactly the
|
||||
* model's suffix. This is the operation readdir depends on. */
|
||||
bad = 0;
|
||||
for (int trial = 0; trial < 200 && !bad; trial++) {
|
||||
struct schfs_key start;
|
||||
memset(&start, 0, sizeof(start));
|
||||
start.objectid = rnd() % 220;
|
||||
start.type = (uint8_t)(rnd() % 5);
|
||||
start.offset = rnd() % 60;
|
||||
|
||||
int expect = 0;
|
||||
while (expect < n && ref_key_cmp(&sorted[expect].key, &start) < 0)
|
||||
expect++;
|
||||
|
||||
int r = schfs_cursor_seek(&c, &start);
|
||||
if (r == -2) { bad = 1; break; }
|
||||
if (expect == n) {
|
||||
if (r == 0 && schfs_cursor_key(&c) != NULL) bad = 1;
|
||||
continue;
|
||||
}
|
||||
if (r != 0) { bad = 1; break; }
|
||||
const struct schfs_key *k = schfs_cursor_key(&c);
|
||||
if (!k || ref_key_cmp(k, &sorted[expect].key) != 0) {
|
||||
fprintf(stderr, " seek(%llu,%u,%llu) landed wrong\n",
|
||||
(unsigned long long)start.objectid, start.type,
|
||||
(unsigned long long)start.offset);
|
||||
bad = 1;
|
||||
}
|
||||
}
|
||||
if (bad) {
|
||||
fprintf(stderr, "FAIL: range seek disagrees with the model\n");
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] 200 range seeks land on the right record\n");
|
||||
}
|
||||
|
||||
/* (d) lookup_le must find the greatest key <= the probe, which is how
|
||||
* a file read finds the extent covering a byte offset. */
|
||||
bad = 0;
|
||||
for (int trial = 0; trial < 200 && !bad; trial++) {
|
||||
struct schfs_key probe;
|
||||
memset(&probe, 0, sizeof(probe));
|
||||
probe.objectid = rnd() % 220;
|
||||
probe.type = (uint8_t)(rnd() % 5);
|
||||
probe.offset = rnd() % 60;
|
||||
|
||||
int expect = -1;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (ref_key_cmp(&sorted[i].key, &probe) <= 0)
|
||||
expect = i;
|
||||
else
|
||||
break;
|
||||
|
||||
int r = schfs_btree_lookup_le(&c, &probe);
|
||||
if (expect < 0) {
|
||||
if (r != -1) {
|
||||
fprintf(stderr, " lookup_le below all keys returned %d\n", r);
|
||||
bad = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (r != 0) {
|
||||
fprintf(stderr, " lookup_le returned %d, expected a hit\n", r);
|
||||
bad = 1;
|
||||
break;
|
||||
}
|
||||
const struct schfs_key *k = schfs_cursor_key(&c);
|
||||
if (!k || ref_key_cmp(k, &sorted[expect].key) != 0) {
|
||||
fprintf(stderr, " lookup_le(%llu,%u,%llu) landed wrong\n",
|
||||
(unsigned long long)probe.objectid, probe.type,
|
||||
(unsigned long long)probe.offset);
|
||||
bad = 1;
|
||||
}
|
||||
}
|
||||
if (bad) {
|
||||
fprintf(stderr, "FAIL: lookup_le disagrees with the model\n");
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] 200 predecessor lookups match the model\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 4. crash safety ---- */
|
||||
{
|
||||
struct memdev *d = &dev_storage;
|
||||
md_commit(d, &t);
|
||||
uint64_t saved_root = t.root;
|
||||
uint32_t saved_crc = t.root_crc;
|
||||
uint64_t saved_gen = t.generation;
|
||||
|
||||
/* Snapshot what the committed tree contains. */
|
||||
static struct ref_ent snap[MAXREF];
|
||||
int nsnap = 0;
|
||||
for (int i = 0; i < nref; i++)
|
||||
if (ref[i].live) snap[nsnap++] = ref[i];
|
||||
|
||||
/* Now do a pile of work and then throw the writes away. */
|
||||
for (int op = 0; op < 500; op++) {
|
||||
struct schfs_key k;
|
||||
memset(&k, 0, sizeof(k));
|
||||
k.objectid = 1000 + rnd() % 500;
|
||||
k.type = 1;
|
||||
k.offset = rnd() % 100;
|
||||
unsigned char val[32];
|
||||
memset(val, (int)(rnd() & 0xff), sizeof(val));
|
||||
schfs_btree_insert(&t, &CTX, &k, val, sizeof(val));
|
||||
}
|
||||
md_rollback(d);
|
||||
t.root = saved_root;
|
||||
t.root_crc = saved_crc;
|
||||
t.generation = saved_gen;
|
||||
|
||||
/* The committed tree must be intact and unchanged. */
|
||||
nref = 0;
|
||||
for (int i = 0; i < nsnap; i++) ref[nref++] = snap[i];
|
||||
|
||||
if (verify(&t, "after simulated crash")) {
|
||||
fprintf(stderr, "FAIL: tree did not survive rollback\n");
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] tree intact after discarding 500 uncommitted ops\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 5. corruption is detected, not silently returned ---- */
|
||||
{
|
||||
struct memdev *d = &dev_storage;
|
||||
unsigned char save[SCHFS_BLKSIZE];
|
||||
uint64_t victim = t.root;
|
||||
memcpy(save, d->blk[victim], SCHFS_BLKSIZE);
|
||||
|
||||
d->blk[victim][sizeof(struct schfs_node_hdr) + 4] ^= 0xff;
|
||||
int r = schfs_btree_check(&t);
|
||||
if (r != -2) {
|
||||
fprintf(stderr, "FAIL: flipped a bit in the root and the tree "
|
||||
"still verified (got %d, wanted -2)\n", r);
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] bit flip in a node is caught by the checksum\n");
|
||||
}
|
||||
memcpy(d->blk[victim], save, SCHFS_BLKSIZE);
|
||||
|
||||
/* A block written to the wrong address still checksums correctly --
|
||||
only the self-pointer catches it. */
|
||||
uint64_t elsewhere = md_alloc(d);
|
||||
if (elsewhere) {
|
||||
memcpy(d->blk[elsewhere], d->blk[victim], SCHFS_BLKSIZE);
|
||||
char tmp[SCHFS_BLKSIZE];
|
||||
if (node_read(&bd, elsewhere, 0, tmp) != -2) {
|
||||
fprintf(stderr, "FAIL: misdirected write not detected\n");
|
||||
failures++;
|
||||
} else {
|
||||
printf(" [ok] misdirected write caught by the self-pointer\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n%s\n", failures ? "FAILED" : "all checks passed");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
19
tools/sign.pl
Normal file
19
tools/sign.pl
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
open(SIG, $ARGV[0]) || die "open $ARGV[0]: $!";
|
||||
|
||||
$n = sysread(SIG, $buf, 1000);
|
||||
|
||||
if($n > 510){
|
||||
print STDERR "boot block too large: $n bytes (max 510)\n";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
print STDERR "boot block is $n bytes (max 510)\n";
|
||||
|
||||
$buf .= "\0" x (510-$n);
|
||||
$buf .= "\x55\xAA";
|
||||
|
||||
open(SIG, ">$ARGV[0]") || die "open >$ARGV[0]: $!";
|
||||
print SIG $buf;
|
||||
close SIG;
|
||||
134
tools/splashshot.ps1
Normal file
134
tools/splashshot.ps1
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Photograph the boot splash.
|
||||
#
|
||||
# The splash is on screen from the moment stage2 draws it until
|
||||
# console_takeover() clears it at the first exec(), which is a window of a
|
||||
# couple of seconds -- so a single screendump at a guessed delay is a coin
|
||||
# flip. This takes a burst instead and keeps every frame, which makes the
|
||||
# question "what does boot actually look like, second by second" answerable
|
||||
# rather than a matter of catching it at the right instant.
|
||||
#
|
||||
# Same shape as tools/crashshot.ps1: monitor on TCP because QEMU on Windows has
|
||||
# no unix sockets, serial to a logfile so the transcript survives.
|
||||
#
|
||||
# tools\splashshot.ps1 frames every 400ms for 8s
|
||||
# tools\splashshot.ps1 -Every 200 -For 4
|
||||
|
||||
param(
|
||||
[int]$Every = 400, # milliseconds between frames
|
||||
[int]$For = 8, # seconds to keep capturing
|
||||
[string]$OutDir = "build\shots",
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$log = Join-Path $root 'build\splashshot.log'
|
||||
$dir = Join-Path $root $OutDir
|
||||
|
||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force $dir | Out-Null
|
||||
Remove-Item -Force $log -ErrorAction SilentlyContinue
|
||||
|
||||
$hda = Join-Path $root 'schunix.img'
|
||||
$hdb = Join-Path $root 'build\fs.img'
|
||||
|
||||
$monPort = 55556 # not crashshot's port, so the two can run back to back
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
# -snapshot with file.locking=off, rather than booting from copies. QEMU
|
||||
# takes an exclusive lock on its drive images, so an interactive session the
|
||||
# developer already has open would otherwise make this fail with "Failed to
|
||||
# get write lock" -- and a capture script is exactly the thing you want to
|
||||
# run without first shutting down what you were looking at. Copying was the
|
||||
# first fix and it meant duplicating build\fs.img, all 153 MB of it, on
|
||||
# every run. -snapshot keeps writes in a temporary overlay, so the originals
|
||||
# are untouched either way.
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk,file.locking=off"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk,file.locking=off"
|
||||
'-snapshot'
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
$frames = @()
|
||||
try {
|
||||
# Connect immediately -- the interesting frames are the early ones, so any
|
||||
# fixed warm-up sleep here is time spent missing them.
|
||||
$client = $null
|
||||
for ($i = 0; $i -lt 40 -and -not $client; $i++) {
|
||||
try { $client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort) }
|
||||
catch { Start-Sleep -Milliseconds 100 }
|
||||
}
|
||||
if (-not $client) { throw "could not reach the QEMU monitor on port $monPort" }
|
||||
|
||||
$writer = New-Object System.IO.StreamWriter($client.GetStream())
|
||||
$writer.AutoFlush = $true
|
||||
|
||||
$n = [int](($For * 1000) / $Every)
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
for ($i = 0; $i -lt $n; $i++) {
|
||||
$ms = [int]$sw.ElapsedMilliseconds
|
||||
$f = Join-Path $dir ("{0:d5}ms.ppm" -f $ms)
|
||||
$writer.WriteLine("screendump $f")
|
||||
$frames += $f
|
||||
Start-Sleep -Milliseconds $Every
|
||||
}
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
# Report each frame by its distinct-colour count. Two colours means a text
|
||||
# console (or a blank screen); an anti-aliased logo pushes it into the dozens.
|
||||
# The same reason the full-stack harness counted pixels rather than log lines: a capture
|
||||
# that exists is not a capture that drew anything.
|
||||
Write-Host "==> frames" -ForegroundColor Cyan
|
||||
$kept = 0
|
||||
foreach ($f in $frames) {
|
||||
if (-not (Test-Path $f)) { continue }
|
||||
$kept++
|
||||
$b = [System.IO.File]::ReadAllBytes($f)
|
||||
# PPM P6: "P6\n<w> <h>\n255\n" then raw RGB. Skip three whitespace-run
|
||||
# separated header fields rather than assuming a fixed header length --
|
||||
# QEMU's is compact but that is not guaranteed.
|
||||
$pos = 2; $fields = 0
|
||||
while ($fields -lt 3 -and $pos -lt $b.Length) {
|
||||
while ($pos -lt $b.Length -and [char]$b[$pos] -match '\s') { $pos++ }
|
||||
while ($pos -lt $b.Length -and [char]$b[$pos] -notmatch '\s') { $pos++ }
|
||||
$fields++
|
||||
}
|
||||
$pos++
|
||||
$seen = @{}
|
||||
for ($i = $pos; $i + 2 -lt $b.Length; $i += 3 * 7) { # sample every 7th pixel
|
||||
$seen[($b[$i] -shl 16) -bor ($b[$i+1] -shl 8) -bor $b[$i+2]] = 1
|
||||
}
|
||||
$name = Split-Path $f -Leaf
|
||||
$c = $seen.Count
|
||||
$tag = if ($c -ge 16) { 'graphical' } elseif ($c -le 3) { 'flat/text' } else { 'sparse' }
|
||||
$col = if ($c -ge 16) { 'Green' } else { 'DarkGray' }
|
||||
Write-Host (" {0,-12} {1,4} colours {2}" -f $name, $c, $tag) -ForegroundColor $col
|
||||
}
|
||||
|
||||
if ($kept -eq 0) { Write-Host "FAIL: no frames captured" -ForegroundColor Red; exit 1 }
|
||||
Write-Host "==> $kept frames in $dir" -ForegroundColor Green
|
||||
|
||||
# Convert to PNG if ImageMagick is around, so they can just be looked at.
|
||||
if (Get-Command magick -ErrorAction SilentlyContinue) {
|
||||
foreach ($f in $frames) {
|
||||
if (Test-Path $f) { & magick $f ([System.IO.Path]::ChangeExtension($f, '.png')) }
|
||||
}
|
||||
Write-Host "==> PNGs alongside" -ForegroundColor Green
|
||||
}
|
||||
116
tools/textshot.ps1
Normal file
116
tools/textshot.ps1
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Boot Schunix, run the text-rendering self-test, and capture the result as an image.
|
||||
#
|
||||
# The display class cannot be checked without a display: the assertion is "did
|
||||
# the right pixels reach the screen", and the only instrument for that is a
|
||||
# screendump. This drives it through `echo t > /proc/sysrq` and takes a picture.
|
||||
#
|
||||
# What the pattern proves: four solid quadrants in theme colours (a stride error
|
||||
# would slant the bands, a bpp error would change the colours), a white frame one
|
||||
# pixel inside the edge (an off-by-one in the present rectangle would clip it),
|
||||
# and the fact that every pixel was drawn into RAM before a single byte reached
|
||||
# the framebuffer -- which is what double buffering means.
|
||||
#
|
||||
# QEMU's monitor is on its own socket so the serial console stays free for the
|
||||
# shell. screendump writes a PPM; the PNG conversion is optional and skipped if
|
||||
# ImageMagick is not around.
|
||||
#
|
||||
# tools\crashshot.ps1 crash the ramdisk-root build
|
||||
# tools\crashshot.ps1 -SchFS with a SchFS volume attached
|
||||
|
||||
param(
|
||||
[switch]$SchFS,
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Out = "build\textshot.ppm",
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$hda = Join-Path $root 'schunix.img'
|
||||
$hdb = if ($SchFS) { Join-Path $root 'build\schfs.img' } else { Join-Path $root 'build\fs.img' }
|
||||
$log = Join-Path $root 'build\crashshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$mon = Join-Path $root 'build\qemu-mon.sock'
|
||||
|
||||
Remove-Item -Force $log, $ppm -ErrorAction SilentlyContinue
|
||||
|
||||
# The monitor goes to a TCP port rather than a unix socket: PowerShell can open
|
||||
# a TCP client without any extra machinery, and QEMU on Windows has no unix
|
||||
# sockets anyway.
|
||||
$monPort = 55555
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host "==> booting" -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
function Send-Line($text) {
|
||||
foreach ($ch in ($text + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 30 # serial has no flow control
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
|
||||
# Show the device list first, so the transcript records what was bound.
|
||||
Write-Host " > listing devices" -ForegroundColor DarkGray
|
||||
Send-Line 'cat /proc/devices'
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
Write-Host " > echo t > /proc/sysrq" -ForegroundColor DarkGray
|
||||
Send-Line 'echo t > /proc/sysrq'
|
||||
Start-Sleep -Seconds 4
|
||||
|
||||
Write-Host "==> capturing the framebuffer" -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$stream = $client.GetStream()
|
||||
$writer = New-Object System.IO.StreamWriter($stream)
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
# QEMU on Windows wants a native path here, not the one PowerShell shows.
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $log) {
|
||||
$text = Get-Content $log -Raw
|
||||
Write-Host "==> serial transcript" -ForegroundColor Cyan
|
||||
# Only the crash report, not the whole boot.
|
||||
$idx = $text.IndexOf('display: presented')
|
||||
if ($idx -ge 0) { Write-Host $text.Substring($idx) } else { Write-Host $text }
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
$size = (Get-Item $ppm).Length
|
||||
Write-Host "==> $ppm ($size bytes)" -ForegroundColor Green
|
||||
$png = [System.IO.Path]::ChangeExtension($ppm, '.png')
|
||||
$magick = Get-Command magick -ErrorAction SilentlyContinue
|
||||
if ($magick) {
|
||||
& magick $ppm $png
|
||||
Write-Host "==> $png" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "FAIL: no screendump was produced" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
81
tools/ttftest.c
Normal file
81
tools/ttftest.c
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Host harness for kernel/dev/ttf.c.
|
||||
//
|
||||
// A rasteriser is checked by looking at it. This renders a string with the real
|
||||
// font and writes a PGM, which is the smallest image format that needs no
|
||||
// compression -- the point is the glyphs, not the container.
|
||||
//
|
||||
// build/tools/ttftest <font.ttf> <px> "text" out.pgm
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TTF_HOST
|
||||
#include "../kernel/dev/ttf.c"
|
||||
|
||||
int ttf_render(struct ttf *, uint32, struct glyph *, unsigned char *, int, int *, int *);
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 5) { fprintf(stderr, "usage: ttftest font.ttf px \"text\" out.pgm\n"); return 1; }
|
||||
const char *path = argv[1];
|
||||
int px = atoi(argv[2]);
|
||||
const char *text = argv[3];
|
||||
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp) { perror(path); return 1; }
|
||||
fseek(fp, 0, SEEK_END); long n = ftell(fp); fseek(fp, 0, SEEK_SET);
|
||||
unsigned char *buf = malloc(n);
|
||||
if (fread(buf, 1, n, fp) != (size_t)n) { fprintf(stderr, "short read\n"); return 1; }
|
||||
fclose(fp);
|
||||
|
||||
struct ttf f;
|
||||
if (ttf_open(&f, buf, (uint32)n, px) != 0) {
|
||||
fprintf(stderr, "not a TrueType font this can read\n"); return 1;
|
||||
}
|
||||
printf("unitsPerEm %d, %d glyphs, ascent %d descent %d, %s loca\n",
|
||||
f.units_per_em, f.num_glyphs, f.ascent, f.descent,
|
||||
f.long_loca ? "long" : "short");
|
||||
|
||||
struct font *fo = ttf_font(&f);
|
||||
int H = fo->ops->height(fo) + px; // room for descenders and padding
|
||||
int base = fo->ops->baseline(fo) + px / 2;
|
||||
int W = 16;
|
||||
for (const char *p = text; *p; p++) W += px; // generous
|
||||
|
||||
unsigned char *img = calloc(W * H, 1);
|
||||
int pen = 8, drawn = 0, missing = 0;
|
||||
|
||||
for (const char *p = text; *p; p++) {
|
||||
struct glyph g;
|
||||
// Through the provider, not ttf_render, so the glyph cache is on the
|
||||
// path being tested rather than bypassed by the harness.
|
||||
if (fo->ops->glyph(fo, (unsigned char)*p, &g) != 0) {
|
||||
missing++;
|
||||
pen += px / 2;
|
||||
continue;
|
||||
}
|
||||
for (int gy = 0; gy < g.h; gy++)
|
||||
for (int gx = 0; gx < g.w; gx++) {
|
||||
int X = pen + g.bearing_x + gx;
|
||||
int Y = base - g.bearing_y + gy;
|
||||
if (X < 0 || Y < 0 || X >= W || Y >= H) continue;
|
||||
int v = img[Y * W + X] + g.cov[gy * g.w + gx];
|
||||
img[Y * W + X] = v > 255 ? 255 : v;
|
||||
}
|
||||
pen += g.advance;
|
||||
drawn++;
|
||||
}
|
||||
|
||||
// White on black inverted to black on white, which is how text is read.
|
||||
for (int i = 0; i < W * H; i++) img[i] = 255 - img[i];
|
||||
|
||||
FILE *o = fopen(argv[4], "wb");
|
||||
fprintf(o, "P5\n%d %d\n255\n", W, H);
|
||||
fwrite(img, 1, W * H, o);
|
||||
fclose(o);
|
||||
|
||||
printf("%d glyphs drawn, %d missing, %d px advance, image %dx%d -> %s\n",
|
||||
drawn, missing, pen - 8, W, H, argv[4]);
|
||||
return drawn ? 0 : 1;
|
||||
}
|
||||
92
tools/ttyshot.ps1
Normal file
92
tools/ttyshot.ps1
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Photograph the framebuffer console after driving it with escape sequences.
|
||||
#
|
||||
# The serial line and the framebuffer are two different consumers of console
|
||||
# output, and they behave differently on purpose: escapes are passed through
|
||||
# to the serial line verbatim (a real terminal emulator is on the other end
|
||||
# and understands them better than we do) and *interpreted* for the
|
||||
# framebuffer. So the serial transcript can never tell you whether the escape
|
||||
# parser works -- it contains the raw bytes either way. Only a picture can.
|
||||
#
|
||||
# tools\ttyshot.ps1
|
||||
|
||||
param(
|
||||
[int]$BootSeconds = 22,
|
||||
[string]$Out = "build\ttyshot.ppm",
|
||||
[string]$Qemu = "$env:USERPROFILE\scoop\apps\qemu\current\qemu-system-x86_64.exe"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$hda = Join-Path $root 'schunix.img'
|
||||
$hdb = Join-Path $root 'build\fs.img'
|
||||
$log = Join-Path $root 'build\ttyshot.log'
|
||||
$ppm = Join-Path $root $Out
|
||||
$monPort = 55571
|
||||
|
||||
# Each line exercises one thing the console could not do before: colour,
|
||||
# bright colour, background colour, cursor addressing, erase-to-end-of-line,
|
||||
# and non-ASCII text.
|
||||
$commands = @(
|
||||
'clear'
|
||||
'printf ''\033[31mRED \033[32mGREEN \033[34mBLUE \033[35mMAGENTA \033[36mCYAN \033[0mdefault\n'''
|
||||
'printf ''\033[1;31mbold red \033[1;32mbold green \033[1;34mbold blue\033[0m\n'''
|
||||
'printf ''\033[37;44m white on blue \033[0m normal\n'''
|
||||
'printf ''caf\303\251 \303\251t\303\251 na\303\257ve \303\274ber sch\303\266n a\303\261o\n'''
|
||||
'printf ''ERASED-THIS\033[13G\033[Kkept this\n'''
|
||||
'printf ''\033[12;40Hcursor addressed to row 12 col 40\n'''
|
||||
# Tab used to be drawn as glyph 9, which in CP437 is a hollow circle --
|
||||
# so pressing Tab printed a circle instead of moving the cursor. Every
|
||||
# other control character had the same problem waiting behind it.
|
||||
'printf ''A\tB\tC\tD -- tabs, not circles\n'''
|
||||
)
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $Qemu
|
||||
$psi.Arguments = @(
|
||||
"-drive file=`"$hda`",format=raw,index=0,media=disk"
|
||||
"-drive file=`"$hdb`",format=raw,index=1,media=disk"
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
'-serial chardev:c0'
|
||||
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
|
||||
) -join ' '
|
||||
$psi.RedirectStandardInput = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
Write-Host '==> booting' -ForegroundColor Cyan
|
||||
$p = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
try {
|
||||
Start-Sleep -Seconds $BootSeconds
|
||||
foreach ($cmd in $commands) {
|
||||
Write-Host " > $cmd" -ForegroundColor DarkGray
|
||||
foreach ($ch in ($cmd + "`n").ToCharArray()) {
|
||||
$p.StandardInput.Write($ch)
|
||||
$p.StandardInput.Flush()
|
||||
Start-Sleep -Milliseconds 20 # serial has no flow control
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
Write-Host '==> capturing the framebuffer' -ForegroundColor Cyan
|
||||
$client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort)
|
||||
$writer = New-Object System.IO.StreamWriter($client.GetStream())
|
||||
$writer.AutoFlush = $true
|
||||
Start-Sleep -Milliseconds 500
|
||||
$writer.WriteLine("screendump $ppm")
|
||||
Start-Sleep -Seconds 2
|
||||
$writer.WriteLine('quit')
|
||||
Start-Sleep -Milliseconds 500
|
||||
$client.Close()
|
||||
} finally {
|
||||
if (-not $p.HasExited) { $p.Kill() }
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
if (Test-Path $ppm) {
|
||||
Write-Host "==> $ppm ($((Get-Item $ppm).Length) bytes)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host 'FAIL: no screendump was produced' -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
27
tools/vectors.pl
Normal file
27
tools/vectors.pl
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/perl -w
|
||||
|
||||
# Generate vectors.S, the trap/interrupt entry points.
|
||||
# There has to be one entry point per interrupt number
|
||||
# since otherwise there's no way for trap() to discover
|
||||
# the interrupt number.
|
||||
|
||||
print "# generated by vectors.pl - do not edit\n";
|
||||
print "# handlers\n";
|
||||
print ".global alltraps\n";
|
||||
for(my $i = 0; $i < 256; $i++){
|
||||
# print ".global vector$i\n"; # might be useful for debugging
|
||||
print "vector$i:\n";
|
||||
if(!($i == 8 || ($i >= 10 && $i <= 14) || $i == 17)){
|
||||
print " push \$0\n";
|
||||
}
|
||||
print " push \$$i\n";
|
||||
print " jmp alltraps\n";
|
||||
}
|
||||
|
||||
print "\n# vector table\n";
|
||||
print ".data\n";
|
||||
print ".global vectors\n";
|
||||
print "vectors:\n";
|
||||
for(my $i = 0; $i < 256; $i++){
|
||||
print " .quad vector$i\n";
|
||||
}
|
||||
Loading…
Reference in a new issue