Configuration files, and boot.cfg as the first of them

One setting to a line: a key, a space, the rest of the line is the value.
A semicolon starts a comment. The format was noticed rather than designed -
textSplit already cuts the first word off a line and leaves the rest, and
textSame already insists two strings end together, so reading a setting is
those two routines and a loop. It is also what the shell reads, which makes
a configuration line a command line the machine reads instead of a person
typing one.

The format was chosen by asking what the BOOT LOADER could manage, because
it is the worst case in every direction: a few kilobytes, no operating
system to report to, and if it fails the machine does not start. Two
formats would be worse than one and the loader cannot have the richer one.

CONFIGURATION IS ADVICE. A missing file, a missing key, an unusable value,
a line too long to read: all of them mean use the default and none is a
failure. BUT QUIET IS NOT SILENT - a setting somebody meant, which did not
take effect, says so. That was the user's addition and it is the better
rule: the default alone leaves the only symptom being that the machine did
not do what somebody asked.

So two routines. cfgGet reads and says nothing, because reading three
settings should not report one bad line three times. cfgCheck reads the
file once and reports, and is handed the caller's list of keys - whether a
key means anything is the only part of this a shared reader cannot judge.

/System/Boot/ holds the boot files, and stage two reads boot.cfg for what
to start, with a fallback to try if it does not work and a name compiled in
for when the file says nothing.

THE TEST FOUND A REAL BUG, and it is the interaction I would not have
thought to look for. First-match-wins met an empty value: a file with

  system
  system /System/Boot/bare.bin

matched the first line, handed back nothing, and the machine tried to start
a file with no name while a good setting sat underneath. An unusable value
is an absent one - which is what "configuration is advice" says, and this
is where it earns its keep.

