A listing says what is left of the disk, and what will fit

dir said what was there and nothing about what was left. SplitDisk has
printed the free figure since it was written, so the machine's own
listing was the poorer of the two implementations at describing the same
disk.

  43 files, 3 directories
  46 of 64 entries, 1893 blocks free

Entries first, because they are the ceiling nobody notices until they hit
it: a disk of small files runs out of directory slots long before it runs
out of blocks.

COUNTED RATHER THAN ASKED. The superblock keeps a free count and this
file calls it "a note rather than the truth" in three places. sbfsSpace
reads the whole directory table instead, which costs a read per directory
block and is the answer rather than a guess. SplitDisk goes on reading
the note and saying when it is stale, which is the right place for that
check - the host tool is what you audit a disk with.

---- And it is a fact about the disk, not about where you are ----

The first version added the blocks up as the LISTING walked past them,
which cost no extra read and was wrong: that walk stops only on entries
in the working directory, so the same disk came out as 1,996 blocks free
from the root and 2,025 from /Apps. Comparing against SplitDisk is what
said so, which is what having two implementations is for.

---- The longest run, which is what decides whether a file fits ----

  4 of 16 entries, 37 blocks free
  the longest run is 25

Files are laid down contiguously, so the free total does not say whether
a file will fit. Both implementations learn it, from one specification.

Said only when it differs from the free total. Deleting is what fragments
a contiguous store, and a disk that has only been appended to has one gap
at the end - so on a healthy disk this is silent, and a line that appears
only when something is wrong is a line somebody reads.

There is no sort on this machine and the entries are in no order, so a
candidate walks the disk: each pass finds the used extent nearest at or
after it, and anything the candidate stands inside pushes it to the far
end and starts the pass again. The same trick allocating uses. So it
costs a pass per gap rather than per file - nearly nothing on a disk with
one gap, more the more fragmented the disk is, which is the right way
round.

holes.img is six files with the second and fourth deleted, because no
other disk here can show any of this: none of them has ever had anything
deleted from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-06 18:19:04 -04:00
co-authored by Claude Opus 5
parent 2ad8edf9bc
commit 323d7a0330
32 changed files with 649 additions and 2 deletions
+59
View File
@@ -433,6 +433,56 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
return (uint16_t)(super->diskBlocks - overhead - used);
}
// ---- The longest run of free blocks there is ----
//
// The number that says whether a file will fit. Files are laid down contiguously - first fit,
// with the directory itself as the map - so a disk with a thousand blocks free in ten
// scattered pieces refuses a file of two hundred, and the free total gives no hint of it.
//
// A sweep rather than a sort: a candidate walks the disk, each pass looking for the used
// extent nearest at or after it, and anything the candidate is standing inside pushes it to
// the far end and starts the pass again. That is what sbfs.asm does, because that machine has
// no sort - and the two implementations of this format are worth more agreeing than they are
// each being clever separately.
static uint16_t largestRun(const Directory *directory, const Superblock *super) {
const uint32_t firstData = (uint32_t)super->directoryStart + super->directoryBlocks;
uint32_t candidate = firstData;
uint32_t biggest = 0;
for (;;) {
uint32_t next = super->diskBlocks;
int moved = 0;
for (int i = 0; i < directory->entries; i++) {
const uint8_t *entry = entryAt(directory, i);
if (!entryInUse(entry) || entryIsDirectory(entry)) {
continue;
}
const uint32_t start = readWord(entry + SBFS_ENTRY_START);
const uint32_t end = start + entryBlocksUsed(entry);
if (end <= candidate) {
continue;
}
if (start <= candidate) {
candidate = end;
moved = 1;
continue;
}
if (start < next) {
next = start;
}
}
if (moved) {
continue;
}
if (next - candidate > biggest) {
biggest = next - candidate;
}
if (next >= super->diskBlocks) {
return (uint16_t)biggest;
}
candidate = next;
}
}
// ---- Commands ----
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks,
@@ -647,6 +697,15 @@ static int commandList(const char *path, const char *within) {
}
printf(".\n");
// Said only when it is not all of it. On a disk that has only been appended to the two
// are the same number, and printing it twice would be noise on every healthy disk - a
// line that appears only when something is wrong is a line somebody reads.
uint16_t run = largestRun(&directory, &super);
if (run != counted) {
printf("The longest run of free blocks is %u, so nothing bigger than that will"
" fit.\n", run);
}
// A save that stopped between deleting the old entry and naming the new one. The
// bytes are all there under the temporary's name and one rename brings them back,
// which is the whole of the recovery this format offers - so the thing that matters