Say a temporary is one in the entry, not in its name

Saving something that already exists writes a temporary, deletes the
original and gives the temporary its name, so that nothing is lost if the
writing fails. The temporary was told apart from a real file by being
called sbfs.part or sbfs.out - and those are legal names. Starting a save
deleted whatever answered to one as stale scratch, so saving anything at
all in a directory destroyed your own file of that name there, silently.

Flag bit 0x04 now says it. The property is not in the contents - the same
bytes become the finished file the instant the rename lands - so it belongs
in the entry, which is the thing the commit changes. sbfsCreateTempAt is
the door temporaries come in by, the commit writes the flags flat along
with the name, and cleanup wipes what it finds only if the entry says it is
ours. Anything else stops the save instead.

The bit is also the recovery. Both listings show an unfinished write rather
than sizing it, because the size in the entry is the room that was asked
for and not what was written: "<unfinished>" from dir, and a line from
SplitDisk saying the blocks are held and a rename brings the data back.
That was the gap in what the last commit documented - the data survived a
crash and nothing would show you where it was.

Four new agreement checks, three of which fail with the guards removed. The
fourth needed rebuilding first: both tests started on one disk, and the
first save ate the sbfs.part that was the second test's SOURCE, so the copy
failed for want of a file, never opened a stream, and passed while
reporting on nothing. A disk each. The fifth check forges the wreckage by
setting the flag on a finished file, since nothing here can crash a save
half way through.

No version bump: a committed file never carries the bit, so a disk this
writes is byte for byte the disk the old code wrote, which the whole-image
comparisons confirm. Only the wreckage differs, and older code reads that
as an ordinary file - which is what it did before.
This commit is contained in:
Anachronaut
2026-08-25 23:22:37 -04:00
parent d6cbbb5034
commit ce8fb721fe
6 changed files with 242 additions and 13 deletions
+19 -2
View File
@@ -761,9 +761,26 @@ A create can be refused for want of a run long enough even on a disk with plenty
The ordering protects the original against **every way a save can fail while it is running**, and it is worth naming those, because they are the ones that actually happen: there is no run of free blocks long enough, or none at all; the disk refuses a block write; the name turns out to belong to a directory; the writer gives up part way through. In all of them the file that was already there is untouched, and what is lost is the temporary, which nothing had come to depend on yet.
It is **not** power-loss atomic, and nothing about SBFS claims it is. The commit is two block writes - delete the old entry, then give the temporary its name - and a machine that stops between them leaves the old file gone and the new one under the temporary's name. Both writes are to the directory, so `sbfs.part` or `sbfs.out` is sitting there holding every byte of the work; the data survives and the name does not, and putting it right is one `rename` typed by hand.
It is **not** power-loss atomic, and nothing about SBFS claims it is. The commit is two block writes - delete the old entry, then give the temporary its name - and a machine that stops between them leaves the old file gone and the new one under the temporary's name. Both writes are to the directory, so `sbfs.part` or `sbfs.out` is sitting there holding every byte of the work; the data survives and the name does not, and putting it right is one `rename` typed by hand. `dir` marks it `<unfinished>` so that it can be found, which is the whole of the recovery this format offers.
Closing that window means a journal or a second copy of the directory, and both are a great deal of machinery to buy back a two-write gap on a machine with no power failures to speak of. The honest description is the one to write down: **safe against the failures of ordinary operation, not against the machine stopping.** A future consistency check at boot could reclaim an abandoned temporary, and would want the entry to say it is one rather than to be recognised by its name.
Closing that window means a journal or a second copy of the directory, and both are a great deal of machinery to buy back a two-write gap on a machine with no power failures to speak of. The honest description is the one to write down: **safe against the failures of ordinary operation, not against the machine stopping.**
#### What tells a temporary from a file:
While the save runs the temporary is an ordinary entry in every way that matters - it holds real blocks and answers to a name - and the only thing that makes it different is that nobody has committed it yet. That is **not a property of its contents.** The same bytes become the finished file the instant the rename lands, so there is nothing to put inside it that would be true. It belongs in the entry, which is the thing the commit changes, and it is **flag bit `0x04`**.
It used to be told apart by being *called* `sbfs.part` or `sbfs.out`, and those are names anybody is entitled to give a file of their own. Starting a save deleted whatever answered to one, as stale scratch - so saving anything at all in a directory destroyed your file of that name there, silently, and the first you would know of it is going to look for it. A file that does not carry the flag now belongs to somebody, and the save is **refused** rather than helping itself to the name.
The same bit is what makes an interrupted save recoverable. Both listings show an unfinished write rather than sizing it, because the size in the entry is the room it asked for and not what was written into it:
```
> dir
stranded.txt <unfinished>
```
Rename it to keep the data, delete it to give the blocks back. Nothing reclaims it on its own; a boot-time consistency check could, and this is the field it would read.
**A committed file never carries the bit**, so a disk this writes is byte for byte the disk the older code wrote - the agreement tests compare whole images and say so. Only the wreckage of a save that stopped looks different, and code that has never heard of the flag reads that as an ordinary file, which is exactly what it did before.
Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe.