diff --git a/Programs/CosmOS/Apps/Crash.asm b/Programs/CosmOS/Apps/Crash.asm new file mode 100644 index 0000000..846d060 --- /dev/null +++ b/Programs/CosmOS/Apps/Crash.asm @@ -0,0 +1,145 @@ +; Breaks on purpose, in whichever of the four ways it is asked for. +; +; Every one of these used to stop the machine and print a line to a standard error that +; nobody behind a window is looking at, so the machine appeared to hang. The system catches +; all of them now and says what happened and where, and this is what says so - and what a +; person can run when they want to see the fault screen without having written a bug first. +; +; Crash opcode a byte in the middle of the code that does not decode +; Crash service a SWI naming a service the system does not implement +; Crash bank a transfer out of a bank that has nothing registered in it +; Crash device asking the console to interrupt, with no handler installed +; Crash blind the same bad byte, but from a screen with nowhere to print at all +; +; The last one is the odd one: it is not something this program does wrong, it is something +; it fails to have done. The interrupt arrives from outside once the console has anything to +; say, and with no input it is the END of input that arrives. +; +; Written by Anachronaut + +#Include services.asm + +#Program + + #Base 0x4000 + +start: + SETD.0 Argument + INIB 0d15 + SWI osArgument + + SETD.0 Argument + SETD.1 WordOpcode + CALL textSame + BRQ crashOpcode + SETD.0 Argument + SETD.1 WordService + CALL textSame + BRQ crashService + SETD.0 Argument + SETD.1 WordBank + CALL textSame + BRQ crashBank + SETD.0 Argument + SETD.1 WordDevice + CALL textSame + BRQ crashDevice + SETD.0 Argument + SETD.1 WordBlind + CALL textSame + BRQ crashBlind + + SETD.0 Usage + SWI osPrintString + INIA 0x0A + OUTA 0x00 + INIA 0x01 + SWI osExit + +; ---- The case the fault screen exists for ---- +; +; Bitmap mode has no text rows, so the console draws NOTHING there: a program that faults +; here leaves the system with a message to print and nowhere to print it. Putting the screen +; back into a mode that has characters in it is the difference between a diagnosis and a +; machine that appears to have hung. +crashBlind: + ; ---- And with the colours ruined as well ---- + ; + ; A known mode is only half a known screen. This makes the ink of attribute one the same as + ; its paper, which is what a program that wrote its own palette can easily leave behind - + ; and a message printed into that is perfectly present and completely invisible. + INIA 0d4 + OUTA 0xE3 + INIA 0x30 + OUTA 0xE2 + INIA 0x03 + OUTA 0xE8 ; Video memory as bank four. + + INIA 0d4 + OUTA 0xE3 + INIA 0xFC + OUTA 0xE4 + INIA 0x40 + OUTA 0xE5 ; 0xFC40, the two entries attribute one draws from. + RSTA + INIB 0d8 +crashBlindWipe: + OUTA 0xE9 + DECB + BNB crashBlindWipe ; Both of them black, ink and paper alike. + + INIA 0x02 + OUTA 0x31 + +crashOpcode: + 0x00 ; Not an instruction, and never will be. + +crashService: + SWI 0d40 ; Forty is nobody's. + +crashBank: + INIA 0d9 ; Nothing is registered there. + OUTA 0xE0 + RSTA + OUTA 0xE1 + OUTA 0xE2 + INIA 0d1 + OUTA 0xE3 ; Into Data Memory. + RSTA + OUTA 0xE4 + OUTA 0xE5 + OUTA 0xE6 + INIA 0d16 + OUTA 0xE7 + INIA 0x01 + OUTA 0xE8 ; Blit, from a bank that is not there. + +crashDevice: + INIA 0x02 ; Interrupt me when the console has something to say. + OUTA 0x02 + SIF +crashWait: + ; Never touches the console, so whatever happens next came from outside. + BRI crashWait + +#Data + + #Base 0x2000 + +WordOpcode: +"opcode" +WordService: +"service" +WordBank: +"bank" +WordDevice: +"device" +WordBlind: +"blind" +Usage: +"Crash opcode | service | bank | device | blind" + +Argument: + #Reserve 0d16 + +#Include text.asm diff --git a/Programs/CosmOS/Assembler/Asm.asm b/Programs/CosmOS/Assembler/Asm.asm index 39308cb..16e2709 100644 --- a/Programs/CosmOS/Assembler/Asm.asm +++ b/Programs/CosmOS/Assembler/Asm.asm @@ -2258,7 +2258,7 @@ ReservedLeft: ReservedWalk: 0x00 0x00 ReservedCount: - 0d5 + 0d7 VecHandlerName: #Reserve 0d23 @@ -2285,6 +2285,12 @@ ReservedNames: "BankFault" #Reserve 0d5 0d4 +"NoHandler" + #Reserve 0d5 + 0d5 +"NoDevice" + #Reserve 0d6 + 0d6 ProgPut: 0x00 0x00 diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 8827a43..6e218bf 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -599,8 +599,48 @@ from every assembly file in it. Several are old programs written for the bare ma | Wander | Goes to the directory it is given and reads a file there by a bare name. The only thing that moves the machine from inside a program, and so the only thing that can check the shell puts the working directory back afterwards. | | More | A forward-only pager. Space advances a screen, Return one line, and q stops. | | Press | Says what the console handed it, in hexadecimal and by name. It reads a line and then keys, because the keys that are not characters are dropped in line mode and delivered in key mode, and both halves of that rule want showing. | +| Crash | Breaks on purpose, in whichever of the four ways the system now catches, so that a fault screen can be looked at without having written a bug first. | | Mode | Forty columns or eighty, whichever the screen is not in. Ten instructions and no data at all, which is the point of it: it is the smallest shape a loadable program can take, and the loader used to stop the machine dead on one. | +### When Something Goes Wrong: + +A fault used to stop the machine and print a line to whatever was behind it. On a terminal +that is a diagnosis; behind a window it is a frozen picture and no reason at all, because the +message went to a standard error nobody was looking at. **The machine looked hung and was +not** - it had stopped, and said so somewhere invisible. + +CosmOS catches all five faults the machine can raise and says what happened on the screen: + +``` +> Crash opcode +that byte is not an instruction, at 404E + A 00 B 0F Q 00 +the program was stopped +> +``` + +**A fault ends the program, not the machine.** That is not a compromise. A bare `RETI` from +most faults meets the very instruction that failed and fails again, so carrying on is not on +offer - but the machine is almost never what is broken. Everything the shell puts back when a +program exits, which is the Stack, any vectors it installed, the drive, the working directory, +the console and the screen, is exactly what wants putting back after one dies. So you are +returned to the prompt, and the program is recorded as having stopped rather than finished. + +A fault *below* where programs load is the system's own code, and there is nothing to go back +to. That one says so and stops. + +**The screen is put back into a mode text can be seen in first**, and that is the part that +matters most rather than the part that is prettiest. A program that faulted in bitmap mode +left the console with no text rows at all, so it draws nothing - the message about what went +wrong would be perfectly correct and completely invisible. Two palette entries are rewritten +for the same reason, since a program that wrote its own colours can leave every ink the same +as every paper. Only the two the message needs are touched; the rest of what the program +chose is left alone. + +The address is where it happened, and it is exact. For a missing service or a device with +nobody listening it is the address *after* the instruction, because those two are the faults +where the instruction did dispatch and it was the entry that was empty. + ### The Monitor: The monitor is **part of the shell**, not a program the shell loads, and that is the whole reason it works. A loaded program occupies the one place a loaded program goes, so a monitor that was an application could never look at any other application: loading the thing you wanted to inspect would replace the thing doing the inspecting. diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 2a88e4a..354d966 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -1260,6 +1260,204 @@ historyAddDone: STA.1 RET +; ---- When something goes wrong that nothing can carry on past ---- +; +; A fault used to stop the machine and print a line to whatever was behind it. On a terminal +; that is a diagnosis; behind a window it is a frozen picture and no reason at all, because +; the message goes to a standard error nobody is looking at. The machine looked hung and was +; not - it had stopped, and said so somewhere invisible. +; +; So the system catches all five and says it on the screen instead. +; +; ---- A fault ends the PROGRAM, not the machine ---- +; +; That is the answer to "carry on or start again", and it is not a compromise: a bare RETI +; from most of these meets the very instruction that failed and fails again, so carrying on +; is not on offer. But the machine is almost never what is broken. Everything the shell puts +; back when a program exits - the Stack, the vectors it installed, the drive, the working +; directory, the console, the screen - is exactly what wants putting back after one dies, so +; a fault in a loaded program joins handleExit and you are back at the prompt. +; +; A fault BELOW where programs load is the system's own, and there is nothing to go back to. +; That one says so and stops. +; +; ---- What a handler must not do ---- +; +; Fault. There is no double fault rule on this machine: a handler that commits the fault it +; was called about is called again, forever, and each time costs another frame of Stack. So +; nothing below asks for a service, touches a disk, or reaches anything that can refuse. +faultBadOpcode: + SETD.0 FaultOpcode + BRI faultPlain +faultGuard: + SETD.0 FaultGuard + BRI faultPlain +faultBank: + SETD.0 FaultBank +faultPlain: + ; The Stack Pointer names the frame, and it has to be taken before anything pushes. Nothing + ; above here does: a SETD and a branch move no Stack. + MVSD.3 + RSTA + SETD.1 FaultNumbered + STA.1 + BRI faultSay + +faultNoHandler: + SETD.0 FaultNoHandler + BRI faultNumbered +faultNoDevice: + SETD.0 FaultNoDevice +faultNumbered: + MVSD.3 + ; These two are the only faults that say WHICH one, and the machine hands that over in Q - + ; the one thing a handler here is given in a register. Kept at once, before printing can + ; disturb it. + MVQA + SETD.1 FaultNumber + STA.1 + INIA 0x01 + SETD.1 FaultNumbered + STA.1 + +faultSay: + ; The screen before the words. There is no use saying any of this somewhere it cannot be + ; read, and a program that faulted may have left the screen with nowhere to put a letter. + CALL faultScreen + + CALL printString + SETD.1 FaultNumbered + LDA.1 + BRA faultWhere + SETD.1 FaultNumber + LDA.1 + CALL printByteHex +faultWhere: + SETD.0 FaultAt + CALL printString + CALL faultAddress + CALL newLine + + ; The registers as the frame kept them, which is what they were when it happened. + SETD.0 FaultRegisters + CALL printString + PSHD.3 + POPD.0 + DPUP.0 0d3 + LDA.0 + CALL printByteHex + SETD.0 FaultB + CALL printString + PSHD.3 + POPD.0 + DPUP.0 0d4 + LDA.0 + CALL printByteHex + SETD.0 FaultQ + CALL printString + PSHD.3 + POPD.0 + DPUP.0 0d2 + LDA.0 + CALL printByteHex + CALL newLine + + ; ---- Whose fault it was ---- + ; + ; Where it happened says which, and one byte of the address decides it: a loaded program + ; begins at 0x4000 and everything below that is the system. + PSHD.3 + POPD.0 + DPUP.0 0d13 + LDA.0 + INIB 0x40 + CCF + SUB + BRC faultInSystem + + ; A program that will not be carrying on. handleExit does all of the putting back, and + ; takes the status the program is deemed to have stopped with in A. + INIA 0x01 + SETD.1 FaultStopped + STA.1 + INIA 0xFF + BRI handleExit + +faultInSystem: + ; Nothing to go back to: the shell IS what faulted, and its Stack, its variables and its + ; place in its own code are all suspect. Saying so and stopping is the only honest answer, + ; and it is a great deal better than the frozen picture this used to be. + SETD.0 FaultSystem + CALL printString + CALL newLine + HALT + +; The two bytes of the address in the frame, printed high half first. +faultAddress: + PSHD.3 + POPD.0 + DPUP.0 0d13 + LDA.0 + CALL printByteHex + INCD.0 + LDA.0 + CALL printByteHex + RET + +; ---- A screen this can be read on ---- +; +; "Put the screen in a known mode" is not tidiness. A program that left the screen in bitmap +; mode left nowhere to draw a character at all - the console draws nothing when there are no +; text rows - so without this, the message about what went wrong is invisible, which is the +; one thing it must never be. +faultScreen: + INIA 0x01 + OUTA 0x31 ; Eighty columns of text, whatever was being used. + + ; And the whole view back to the corner. A scrolled origin or a fraction of a cell puts + ; every character somewhere other than where it says it is. + RSTA + OUTA 0x34 + OUTA 0x36 + OUTA 0x37 + OUTA 0x38 + + ; ---- Colours it can be read in ---- + ; + ; A known mode is only half of a known screen: a program that wrote its own palette may + ; have left every ink the same as every paper. Attribute one draws in palette entries 16 + ; and 17, so those two are written and the rest of the program's colours are left alone - + ; there is nothing to be gained here by taking away more than is needed. + CALL screenBank + INIA 0d4 + OUTA 0xE3 + INIA 0xFC + OUTA 0xE4 + INIA 0x40 + OUTA 0xE5 ; 0xFC40, which is entry sixteen. + + RSTA + OUTA 0xE9 + OUTA 0xE9 + OUTA 0xE9 + OUTA 0xE9 ; Black paper. + INIA 0xD0 + OUTA 0xE9 + INIA 0x40 + OUTA 0xE9 + INIA 0x38 + OUTA 0xE9 + RSTA + OUTA 0xE9 ; Red ink, the same red the machine wakes up with. + + INIA 0x01 + OUTA 0x06 ; And draw in it. + + ; A console that can be printed to at all: line mode, a cursor, nothing interrupting. + INIA 0x04 + OUTA 0x02 + RET + ; ---- A command that did not work ---- ; ; The one place a failure is recorded, so that the thing reading lines out of a file can @@ -4010,7 +4208,26 @@ exitWorked: INIA 0x04 OUTA 0x02 + ; The ink too, and for the same reason as the cursor: a program that chose a colour is not + ; around to put it back, and neither is one the fault screen printed for. The shell owns + ; the prompt, so the shell is what makes sure it is readable. + RSTA + OUTA 0x06 + + ; ---- Finished, or stopped ---- + ; + ; A program that faulted did not finish, and saying so would be the shell's own word + ; against what the fault screen just said in red immediately above it. + SETD.1 FaultStopped + LDA.1 + BRA exitFinished + RSTA + STA.1 ; Cleared, so the next program is not blamed for this one. + SETD.0 Stopped + BRI exitSay +exitFinished: SETD.0 Finished +exitSay: CALL printString CALL newLine BRI prompt @@ -5259,6 +5476,28 @@ LoadedText: "loaded, starting at " NothingLoaded: "nothing is loaded" +Stopped: +"the program was stopped" +FaultOpcode: +"that byte is not an instruction" +FaultGuard: +"a write into a fenced off part of a bank" +FaultBank: +"a bank that is not there, or an address past its end" +FaultNoHandler: +"nothing is installed at service " +FaultNoDevice: +"nothing is installed for the device on port " +FaultAt: +", at " +FaultRegisters: +" A " +FaultB: +" B " +FaultQ: +" Q " +FaultSystem: +"that was the system itself, so there is nowhere to carry on from. Start the machine again." Finished: "finished" @@ -5682,6 +5921,19 @@ EditWalkBack: EditWasControl: 0x00 +; ---- What the fault screen is saying ---- +; +; Whether this cause names an entry as well as itself, and which one. Only the two faults +; about a missing handler do, and for those the machine puts the number in Q. +FaultNumbered: + 0x00 +FaultNumber: + 0x00 +; Set when a program is being ended by a fault rather than by asking. handleExit reads it to +; choose its last word, and clears it. +FaultStopped: + 0x00 + ; Whether the line being read is one to remember. The shell's are; a program's are not. EditKeepHistory: 0x00 @@ -5726,6 +5978,11 @@ CommandLine: #Vectors Boot boot + BadOpcode faultBadOpcode + GuardViolation faultGuard + BankFault faultBank + NoHandler faultNoHandler + NoDevice faultNoDevice osPrintString handlePrintString osReadLine handleReadLine osExit handleExit diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index e458407..88170ac 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -79,7 +79,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 194 tests, of which 132 run, 35 +everything it printed against a file in `Tests/expected`. 196 tests, of which 134 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/disk b/Tests/disk new file mode 100755 index 0000000..0976324 --- /dev/null +++ b/Tests/disk @@ -0,0 +1,277 @@ +#!/usr/bin/env bash +# Checks SplitDisk against the SBFS format. +# +# The tool and the SplitBit side are two implementations of one written specification, +# and nothing but that document keeps them the same. This checks the host half on its +# own: that a file put onto a disk comes back off it byte for byte, that the sizes which +# exercise the block and tail arithmetic all survive, and that the things the format +# says cannot happen are refused rather than half done. +# +# Written by Anachronaut + +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TOOL="$ROOT/SplitDisk" +WORK="$ROOT/Tests/build/disk" + +PASS=0 +FAIL=0 +FAILED_NAMES=() + +GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m' +[ -t 1 ] || { GREEN=""; RED=""; RESET=""; } + +check() { + local name="$1"; shift + if "$@" >/dev/null 2>&1; then + PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name" + else + FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name") + printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "$name" + fi +} + +# The opposite: the command is supposed to fail, and passing would be the bug. +refuses() { + local name="$1"; shift + if "$@" >/dev/null 2>&1; then + FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name") + printf " [%sFAIL%s] %s (it was allowed)\n" "$RED" "$RESET" "$name" + else + PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name" + fi +} + +if [ ! -x "$TOOL" ]; then + echo "SplitDisk is not built." + exit 1 +fi + +rm -rf "$WORK"; mkdir -p "$WORK" +cd "$WORK" || exit 1 + +echo "Checking SplitDisk against the SBFS format." + +check "format a disk" "$TOOL" format work.img 64 2 +refuses "refuse a disk with no room" "$TOOL" format tiny.img 2 4 +refuses "refuse an unformatted disk" "$TOOL" list /dev/null + +# The sizes that exercise every corner of blocks-plus-tail: nothing at all, less than a +# block, exactly a block, a part block, and an exact multiple. +: > empty.bin +printf 'x' > one.bin +head -c 256 /dev/urandom > exact.bin +head -c 700 /dev/urandom > part.bin +head -c 768 /dev/urandom > whole.bin + +for f in empty.bin one.bin exact.bin part.bin whole.bin; do + check "put $f" "$TOOL" put work.img "$f" +done + +roundTrip() { + "$TOOL" get work.img "$1" "got_$1" >/dev/null 2>&1 || return 1 + cmp -s "$1" "got_$1" +} +for f in empty.bin one.bin exact.bin part.bin whole.bin; do + check "$f comes back byte for byte" roundTrip "$f" +done + +refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin twentyNineCharactersLong.asm +refuses "refuse a duplicate name" "$TOOL" put work.img one.bin +refuses "refuse a file that is not there" "$TOOL" get work.img nosuch.bin out.bin +check "delete" "$TOOL" delete work.img one.bin +refuses "the deleted file is gone" "$TOOL" get work.img one.bin out.bin +check "the name can be used again" "$TOOL" put work.img one.bin + +# Contiguous files mean a disk can have room without having room in one piece. That is a +# consequence of the format rather than a bug, so it is checked rather than worked around. +"$TOOL" format frag.img 16 1 >/dev/null 2>&1 +head -c 1024 /dev/urandom > a.bin; cp a.bin b.bin; cp a.bin c.bin +"$TOOL" put frag.img a.bin >/dev/null 2>&1 +"$TOOL" put frag.img b.bin >/dev/null 2>&1 +"$TOOL" put frag.img c.bin >/dev/null 2>&1 +"$TOOL" delete frag.img a.bin >/dev/null 2>&1 +"$TOOL" delete frag.img c.bin >/dev/null 2>&1 +head -c 2048 /dev/urandom > big.bin +refuses "refuse a file with no run long enough" "$TOOL" put frag.img big.bin +head -c 512 /dev/urandom > fits.bin +check "but one that fits the gap goes on" "$TOOL" put frag.img fits.bin + +# ---- Directories ---- +# +# Version two, which adds a parent to each entry and a flag bit saying an entry is a +# directory. Both come out of bytes the entry had already set aside, so nothing moved and +# a version one disk needs no converting: zero in those bytes means the root, which is +# exactly where every file on a flat disk is. +# +# The version is therefore a statement about what is ON a disk rather than about what made +# it, and these check that it is only raised when it becomes true. +"$TOOL" format tree.img 64 2 >/dev/null 2>&1 +printf 'a file in the root' > root.txt +check "a fresh disk is flat" "$TOOL" put tree.img root.txt +version() { "$TOOL" list "$1" 2>/dev/null | head -1 | grep -q "version $2"; } +check "and says it is version 1" version tree.img 1 +check "make a directory" "$TOOL" mkdir tree.img /Apps +check "which raises it to version 2" version tree.img 2 +check "make one inside it" "$TOOL" mkdir tree.img /Apps/Source +check "put a file down a path" "$TOOL" put tree.img root.txt /Apps/Source/deep.txt + +# The point of the whole exercise: a name means something different in each place, so the +# same one can be used twice without either being in the other's way. +check "the same name in two places" "$TOOL" put tree.img root.txt /Apps/root.txt +roundTripAt() { + "$TOOL" get tree.img "$1" got_deep.txt >/dev/null 2>&1 || return 1 + cmp -s root.txt got_deep.txt +} +check "it comes back byte for byte" roundTripAt /Apps/Source/deep.txt +check ". and .. walk the path" roundTripAt /Apps/./Source/../root.txt +check ".. from the root is the root" roundTripAt /Apps/../../root.txt + +# Each of these is a way the tree could be made to contradict itself, and each is refused +# rather than half done. +refuses "no file where a directory goes" "$TOOL" put tree.img root.txt /root.txt/x.txt +refuses "no putting into thin air" "$TOOL" put tree.img root.txt /Nowhere/x.txt +refuses "no duplicate in one directory" "$TOOL" mkdir tree.img /Apps +refuses "no getting a directory" "$TOOL" get tree.img /Apps out.bin +refuses "delete will not take a directory" "$TOOL" delete tree.img /Apps +refuses "rmdir will not take a file" "$TOOL" rmdir tree.img /root.txt +refuses "nor the root" "$TOOL" rmdir tree.img / + +# THE REFUSAL THAT MATTERS MOST. Parents are entry indices and a freed index is handed out +# again, so removing a directory with things still in it would let the next file created +# adopt them. Emptying it first is the only safe order. +refuses "no removing an occupied one" "$TOOL" rmdir tree.img /Apps/Source +check "empty it first" "$TOOL" delete tree.img /Apps/Source/deep.txt +check "then it goes" "$TOOL" rmdir tree.img /Apps/Source + +# A path is names with separators between them, and a name is still twenty two characters. +refuses "refuse a 23 character component" "$TOOL" mkdir tree.img /Apps/abcdefghijklmnopqrstuvw +refuses "refuse a path naming nothing" "$TOOL" mkdir tree.img /Apps/ + +# A directory costs an entry and no blocks at all, which is what keeps the flat array of +# entries the whole allocation map. If a directory ever took a block, this would drop. +blocksFree() { "$TOOL" list "$1" 2>/dev/null | tail -1 | sed 's/.*used, //; s/ blocks free.*//'; } +before=$(blocksFree tree.img) +"$TOOL" mkdir tree.img /Empty >/dev/null 2>&1 +check "a directory costs no blocks" [ "$before" = "$(blocksFree tree.img)" ] + +# ---- A directory no bigger than the parent field can name ---- +# +# Eight entries to a block and the parent is an index plus one in two bytes, so entry +# 65535 has no parent number: adding one wraps to zero, and zero is the root. Such an +# entry does not refuse what is put inside it. It writes the thing into the ROOT while +# reporting the path that was asked for, and then cannot find it again - so the same +# create succeeds over and over, piling up entries of one name in one directory, which is +# the exact corruption rename exists to refuse. +refuses "no directory past the wrap" "$TOOL" format huge.img 65535 8192 +check "the largest that fits" "$TOOL" format huge.img 65535 8191 + +# And a disk claiming one, which is what something that never checked would have written. +# The claim is in the superblock, so it does not need a disk that size to be made. +"$TOOL" format lying.img 64 2 >/dev/null +printf '\x20\x00' | dd of=lying.img bs=1 seek=10 conv=notrunc status=none +refuses "nor reading one that claims it" "$TOOL" list lying.img + +# ---- A boot area, and the two halves of the superblock that describe it ---- +# +# bootBlocks and directoryStart say the same thing from two sides, so a disk where they +# disagree is one where there is no way to tell which is wrong. Both are refused. +check "format with a boot area" "$TOOL" format boot.img 512 4 32 +check "and it reads back" "$TOOL" list boot.img +refuses "no boot area bigger than a disk" "$TOOL" format small.img 32 2 64 +check "and none at all is still fine" "$TOOL" format plain.img 64 2 + +bootField() { python3 -c " +import sys +f = open(sys.argv[1], 'r+b'); f.seek(int(sys.argv[2])); f.write(bytes.fromhex(sys.argv[3])) +" "$@"; } +cp boot.img lying.boot.img +bootField lying.boot.img 14 0010 # Claims 16 blocks a slot, directory says 32. +refuses "nor a boot area that disagrees" "$TOOL" list lying.boot.img +cp boot.img badslot.img +bootField badslot.img 16 07 # Names slot 7, and there are two. +refuses "nor a slot that does not exist" "$TOOL" list badslot.img + +# ---- Writing a boot slot, and choosing between them ---- +# +# Two commands rather than one, deliberately: writing a slot and starting from it are +# different decisions, and joining them would make every write a commitment. +printf 'not really a bootloader' > stage.bin +check "write a boot slot" "$TOOL" boot boot.img stage.bin 0 +check "and the other one" "$TOOL" boot boot.img stage.bin 1 +check "choose which one starts" "$TOOL" bootslot boot.img 1 +refuses "no third slot to write" "$TOOL" boot boot.img stage.bin 2 +refuses "nor a third to choose" "$TOOL" bootslot boot.img 2 +refuses "no boot slot without an area" "$TOOL" boot plain.img stage.bin 0 + +# A slot holds what it holds. Something too big for one is refused rather than cut off, +# because half a bootloader is the failure with no way back. +head -c 9000 /dev/zero > toobig.bin # A slot on boot.img is 32 blocks, so 8192. +refuses "nor more than a slot holds" "$TOOL" boot boot.img toobig.bin 0 + +# THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot still holding the +# tail of whatever was there before is one whose contents depend on its history. +printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > long.bin +"$TOOL" boot boot.img long.bin 0 >/dev/null +"$TOOL" boot boot.img stage.bin 0 >/dev/null +check "and it is written whole" python3 -c " +import sys +d = open('boot.img','rb').read() +slot = d[256:256 + 32 * 256] +sys.exit(1 if b'aaaa' in slot else 0)" + +# ---- How the last start went ---- +check "the boot state reads" "$TOOL" bootstate boot.img +check "and can be set" "$TOOL" bootstate boot.img 2 +refuses "but only to a state there is" "$TOOL" bootstate boot.img 7 +check "a fresh disk is settled" python3 -c " +import sys +sys.exit(0 if open('plain.img','rb').read()[17] == 0 else 1)" + +# ---- Mirroring a host directory ---- +# +# What the system disk is built with. Every file goes through put and every directory +# through mkdir, so this is a walk over machinery already checked above - what wants +# checking is the walk: that it goes all the way down, that it leaves behind what it was +# told to, and that it REFUSES a name the format cannot hold rather than skipping it, since +# a disk quietly missing a file is the failure a mirror exists to prevent. +mkdir -p tree/inner/deeper tree/leave +printf 'top' > tree/top.txt +printf 'inner' > tree/inner/middle.txt +printf 'deep' > tree/inner/deeper/bottom.txt +printf 'not this' > tree/leave/ignored.txt +: > tree/.hidden + +"$TOOL" format mirror.img 256 8 >/dev/null +check "mirror a directory tree" "$TOOL" mirror mirror.img tree / + +"$TOOL" list mirror.img > mirrored.txt 2>&1 +grep -q '/inner/deeper/bottom.txt' mirrored.txt \ + && { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "it goes all the way down"; } \ + || { FAIL=$((FAIL + 1)); FAILED_NAMES+=("depth"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "it goes all the way down"; } +grep -q 'hidden' mirrored.txt \ + && { FAIL=$((FAIL + 1)); FAILED_NAMES+=("hidden"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and leaves dotfiles behind"; } \ + || { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and leaves dotfiles behind"; } + +# Named on the command line, which is how a project keeps what it builds out of what it +# wrote. +"$TOOL" format skipped.img 256 8 >/dev/null +check "mirror with something left out" "$TOOL" mirror skipped.img tree / leave +"$TOOL" list skipped.img > skipped.txt 2>&1 +grep -q 'ignored.txt' skipped.txt \ + && { FAIL=$((FAIL + 1)); FAILED_NAMES+=("skip"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and the skipped one is not there"; } \ + || { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and the skipped one is not there"; } + +# Twenty-three characters, one more than a directory entry holds. +printf 'too long' > tree/aNameOfTwentyThreeChars +"$TOOL" format refused.img 256 8 >/dev/null +refuses "a name too long stops the mirror" "$TOOL" mirror refused.img tree / +rm -f tree/aNameOfTwentyThreeChars + +echo +if [ "$FAIL" -eq 0 ]; then + echo "All $PASS disk tool checks passed." + exit 0 +fi +echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}" +exit 1 diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 28d1470..4c4a5bd 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -22,6 +22,7 @@ Break.sbx 149 Grid.sbx 559 Press.sbx 872 Mode.sbx 48 +Crash.sbx 632 notes.txt 21 Apps