cfgBare starts an image with no operating system in it at all, which is
what loading an ordinary boot image buys: a program wanting the whole
machine is a file like any other, chosen the same way the system is. Three
disks differing ONLY in boot.cfg, so each is a test of the file rather than
of the machinery under it.
This commit is contained in:
Anachronaut
2026-08-27 16:04:21 -04:00
parent 54ff7196c9
commit 546f336823
11 changed files with 767 additions and 12 deletions
+469
View File
@@ -0,0 +1,469 @@
; config.asm
; Reading a configuration file.
;
; One setting to a line: a key, a space, and the rest of the line is the value. A semicolon
; starts a comment and a blank line is nothing. That format was not designed so much as
; noticed - textSplit already cuts the first word off a line and leaves the rest, and
; textSame already compares two strings and insists they end together, so reading a setting
; is those two routines and a loop. It is also the shape the shell reads, which means a
; configuration line is a command line the machine reads instead of a person typing one.
;
; ---- Configuration is advice ----
;
; A file that is not there, a key that is not in it, and a value that makes no sense are
; all the same answer: use the default. Nothing here reports a failure for any of them,
; because a program that cannot run without its configuration has turned its configuration
; into a single point of failure - and for the thing that starts the machine, that would
; mean a mistyped file is a machine that does not start.
;
; ---- But quiet is not the same as silent ----
;
; A setting that was meant and did not take effect should say so, or the only symptom is
; that the machine did not do what somebody asked it to. So there are TWO routines rather
; than one: cfgGet reads, and never says anything; cfgCheck reads the whole file once and
; reports what it did not understand.
;
; They are separate because they know different things. A malformed line is malformed to
; anybody, so cfgGet's own scan could spot one - but reading three settings would then
; report the same bad line three times. And an unknown KEY is not something this can judge
; at all: only the caller knows which keys mean anything to it, which is why cfgCheck is
; given a table of them.
;
; Written by Anachronaut
#Program
; ---- Reading the file in ----
;
; DP0 names a path, DP1 names a buffer, and A is how many blocks the buffer holds.
;
; Q is zero if there is something to read, INCLUDING WHEN THERE IS NO FILE. A missing
; configuration file is a file with no settings in it, which is a perfectly ordinary thing
; for a disk to have, and the caller wanting the defaults gets them either way.
cfgLoad:
SETD.2 CfgRoom
STA.2
SETD.2 CfgBufferAt
STD.1.2
; Nothing loaded until something is.
RSTA
SETD.2 CfgLength
STA.2
INCD.2
STA.2
CALL sbfsFind
BNQ cfgNoFile
; A file too big for the buffer is read as far as it fits rather than refused: the
; settings at the top still work, and the ones past the end are missing keys, which is
; a case every caller already handles.
SETD.2 SbfsFileBlocks
INCD.2
LDA.2
SETD.2 CfgRoom
LDB.2
CCF
SUB
BNC cfgTooBig
SETD.2 CfgBufferAt
LDD.1.2
CALL sbfsRead
BNQ cfgNoFile
; How many bytes of it are real. A file is whole blocks and then a tail, which is the
; block count in the high byte and the tail in the low one.
SETD.2 SbfsFileBlocks
INCD.2
LDA.2
SETD.2 CfgLength
STA.2
SETD.2 SbfsFileTail
LDA.2
SETD.2 CfgLength
INCD.2
STA.2
cfgReady:
RSTA
RSTB
CCF
ADD
RET
cfgTooBig:
; As much of it as there is room for, which is every whole block of the buffer.
SETD.2 CfgRoom
LDA.2
SETD.2 SbfsFileBlocks
INCD.2
STA.2
RSTA
SETD.2 SbfsFileTail
STA.2
SETD.2 CfgBufferAt
LDD.1.2
CALL sbfsRead
BNQ cfgNoFile
SETD.2 CfgRoom
LDA.2
SETD.2 CfgLength
STA.2
RSTA
INCD.2
STA.2
BRI cfgReady
cfgNoFile:
; No file, or a disk that would not give it up. Neither is a failure: it is a file with
; nothing in it, and every key will be missing, which is what a default is for.
RSTA
SETD.2 CfgLength
STA.2
INCD.2
STA.2
BRI cfgReady
; ---- Walking it a line at a time ----
;
; cfgRewind puts the walk back at the top. cfgLine copies the next line into CfgLine and
; leaves Q zero if there was one.
;
; A line longer than the buffer holds is copied as far as it goes and the rest of it is
; skipped, with CfgLong set to say so. Ignoring it is what the format asks for; saying that
; it was ignored is what cfgCheck is for.
cfgRewind:
SETD.0 CfgBufferAt
SETD.1 CfgWalk
CALL sbfsCopyWord
SETD.0 CfgLength
SETD.1 CfgLeft
CALL sbfsCopyWord
RET
cfgLine:
RSTA
SETD.0 CfgLong
STA.0
SETD.0 CfgFill
STA.0
SETD.0 CfgLeft
LDA.0
INCD.0
LDB.0
OR
BRQ cfgLineNone
SETD.2 CfgWalk
LDD.0.2
SETD.1 CfgLine
cfgLineLoop:
SETD.2 CfgLeft
LDA.2
INCD.2
LDB.2
OR
BRQ cfgLineEnd
LDA.0
INIB 0x0A
CCF
SUB
BRQ cfgLineBreak
; Room for it, or the line is one of the long ones and only its beginning is kept.
SETD.2 CfgFill
LDB.2
INIA 0d128
CCF
SUB
BRQ cfgLineOverflow
LDA.0
STA.1
INCD.1
LDA.2
INCA
STA.2
cfgLineStep:
INCD.0
RCAL cfgSpent
BRI cfgLineLoop
cfgLineOverflow:
INIA 0x01
SETD.2 CfgLong
STA.2
BRI cfgLineStep
cfgLineBreak:
INCD.0
RCAL cfgSpent
cfgLineEnd:
RSTA
STA.1 ; The zero that makes what was copied a string.
SETD.2 CfgWalk
STD.0.2
RSTA
RSTB
CCF
ADD
RET
cfgLineNone:
RSTA
INIB 0d1
CCF
ADD
RET
; One byte of the file accounted for.
cfgSpent:
SETD.2 CfgLeft
INCD.2
LDA.2
BNA cfgSpentLow
SETD.2 CfgLeft
LDA.2
DECA
STA.2
INCD.2
INIA 0xFF
STA.2
RRET
cfgSpentLow:
DECA
STA.2
RRET
; ---- What a line turns out to be ----
;
; Q is zero if there is a setting on it, and then CfgLine names the key and CfgValue names
; the value. Q is not zero for a blank line or a comment, which are not settings and are
; not mistakes either.
cfgParse:
RSTA
SETD.0 CfgNoValue
STA.0
; A LINE THAT WAS CUT SHORT IS NOT A SETTING. Its key may look perfectly good and its
; value is whatever fitted, so treating it as one would hand back an answer that is wrong
; rather than missing - and a missing setting gets the default, which is the safe thing.
; Reported by cfgCheck, ignored by everything.
SETD.0 CfgLong
LDA.0
BNA cfgParseNothing
SETD.0 CfgLine
LDA.0
BRA cfgParseNothing
INIB 0x3B ; A semicolon starts a comment.
CCF
SUB
BRQ cfgParseNothing
CALL textSplit
SETD.0 TextRest
SETD.1 CfgValue
CALL sbfsCopyWord
; A KEY WITH NOTHING AFTER IT IS NOT A SETTING, and finding that out here rather than in
; each caller is what makes first-match-wins safe. A file with
;
; system
; system /System/Boot/cosmos.bin
;
; in it would otherwise match the first line, hand back an empty value, and the machine
; would try to start a file with no name while a perfectly good setting sat underneath.
; An unusable value is an absent one, which is what C3 says and where it earns its keep.
SETD.2 CfgValue
LDD.0.2
LDA.0
BRA cfgParseNoValue
RSTA
RSTB
CCF
ADD
RET
cfgParseNoValue:
; Told apart from a blank line, because one of them is a mistake and the other is not.
INIA 0x01
SETD.0 CfgNoValue
STA.0
cfgParseNothing:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Asking for one setting ----
;
; DP0 names the key. Q is zero if the file had it, and CfgValue then names the value.
; Says nothing about anything, ever.
cfgGet:
SETD.1 CfgWanted
STD.0.1
CALL cfgRewind
cfgGetLoop:
CALL cfgLine
BNQ cfgGetMissing
CALL cfgParse
BNQ cfgGetLoop
SETD.0 CfgLine
SETD.2 CfgWanted
LDD.1.2
CALL textSame
BNQ cfgGetLoop
RSTA
RSTB
CCF
ADD
RET
cfgGetMissing:
RSTA
INIB 0d1
CCF
ADD
RET
; ---- Saying what was not understood ----
;
; DP0 names a table of the keys the caller knows: strings one after another, ended by an
; empty one. Every line is looked at once, and anything that will not take effect is
; reported with enough of itself to be found and fixed.
;
; Nothing here changes what any setting does. The defaults have already been used by the
; time this runs, or will be; this exists so that a setting somebody meant, which did not
; happen, does not do so in silence.
cfgCheck:
SETD.1 CfgKnown
STD.0.1
CALL cfgRewind
cfgCheckLoop:
CALL cfgLine
BNQ cfgCheckDone
SETD.0 CfgLong
LDA.0
BRA cfgCheckParse
SETD.0 CfgLongText
RCAL cfgSay
SETD.0 CfgLine
RCAL cfgSay
RCAL cfgNewLine
BRI cfgCheckLoop ; Said once. What its key looks like is not worth a second remark.
cfgCheckParse:
CALL cfgParse
BRQ cfgCheckKnown
; Not a setting. A blank line or a comment is not a mistake and gets no remark; a key
; somebody started and did not finish is, and gets one.
SETD.0 CfgNoValue
LDA.0
BRA cfgCheckLoop
SETD.0 CfgEmptyText
RCAL cfgSay
SETD.0 CfgLine
RCAL cfgSay
RCAL cfgNewLine
BRI cfgCheckLoop
cfgCheckKnown:
SETD.2 CfgKnown
LDD.1.2
cfgCheckNext:
LDA.1
BRA cfgCheckUnknown ; The empty name that ends the table.
SETD.0 CfgLine
CALL textSame
BRQ cfgCheckLoop ; A key this caller knows, so there is nothing to say.
; Past this name and on to the next one.
cfgCheckSkip:
LDA.1
BRA cfgCheckSkipped
INCD.1
BRI cfgCheckSkip
cfgCheckSkipped:
INCD.1
BRI cfgCheckNext
cfgCheckUnknown:
SETD.0 CfgUnknownText
RCAL cfgSay
SETD.0 CfgLine
RCAL cfgSay
RCAL cfgNewLine
BRI cfgCheckLoop
cfgCheckDone:
RET
; ---- Saying things ----
;
; Its own rather than the console's, because the first thing to read a configuration file
; is the boot loader and everything it uses has to fit in a boot slot.
cfgSay:
LDA.0
BRA cfgSaid
OUTA 0x00
INCD.0
BRI cfgSay
cfgSaid:
RRET
cfgNewLine:
INIA 0x0A
OUTA 0x00
RRET
#Data
CfgLongText:
"a line too long to read: "
CfgEmptyText:
"a setting with no value: "
CfgUnknownText:
"a setting nothing asked for: "
CfgBufferAt:
0x00 0x00
CfgRoom:
0x00
CfgLength:
0x00 0x00
CfgWalk:
0x00 0x00
CfgLeft:
0x00 0x00
CfgFill:
0x00
CfgLong:
0x00
CfgNoValue:
0x00
CfgWanted:
0x00 0x00
CfgKnown:
0x00 0x00
CfgValue:
0x00 0x00
; One line, and the number is the one the format says: a key of twenty two and a path of
; the length a path is allowed to be leaves room to spare in a hundred and twenty eight.
CfgLine:
#Reserve 0d129