diff --git a/README.md b/README.md index eb50be9..2c09c7d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ diskii ====== +**Note:** diskii is not stable yet! I don't expect to remove +functionality, but I'm still experimenting with the command syntax and +organization, so don't get too comfy with it yet. + diskii is a commandline tool for working with Apple II disk images. It is also a library of code that can be used by other Go programs. diff --git a/cmd/cat.go b/cmd/cat.go new file mode 100644 index 0000000..73235d8 --- /dev/null +++ b/cmd/cat.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Zellyn Hunter + +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/zellyn/diskii/lib/disk" + _ "github.com/zellyn/diskii/lib/dos33" + _ "github.com/zellyn/diskii/lib/supermon" +) + +// catCmd represents the cat command, used to catalog a disk or +// directory. +var catCmd = &cobra.Command{ + Use: "cat", + Short: "print a list of files", + Long: `Catalog a disk or subdirectory.`, + Run: func(cmd *cobra.Command, args []string) { + if err := runCat(args); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(-1) + } + }, +} + +func init() { + RootCmd.AddCommand(catCmd) +} + +// runCat performs the actual catalog logic. +func runCat(args []string) error { + if len(args) < 1 || len(args) > 2 { + return fmt.Errorf("cat expects a disk image filename, and an optional subdirectory") + } + sd, err := disk.Open(args[0]) + if err != nil { + return err + } + op, err := disk.OperatorFor(sd) + if err != nil { + return err + } + subdir := "" + if len(args) == 2 { + if !op.HasSubdirs() { + return fmt.Errorf("Disks of type %q cannot have subdirectories", op.Name()) + } + subdir = args[1] + } + fds, err := op.Catalog(subdir) + if err != nil { + return err + } + for _, fd := range fds { + fmt.Println(fd.Name) + } + return nil +} diff --git a/cmd/decode.go b/cmd/decode.go index 717daa6..1b178ce 100644 --- a/cmd/decode.go +++ b/cmd/decode.go @@ -32,6 +32,19 @@ decode - # read stdin`, }, } +func init() { + applesoftCmd.AddCommand(decodeCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // decodeCmd.PersistentFlags().String("foo", "", "A help for foo") + + decodeCmd.Flags().Uint16VarP(&location, "location", "l", 0x801, "Starting program location in memory") + decodeCmd.Flags().BoolVarP(&rawControlCodes, "raw", "r", false, "Print raw control codes (no escaping)") +} + // runDecode performs the actual decode logic. func runDecode(args []string) error { if len(args) != 1 { @@ -52,16 +65,3 @@ func runDecode(args []string) error { } return nil } - -func init() { - applesoftCmd.AddCommand(decodeCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // decodeCmd.PersistentFlags().String("foo", "", "A help for foo") - - decodeCmd.Flags().Uint16VarP(&location, "location", "l", 0x801, "Starting program location in memory") - decodeCmd.Flags().BoolVarP(&rawControlCodes, "raw", "r", false, "Print raw control codes (no escaping)") -} diff --git a/lib/disk/disk.go b/lib/disk/disk.go index 1b2c302..5a80eb3 100644 --- a/lib/disk/disk.go +++ b/lib/disk/disk.go @@ -6,7 +6,16 @@ package disk import ( "fmt" - "io/ioutil" + "path" + "strings" +) + +const ( + DOS33Tracks = 35 // Tracks per disk + DOS33Sectors = 16 // Sectors per track + // DOS33DiskBytes is the number of bytes on a DOS 3.3 disk. + DOS33DiskBytes = 143360 // 35 tracks * 16 sectors * 256 bytes + DOS33TrackBytes = 256 * DOS33Sectors // Bytes per track ) // Dos33LogicalToPhysicalSectorMap maps logical sector numbers to physical ones. @@ -106,96 +115,22 @@ func (md MappedDisk) WriteLogicalSector(track byte, sector byte, data []byte) er return md.sectorDisk.WritePhysicalSector(track, physicalSector, data) } -// Sectors returns the number of sectors on the DSK image. +// Sectors returns the number of sectors in the disk image. func (md MappedDisk) Sectors() byte { return md.sectorDisk.Sectors() } -// Tracks returns the number of tracks on the DSK image. +// Tracks returns the number of tracks in the disk image. func (md MappedDisk) Tracks() byte { return md.sectorDisk.Tracks() } -const ( - DOS33Tracks = 35 // Tracks per disk - DOS33Sectors = 16 // Sectors per track - // DOS33DiskBytes is the number of bytes on a DOS 3.3 disk. - DOS33DiskBytes = 143360 // 35 tracks * 16 sectors * 256 bytes - DOS33TrackBytes = 256 * DOS33Sectors // Bytes per track -) - -// DSK represents a .dsk disk image. -type DSK struct { - data []byte // The actual data in the file - sectors byte // Number of sectors per track - physicalToStored []byte // Map of physical on-disk sector numbers to sectors in the disk image - bytesPerTrack int // Number of bytes per track - tracks byte // Number of tracks -} - -var _ SectorDisk = (*DSK)(nil) - -// LoadDSK loads a .dsk image from a file. -func LoadDSK(filename string) (DSK, error) { - bb, err := ioutil.ReadFile(filename) - if err != nil { - return DSK{}, err - } - // TODO(zellyn): handle 13-sector disks. - if len(bb) != DOS33DiskBytes { - return DSK{}, fmt.Errorf("Expected file %q to contain %d bytes, but got %d.", filename, DOS33DiskBytes, len(bb)) - } - return DSK{ - data: bb, - sectors: 16, - physicalToStored: Dos33PhysicalToLogicalSectorMap, - bytesPerTrack: 16 * 256, - tracks: DOS33Tracks, - }, nil -} - -// ReadPhysicalSector reads a single physical sector from the disk. It -// always returns 256 byes. -func (d DSK) ReadPhysicalSector(track byte, sector byte) ([]byte, error) { - if track >= d.tracks { - return nil, fmt.Errorf("ReadPhysicalSector expected track between 0 and %d; got %d", d.tracks-1, track) - } - if sector >= d.sectors { - return nil, fmt.Errorf("ReadPhysicalSector expected sector between 0 and %d; got %d", d.sectors-1, sector) - } - - storedSector := d.physicalToStored[int(sector)] - start := int(track)*d.bytesPerTrack + 256*int(storedSector) - buf := make([]byte, 256) - copy(buf, d.data[start:start+256]) - return buf, nil -} - -// WritePhysicalSector writes a single physical sector to a disk. It -// expects exactly 256 bytes. -func (d DSK) WritePhysicalSector(track byte, sector byte, data []byte) error { - if track >= d.tracks { - return fmt.Errorf("WritePhysicalSector expected track between 0 and %d; got %d", d.tracks-1, track) - } - if sector >= d.sectors { - return fmt.Errorf("WritePhysicalSector expected sector between 0 and %d; got %d", d.sectors-1, sector) - } - if len(data) != 256 { - return fmt.Errorf("WritePhysicalSector expects data of length 256; got %d", len(data)) - } - - storedSector := d.physicalToStored[int(sector)] - start := int(track)*d.bytesPerTrack + 256*int(storedSector) - copy(d.data[start:start+256], data) - return nil -} - -// Sectors returns the number of sectors on the DSK image. -func (d DSK) Sectors() byte { - return d.sectors -} - -// Tracks returns the number of tracks on the DSK image. -func (d DSK) Tracks() byte { - return d.tracks +// Open opens a disk image by filename. +func Open(filename string) (SectorDisk, error) { + ext := strings.ToLower(path.Ext(filename)) + switch ext { + case ".dsk": + return LoadDSK(filename) + } + return nil, fmt.Errorf("Unimplemented/unknown disk file extension %q", ext) } diff --git a/lib/disk/dsk.go b/lib/disk/dsk.go new file mode 100644 index 0000000..2671793 --- /dev/null +++ b/lib/disk/dsk.go @@ -0,0 +1,86 @@ +// Copyright © 2016 Zellyn Hunter + +// dsk.go contains logic for reading ".dsk" disk images. + +package disk + +import ( + "fmt" + "io/ioutil" +) + +// DSK represents a .dsk disk image. +type DSK struct { + data []byte // The actual data in the file + sectors byte // Number of sectors per track + physicalToStored []byte // Map of physical on-disk sector numbers to sectors in the disk image + bytesPerTrack int // Number of bytes per track + tracks byte // Number of tracks +} + +var _ SectorDisk = (*DSK)(nil) + +// LoadDSK loads a .dsk image from a file. +func LoadDSK(filename string) (DSK, error) { + bb, err := ioutil.ReadFile(filename) + if err != nil { + return DSK{}, err + } + // TODO(zellyn): handle 13-sector disks. + if len(bb) != DOS33DiskBytes { + return DSK{}, fmt.Errorf("Expected file %q to contain %d bytes, but got %d.", filename, DOS33DiskBytes, len(bb)) + } + return DSK{ + data: bb, + sectors: 16, + physicalToStored: Dos33PhysicalToLogicalSectorMap, + bytesPerTrack: 16 * 256, + tracks: DOS33Tracks, + }, nil +} + +// ReadPhysicalSector reads a single physical sector from the disk. It +// always returns 256 byes. +func (d DSK) ReadPhysicalSector(track byte, sector byte) ([]byte, error) { + if track >= d.tracks { + return nil, fmt.Errorf("ReadPhysicalSector expected track between 0 and %d; got %d", d.tracks-1, track) + } + if sector >= d.sectors { + return nil, fmt.Errorf("ReadPhysicalSector expected sector between 0 and %d; got %d", d.sectors-1, sector) + } + + storedSector := d.physicalToStored[int(sector)] + start := int(track)*d.bytesPerTrack + 256*int(storedSector) + buf := make([]byte, 256) + copy(buf, d.data[start:start+256]) + return buf, nil +} + +// WritePhysicalSector writes a single physical sector to a disk. It +// expects exactly 256 bytes. +func (d DSK) WritePhysicalSector(track byte, sector byte, data []byte) error { + if track >= d.tracks { + return fmt.Errorf("WritePhysicalSector expected track between 0 and %d; got %d", d.tracks-1, track) + } + if sector >= d.sectors { + return fmt.Errorf("WritePhysicalSector expected sector between 0 and %d; got %d", d.sectors-1, sector) + } + if len(data) != 256 { + return fmt.Errorf("WritePhysicalSector expects data of length 256; got %d", len(data)) + } + + storedSector := d.physicalToStored[int(sector)] + start := int(track)*d.bytesPerTrack + 256*int(storedSector) + copy(d.data[start:start+256], data) + return nil +} + +// Sectors returns the number of sectors on the DSK image. +func (d DSK) Sectors() byte { + return d.sectors +} + +// Tracks returns the number of tracks on the DSK image. +func (d DSK) Tracks() byte { + return d.tracks +} diff --git a/lib/disk/ops.go b/lib/disk/ops.go new file mode 100644 index 0000000..ff94df1 --- /dev/null +++ b/lib/disk/ops.go @@ -0,0 +1,72 @@ +// Copyright © 2016 Zellyn Hunter + +// ops.go contains the interfaces and helper functions for operating +// on disk images logically: catalog, rename, delete, create files, +// etc. + +package disk + +import ( + "errors" + "fmt" + "sort" + "strings" +) + +// Descriptor describes a file's characteristics. +type Descriptor struct { + Name string + Sectors int + Length int + Locked bool +} + +// Operator is the interface that can operate on disks. +type Operator interface { + // Name returns the name of the operator. + Name() string + // HasSubdirs returns true if the underlying operating system on the + // disk allows subdirectories. + HasSubdirs() bool + // Catalog returns a catalog of disk entries. subdir should be empty + // for operating systems that do not support subdirectories. + Catalog(subdir string) ([]Descriptor, error) +} + +// operatorFactory is the type of functions that accept a SectorDisk, +// and may return an Operator interface to operate on it. +type operatorFactory func(SectorDisk) (Operator, error) + +// operatorFactories is the map of currently-registered operator +// factories. +var operatorFactories map[string]operatorFactory + +func init() { + operatorFactories = make(map[string]operatorFactory) +} + +// RegisterOperatorFactory registers an operator factory with the +// given name: a function that accepts a SectorDisk, and may return an +// Operator. It doesn't lock operatorFactories: it is expected to be +// called only from package `init` functions. +func RegisterOperatorFactory(name string, factory operatorFactory) { + operatorFactories[name] = factory +} + +// OperatorFor returns an Operator for the given SectorDisk, if possible. +func OperatorFor(sd SectorDisk) (Operator, error) { + if len(operatorFactories) == 0 { + return nil, errors.New("Cannot find an operator matching the given disk image (none registered)") + } + for _, factory := range operatorFactories { + if operator, err := factory(sd); err == nil { + return operator, nil + } + } + names := make([]string, 0, len(operatorFactories)) + for name := range operatorFactories { + names = append(names, `"`+name+`"`) + } + sort.Strings(names) + return nil, fmt.Errorf("Cannot find an operator matching the given disk image (tried %s)", strings.Join(names, ", ")) +} diff --git a/lib/dos33/dos33.go b/lib/dos33/dos33.go index 370cff6..a930ac2 100644 --- a/lib/dos33/dos33.go +++ b/lib/dos33/dos33.go @@ -454,3 +454,63 @@ func ReadCatalog(d disk.LogicalSectorDisk) (files, deleted []FileDesc, err error } return files, deleted, nil } + +// operator is a disk.Operator - an interface for performing +// high-level operations on files and directories. +type operator struct { + lsd disk.LogicalSectorDisk +} + +var _ disk.Operator = operator{} + +// operatorName is the keyword name for the operator that undestands +// dos33 disks. +const operatorName = "dos33" + +// Name returns the name of the operator. +func (o operator) Name() string { + return operatorName +} + +// HasSubdirs returns true if the underlying operating system on the +// disk allows subdirectories. +func (o operator) HasSubdirs() bool { + return false +} + +// Catalog returns a catalog of disk entries. subdir should be empty +// for operating systems that do not support subdirectories. +func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) { + fds, _, err := ReadCatalog(o.lsd) + if err != nil { + return nil, err + } + descs := make([]disk.Descriptor, 0, len(fds)) + for _, fd := range fds { + descs = append(descs, disk.Descriptor{ + Name: fd.FilenameString(), + Sectors: int(fd.SectorCount), + Length: -1, // TODO(zellyn): read actual file length + Locked: (fd.Filetype & FiletypeLocked) > 0, + }) + } + return descs, nil +} + +// operatorFactory is the factory that returns dos33 operators given +// disk images. +func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) { + lsd, err := disk.NewMappedDisk(sd, disk.Dos33LogicalToPhysicalSectorMap) + if err != nil { + return nil, err + } + _, _, err = ReadCatalog(lsd) + if err != nil { + return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err) + } + return operator{lsd: lsd}, nil +} + +func init() { + disk.RegisterOperatorFactory(operatorName, operatorFactory) +} diff --git a/lib/supermon/supermon.go b/lib/supermon/supermon.go index 4548c3a..f1ede6d 100644 --- a/lib/supermon/supermon.go +++ b/lib/supermon/supermon.go @@ -8,6 +8,7 @@ package supermon import ( "fmt" + "strings" "github.com/zellyn/diskii/lib/disk" ) @@ -211,3 +212,88 @@ func (st SymbolTable) SymbolsByAddress() map[uint16][]Symbol { } return result } + +func FilenameString(file byte, symbols []Symbol) string { + if len(symbols) > 0 { + for _, symbol := range symbols { + if strings.HasPrefix(symbol.Name, "F") { + return symbol.Name + } + } + return symbols[0].Name + } + return fmt.Sprintf("%02X", file) +} + +// operator is a disk.Operator - an interface for performing +// high-level operations on files and directories. +type operator struct { + sd disk.SectorDisk + sm SectorMap + st SymbolTable + symbols map[uint16][]Symbol +} + +var _ disk.Operator = operator{} + +// operatorName is the keyword name for the operator that undestands +// NakedOS/Super-Mon disks. +const operatorName = "nakedos" + +// Name returns the name of the operator. +func (o operator) Name() string { + return operatorName +} + +// HasSubdirs returns true if the underlying operating system on the +// disk allows subdirectories. +func (o operator) HasSubdirs() bool { + return false +} + +// Catalog returns a catalog of disk entries. subdir should be empty +// for operating systems that do not support subdirectories. +func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) { + var descs []disk.Descriptor + sectorsByFile := o.sm.SectorsByFile() + for file := byte(1); file < FileReserved; file++ { + l := len(sectorsByFile[file]) + if l == 0 { + continue + } + fileAddr := uint16(0xDF00) + uint16(file) + descs = append(descs, disk.Descriptor{ + Name: FilenameString(file, o.symbols[fileAddr]), + Sectors: l, + Length: l * 256, + Locked: false, + }) + } + return descs, nil +} + +// operatorFactory is the factory that returns dos33 operators given +// disk images. +func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) { + sm, err := LoadSectorMap(sd) + if err != nil { + return nil, err + } + if err := sm.Verify(); err != nil { + return nil, err + } + + op := operator{sd: sd, sm: sm} + + st, err := sm.ReadSymbolTable(sd) + if err == nil { + op.st = st + op.symbols = st.SymbolsByAddress() + } + + return op, nil +} + +func init() { + disk.RegisterOperatorFactory(operatorName, operatorFactory) +}