From 0fdf6f05c4dd0faad7b843001435cd3fb08037f7 Mon Sep 17 00:00:00 2001 From: Zellyn Hunter Date: Tue, 22 Nov 2016 22:38:20 -0500 Subject: [PATCH] Add stubbed-out delete, supermon symbol encoding --- cmd/delete.go | 68 +++++++++++++++++++++++++++++++ lib/disk/disk.go | 10 +++++ lib/disk/dsk.go | 6 +++ lib/disk/ops.go | 3 ++ lib/dos3/dos3.go | 6 +++ lib/supermon/supermon.go | 76 +++++++++++++++++++++++++++++++---- lib/supermon/supermon_test.go | 41 +++++++++++++++++++ 7 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 cmd/delete.go diff --git a/cmd/delete.go b/cmd/delete.go new file mode 100644 index 0000000..b5ec26d --- /dev/null +++ b/cmd/delete.go @@ -0,0 +1,68 @@ +// Copyright © 2016 Zellyn Hunter + +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/zellyn/diskii/lib/disk" + _ "github.com/zellyn/diskii/lib/dos3" + _ "github.com/zellyn/diskii/lib/supermon" +) + +// deleteCmd represents the delete command, used to delete a file. +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "delete a file", + Long: `Delete a file. + +delete disk-image.dsk HELLO +`, + Run: func(cmd *cobra.Command, args []string) { + if err := runDelete(args); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(-1) + } + }, +} + +func init() { + RootCmd.AddCommand(deleteCmd) +} + +// runDelete performs the actual delete logic. +func runDelete(args []string) error { + if len(args) != 2 { + return fmt.Errorf("delete expects a disk image filename, and a filename") + } + sd, err := disk.Open(args[0]) + if err != nil { + return err + } + op, err := disk.OperatorFor(sd) + if err != nil { + return err + } + deleted, err := op.Delete(args[1]) + if err != nil { + return err + } + if !deleted { + // TODO(zellyn): implement -f flag to not warn on nonexistence. + return fmt.Errorf("file %q not found", args[1]) + } + f, err := os.Create(args[0]) + if err != nil { + return err + } + _, err = sd.Write(f) + if err != nil { + return err + } + if err = f.Close(); err != nil { + return err + } + return nil +} diff --git a/lib/disk/disk.go b/lib/disk/disk.go index 5a80eb3..5df5d3b 100644 --- a/lib/disk/disk.go +++ b/lib/disk/disk.go @@ -6,6 +6,7 @@ package disk import ( "fmt" + "io" "path" "strings" ) @@ -49,6 +50,8 @@ type SectorDisk interface { Sectors() byte // Tracks returns the number of tracks on the SectorDisk Tracks() byte + // Write writes the disk contents to the given file. + Write(io.Writer) (int, error) } type LogicalSectorDisk interface { @@ -62,6 +65,8 @@ type LogicalSectorDisk interface { Sectors() byte // Tracks returns the number of tracks on the SectorDisk Tracks() byte + // Write writes the disk contents to the given file. + Write(io.Writer) (int, error) } // MappedDisk wraps a SectorDisk as a LogicalSectorDisk, handling the @@ -125,6 +130,11 @@ func (md MappedDisk) Tracks() byte { return md.sectorDisk.Tracks() } +// Write writes the disk contents to the given file. +func (md MappedDisk) Write(w io.Writer) (n int, err error) { + return md.sectorDisk.Write(w) +} + // Open opens a disk image by filename. func Open(filename string) (SectorDisk, error) { ext := strings.ToLower(path.Ext(filename)) diff --git a/lib/disk/dsk.go b/lib/disk/dsk.go index 2671793..8312786 100644 --- a/lib/disk/dsk.go +++ b/lib/disk/dsk.go @@ -6,6 +6,7 @@ package disk import ( "fmt" + "io" "io/ioutil" ) @@ -84,3 +85,8 @@ func (d DSK) Sectors() byte { func (d DSK) Tracks() byte { return d.tracks } + +// Write writes the disk contents to the given file. +func (d DSK) Write(w io.Writer) (n int, err error) { + return w.Write(d.data) +} diff --git a/lib/disk/ops.go b/lib/disk/ops.go index 13bc103..b2783b6 100644 --- a/lib/disk/ops.go +++ b/lib/disk/ops.go @@ -83,6 +83,9 @@ type Operator interface { Catalog(subdir string) ([]Descriptor, error) // GetFile retrieves a file by name. GetFile(filename string) (FileInfo, error) + // Delete deletes a file by name. It returns true if the file was + // deleted, false if it didn't exist. + Delete(filename string) (bool, error) } // FileInfo represents a file descriptor plus the content. diff --git a/lib/dos3/dos3.go b/lib/dos3/dos3.go index fe0e4b1..05a00cd 100644 --- a/lib/dos3/dos3.go +++ b/lib/dos3/dos3.go @@ -651,6 +651,12 @@ func (o operator) GetFile(filename string) (disk.FileInfo, error) { return disk.FileInfo{}, fmt.Errorf("%s does not yet implement `GetFile` for filetype %s", operatorName, errType) } +// Delete deletes a file by name. It returns true if the file was +// deleted, false if it didn't exist. +func (o operator) Delete(filename string) (bool, error) { + return false, fmt.Errorf("%s does not implement Delete yet", operatorName) +} + // operatorFactory is the factory that returns dos3 operators given // disk images. func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) { diff --git a/lib/supermon/supermon.go b/lib/supermon/supermon.go index cf02d16..f6cdfa7 100644 --- a/lib/supermon/supermon.go +++ b/lib/supermon/supermon.go @@ -4,9 +4,8 @@ // structures of NakedOS/Super-Mon disks. package supermon -// TODO(zellyn): remove panics. - import ( + "encoding/binary" "fmt" "strconv" "strings" @@ -152,6 +151,44 @@ func decodeSymbol(five []byte, extra byte) string { return result } +// encodeSymbol encodes a symbol name into the five+1 bytes used in a +// Super-Mon encoded symbol table entry. The returned byte array will +// always be six bytes long. If it can't be encoded, it returns an +// error. +func encodeSymbol(name string) (six []byte, err error) { + if len(name) > 9 { + return nil, fmt.Errorf("invalid Super-Mon symbol %q: too long", name) + } + if len(name) < 3 { + return nil, fmt.Errorf("invalid Super-Mon symbol %q: too short", name) + } + nm := []byte(strings.ToUpper(name)) + value := uint64(0) + bits := 0 + for i := len(nm) - 1; i >= 0; i-- { + ch := nm[i] + switch { + case 'A' <= ch && ch <= 'Z': + value = value<<5 + uint64(ch-'@') + bits += 5 + case '0' <= ch && ch <= '4': + value = value<<6 + 0x1b + uint64(ch-'0') + bits += 6 + case '5' <= ch && ch <= '9': + value = value<<6 + 0x3b + uint64(ch-'5') + bits += 6 + } + if bits > 48 { + return nil, fmt.Errorf("invalid Super-Mon symbol %q: too long", name) + } + } + eight := make([]byte, 8) + six = make([]byte, 6) + binary.LittleEndian.PutUint64(eight, value) + copy(six, eight) + return six, nil +} + // SymbolTable represents an entire Super-Mon symbol table. It'll // always be 819 entries long, because it includes blanks. type SymbolTable []Symbol @@ -192,7 +229,7 @@ func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) { if (linkAddr-0xD000)%5 != 0 { return nil, fmt.Errorf("Expected symbol table link address to 0xD000+5x; got 0x%04X", linkAddr) } - link = (int(linkAddr) - 0xD000) % 5 + link = (int(linkAddr) - 0xD000) / 5 } extra := symtbl1[i+4] copy(five, symtbl2[i:i+5]) @@ -205,11 +242,27 @@ func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) { table = append(table, symbol) } - // TODO(zellyn): check link addresses. + for i, sym := range table { + if sym.Address != 0 && sym.Link != -1 { + if sym.Link == i { + return nil, fmt.Errorf("Symbol %q (0x%04X) links to itself", sym.Name, sym.Address) + } + linkSym := table[sym.Link] + if addrHash(sym.Address) != addrHash(linkSym.Address) { + return nil, fmt.Errorf("Symbol %q (0x%04X) with hash 0x%02X links to symbol %q (0x%04X) with hash 0x%02X", + sym.Name, sym.Address, addrHash(sym.Address), linkSym.Name, linkSym.Address, addrHash(linkSym.Address)) + } + } + } return table, nil } +// addrHash computes the SuperMon hash for an address. +func addrHash(addr uint16) byte { + return (byte(addr) ^ byte(addr>>8)) & 0x7f +} + // SymbolsByAddress returns a map of addresses to slices of symbols. func (st SymbolTable) SymbolsByAddress() map[uint16][]Symbol { result := map[uint16][]Symbol{} @@ -319,12 +372,21 @@ func (o operator) GetFile(filename string) (disk.FileInfo, error) { Length: len(data), Locked: false, Type: disk.FiletypeBinary, - // TODO(zellyn): Set StartAddress if we know it. } - return disk.FileInfo{ + fi := disk.FileInfo{ Descriptor: desc, Data: data, - }, nil + } + if file == 1 { + fi.StartAddress = 0x1800 + } + return fi, nil +} + +// Delete deletes a file by name. It returns true if the file was +// deleted, false if it didn't exist. +func (o operator) Delete(filename string) (bool, error) { + return false, fmt.Errorf("%s does not implement Delete yet", operatorName) } // operatorFactory is the factory that returns supermon operators diff --git a/lib/supermon/supermon_test.go b/lib/supermon/supermon_test.go index 2c8ac15..cf58562 100644 --- a/lib/supermon/supermon_test.go +++ b/lib/supermon/supermon_test.go @@ -3,6 +3,7 @@ package supermon import ( + "strings" "testing" "github.com/zellyn/diskii/lib/disk" @@ -137,3 +138,43 @@ No more; and by a sleep, to say we end t.Errorf("Incorrect result for GetFile(\"TOBE\"): want %q; got %q", want, got) } } + +// TestEncodeDecode tests encoding and decoding of Super-Mon symbol +// table entries. +func TestEncodeDecode(t *testing.T) { + testdata := []struct { + sym string + valid bool + }{ + {"ABC", true}, + {"abc", true}, + {"ABCDEFGHI", true}, + {"abcdefghi", true}, + {"ABCDEF123", true}, + {"abcdef123", true}, + + {"AB", false}, + {"ab", false}, + {"ABCDE1234", false}, + {"abcde1234", false}, + } + + for _, tt := range testdata { + if !tt.valid { + if _, err := encodeSymbol(tt.sym); err == nil { + t.Errorf("Expected symbol %q to be invalid, but wansn't", tt.sym) + } + continue + } + + bytes, err := encodeSymbol(tt.sym) + if err != nil { + t.Errorf("Unexpected error encoding symbol %q", tt.sym) + continue + } + sym := decodeSymbol(bytes[:5], bytes[5]) + if sym != strings.ToUpper(tt.sym) { + t.Errorf("Symbol %q encodes to %q", tt.sym, sym) + } + } +}