Implemented `cat` command for dos3.3 and NakedOS

- Added a generic `Operator` registry and implemented Operators for
  dos3.3 and NakeOS/Super-Mon disks.
- Currently Operators implement only the `Catalog` command.
This commit is contained in:
Zellyn Hunter 2016-11-13 16:59:38 -05:00
parent c7cb3bb5ce
commit f57c3a0c06
8 changed files with 402 additions and 98 deletions

View File

@ -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.

61
cmd/cat.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
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
}

View File

@ -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)")
}

View File

@ -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)
}

86
lib/disk/dsk.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// 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
}

72
lib/disk/ops.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// 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, ", "))
}

View File

@ -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)
}

View File

@ -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)
}