diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index cab94b7..5fc9765 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -17,6 +17,9 @@ for itself. - Interactive Shell: Read commands from the SplitBit console and continue until `exit` or the end of input. - SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk. +- Paths: Anywhere a filename is taken, a path may be given instead - names with `/` + between them, walked from the root, with `.` and `..`. Directories are read but not yet + made; the host tool makes them. - Loadable Applications: Validate SBEX files, copy their Program and Data segments into the addresses for which they were assembled, and start them at their declared entry point. @@ -83,7 +86,7 @@ CosmOS currently provides these built-in commands: | Command | Description | | -- | -- | | `dir` | List the files on the mounted disk and their sizes. | -| `load ` | Read and validate an SBEX application, then place its code and data where its header requests. | +| `load ` | Read and validate an SBEX application, then place its code and data where its header requests. | | `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. | | ` [words]` | Any word the shell does not recognise is looked for on the disk as `.sbx`, and loaded and started if it is there. | | `delete ` | Remove a file from the filesystem and release its blocks. | @@ -130,6 +133,34 @@ facility and as a test that CosmOS correctly restores its Stack and vector table every run; and `load` is how the monitor puts an arbitrary file in front of itself, which is a thing typing a name deliberately cannot do. +### Paths: + +Everywhere CosmOS takes a filename it will take a path: names with `/` between them, +walked from the root, with `.` meaning where you are and `..` meaning the directory above. +`..` from the root is the root. A bare name is a path of one name, so nothing written +before directories existed had to change. + +```text +> load /Apps/Snake.sbx +> Type /Notes/today.txt +``` + +**Programs did not have to be taught any of this.** Path resolution lives inside +`sbfsFind`, below the services, so `osFileInfo`, `osFileBlock`, `osFileSave`, +`osFileDelete` and `osFileRename` all still take a pointer to a name - and a path is +simply a longer name. `Type`, `More`, `Edit` and the assembler gained subdirectories +without a line being changed in any of them. + +Each *name* along a path is still the 22 characters a directory entry holds, and a longer +one is refused rather than cut short, because a name cut to 22 characters is a different +name that might well be some other file's. + +At this stage CosmOS **reads** directories and does not make them. There is no `cd`, no +`mkdir`, and no working directory: every path is from the root. Files a program writes go +in the root. Deleting or renaming a directory is refused - deleting one would free its +entry index, and since a parent is written as an index, the next file created would take +that index and inherit its children. + ### Starting An Application By Name: A word the shell has no command for is not immediately an error. Before saying so, the @@ -150,15 +181,18 @@ trustworthy. cannot be started by typing what it is called, whatever happens to be inside it. Only `load` reaches a file by its literal name. +A path works here too, so `/Apps/Say hello` starts `/Apps/Say.sbx` and gives it `hello`. + **A file that is found but is broken says so.** If `notes.sbx` exists and is not an SBEX program, typing `notes` reports `not a program` rather than `I do not know: notes`. Reporting an unknown command about a file that is sitting on the disk would send somebody looking in the wrong place. Names are matched exactly, including case, because every other name on the filesystem is. -A directory entry holds twenty two characters and four are spoken for by the extension, so -eighteen is the longest a bare name can be; a longer word is reported as unknown, which is -the truth, since no file of that name can exist. +What limits the typed word is the buffer it is built in rather than the format: each name +along a path is still twenty two characters, and the path walker refuses a longer one +rather than cutting it down. A word that will not fit is reported as unknown, which is the +truth, since nothing the shell can reach is called that. CosmOS also boots without a disk. It reports that no filesystem was found, leaves the shell and memory monitor available, and refuses commands that require a mounted disk diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 5c31602..29a1385 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -269,6 +269,8 @@ doDir: RSTA SETD.0 DirSeen STA.0 + SETD.0 DirFolders + STA.0 CALL sbfsFirst BRI dirCheck @@ -290,6 +292,15 @@ dirCheck: MVQA CALL printSpaces + ; A directory says so instead of saying a size. It has no blocks, so the arithmetic + ; below would call it a file of no bytes - which is a different thing that happens to + ; look the same from here. + SETD.0 SbfsFoundFlags + LDA.0 + INIB 0x02 + AND + BNQ dirIsDirectory + ; A file's length is its block count times 256 plus its tail, which is the block count ; in the high byte and the tail in the low one. Nothing has to multiply anything. SETD.0 SbfsFileBlocks @@ -308,7 +319,35 @@ dirCheck: CALL newLine BRI dirStep +dirIsDirectory: + SETD.0 DirFolders + LDA.0 + INCA + STA.0 + SETD.0 DirectoryText + CALL printString + CALL newLine + BRI dirStep + dirDone: + ; Directories were counted alongside the files and now come back out of the total, so + ; that "three files" means three files. Saying it any other way makes the number + ; disagree with the listing right above it, which is the sort of thing that teaches + ; somebody not to trust the listing. + SETD.0 DirFolders + LDA.0 + SETD.1 DirTaken + STA.1 + SETD.0 DirSeen + LDA.0 + SETD.1 DirTaken + LDB.1 + CCF + SUB + MVQA + SETD.0 DirSeen + STA.0 + SETD.0 DirSeen LDA.0 CALL printByteDecimal @@ -323,6 +362,28 @@ dirOne: SETD.0 FileText dirCount: CALL printString + + ; And how many of them were directories, but only when there were any. A disk with none + ; on it should read exactly the way it always did. + SETD.0 DirFolders + LDA.0 + BRA dirNoFolders + SETD.0 AndText + CALL printString + SETD.0 DirFolders + LDA.0 + CALL printByteDecimal + SETD.0 DirFolders + LDA.0 + DECA + BRA dirOneFolder + SETD.0 FoldersText + BRI dirFolderCount +dirOneFolder: + SETD.0 FolderText +dirFolderCount: + CALL printString +dirNoFolders: CALL newLine BRI prompt @@ -370,10 +431,11 @@ widthDone: ; typing what it is called, whatever is inside it. Only load reaches a file by its whole ; name, which is why the monitor can still put any file at all in front of itself. ; -; NameOk is one if there is a name in ProgramName and zero if the word could not be made -; into one. A directory entry holds twenty two characters and four of those are spoken for -; by the extension, so eighteen is as long as a bare name can be. Being refused here reads -; as an unknown command, which is the truth: no file of that name can exist. +; NameOk is one if there is a path in ProgramName and zero if the word could not be made +; into one. What limits it is the buffer, not the format: each NAME along a path is still +; twenty two characters, and the path walker refuses a longer one rather than cutting it +; down. Being refused here reads as an unknown command, which is the truth: nothing this +; shell can reach is called that. nameProgram: RSTA SETD.0 NameOk @@ -381,7 +443,7 @@ nameProgram: SETD.0 CommandLine SETD.1 ProgramName - INIB 0d22 ; How much of the twenty two is left. + INIB 0d59 ; What is left of the buffer, less the four for the extension. nameCopy: LDA.0 BRA nameCopied @@ -390,7 +452,7 @@ nameCopy: INCD.1 DECB BNB nameCopy - RET ; Twenty two characters and still going. Not a name. + RET ; Longer than the buffer holds, so it is not a path either. nameCopied: ; DP1 is on the byte after the word, which is where an extension would go, and B is what @@ -402,7 +464,7 @@ nameCopied: ; Only a word of four characters or more can already end in ".sbx". Stepping back four to ; look at a shorter one would read whatever happens to sit in front of the buffer. - INIA 0d18 + INIA 0d55 CCF SUB BRC nameAppend @@ -468,10 +530,10 @@ doLoad: LDA.0 BRA loadNothingNamed - ; The name exactly as typed. load is how a file is reached by its whole name, so nothing + ; The path exactly as typed. load is how a file is reached by its whole name, so nothing ; is added to it and nothing is assumed about what it ends in. SETD.1 ProgramName - INIB 0d23 + INIB 0d64 CALL copyText CALL loadProgram @@ -512,6 +574,7 @@ loadNothingNamed: ; 4 it is not a program ; 5 a version of the format this loader does not know ; 6 more vectors than there is room to keep +; 7 it is a directory ; ; Two is the one worth telling apart from the others. It is the only outcome where nothing ; was wrong with the disk or with a file, and so the only one a caller can fairly report as @@ -530,6 +593,17 @@ loadProgram: CALL sbfsFind BNQ loadMissing + ; A DIRECTORY IS REFUSED HERE AND NOT LEFT TO THE MAGIC CHECK BELOW. It has no blocks, + ; so reading it reads nothing and leaves the staging area holding whatever was staged + ; last - which, if that was a program, still says "SBEX" and still has a working entry + ; address in it. Loading a directory would quietly hand back the program before it, and + ; running it would look like the directory had run. + SETD.0 SbfsFoundFlags + LDA.0 + INIB 0x02 + AND + BNQ loadIsDirectory + SETD.1 0x80 0x00 CALL sbfsRead BNQ loadUnreadable @@ -739,6 +813,10 @@ loadWrongVersion: loadTooManyVectors: INIA 0d6 SETD.0 TooManyVectors + BRI loadRefuse +loadIsDirectory: + INIA 0d7 + SETD.0 IsDirectory loadRefuse: SETD.1 LoadMessage STD.0.1 @@ -774,9 +852,26 @@ doDelete: deleteWhat: SETD.0 DeleteWhat BRI fileComplain + deleteFailed: + ; Two refusals arrive here as one. Asking again costs a walk of the directory, which is + ; nothing on a path nobody takes twice, and it is the difference between "you typed a + ; name that is not there" and "that is a directory, and delete does not take those". + SETD.1 TextRest + LDD.0.1 + CALL sbfsFind + BNQ deleteNoSuch + SETD.0 SbfsFoundFlags + LDA.0 + INIB 0x02 + AND + BNQ deleteIsDirectory +deleteNoSuch: SETD.0 NoSuchFile BRI fileComplain +deleteIsDirectory: + SETD.0 IsDirectory + BRI fileComplain doRename: SETD.0 DiskReady @@ -1230,8 +1325,15 @@ fileLookupSearch: ; Remember it. DP0 still names the file: a CALL puts the pointers back, which is the one ; place that convention is a convenience rather than an obstacle. + ; + ; A WHOLE PATH IS KEPT, not the twenty two bytes a name has. Keeping twenty two of a + ; longer path cannot hand back the wrong file - textSame wants both strings to end in + ; the same place, so a cut down entry misses rather than matching something else - but + ; it can never match either, so every path longer than a name would go to the disk every + ; single time and the cache would quietly stop being one. SETD.1 FileCacheName - CALL sbfsKeepName + INIB 0d64 + CALL copyText SETD.0 FileCacheStart SETD.2 SbfsFileStart CALL sbfsSetWord @@ -2713,6 +2815,16 @@ Unknown: "I do not know: " Farewell: "halted" +DirectoryText: +"" +IsDirectory: +"that is a directory" +AndText: +", " +FoldersText: +" directories" +FolderText: +" directory" FilesText: " files" FileText: @@ -2861,6 +2973,10 @@ HelpName: ExitName: "exit" +DirFolders: + 0x00 +DirTaken: + 0x00 DiskReady: 0x00 LoadedOk: @@ -2872,10 +2988,16 @@ LoadStatus: LoadMessage: 0x00 0x00 -; The name of the file to load, which load copies out of the line as typed and a typed -; program name is built into. Twenty two characters and the zero that ends them. +; THE PATH of the file to load, which load copies out of the line as typed and a typed +; program name is built into. +; +; Sixty four rather than the twenty three a NAME needs. It held a name when a disk was +; flat and there was nothing else to hold, and leaving it that size once paths existed cut +; every path longer than twenty two characters down to twenty two - which is not a failure +; that looks like one. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/", +; resolved perfectly well, and reported that the program was a directory. ProgramName: - #Reserve 0d23 + #Reserve 0d64 NameOk: 0x00 NameLeft: @@ -2924,7 +3046,7 @@ PrintNumber: FileCacheValid: 0x00 FileCacheName: - #Reserve 0d23 + #Reserve 0d64 FileCacheStart: 0x00 0x00 FileCacheBlocks: diff --git a/Programs/CosmOS/Source/sbfs.asm b/Programs/CosmOS/Source/sbfs.asm index e39871d..4a48b18 100644 --- a/Programs/CosmOS/Source/sbfs.asm +++ b/Programs/CosmOS/Source/sbfs.asm @@ -60,13 +60,25 @@ sbfsMagicSame: BNA sbfsMagicLoop sbfsMagicDone: - ; The version has to be one we understand. + ; The version has to be one we understand, and there are two of them. + ; + ; Version one is flat: every file is in the root because there is nowhere else. Version + ; two gives each entry a parent, and writes it as the entry's index PLUS ONE - so the + ; zeroes a version one disk has in those bytes read as "in the root", which is exactly + ; where all of its files are. A version one disk needs nothing done to it to be read + ; here, and that is why both numbers are taken rather than one being converted. SETD.0 SbfsBuffer DPUP.0 0d04 LDA.0 INIB 0d1 XOR BRQ sbfsGeometry + SETD.0 SbfsBuffer + DPUP.0 0d04 + LDA.0 + INIB 0d2 + XOR + BRQ sbfsGeometry RET ; A version we do not know. sbfsGeometry: @@ -90,60 +102,155 @@ sbfsGeometry: ADD ; Q is zero: mounted. RET -; Finds a file by name. DP0 points at a name, ending in a zero byte. Q is zero if it was -; found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it. -sbfsFind: - SETD.1 SbfsWanted - CALL sbfsKeepName +; ---- Finding something by path ---- +; +; DP0 points at a path ending in a zero byte: names with '/' between them, walked from the +; root. Q is zero if it was found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail +; describe it, SbfsFoundFlags says what kind of thing it is, and DP3 is left on the entry +; with SbfsBlock on the directory block it came out of - which is what deleting and +; renaming need in order to change it and put it back. +; +; A BARE NAME IS A PATH OF ONE NAME, so everything written before there were directories +; still works and still costs one walk of the directory. +; +; Each name is looked for among the entries whose parent is where the walk has got to. +; There is no list of children anywhere: being a child is a fact written in the child, so +; finding them means looking at all of them. That is the same walk the flat version did +; with one more thing compared, which is why a flat disk costs what it always did - and on +; a version one disk every entry says "the root", so the comparison is free and true. - ; Start at the first directory block and work through them all. - SETD.0 SbfsDirStart - SETD.1 SbfsBlock - CALL sbfsCopyWord - SETD.0 SbfsDirBlocks +sbfsFind: + SETD.1 SbfsPathAt + STD.0.1 + + ; The walk starts at the root, which is zero: a parent is an entry index PLUS ONE, and + ; the root is not an entry. + SETD.0 SbfsAt + RSTA + STA.0 + INCD.0 + STA.0 + + ; Nothing described yet. An empty path comes back as missing, which is right: the root + ; is not a file. + RSTA + SETD.0 SbfsFieldsValid + STA.0 + +sbfsFindName: + CALL sbfsPathNext + SETD.0 SbfsPathState + LDA.0 + BRA sbfsFindGotName + INIB 0d1 + CCF + SUB + BRQ sbfsFindEnd ; The path ran out. + BRI sbfsFindMissing ; A name longer than a name can be, so nothing is called it. + +sbfsFindGotName: + ; "." and ".." belong to the filesystem and are answered here rather than looked for. + ; Nothing on a disk is ever called either of them, so this takes nothing away. + SETD.0 SbfsWanted + LDA.0 + INIB 0x2E + CCF + SUB + BNQ sbfsFindLook INCD.0 LDA.0 - SETD.1 SbfsLeft - STA.1 ; Only the low byte: a directory of 256 blocks is 2048 files. + BRA sbfsFindName ; "." on its own is where the walk already is. + INIB 0x2E + CCF + SUB + BNQ sbfsFindLook + INCD.0 + LDA.0 + BRA sbfsFindUp ; ".." on its own goes back the way it came. -sbfsFindBlock: - CALL sbfsReadBlock - BRQ sbfsFindLoaded - RET ; The read failed. -sbfsFindLoaded: - SETD.1 SbfsBuffer - CALL sbfsBufferOut - - ; Eight entries to a block, thirty two bytes each. - SETD.2 SbfsBuffer - INIA 0d8 - SETD.1 SbfsCount - STA.1 - -sbfsFindEntry: - LDA.2 - INIB 0x01 +sbfsFindLook: + ; Only a directory can be looked inside. Where the walk is now was put there by the last + ; scan, so what kind of thing it is has already been read out of its entry. + SETD.0 SbfsAt + LDA.0 + INCD.0 + LDB.0 + OR + BRQ sbfsFindScan ; At the root, which can always be looked inside. + SETD.0 SbfsFoundFlags + LDA.0 + INIB 0x02 AND - BRQ sbfsFindNext ; The in use bit is down, so this entry is free. - CALL sbfsMatchEntry - BRQ sbfsFindFound + BRQ sbfsFindMissing ; Not a directory, so there is nothing inside it. -sbfsFindNext: - DPUP.2 0d32 - LDA.1 - DECA - STA.1 - BNA sbfsFindEntry +sbfsFindScan: + CALL sbfsScanFor + BNQ sbfsFindMissing + BRI sbfsFindName -sbfsFindNextBlock: - SETD.0 SbfsBlock - CALL sbfsStepWord - SETD.1 SbfsLeft - LDA.1 - DECA - STA.1 - BRA sbfsFindMissing - BRI sbfsFindBlock +sbfsFindUp: + ; ".." from the root is the root. Every filesystem answers this way, and it is the only + ; answer that cannot walk off the top of the disk. + SETD.0 SbfsAt + LDA.0 + INCD.0 + LDB.0 + OR + BRQ sbfsFindName + + ; Where the walk is, as an index rather than as a parent. + SETD.0 SbfsAt + SETD.1 SbfsTarget + CALL sbfsCopyWord + SETD.0 SbfsTarget + CALL sbfsBackWord + + CALL sbfsAtIndex + BNQ sbfsFindMissing ; The disk would not read. + + SETD.0 SbfsUpParent + SETD.1 SbfsAt + CALL sbfsCopyWord + + ; What it landed on is a directory, because a parent always is. What describes it, on + ; the other hand, is the entry it came FROM - so that is marked as not to be believed, + ; and loaded properly if the path stops here. + INIA 0x02 + SETD.0 SbfsFoundFlags + STA.0 + RSTA + SETD.0 SbfsFieldsValid + STA.0 + BRI sbfsFindName + +sbfsFindEnd: + ; Did the walk get anywhere at all? The root is not a file. + SETD.0 SbfsAt + LDA.0 + INCD.0 + LDB.0 + OR + BRQ sbfsFindMissing + + SETD.0 SbfsFieldsValid + LDA.0 + BNA sbfsFindGood + + ; The path ended on a "..", so what describes the entry is one place out of date. + SETD.0 SbfsAt + SETD.1 SbfsTarget + CALL sbfsCopyWord + SETD.0 SbfsTarget + CALL sbfsBackWord + CALL sbfsAtIndex + BNQ sbfsFindMissing + +sbfsFindGood: + RSTA + RSTB + CCF + ADD ; Q is zero: found. + RET sbfsFindMissing: ; Nothing of that name. Q has to be something other than zero to say so. @@ -153,13 +260,295 @@ sbfsFindMissing: ADD RET -sbfsFindFound: +; ---- Taking a path apart ---- +; +; Copies the next name out of the path into SbfsWanted, padded with zeroes to twenty two +; the way an entry holds one, and moves SbfsPathAt past it. +; +; SbfsPathState says what happened: zero for a name, one for the end of the path, two for +; a name longer than a name can be. THE LAST IS REFUSED RATHER THAN CUT SHORT, because a +; name cut to twenty two characters is a different name, and might well be some other +; file's. +; +; Separators are skipped rather than counted, so "/Apps/", "Apps" and "//Apps" all walk +; the same way and a trailing one simply ends the path. +sbfsPathNext: + RSTA + SETD.0 SbfsPathState + STA.0 + + SETD.0 SbfsPathAt + LDD.1.0 + +sbfsPathSkip: + LDA.1 + BRA sbfsPathEnd + INIB 0x2F + CCF + SUB + BNQ sbfsPathName + INCD.1 + BRI sbfsPathSkip + +sbfsPathEnd: + INIA 0d1 + SETD.0 SbfsPathState + STA.0 + BRI sbfsPathSave + +sbfsPathName: + SETD.2 SbfsWanted + INIA 0d22 + SETD.0 SbfsPathLeft + STA.0 + +sbfsPathCopy: + LDA.1 + BRA sbfsPathPad ; The path ended, so the name ended with it. + INIB 0x2F + CCF + SUB + BRQ sbfsPathPad ; A separator ends the name. + + SETD.0 SbfsPathLeft + LDA.0 + BRA sbfsPathTooLong ; Twenty two already, and there is still more of it. + DECA + STA.0 + + LDA.1 + STA.2 + INCD.1 + INCD.2 + BRI sbfsPathCopy + +sbfsPathPad: + ; Zeroes out to the twenty two, so that comparing the whole field compares the name. + SETD.0 SbfsPathLeft + LDA.0 + BRA sbfsPathSave +sbfsPathPadLoop: + RSTA + STA.2 + INCD.2 + SETD.0 SbfsPathLeft + LDA.0 + DECA + STA.0 + BNA sbfsPathPadLoop + +sbfsPathSave: + SETD.0 SbfsPathAt + STD.1.0 + RET + +sbfsPathTooLong: + INIA 0d2 + SETD.0 SbfsPathState + STA.0 + RET + +; ---- Looking in one directory ---- +; +; Looks for the name in SbfsWanted among the entries whose parent is SbfsAt. Q is zero if +; it is there, and then the walk moves onto it and everything that describes it is read +; out of its entry. DP3 is left on that entry. +sbfsScanFor: + SETD.0 SbfsDirStart + SETD.1 SbfsBlock + CALL sbfsCopyWord + SETD.0 SbfsDirBlocks + INCD.0 + LDA.0 + SETD.1 SbfsLeft + STA.1 ; Only the low byte: a directory of 256 blocks is 2048 entries. + + ; Entries are COUNTED rather than worked out from where they sit. An index becomes a + ; block and an offset by dividing by eight, and this machine has no divide; counting + ; costs one step per entry through a walk that was happening anyway. + SETD.0 SbfsScanIndex + RSTA + STA.0 + INCD.0 + STA.0 + +sbfsScanBlock: + CALL sbfsReadBlock + BRQ sbfsScanLoaded + RET ; The read failed. +sbfsScanLoaded: + SETD.1 SbfsBuffer + CALL sbfsBufferOut + + ; Eight entries to a block, thirty two bytes each. + SETD.2 SbfsBuffer + INIA 0d8 + SETD.1 SbfsCount + STA.1 + +sbfsScanEntry: + LDA.2 + INIB 0x01 + AND + BRQ sbfsScanNext ; The in use bit is down, so this entry is free. + CALL sbfsMatchParent + BNQ sbfsScanNext ; It lives somewhere else. + CALL sbfsMatchEntry + BRQ sbfsScanFound + +sbfsScanNext: + SETD.0 SbfsScanIndex + CALL sbfsStepWord + DPUP.2 0d32 + LDA.1 + DECA + STA.1 + BNA sbfsScanEntry + +sbfsScanNextBlock: + SETD.0 SbfsBlock + CALL sbfsStepWord + SETD.1 SbfsLeft + LDA.1 + DECA + STA.1 + BRA sbfsScanMissing + BRI sbfsScanBlock + +sbfsScanMissing: + RSTA + INIB 0d1 + CCF + ADD + RET + +sbfsScanFound: ; DP2 is on the entry. Keep it in DP3 while the pieces are taken out of it: DP3 is the ; pointer a CALL does not put back, so it is the only one that survives a subroutine. - ; Copying it into DP0 has to be done here rather than in a routine of its own, for the - ; same reason: a routine that set DP0 would have the assignment undone by its own RET. PSHD.2 POPD.3 + CALL sbfsTakeEntry + + ; The walk moves onto what it found, written the way a parent is written. + SETD.0 SbfsScanIndex + SETD.1 SbfsAt + CALL sbfsCopyWord + SETD.0 SbfsAt + CALL sbfsStepWord + + RSTA + RSTB + CCF + ADD ; Q is zero: found. + RET + +; DP2 is on an entry. Q is zero if it lives in the directory the walk is looking in. +; +; On a version one disk these two bytes are zero in every entry, and the walk starts at +; zero, so this always agrees - which is exactly how a flat disk reads correctly here +; without anything having been done to it. +sbfsMatchParent: + PSHD.2 + POPD.0 + DPUP.0 0d28 + SETD.2 SbfsAt + CALL sbfsSameByte + BNQ sbfsMatchParentDone + INCD.0 + INCD.2 + CALL sbfsSameByte +sbfsMatchParentDone: + RET + +; ---- Going back up ---- +; +; Walks the directory to the entry SbfsTarget names and takes it apart, SbfsUpParent +; included. Q is zero if it got there. +; +; WALKING TO AN INDEX RATHER THAN WORKING OUT WHERE IT SITS. An index becomes a block and +; an offset by dividing by eight, and this machine has no divide. The walk costs a few +; block reads and no arithmetic at all, and only ".." ever needs it. +sbfsAtIndex: + SETD.0 SbfsDirStart + SETD.1 SbfsBlock + CALL sbfsCopyWord + SETD.0 SbfsDirBlocks + INCD.0 + LDA.0 + SETD.1 SbfsLeft + STA.1 + + SETD.0 SbfsScanIndex + RSTA + STA.0 + INCD.0 + STA.0 + +sbfsAtBlock: + CALL sbfsReadBlock + BRQ sbfsAtLoaded + RET ; The read failed. +sbfsAtLoaded: + SETD.1 SbfsBuffer + CALL sbfsBufferOut + SETD.2 SbfsBuffer + INIA 0d8 + SETD.1 SbfsCount + STA.1 + +sbfsAtEntry: + ; Where the entry is has to be put down for the length of the comparison, because + ; comparing two numbers wants DP2 for one of them. + PSHD.2 + SETD.0 SbfsScanIndex + SETD.2 SbfsTarget + CALL sbfsCompareWord + POPD.2 + BRQ sbfsAtFound + + SETD.0 SbfsScanIndex + CALL sbfsStepWord + DPUP.2 0d32 + LDA.1 + DECA + STA.1 + BNA sbfsAtEntry + + SETD.0 SbfsBlock + CALL sbfsStepWord + SETD.1 SbfsLeft + LDA.1 + DECA + STA.1 + BRA sbfsAtMissing + BRI sbfsAtBlock + +sbfsAtMissing: + RSTA + INIB 0d1 + CCF + ADD + RET + +sbfsAtFound: + PSHD.2 + POPD.3 + CALL sbfsTakeEntry + RSTA + RSTB + CCF + ADD + RET + +; DP3 is on an entry. Takes it apart into everything that describes what was found. +; +; Written once and called from both the scan and the walk to an index, because two copies +; of "which byte means what" is the kind of thing that stays right until one of them is +; changed. +sbfsTakeEntry: + LDA.3 + SETD.0 SbfsFoundFlags + STA.0 PSHD.3 POPD.0 @@ -180,10 +569,29 @@ sbfsFindFound: SETD.1 SbfsFileTail STA.1 - RSTA - RSTB - CCF - ADD ; Q is zero: found. + PSHD.3 + POPD.0 + DPUP.0 0d28 + SETD.1 SbfsUpParent + CALL sbfsCopyWord + + INIA 0x01 + SETD.0 SbfsFieldsValid + STA.0 + RET + +; Takes one from the two byte number at DP0, the other way round from sbfsStepWord. +sbfsBackWord: + INCD.0 + LDA.0 + DECA + STA.0 + BNC sbfsBackDone ; It did not borrow, so the high byte is untouched. + DECD.0 + LDA.0 + DECA + STA.0 +sbfsBackDone: RET ; ---- Walking the directory ---- @@ -306,6 +714,15 @@ sbfsWalkEnd: ; Takes the pieces out of the entry the walk stopped on. Everything lands in memory, ; because that is the only place a subroutine can leave anything. sbfsWalkTake: + ; What kind of thing it is, so that a listing can say so. A directory has no blocks, and + ; without this it would be handed out as a file of no bytes - which is a different thing + ; that happens to look the same from here. + SETD.1 SbfsWalkAt + LDD.0.1 + LDA.0 + SETD.1 SbfsFoundFlags + STA.1 + SETD.1 SbfsWalkAt LDD.0.1 INCD.0 @@ -606,6 +1023,26 @@ sbfsAllocEntry: INIB 0x01 AND BRQ sbfsAllocNext ; A free entry holds nothing, so it is in nobody's way. + + ; NOR DOES A DIRECTORY, and this says so rather than leaving it to be true by accident. + ; + ; It would be true by accident. A directory has no blocks AND no start, so the bounds + ; below come out as nought to nought, and the first half of the overlap test - does the + ; candidate begin before this entry ends - is false for every candidate there is, since + ; they all begin past the directory. Taking these four instructions out changes nothing + ; that any test can see, and that was checked rather than assumed. + ; + ; They stay because what keeps them right is a value written somewhere else. The day + ; something gives a directory entry a start - a note about where its children begin, a + ; use for a field that is spare today - the overlap test starts answering yes to runs + ; that straddle it, and blocks stop being usable for no reason anybody could see from + ; here. Four instructions to say "a directory is in nobody's way" where it is meant is + ; cheaper than finding that out. + LDA.2 + INIB 0x02 + AND + BNQ sbfsAllocNext + CALL sbfsEntryBounds ; The comparisons need DP2 for their own purposes, so where this entry is goes on the @@ -798,6 +1235,20 @@ sbfsCreateFill: LDA.0 STA.1 + ; THE ROOT, SAID OUT LOUD. A new file goes in the root because nothing here can put one + ; anywhere else yet, and the two bytes that say so are written rather than assumed to be + ; zero already. They would be - a free entry has been wiped by delete or has never been + ; used - but that is a fact about two other routines, and a fact kept somewhere else is + ; one that can be changed without this noticing. A file appearing inside a directory it + ; was never put in is not a failure anybody would think to look for. + PSHD.3 + POPD.1 + DPUP.1 0d28 + RSTA + STA.1 + INCD.1 + STA.1 + PSHD.3 POPD.1 DPUP.1 0d06 @@ -1059,6 +1510,17 @@ sbfsDelete: CALL sbfsFind BNQ sbfsDeleteFailed + ; A DIRECTORY IS NOT DELETED HERE, AND THAT REFUSAL IS NOT POLITENESS. A parent is an + ; entry index, and a freed index is handed straight to the next thing put on the disk - + ; so the children of a directory wiped from under them would reappear inside whatever + ; took its place. Nothing points downward, so there is no way to find them afterwards + ; and no way to notice. Emptying it first is the only safe order there is. + SETD.0 SbfsFoundFlags + LDA.0 + INIB 0x02 + AND + BNQ sbfsDeleteFailed + ; How much room it was taking, worked out before the entry that says so is thrown away. CALL sbfsFileExtent @@ -1110,6 +1572,10 @@ sbfsDeleteFailed: ; ; DP0 is the name a file has, DP1 is the name it should have. Q is zero if it was renamed. ; +; A file keeps the directory it is in: only the twenty two bytes of the name change, and +; the parent is not among them. Moving something between directories is a different job +; and is not this one. +; ; Only the twenty two bytes of the name change, so no data moves and no block is touched ; but the one holding the entry. THAT IS WHAT MAKES A SAFE SAVE POSSIBLE. Writing a file ; that has grown means putting it somewhere else, and the obvious order - throw the old one @@ -1138,6 +1604,15 @@ sbfsRename: CALL sbfsFind BNQ sbfsRenameFailed + ; Not a directory. Renaming one would be safe enough, but the shell has no way to make + ; one yet, so allowing it here would only be a way to reach something that cannot be + ; got at any other way. + SETD.0 SbfsFoundFlags + LDA.0 + INIB 0x02 + AND + BNQ sbfsRenameFailed + PSHD.3 POPD.1 DPUP.1 0d06 ; Past the flags, the start, the block count and the tail. @@ -1201,6 +1676,20 @@ sbfsSaveFile: SETD.1 SbfsSaveTail STA.1 + ; Refused up front if the name belongs to a directory. Left to itself the delete below + ; would refuse it, the rename at the end would refuse it too, and the save would fail + ; having already written a temporary that nothing would ever come back for. + SETD.2 SbfsSaveName + LDD.0.2 + CALL sbfsFind + BNQ sbfsSaveNotThere + SETD.0 SbfsFoundFlags + LDA.0 + INIB 0x02 + AND + BNQ sbfsSaveFailed +sbfsSaveNotThere: + ; A temporary left behind by a save that did not finish would be in the way. Whether ; there was one is not worth asking about, since either answer leads here. SETD.0 SbfsTempName @@ -1281,6 +1770,33 @@ SbfsMatchLeft: 0x00 SbfsLeft: 0x00 +; ---- What walking a path keeps ---- +; +; SbfsAt is where the walk has got to, written the way a parent is written: an entry index +; plus one, so that zero is the root and a version one disk's zeroes already say it. +SbfsPathAt: + 0x00 0x00 +SbfsPathState: + 0x00 +SbfsPathLeft: + 0x00 +SbfsAt: + 0x00 0x00 +SbfsScanIndex: + 0x00 0x00 +SbfsTarget: + 0x00 0x00 +SbfsUpParent: + 0x00 0x00 +SbfsFoundFlags: + 0x00 + +; Whether what is in SbfsFileStart and the rest really describes where the walk is now. +; Going up leaves them describing the entry it came from, and only the end of a path is +; close enough to care. +SbfsFieldsValid: + 0x00 + SbfsIndex: 0x00 0x00 diff --git a/Tests/expected/cosmosBreak.out b/Tests/expected/cosmosBreak.out index bdbe709..9c7e016 100644 --- a/Tests/expected/cosmosBreak.out +++ b/Tests/expected/cosmosBreak.out @@ -3,11 +3,11 @@ CosmOS > two stops, and what the registers were at each break at 200E A 11 B 22 Q 00 status 00 -DP0 1030 DP1 06C2 DP2 0000 DP3 2000 SP FFFF +DP0 1030 DP1 0730 DP2 0000 DP3 2000 SP FFFF press a key break at 2023 A 44 B 55 Q 00 status 00 -DP0 1000 DP1 06C2 DP2 0000 DP3 2000 SP FFF5 +DP0 1000 DP1 0730 DP2 0000 DP3 2000 SP FFF5 press a key carried on to the end finished diff --git a/Tests/expected/cosmosTree.out b/Tests/expected/cosmosTree.out new file mode 100644 index 0000000..134e9c5 --- /dev/null +++ b/Tests/expected/cosmosTree.out @@ -0,0 +1,35 @@ +CosmOS +> Apps +Deep +Say.sbx 155 +Say.sbx 52 +rooted.txt 21 +3 files, 2 directories +> loaded, starting at 2000 +> it says: the shallow one +finished +> loaded, starting at 2000 +> Hello, World! +finished +> loaded, starting at 2000 +> it says: the shallow one again +finished +> loaded, starting at 2000 +> Hello, World! +finished +> no such file +> no such file +> that is a directory +> no such file +> no such file +> no such file +> that is a directory +> gone +> Apps +Deep +Say.sbx 155 +rooted.txt 21 +2 files, 2 directories +> halted +Execution halted. +[exit 0] diff --git a/Tests/expected/cosmosTreeWrite.out b/Tests/expected/cosmosTreeWrite.out new file mode 100644 index 0000000..53f030c --- /dev/null +++ b/Tests/expected/cosmosTreeWrite.out @@ -0,0 +1,17 @@ +CosmOS +> Folder +Inner +Edit.sbx 1996 +1 file, 2 directories +> loaded, starting at 2000 +> note.txt, 0 lines +> : : : > written, 67 bytes +> finished +> Folder +Inner +Edit.sbx 1996 +note.txt 67 +2 files, 2 directories +> halted +Execution halted. +[exit 0] diff --git a/Tests/input/cosmosTree.in b/Tests/input/cosmosTree.in new file mode 100644 index 0000000..be90f67 --- /dev/null +++ b/Tests/input/cosmosTree.in @@ -0,0 +1,19 @@ +dir +load /Apps/Say.sbx +run the shallow one +load /Apps/Deep/Say.sbx +run the deep one +load /Apps/./Deep/../Say.sbx +run the shallow one again +load /Apps/Deep/../../Apps/./Deep/Say.sbx +run the deep one the long way round +load /rooted.txt/Say.sbx +load /Apps/Missing/Say.sbx +load /Apps +load /Apps/abcdefghijklmnopqrstuvw +load / +load /Apps/Deep/../.. +delete /Apps +delete /Apps/Deep/Say.sbx +dir +exit diff --git a/Tests/input/cosmosTreeWrite.in b/Tests/input/cosmosTreeWrite.in new file mode 100644 index 0000000..cfaaf14 --- /dev/null +++ b/Tests/input/cosmosTreeWrite.in @@ -0,0 +1,11 @@ +dir +load Edit.sbx +run note.txt +a +a line written onto a disk with directories on it +and a second one +. +w +q +dir +exit diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index e2a19c2..61fe847 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -9,7 +9,13 @@ # Written by Anachronaut set -eu -BUILD="$1" + +# Made absolute before anything else. This script cds into its own working directory part +# of the way down, so a relative build path would be measured from the wrong place from +# there on - and the way that failed was not an error but a disk quietly missing some of +# the files it was supposed to have, which is a much worse thing to debug. +mkdir -p "$1" +BUILD="$(cd "$1" && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)" TOOL="$ROOT/SplitDisk" DISKS="$BUILD/disks" @@ -135,6 +141,17 @@ printf 'the second one' > two.txt "$ROOT/Programs/CosmOS/Apps/Edit.asm" -o "$WORK/Edit.sbx" >/dev/null "$TOOL" put "$DISKS/editor.img" "$WORK/Edit.sbx" >/dev/null +# A disk that has directories on it AND gets written to, which nothing else covers: every +# other version two fixture is read only, and every fixture that is written to is flat. +# Making a file runs the allocator over a directory entry, and a directory has no blocks, +# so it must be in nobody's way while a run of free ones is looked for. Its own disk, for +# the same reason the editor has one: what it writes would otherwise turn up in the +# listing of every test that came after it. +"$TOOL" format "$DISKS/treewrite.img" 256 2 >/dev/null +"$TOOL" mkdir "$DISKS/treewrite.img" /Folder >/dev/null +"$TOOL" mkdir "$DISKS/treewrite.img" /Folder/Inner >/dev/null +"$TOOL" put "$DISKS/treewrite.img" "$WORK/Edit.sbx" >/dev/null + # A disk for the file services, holding nothing but the program that exercises them. Its # own, because that program writes: it tidies up after itself, but a run that stopped part # way would leave a document behind on a fixture every later test reads. @@ -177,6 +194,31 @@ awk 'BEGIN { for (i = 0; i < 30; i++) printf "line %02d: ABCDEFGHIJKLMNOPQRSTUVW "$ROOT/Programs/CosmOS/Apps/More.asm" -o "$WORK/More.sbx" >/dev/null "$TOOL" put "$DISKS/type.img" "$WORK/More.sbx" >/dev/null +# A disk with directories on it, which is the whole of what version two adds. Built by the +# host tool, because at this rung the machine can read a tree and not yet make one - and +# that split is the point: the two implementations are checked against each other rather +# than each against itself. +# +# ONE NAME IS PUT DOWN TWICE, in two directories, because a name meaning something +# different in each place is what directories are for and what a flat filesystem cannot do. +# +# The two are DIFFERENT PROGRAMS on purpose. Say echoes whatever it is given and hello +# prints one fixed line, so which of them ran is written in the output. Two copies of one +# program would have been the easier fixture and a useless one: resolving to the wrong +# Say.sbx would have printed exactly what resolving to the right one prints, and the test +# would have passed while the path walk was ignoring directories entirely. +"$TOOL" format "$DISKS/tree.img" 256 4 >/dev/null +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/hello.asm" -o "$WORK/treeHello.sbx" >/dev/null +"$TOOL" mkdir "$DISKS/tree.img" /Apps >/dev/null +"$TOOL" mkdir "$DISKS/tree.img" /Apps/Deep >/dev/null +"$TOOL" put "$DISKS/tree.img" "$WORK/Say.sbx" /Apps/Say.sbx >/dev/null +"$TOOL" put "$DISKS/tree.img" "$WORK/treeHello.sbx" /Apps/Deep/Say.sbx >/dev/null +printf 'this is not a program' > rooted.txt +"$TOOL" put "$DISKS/tree.img" rooted.txt >/dev/null + # A disk for invoking a program by typing its name. Four files, each there to say one # thing about how a typed word turns into a file name. # diff --git a/Tests/manifest b/Tests/manifest index e3e7b7d..79f96da 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -324,6 +324,25 @@ cosmosType | CosmOS/Source/cosmos.asm | run | cosmosTyp # with Space, and one line with Return. The input is intentionally packed so that the one # byte the pager consumes leaves the next shell command immediately behind it. cosmosMore | CosmOS/Source/cosmos.asm | run | cosmosMore.in | - | disks/type.img +# Reading a disk that has directories on it. The machine can walk a path at this point but +# cannot make a directory, so the disk is built by the host tool and read here - which is +# the two implementations checking each other rather than either checking itself. +# +# The two Say.sbx are DIFFERENT PROGRAMS under one name - Say echoes its argument, hello +# prints one fixed line - so which one a path reached is written in the output. Two copies +# of the same program would have passed this test with the parent comparison removed +# altogether, which is the one thing it exists to check. +# Then the ways a path does not resolve: through a file, into a directory that is not +# there, onto a directory rather than a program, a component too long to be a name, and +# the root itself. Last, that delete refuses a directory - the refusal that keeps a freed +# entry index from being handed out with children still pointing at it. +cosmosTree | CosmOS/Source/cosmos.asm | run | cosmosTree.in | - | disks/tree.img +# Writing a file onto a disk that has directories on it. The document goes in the root, +# because nothing on the machine can put one anywhere else yet, and the point is that it +# gets there at all: making a file walks the allocator over two directory entries, which +# hold no blocks and must therefore be in nobody's way when a run of free ones is wanted. +# The listing afterwards says where it landed and how big it is. +cosmosTreeWrite | CosmOS/Source/cosmos.asm | run | cosmosTreeWrite.in | - | disks/treewrite.img # Typing a program's name starts it. The disk is built so that each line of the input asks # a different question of the one rule: "Say hello there" is a bare name with an argument, # "Say.sbx" is the same file spelled out in full, and "run once more" says the program that