#!/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 16bitSegmentedSieveModern.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)" ] 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