From 55a219f40f8d8b99ddd0e4aa91f1ff1340e35cc7 Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Thu, 9 Sep 2021 19:01:17 +0200 Subject: [PATCH] Better OSCLI parsing, *DRIVE, OSFILE01,2,3,4,6 ad 7 --- acornMemory.go | 4 +- files.go | 6 +- osCLI.go | 301 +++++++++++++++++++++++++++++++++---------------- osFile.go | 133 ++++++++++++++++++---- 4 files changed, 325 insertions(+), 119 deletions(-) diff --git a/acornMemory.go b/acornMemory.go index 42d14fb..fba1d04 100644 --- a/acornMemory.go +++ b/acornMemory.go @@ -3,7 +3,7 @@ package main import ( _ "embed" "fmt" - "io/ioutil" + "os" ) type acornMemory struct { @@ -104,7 +104,7 @@ func (m *acornMemory) loadFirmware() { } func (m *acornMemory) loadRom(filename string, slot uint8) { - data, err := ioutil.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { panic(err) } diff --git a/files.go b/files.go index e058d95..3316eb7 100644 --- a/files.go +++ b/files.go @@ -32,17 +32,17 @@ func (env *environment) openFile(filename string, mode uint8) uint8 { switch mode { case 0x40: // Open file for input only //env.file[i], err = os.Open(filename) - env.file[i], err = os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0644) + env.file[i], err = os.OpenFile(filename, os.O_RDONLY /*|os.O_CREATE*/, 0644) case 0x80: // Open file for output only env.file[i], err = os.Create(filename) - case 0xc0: // Open file foe update + case 0xc0: // Open file for update env.file[i], err = os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) default: env.raiseError(errorTodo, fmt.Sprintf("Unknown open mode for OSFIND 0x%02x", mode)) i = -1 } if err != nil { - env.raiseError(errorTodo, err.Error()) + //env.raiseError(errorTodo, err.Error()) i = -1 } diff --git a/osCLI.go b/osCLI.go index 408820c..5d929c4 100644 --- a/osCLI.go +++ b/osCLI.go @@ -22,6 +22,7 @@ var cliCommands = []string{ "CODE", "DIR", "DELETE", + "DRIVE", "EXEC", "HELP", "HOST", // Added for bbz @@ -55,16 +56,11 @@ func execOSCLI(env *environment) { lineNotTerminated := env.mem.peekString(xy, 0x0d) line := lineNotTerminated + "\r" pos := 0 - - for line[pos] == ' ' { // Remove initial spaces - pos++ - } + pos = parseSkipSpaces(line, pos) if line[pos] == '*' { // Skip '*' if present pos++ } - for line[pos] == ' ' { // Remove spaces after '*' - pos++ - } + pos = parseSkipSpaces(line, pos) if line[pos] == '|' || line[pos] == '\r' { // Ignore "*|" or standalone "*" env.log(fmt.Sprintf("OSCLI('%s', CMD=empty)", lineNotTerminated)) return @@ -90,29 +86,22 @@ func execOSCLI(env *environment) { pos++ } } - for line[pos] == ' ' { // Remove spaces after command - pos++ - } - args := line[pos:] - args = strings.TrimRight(args, " \r") + pos = parseSkipSpaces(line, pos) unhandled := false + valid := false - env.log(fmt.Sprintf("OSCLI('%s', CMD='%s', ARGS='%s')", lineNotTerminated, command, args)) + env.log(fmt.Sprintf("OSCLI('%s', CMD='%s', ARGS='%s')", lineNotTerminated, command, lineNotTerminated[pos:])) switch command { case "FX": - params := strings.Split(args, ",") - if params[0] == "" || len(params) > 3 { - env.raiseError(254, "Bad Command") - break - } - argA, err := strconv.Atoi(strings.TrimSpace(params[0])) - if err != nil || (argA&0xff) != argA { + var argA uint8 + pos, argA, valid = parseByte(line, pos) + if !valid { env.raiseError(254, "Bad Command") break } - execOSCLIfx(env, uint8(argA), params[1:]) + execOSCLIfx(env, uint8(argA), line, pos) case "BASIC": // Runs the first language ROM with no service entry @@ -131,13 +120,18 @@ func execOSCLI(env *environment) { fmt.Println("\n<>") case "CODE": - execOSCLIfx(env, 0x88, strings.Split(args, ",")) + execOSCLIfx(env, 0x88, line, pos) case "DELETE": - params := strings.Split(args, " ") - if params[0] != "" { - // Activate spool - filename := cleanFilename(params[0]) + // *DELETE filename + filename := "" + _, filename, valid = parseFilename(line, pos) + if !valid { + env.raiseError(254, "Bad Command") + break + } + + if filename != "" { err := os.Remove(filename) if err != nil { env.raiseError(errorTodo, err.Error()) @@ -145,22 +139,39 @@ func execOSCLI(env *environment) { } case "DIR": - dest := args - if len(dest) == 0 { - var err error - dest, err = os.UserHomeDir() - if err != nil { - env.raiseError(errorTodo, err.Error()) - break - } + path := "" + _, path, valid = parseFilename(line, pos) + if !valid { + env.raiseError(254, "Bad Command") + break + } + + if path == "" || path == "$" { + break + /* + var err error + dest, err = os.UserHomeDir() + if err != nil { + env.raiseError(errorTodo, err.Error()) + break + } + */ } - err := os.Chdir(dest) + err := os.Chdir(path) if err != nil { env.raiseError(206, "Bad directory") break } + case "DRIVE": + var drive uint8 + _, drive, valid = parseByte(line, pos) + if !valid || drive >= 4 { + env.raiseError(205, "Bad drive") + } + // Do nothing. We could use subdirs for drive 1, 2 and 3 + //case "EXEC": case "HELP": @@ -172,6 +183,8 @@ func execOSCLI(env *environment) { env.cpu.SetPC(procOSBYTE_143) case "HOST": + args := line[pos:] + args = strings.TrimSuffix(args, "\r") if len(args) == 0 { env.raiseError(errorTodo, "Command missing for *HOST") break @@ -185,53 +198,57 @@ func execOSCLI(env *environment) { fmt.Println(string(stdout)) case "INFO": - attr := getFileAttributes(env, args) + filename := "" + _, filename, valid = parseFilename(line, pos) + if !valid { + env.raiseError(254, "Bad Command") + break + } + + attr := getFileAttributes(env, filename) if attr.hasMetadata { - fmt.Printf("%s\t %06X %06X %06X\n", args, attr.loadAddress, attr.executionAddress, attr.fileSize) + fmt.Printf("%s\t %06X %06X %06X\n", filename, attr.loadAddress, attr.executionAddress, attr.fileSize) } else { - fmt.Printf("%s\t ?????? ?????? %06X\n", args, attr.fileSize) + fmt.Printf("%s\t ?????? ?????? %06X\n", filename, attr.fileSize) } // case "KEY": case "LOAD": // *LOAD [
] - params := strings.Split(args, " ") - if len(params) > 2 { - env.raiseError(254, "Bad command") - break - } - if len(params) == 0 { + filename := "" + pos, filename, valid = parseFilename(line, pos) + if !valid { env.raiseError(214, "File not found") break } - filename := cleanFilename(params[0]) - loadAddress := loadAddressNull - if len(params) >= 2 { - i, err := strconv.ParseUint(params[1], 16, 32) - if err != nil { + + loadAddress := addressNull + if line[pos] != '\r' { + _, loadAddress, valid = parseDWord(line, pos) + if !valid { env.raiseError(252, "Bad address") break } - loadAddress = uint32(i) } loadFile(env, filename, loadAddress) // case "LINE": case "MOTOR": - execOSCLIfx(env, 0x89, strings.Split(args, ",")) + execOSCLIfx(env, 0x89, line, pos) case "OPT": - execOSCLIfx(env, 0x8b, strings.Split(args, ",")) + execOSCLIfx(env, 0x8b, line, pos) case "QUIT": env.stop = true case "RUN": // *RUN - params := strings.Split(args, " ") - if len(params) == 0 { + filename := "" + _, filename, valid = parseFilename(line, pos) + if !valid { env.raiseError(214, "File not found") break } - filename := cleanFilename(params[0]) - attr := loadFile(env, filename, loadAddressNull) + + attr := loadFile(env, filename, addressNull) if attr.fileType == osFileFound { if attr.hasMetadata { env.cpu.SetPC(uint16(attr.executionAddress)) @@ -241,7 +258,7 @@ func execOSCLI(env *environment) { } case "ROM": - execOSCLIfx(env, 0x8d, strings.Split(args, ",")) + execOSCLIfx(env, 0x8d, line, pos) case "ROMS": selectedROM := env.mem.Peek(sheilaRomLatch) @@ -272,42 +289,61 @@ func execOSCLI(env *environment) { env.mem.Poke(sheilaRomLatch, selectedROM) case "SAVE": - // *SAVE - params := strings.Split(args, " ") - if len(params) < 3 || len(params) > 5 { - env.raiseError(254, "Bad command") + // *SAVE [] [] + // *SAVE A 1a34+2a + filename := "" + pos, filename, valid = parseFilename(line, pos) + if !valid { + env.raiseError(214, "File not found") break } - filename := cleanFilename(params[0]) - - i, err := strconv.ParseUint(params[1], 16, 32) - if err != nil { + var startAddress uint32 + pos, startAddress, valid = parseDWord(line, pos) + if !valid { env.raiseError(252, "Bad address") break } - startAddress := uint32(i) isSize := false - if params[2][0] == '+' { + if line[pos] == '+' { isSize = true - params[2] = params[2][1:] + pos++ } - i, err = strconv.ParseUint(params[2], 16, 32) - if err != nil { + var endAddress uint32 + pos, endAddress, valid = parseDWord(line, pos) + if !valid { env.raiseError(252, "Bad address") break } - endAddress := uint32(i) if isSize { - endAddress = startAddress - endAddress + endAddress = startAddress + endAddress + } + + executionAddress := startAddress + if line[pos] != '\r' { + pos, executionAddress, valid = parseDWord(line, pos) + if !valid { + env.raiseError(252, "Bad address") + break + } } - if len(params) > 3 { - env.notImplemented("*SAVE with execution or reload address") + loadAddress := startAddress + if line[pos] != '\r' { + pos, loadAddress, valid = parseDWord(line, pos) + if !valid { + env.raiseError(252, "Bad address") + break + } + } + + if line[pos] != '\r' { + env.raiseError(254, "Bad command") + break } - saveFile(env, filename, startAddress, startAddress, startAddress, endAddress) + saveFile(env, filename, startAddress, endAddress, executionAddress, loadAddress, false) case "SPOOL": // *SPOOL filename @@ -318,18 +354,22 @@ func execOSCLI(env *environment) { env.mem.Poke(spoolFileHandle, 0) } - params := strings.Split(args, " ") - if params[0] != "" { + filename := "" + _, filename, valid = parseFilename(line, pos) + if !valid { + env.raiseError(214, "File not found") + break + } + if filename != "" { // Activate spool - filename := cleanFilename(params[0]) spoolFile := env.openFile(filename, 0x80 /*open for output*/) env.mem.Poke(spoolFileHandle, spoolFile) } case "TAPE": - execOSCLIfx(env, 0x8c, strings.Split(args, ",")) + execOSCLIfx(env, 0x8c, line, pos) case "TV": - execOSCLIfx(env, 0x90, strings.Split(args, ",")) + execOSCLIfx(env, 0x90, line, pos) default: unhandled = true @@ -344,21 +384,34 @@ func execOSCLI(env *environment) { } } -func execOSCLIfx(env *environment, argA uint8, params []string) { - argX := 0 - if len(params) >= 1 { - var err error - argX, err = strconv.Atoi(strings.TrimSpace(params[0])) - if err != nil || (argX&0xff) != argX { +func execOSCLIfx(env *environment, argA uint8, line string, pos int) { + argX := uint8(0) + argY := uint8(0) + fail := false + + if line[pos] != '\r' { + if line[pos] != ',' { + env.raiseError(254, "Bad Command") + return + } + pos++ // Skip ',' + pos = parseSkipSpaces(line, pos) + pos, argX, fail = parseByte(line, pos) + if fail { env.raiseError(254, "Bad Command") return } } - argY := 0 - if len(params) >= 2 { - var err error - argY, err = strconv.Atoi(strings.TrimSpace(params[1])) - if err != nil || (argY&0xff) != argY { + + if line[pos] != '\r' { + if line[pos] != ',' { + env.raiseError(254, "Bad Command") + return + } + pos++ // Skip ',' + pos = parseSkipSpaces(line, pos) + _, argY, fail = parseByte(line, pos) + if fail { env.raiseError(254, "Bad Command") return } @@ -370,9 +423,67 @@ func execOSCLIfx(env *environment, argA uint8, params []string) { execOSBYTE(env) } -func cleanFilename(filename string) string { - if filename[0] == '"' && len(filename) >= 2 { - return filename[1:(len(filename) - 1)] +func parseFilename(line string, pos int) (int, string, bool) { + terminator := uint8(' ') + if line[pos] == '"' { + terminator = '"' + pos++ + } + cursor := pos + + for line[cursor] != terminator && line[cursor] != '\r' { + cursor++ + } + if terminator != ' ' && line[cursor] != '\r' { + return cursor, "", false + } + filename := line[pos:cursor] + if terminator != ' ' { + cursor++ + } + cursor = parseSkipSpaces(line, cursor) + return cursor, filename, true +} + +func parseSkipSpaces(line string, pos int) int { + for line[pos] == ' ' { // Remove initial spaces + pos++ } - return filename + return pos +} + +func parseByte(line string, pos int) (int, uint8, bool) { + cursor := pos + for line[cursor] >= '0' && line[cursor] <= '9' { + cursor++ + } + if cursor == pos { + return pos, 0, false + } + value, err := strconv.Atoi(line[pos:cursor]) + if err != nil || value > 255 { + return cursor, 0, false + } + + cursor = parseSkipSpaces(line, pos) + return cursor, uint8(value), true +} + +func parseDWord(line string, pos int) (int, uint32, bool) { + cursor := pos + for (line[cursor] >= '0' && line[cursor] <= '9') || + (line[cursor] >= 'a' && line[cursor] <= 'f') || + (line[cursor] >= 'A' && line[cursor] <= 'F') { + cursor++ + } + if cursor == pos { + return pos, 0, false + } + value, err := strconv.ParseUint(line[pos:cursor], 16, 32) + if err != nil { + return cursor, 0, false + } + + cursor = parseSkipSpaces(line, cursor) + return cursor, uint32(value), true } diff --git a/osFile.go b/osFile.go index 7fec4bc..f6450da 100644 --- a/osFile.go +++ b/osFile.go @@ -3,7 +3,6 @@ package main import ( "errors" "fmt" - "io/ioutil" "os" "strconv" "strings" @@ -19,8 +18,7 @@ const ( cbStartAddressOrSize uint16 = 0xa cbEndAddressOrAttributes uint16 = 0xe - loadAddressNull uint32 = ^uint32(0) - executionAddressNull uint32 = ^uint32(0) + addressNull uint32 = ^uint32(0) ) type fileAttributes struct { @@ -29,6 +27,7 @@ type fileAttributes struct { hasMetadata bool loadAddress uint32 executionAddress uint32 + attributes uint32 } func execOSFILE(env *environment) { @@ -53,12 +52,50 @@ func execOSFILE(env *environment) { Save a block of memory as a file using the information provided in the parameter block. */ - attr := saveFile(env, filename, loadAddress, executionAddress, startAddress, endAddress) + attr := saveFile(env, filename, startAddress, endAddress, executionAddress, loadAddress, false) if attr.fileType != osNotFound { updateControlBlock(env, controlBlock, attr) } newA = attr.fileType + case 1: + option = "Write file metadata" + attr := getFileAttributes(env, filename) + if attr.fileType != osNotFound { + attr.loadAddress = loadAddress + attr.executionAddress = executionAddress + attr.attributes = endAddress + writeMetada(env, filename, attr) + } + newA = attr.fileType + + case 2: + option = "Write file reload address" + attr := getFileAttributes(env, filename) + if attr.fileType != osNotFound { + attr.loadAddress = loadAddress + writeMetada(env, filename, attr) + } + newA = attr.fileType + + case 3: + option = "Write file execution address" + attr := getFileAttributes(env, filename) + if attr.fileType != osNotFound { + attr.executionAddress = executionAddress + writeMetada(env, filename, attr) + } + newA = attr.fileType + + case 4: + option = "Write file attributes" + attr := getFileAttributes(env, filename) + if attr.fileType != osNotFound { + attr.attributes = endAddress + writeMetada(env, filename, attr) + } + newA = attr.fileType + case 5: option = "File info" /* @@ -72,6 +109,44 @@ func execOSFILE(env *environment) { } newA = attr.fileType + case 6: + option = "Delete file" + /* + Delete object. If the object does not exist, A returned + as &00. If the object is locked, or is not owned, or is + a directory that is not empty, or is open, then an error + is generated. + */ + err := os.Remove(filename) + var pathError *os.PathError + if errors.As(err, &pathError) { + newA = osNotFound + } else if err != nil { + env.raiseError(errorTodo, err.Error()) + } else { + newA = osFileFound + } + + case 7: + option = "Create an empty file of defined length" + /* + Create an empty file of defined length. Block as for + SAVE. The supplied start address is usually passed as &0 + and the end address as the required length. No data is + transfered, and the file does not necessarily contain + zeros. Some file systems may deliberately overwrite any + existing data in the file. If a file already exists with + the same name, it is overwritten, with the file access + and the case of the name staying the same. If the file + is locked, or a directory exists with the same name, or + the file is open, then an error is generated. + */ + attr := saveFile(env, filename, startAddress, endAddress, executionAddress, loadAddress, true) + if attr.fileType != osNotFound { + updateControlBlock(env, controlBlock, attr) + } + newA = attr.fileType + case 0xff: // Load file into memory option = "Load file" /* @@ -84,22 +159,24 @@ func execOSFILE(env *environment) { */ useLoadAddress := (executionAddress & 0xff) == 0 if !useLoadAddress { - loadAddress = loadAddressNull + loadAddress = addressNull } attr := loadFile(env, filename, loadAddress) //env.mem.pokeDoubleWord(controlBlock+cbStartAddressOrSize, attr.fileSize) - if attr.fileType != osNotFound { - updateControlBlock(env, controlBlock, attr) + if attr.fileType == osNotFound { + env.raiseError(214, "File not found") } + updateControlBlock(env, controlBlock, attr) newA = attr.fileType default: env.notImplemented(fmt.Sprintf("OSFILE(A=%02x)", a)) } - env.mem.pokeWord(controlBlock+cbEndAddressOrAttributes, 0x00 /*attributes*/) - + if a != 1 && a != 5 { + env.mem.pokeWord(controlBlock+cbEndAddressOrAttributes, 0x00 /*attributes*/) + } env.cpu.SetAXYP(newA, x, y, p) env.log(fmt.Sprintf("OSFILE('%s',A=%02x,FCB=%04x,FILE=%s) => %v", option, a, controlBlock, filename, newA)) @@ -112,6 +189,7 @@ func updateControlBlock(env *environment, controlBlock uint16, attr *fileAttribu if attr.hasMetadata { env.mem.pokeDoubleWord(controlBlock+cbLoadAddress, attr.loadAddress) env.mem.pokeDoubleWord(controlBlock+cbExecutionAddress, attr.executionAddress) + env.mem.pokeDoubleWord(controlBlock+cbEndAddressOrAttributes, attr.attributes) } } } @@ -126,7 +204,7 @@ func loadFile(env *environment, filename string, loadAddress uint32) *fileAttrib return attr } - if loadAddress == loadAddressNull { + if loadAddress == addressNull { if !attr.hasMetadata { env.raiseError(errorTodo, "Missing metadata file") attr.fileType = osNotFound @@ -149,15 +227,22 @@ func loadFile(env *environment, filename string, loadAddress uint32) *fileAttrib } func saveFile(env *environment, filename string, - loadAddress uint32, executionAddress uint32, startAddress uint32, endAddress uint32) *fileAttributes { + startAddress uint32, endAddress uint32, executionAddress uint32, loadAddress uint32, blank bool) *fileAttributes { var attr fileAttributes attr.loadAddress = loadAddress attr.executionAddress = executionAddress attr.fileSize = endAddress - startAddress + // attr.attributes = ? - data := env.mem.peekSlice(uint16(startAddress), uint16(attr.fileSize)) - err := ioutil.WriteFile(filename, data, 0644) + var data []uint8 + if blank { + data = make([]uint8, attr.fileSize) + } else { + data = env.mem.peekSlice(uint16(startAddress), uint16(attr.fileSize)) + } + + err := os.WriteFile(filename, data, 0644) if err != nil { env.raiseError(errorTodo, err.Error()) attr.fileType = osNotFound @@ -165,11 +250,7 @@ func saveFile(env *environment, filename string, attr.fileType = osFileFound } - // Write metadata file - // $.BasObj 003000 003100 005000 00 CRC32=614721E1 - metadata := fmt.Sprintf("$.FILE %08X %08X %08X", attr.loadAddress, attr.executionAddress, attr.fileSize) - ioutil.WriteFile(filename+".inf", []byte(metadata), 0644) - + writeMetada(env, filename, &attr) return &attr } @@ -202,7 +283,7 @@ func getFileAttributes(env *environment, filename string) *fileAttributes { return &attr } parts := strings.Fields(string(data)) - if len(parts) < 3 { + if len(parts) < 5 { env.log(fmt.Sprintf("Invalid format for metadata file %s.inf, missing fields", filename)) return &attr } @@ -221,6 +302,20 @@ func getFileAttributes(env *environment, filename string) *fileAttributes { } attr.executionAddress = uint32(i) + i, err = strconv.ParseUint(parts[4], 16, 64) + if err != nil { + env.log(fmt.Sprintf("Invalid format for metadata file %s.inf, bad sttributes '%s'", filename, err.Error())) + return &attr + } + attr.attributes = uint32(i) + attr.hasMetadata = true return &attr } + +func writeMetada(env *environment, filename string, attr *fileAttributes) { + // $.BasObj 003000 003100 005000 00 CRC32=614721E1 + metadata := fmt.Sprintf("$.FILE %08X %08X %08X %02X", + attr.loadAddress, attr.executionAddress, attr.fileSize, attr.attributes) + os.WriteFile(filename+".inf", []byte(metadata), 0644) +}