B1: a boot area on the disk, reserved by arithmetic that was already there
The first rung of booting from disk. A boot area is blocks between the superblock and the directory that the filesystem never allocates and never sees, and NOTHING WAS ADDED TO RESERVE THEM: both implementations work out the first usable block as directoryStart + directoryBlocks, and directoryStart has always been a field rather than a constant. Formatting with the directory moved up reserves everything below it. Neither allocator changed, on either side. Two new superblock fields in bytes that were reserved: bootBlocks at 14, per slot, and bootSlot at 16. A disk made before this has zero in both, which reads as "no boot area" - true, and the same shape as the version two parent field, where the value an older disk already held was the right answer without conversion. TWO SLOTS, ALWAYS. A boot slot is raw blocks with no entry to rename, so the write-a-temporary-and-rename ordering that protects every file cannot protect it, and a machine interrupted while updating its only slot would not boot at all - the one failure on this disk with no way back. Writing the slot that is not live and then moving one byte makes that a machine that boots what it had before. bootBlocks and directoryStart say the same thing from two sides, so a disk where they disagree is refused rather than guessed at, as is one naming a slot that does not exist. Checked where it matters: the HOST formats a disk with a boot area and the MACHINE fills it, then the reserved blocks are compared against zero. The machine's allocator is the one that had no idea any of this was happening, which is what makes that the check worth having. Six host checks besides, including both halves of the superblock disagreeing.
This commit is contained in:
@@ -244,7 +244,7 @@ increment, decrement, addition, and subtraction when their inputs are known.
|
|||||||
|
|
||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `format <image> [blocks] [dirblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 entries. |
|
| `format <image> [blocks] [dirblocks] [bootblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 entries. A fourth number reserves a boot area of two slots that size. |
|
||||||
| `list <image> [path]` | Show the whole disk, or one directory of it. |
|
| `list <image> [path]` | Show the whole disk, or one directory of it. |
|
||||||
| `put <image> <file> [path]` | Put a host file onto it. Without a path it uses the file's own name, which is often longer than the 22 characters a name may be. |
|
| `put <image> <file> [path]` | Put a host file onto it. Without a path it uses the file's own name, which is often longer than the 22 characters a name may be. |
|
||||||
| `get <image> <path> [file]` | Take one off it. |
|
| `get <image> <path> [file]` | Take one off it. |
|
||||||
@@ -252,6 +252,27 @@ increment, decrement, addition, and subtraction when their inputs are known.
|
|||||||
| `mkdir <image> <path>` | Make a directory. |
|
| `mkdir <image> <path>` | Make a directory. |
|
||||||
| `rmdir <image> <path>` | Remove an empty one. |
|
| `rmdir <image> <path>` | Remove an empty one. |
|
||||||
|
|
||||||
|
### The Boot Area:
|
||||||
|
|
||||||
|
Blocks between the superblock and the directory, which the filesystem never allocates and
|
||||||
|
never sees. **Nothing was added to reserve them.** Both implementations work out the first
|
||||||
|
usable block as `directoryStart + directoryBlocks`, and `directoryStart` has always been a
|
||||||
|
field in the superblock rather than a constant, so moving the directory up reserves
|
||||||
|
everything below it by arithmetic that was already there. Neither allocator changed.
|
||||||
|
|
||||||
|
A disk made before any of this has `directoryStart` of 1 and a boot area of zero, which
|
||||||
|
reads as *not bootable* - true, and the same shape as the version two parent field, where
|
||||||
|
the value an older disk already held turned out to be the right answer.
|
||||||
|
|
||||||
|
**There are always two slots**, because a boot slot is raw blocks. A file being rewritten
|
||||||
|
is protected by writing a temporary and renaming it, and there is no name here to rename -
|
||||||
|
so a machine interrupted while updating its only boot slot would not boot at all, which is
|
||||||
|
the one failure on this disk with no way back. Writing the slot that is *not* live and then
|
||||||
|
moving one byte in the superblock turns that into a machine that boots what it had before.
|
||||||
|
|
||||||
|
`bootBlocks` and `directoryStart` describe the same fact from two sides, so a disk where
|
||||||
|
they disagree is refused rather than guessed at.
|
||||||
|
|
||||||
SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine.
|
SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine.
|
||||||
|
|
||||||
Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens `put` says so rather than putting part of a file on.
|
Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens `put` says so rather than putting part of a file on.
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ typedef struct {
|
|||||||
uint16_t directoryStart;
|
uint16_t directoryStart;
|
||||||
uint16_t directoryBlocks;
|
uint16_t directoryBlocks;
|
||||||
uint16_t freeBlocks;
|
uint16_t freeBlocks;
|
||||||
|
uint16_t bootBlocks; // Per slot. Zero on a disk that cannot be booted.
|
||||||
|
uint8_t bootSlot; // Which of the two is live.
|
||||||
} Superblock;
|
} Superblock;
|
||||||
|
|
||||||
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
|
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
|
||||||
@@ -93,6 +95,24 @@ static int readSuperblock(FILE *image, Superblock *super) {
|
|||||||
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
|
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
|
||||||
super->directoryBlocks = directoryBlocks;
|
super->directoryBlocks = directoryBlocks;
|
||||||
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
|
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
|
||||||
|
super->bootBlocks = readWord(block + SBFS_SUPER_BOOTBLOCKS);
|
||||||
|
super->bootSlot = block[SBFS_SUPER_BOOTSLOT];
|
||||||
|
|
||||||
|
// The boot area and the directory's position describe the same fact from two sides,
|
||||||
|
// so they have to agree or one of them is wrong and there is no way to tell which.
|
||||||
|
uint32_t expected = SBFS_FIRST_BOOT_BLOCK
|
||||||
|
+ (uint32_t)super->bootBlocks * SBFS_BOOT_SLOTS;
|
||||||
|
if (super->directoryStart != expected) {
|
||||||
|
fprintf(stderr, "Error: That disk says %u blocks of boot area and puts its"
|
||||||
|
" directory at %u, which should then be %u.\n",
|
||||||
|
super->bootBlocks, super->directoryStart, expected);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (super->bootSlot >= SBFS_BOOT_SLOTS) {
|
||||||
|
fprintf(stderr, "Error: That disk names boot slot %u, and there are %u.\n",
|
||||||
|
super->bootSlot, SBFS_BOOT_SLOTS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +128,8 @@ static int writeSuperblock(FILE *image, const Superblock *super) {
|
|||||||
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
|
writeWord(block + SBFS_SUPER_DIRSTART, super->directoryStart);
|
||||||
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
|
writeWord(block + SBFS_SUPER_DIRBLOCKS, super->directoryBlocks);
|
||||||
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
|
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
|
||||||
|
writeWord(block + SBFS_SUPER_BOOTBLOCKS, super->bootBlocks);
|
||||||
|
block[SBFS_SUPER_BOOTSLOT] = super->bootSlot;
|
||||||
return writeBlock(image, 0, block);
|
return writeBlock(image, 0, block);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +428,8 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
|
|||||||
|
|
||||||
// ---- Commands ----
|
// ---- Commands ----
|
||||||
|
|
||||||
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
|
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
|
||||||
|
uint16_t bootBlocks) {
|
||||||
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
|
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
|
||||||
fprintf(stderr, "Error: %u directory blocks is %u entries, and entry 65535 has no"
|
fprintf(stderr, "Error: %u directory blocks is %u entries, and entry 65535 has no"
|
||||||
" parent number - adding one wraps to zero, which is the root."
|
" parent number - adding one wraps to zero, which is the root."
|
||||||
@@ -415,9 +438,12 @@ static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBl
|
|||||||
SBFS_MAX_DIRECTORY_BLOCKS, SBFS_MAX_ENTRIES);
|
SBFS_MAX_DIRECTORY_BLOCKS, SBFS_MAX_ENTRIES);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (blocks <= 1u + directoryBlocks) {
|
uint32_t overhead = 1u + (uint32_t)bootBlocks * SBFS_BOOT_SLOTS + directoryBlocks;
|
||||||
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a"
|
if (blocks <= overhead) {
|
||||||
" directory of %u.\n", blocks, directoryBlocks);
|
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock, %u of"
|
||||||
|
" boot area and a directory of %u.\n",
|
||||||
|
blocks, (unsigned)((uint32_t)bootBlocks * SBFS_BOOT_SLOTS),
|
||||||
|
directoryBlocks);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Quietly, because a disk that is not there yet is the ordinary case for format and
|
// Quietly, because a disk that is not there yet is the ordinary case for format and
|
||||||
@@ -445,14 +471,26 @@ static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBl
|
|||||||
// what raises it, because mkdir is what makes the difference true.
|
// what raises it, because mkdir is what makes the difference true.
|
||||||
super.version = SBFS_VERSION_FLAT;
|
super.version = SBFS_VERSION_FLAT;
|
||||||
super.diskBlocks = blocks;
|
super.diskBlocks = blocks;
|
||||||
super.directoryStart = SBFS_FIRST_DIRECTORY_BLOCK;
|
// THE DIRECTORY MOVES UP BY THE BOOT AREA, and that is the whole mechanism. Both
|
||||||
|
// implementations already work out the first usable block as directoryStart plus
|
||||||
|
// directoryBlocks, so everything below the directory is reserved by arithmetic that
|
||||||
|
// was there before any of this, and no allocator changed.
|
||||||
|
super.directoryStart = (uint16_t)(SBFS_FIRST_BOOT_BLOCK
|
||||||
|
+ (uint32_t)bootBlocks * SBFS_BOOT_SLOTS);
|
||||||
super.directoryBlocks = directoryBlocks;
|
super.directoryBlocks = directoryBlocks;
|
||||||
super.freeBlocks = (uint16_t)(blocks - 1 - directoryBlocks);
|
super.freeBlocks = (uint16_t)(blocks - overhead);
|
||||||
|
super.bootBlocks = bootBlocks;
|
||||||
|
super.bootSlot = 0;
|
||||||
if (writeSuperblock(image, &super)) {
|
if (writeSuperblock(image, &super)) {
|
||||||
fclose(image);
|
fclose(image);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
fclose(image);
|
fclose(image);
|
||||||
|
if (bootBlocks) {
|
||||||
|
printf("Formatted %s: %u blocks, two boot slots of %u, %u of directory, %u free.\n",
|
||||||
|
path, blocks, bootBlocks, directoryBlocks, super.freeBlocks);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
|
printf("Formatted %s: %u blocks, %u of directory, %u free.\n",
|
||||||
path, blocks, directoryBlocks, super.freeBlocks);
|
path, blocks, directoryBlocks, super.freeBlocks);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -993,12 +1031,21 @@ int main(int argc, char *argv[]) {
|
|||||||
if (strcmp(command, "format") == 0) {
|
if (strcmp(command, "format") == 0) {
|
||||||
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
|
long blocks = (argc > 3) ? strtol(argv[3], NULL, 0) : 512;
|
||||||
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
|
long directoryBlocks = (argc > 4) ? strtol(argv[4], NULL, 0) : SBFS_DEFAULT_DIRECTORY_BLOCKS;
|
||||||
|
// Blocks in EACH boot slot, and there are two of them. Left off, a disk gets no
|
||||||
|
// boot area at all, which is what every disk made before this had.
|
||||||
|
long bootBlocks = (argc > 5) ? strtol(argv[5], NULL, 0) : 0;
|
||||||
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
|
if (blocks < 2 || blocks > 0xFFFF || directoryBlocks < 1 || directoryBlocks > 0xFFFF) {
|
||||||
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
|
fprintf(stderr, "Error: A disk is between 2 and 65535 blocks, with at least"
|
||||||
" one of directory.\n");
|
" one of directory.\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks);
|
if (bootBlocks < 0 || bootBlocks * SBFS_BOOT_SLOTS > 0xFFFE) {
|
||||||
|
fprintf(stderr, "Error: A boot slot is between 0 and %d blocks, and there are"
|
||||||
|
" two of them.\n", 0xFFFE / SBFS_BOOT_SLOTS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return commandFormat(path, (uint16_t)blocks, (uint16_t)directoryBlocks,
|
||||||
|
(uint16_t)bootBlocks);
|
||||||
}
|
}
|
||||||
if (strcmp(command, "list") == 0) {
|
if (strcmp(command, "list") == 0) {
|
||||||
return commandList(path, (argc > 3) ? argv[3] : NULL);
|
return commandList(path, (argc > 3) ? argv[3] : NULL);
|
||||||
|
|||||||
+31
-1
@@ -51,13 +51,43 @@
|
|||||||
// 8 2 First directory block
|
// 8 2 First directory block
|
||||||
// 10 2 Blocks the directory occupies
|
// 10 2 Blocks the directory occupies
|
||||||
// 12 2 Free blocks, a cache rather than the authority
|
// 12 2 Free blocks, a cache rather than the authority
|
||||||
// 14 Reserved to the end of the block
|
// 14 2 Blocks in each boot slot, or zero for a disk that cannot be booted
|
||||||
|
// 16 1 Which boot slot is live, 0 or 1
|
||||||
|
// 17 Reserved to the end of the block
|
||||||
|
|
||||||
#define SBFS_SUPER_VERSION 4
|
#define SBFS_SUPER_VERSION 4
|
||||||
#define SBFS_SUPER_DISK 6
|
#define SBFS_SUPER_DISK 6
|
||||||
#define SBFS_SUPER_DIRSTART 8
|
#define SBFS_SUPER_DIRSTART 8
|
||||||
#define SBFS_SUPER_DIRBLOCKS 10
|
#define SBFS_SUPER_DIRBLOCKS 10
|
||||||
#define SBFS_SUPER_FREE 12
|
#define SBFS_SUPER_FREE 12
|
||||||
|
#define SBFS_SUPER_BOOTBLOCKS 14
|
||||||
|
#define SBFS_SUPER_BOOTSLOT 16
|
||||||
|
|
||||||
|
// ---- The boot area ----
|
||||||
|
//
|
||||||
|
// Blocks between the superblock and the directory, which the filesystem never allocates
|
||||||
|
// and never sees. Nothing had to be added to make room for them: both implementations
|
||||||
|
// work out the first usable data block as directoryStart + directoryBlocks, and
|
||||||
|
// directoryStart is a field rather than a constant, so moving the directory up reserves
|
||||||
|
// everything below it by arithmetic that was already there.
|
||||||
|
//
|
||||||
|
// A DISK MADE BEFORE THIS HAS ZERO HERE, which reads as "no boot area", which is true.
|
||||||
|
// The same shape as the version two parent field: the value an older disk already holds
|
||||||
|
// is the correct answer rather than something needing conversion.
|
||||||
|
//
|
||||||
|
// TWO SLOTS, ALWAYS, and the reason is that a boot slot is raw blocks. A file being
|
||||||
|
// rewritten is protected by writing a temporary and renaming it, and there is no name
|
||||||
|
// here to rename - so a machine interrupted while updating its only boot slot would not
|
||||||
|
// boot at all, which is the one failure on this disk with no way back. Writing the slot
|
||||||
|
// that is not live and then moving one byte turns that into a machine that boots the
|
||||||
|
// version it had before.
|
||||||
|
//
|
||||||
|
// block 0 the superblock
|
||||||
|
// 1 .. bootBlocks slot 0
|
||||||
|
// bootBlocks+1 .. 2*bootBlocks slot 1
|
||||||
|
// directoryStart .. the directory, and then files
|
||||||
|
#define SBFS_BOOT_SLOTS 2
|
||||||
|
#define SBFS_FIRST_BOOT_BLOCK 1
|
||||||
|
|
||||||
// ---- Directory entries ----
|
// ---- Directory entries ----
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -272,6 +272,41 @@ else
|
|||||||
report FAIL "the machine sees it too" "dir showed it as an ordinary file"
|
report FAIL "the machine sees it too" "dir showed it as an ordinary file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---- A boot area is blocks neither of them will touch ----
|
||||||
|
#
|
||||||
|
# Blocks between the superblock and the directory, reserved by moving the directory up
|
||||||
|
# rather than by anything new: both implementations work out the first usable block as
|
||||||
|
# directoryStart + directoryBlocks, and directoryStart has always been a field. So this
|
||||||
|
# checks a claim that no code was written to make true - which is exactly the kind most
|
||||||
|
# worth checking.
|
||||||
|
#
|
||||||
|
# The host formats it and the MACHINE fills it, because the machine's allocator is the one
|
||||||
|
# that had no idea any of this was happening.
|
||||||
|
"$TOOL" format bootarea.img 512 4 32 >/dev/null
|
||||||
|
"$TOOL" mkdir bootarea.img /Apps >/dev/null
|
||||||
|
"$TOOL" put bootarea.img Files.sbx /Apps/Files.sbx >/dev/null
|
||||||
|
printf 'Files\nFiles\nexit\n' \
|
||||||
|
| "$EMU" cosmos.bin --fast --disk bootarea.img > bootarea.txt 2>&1
|
||||||
|
|
||||||
|
if python3 - bootarea.img <<'CHECK'
|
||||||
|
import sys
|
||||||
|
image = open(sys.argv[1], "rb").read()
|
||||||
|
first = int.from_bytes(image[8:10], "big")
|
||||||
|
boot = image[256:first * 256]
|
||||||
|
sys.exit(0 if boot == bytes(len(boot)) else 1)
|
||||||
|
CHECK
|
||||||
|
then
|
||||||
|
report ok "the boot area is left alone" "the machine allocated around it"
|
||||||
|
else
|
||||||
|
report FAIL "the boot area is left alone" "something wrote into the reserved blocks"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "$TOOL" list bootarea.img | grep -q "kept.txt\|1 file\|0 files"; then
|
||||||
|
report ok "and the disk still works" "the host reads what the machine wrote"
|
||||||
|
else
|
||||||
|
report FAIL "and the disk still works" "the host could not read it back"
|
||||||
|
fi
|
||||||
|
|
||||||
# ---- And each can read what the other wrote ----
|
# ---- And each can read what the other wrote ----
|
||||||
#
|
#
|
||||||
# Matching bytes and being readable are not the same claim. A field both of them write
|
# Matching bytes and being readable are not the same claim. A field both of them write
|
||||||
|
|||||||
@@ -172,6 +172,26 @@ check "the largest that fits" "$TOOL" format huge.img 65535 8191
|
|||||||
printf '\x20\x00' | dd of=lying.img bs=1 seek=10 conv=notrunc status=none
|
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
|
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
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if [ "$FAIL" -eq 0 ]; then
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
echo "All $PASS disk tool checks passed."
|
echo "All $PASS disk tool checks passed."
|
||||||
|
|||||||
Reference in New Issue
Block a user