diff --git a/README.md b/README.md index 52fe4c6..62be69c 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ Current disk operations supported: | ---------------- | -------- | ------ | ------------------ | | basic structures | ✓ | ✓ | ✓ | | ls | ✓ | ✓ | ✓ | -| dump | ✓ | ✗ | ✓ | -| put | ✗ | ✗ | ✓ | +| dump | ✗ | ✗ | ✗ | +| put | ✗ | ✗ | ✗ | | dumptext | ✗ | ✗ | ✗ | | delete | ✗ | ✗ | ✗ | | rename | ✗ | ✗ | ✗ | @@ -78,12 +78,9 @@ will be likely to get priority. - [x] Implement `GetFile` for DOS 3.3 - [ ] Add and implement the `-l` flag for `ls` - [x] Add `Delete` to the `disk.Operator` interface - - [x] Implement it for Super-Mon + - [ ] Implement it for Super-Mon - [ ] Implement it for DOS 3.3 -- [ ] Make 13-sector DOS disks work -- [ ] Read/write nybble formats -- [ ] Read/write gzipped files -- [ ] Add basic ProDOS structures +- [x] Add basic ProDOS structures - [ ] Add ProDOS support # Related tools @@ -114,38 +111,34 @@ will be likely to get priority. - `.do` - `.po` -- `.dsk` - could be DO or PO. +- `.dsk` - could be DO or PO. When in doubt, assume DO. -DOS 3.2.1: the 13 sectors are physically skewed on disk. - -DOS 3.3+: the 16 physical sectors are stored in ascending order on disk, not physically skewed at all. The - - -| Logical Sector | DOS 3.3 Physical Sector | ProDOS Physical Sector | -| --------------- | -------------- | ------------- | -| 0 | 0 | x | -| 1 | D | x | -| 2 | B | x | -| 3 | 9 | x | -| 4 | 7 | x | -| 5 | 5 | x | -| 6 | 3 | x | -| 7 | 1 | x | -| 8 | E | x | -| 9 | C | x | -| A | A | x | -| B | 8 | x | -| C | 6 | x | -| D | 4 | x | -| E | 2 | x | -| F | F | x | +| Physical Sectors | DOS 3.2 Logical | DOS 3.3 Logical | ProDOS/Pascal Logical | CP/M Logical | +|------------------|-----------------|-----------------|-----------------------|------------- | +| 0 | 0 | 0 | 0.0 | 0.0 | +| 1 | 1 | 7 | 4.0 | 2.3 | +| 2 | 2 | E | 0.1 | 1.2 | +| 3 | 3 | 6 | 4.1 | 0.1 | +| 4 | 4 | D | 1.0 | 3.0 | +| 5 | 5 | 5 | 5.0 | 1.3 | +| 6 | 6 | C | 1.1 | 0.2 | +| 7 | 7 | 4 | 5.1 | 3.1 | +| 8 | 8 | B | 2.0 | 2.0 | +| 9 | 9 | 3 | 6.0 | 0.3 | +| A | A | A | 2.1 | 3.2 | +| B | B | 2 | 6.1 | 2.1 | +| C | C | 9 | 3.0 | 1.0 | +| D | | 1 | 7.0 | 3.3 | +| E | | 8 | 3.1 | 2.2 | +| F | | F | 7.1 | 1.1 | +_Note: DOS 3.2 rearranged the physical sectors on disk to achieve interleaving._ ### RWTS - DOS Sector mapping: http://www.textfiles.com/apple/ANATOMY/rwts.s.txt and search for INTRLEAV -Mapping from specified sector to physical sector (the reverse of what the comment says): +Mapping from specified sector to physical sector: `00 0D 0B 09 07 05 03 01 0E 0C 0A 08 06 04 02 0F` diff --git a/cmd/catalog.go b/cmd/catalog.go index 8204622..ba4c9b1 100644 --- a/cmd/catalog.go +++ b/cmd/catalog.go @@ -10,19 +10,19 @@ import ( "github.com/zellyn/diskii/types" ) -var shortnames bool // flag for whether to print short filenames -var debug bool - type LsCmd struct { + Order string `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"` + System string `kong:"default='auto',enum='auto,dos3',help='DOS system used for image.'"` + ShortNames bool `kong:"short='s',help='Whether to print short filenames (only makes a difference on Super-Mon disks).'"` Image *os.File `kong:"arg,required,help='Disk/device image to read.'"` Directory string `kong:"arg,optional,help='Directory to list (ProDOS only).'"` } func (l *LsCmd) Run(globals *types.Globals) error { - op, order, err := disk.OpenImage(l.Image, globals) + op, order, err := disk.OpenImage(l.Image, l.Order, l.System, globals) if err != nil { - return err + return fmt.Errorf("%w: %s", err, l.Image.Name()) } if globals.Debug { fmt.Fprintf(os.Stderr, "Opened disk with order %q, system %q\n", order, op.Name()) @@ -38,7 +38,7 @@ func (l *LsCmd) Run(globals *types.Globals) error { return err } for _, fd := range fds { - if !shortnames && fd.Fullname != "" { + if !l.ShortNames && fd.Fullname != "" { fmt.Println(fd.Fullname) } else { fmt.Println(fd.Name) diff --git a/cmd/reorder.go b/cmd/reorder.go new file mode 100644 index 0000000..9cc04b2 --- /dev/null +++ b/cmd/reorder.go @@ -0,0 +1,101 @@ +package cmd + +import ( + "fmt" + "path" + "strings" + + "github.com/zellyn/diskii/disk" + "github.com/zellyn/diskii/helpers" + "github.com/zellyn/diskii/types" +) + +type ReorderCmd struct { + Order string `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"` + NewOrder string `kong:"default='auto',enum='auto,do,po',help='New Logical-to-physical sector order.'"` + Force bool `kong:"short='s',help='Overwrite existing file?'"` + + DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to read.'"` + NewDiskImage string `kong:"arg,optional,type='path',help='Disk image to write, if different.'"` +} + +func (r *ReorderCmd) Run(globals *types.Globals) error { + fromOrderName, toOrderName, err := getOrders(r.DiskImage, r.Order, r.NewDiskImage, r.NewOrder) + if err != nil { + return err + } + frombytes, err := helpers.FileContentsOrStdIn(r.DiskImage) + if err != nil { + return err + } + fromOrder, ok := disk.LogicalToPhysicalByName[fromOrderName] + if !ok { + return fmt.Errorf("internal error: disk order '%s' not found", fromOrderName) + } + toOrder, ok := disk.PhysicalToLogicalByName[toOrderName] + if !ok { + return fmt.Errorf("internal error: disk order '%s' not found", toOrderName) + } + rawbytes, err := disk.Swizzle(frombytes, fromOrder) + if err != nil { + return err + } + tobytes, err := disk.Swizzle(rawbytes, toOrder) + if err != nil { + return err + } + return helpers.WriteOutput(r.NewDiskImage, tobytes, r.DiskImage, r.Force) +} + +// getOrders returns the input order, and the output order. +func getOrders(inFilename string, inOrder string, outFilename string, outOrder string) (string, string, error) { + if inOrder == "auto" && outOrder != "auto" { + return oppositeOrder(outOrder), outOrder, nil + } + if outOrder == "auto" && inOrder != "auto" { + return inOrder, oppositeOrder(inOrder), nil + } + if inOrder != outOrder { + return inOrder, outOrder, nil + } + if inOrder != "auto" { + return "", "", fmt.Errorf("identical order and new-order") + } + + inGuess, outGuess := orderFromFilename(inFilename), orderFromFilename(outFilename) + if inGuess == outGuess { + if inGuess == "" { + return "", "", fmt.Errorf("cannot determine input or output order from file extensions") + } + return "", "", fmt.Errorf("guessed order (%s) from file %q is the same as guessed order (%s) from file %q", inGuess, inFilename, outGuess, outFilename) + } + + if inGuess == "" { + return oppositeOrder(outGuess), outGuess, nil + } + if outGuess == "" { + return inGuess, oppositeOrder(inGuess), nil + } + return inGuess, outGuess, nil +} + +// oppositeOrder returns the opposite order from the input. +func oppositeOrder(order string) string { + if order == "do" { + return "po" + } + return "do" +} + +// orderFromFilename tries to guess the disk order from the filename, using the extension. +func orderFromFilename(filename string) string { + ext := strings.ToLower(path.Ext(filename)) + switch ext { + case ".dsk", ".do": + return "do" + case ".po": + return "po" + default: + return "" + } +} diff --git a/data/data.go b/data/data.go index f14b74e..6f035e5 100644 --- a/data/data.go +++ b/data/data.go @@ -3,16 +3,20 @@ package data import _ "embed" // DOS 3.3 Master Disk. -//go:embed disks/dos33mst.dsk +//go:embed disks/dos33master.dsk var DOS33master_dsk []byte +// DOS 3.3 Master Disk, as a .woz file. +//go:embed disks/dos33master.woz +var DOS33master_woz []byte + // John Brooks' update to ProDOS. // Website: https://prodos8.com // Announcements: https://www.callapple.org/author/jbrooks/ -//go:embed disks/ProDOS_2_4_2.dsk -var ProDOS242_dsk []byte +//go:embed disks/ProDOS_2_4_2.po +var ProDOS242_po []byte -// The new ProDOS sector 0, used on and after the IIGS System 4.0. +// The new ProDOS sector 0, used on and after the IIGS System 4.0. Understands sparse PRODOS.SYSTEM files. //go:embed boot/prodos-new-boot0.bin var ProDOSNewBootSector0 []byte diff --git a/data/disks/ProDOS_2_4_1.dsk b/data/disks/ProDOS_2_4_1.dsk deleted file mode 100644 index ef4e8b1..0000000 Binary files a/data/disks/ProDOS_2_4_1.dsk and /dev/null differ diff --git a/data/disks/ProDOS_2_4_2.dsk b/data/disks/ProDOS_2_4_2.dsk deleted file mode 100644 index 6530d52..0000000 Binary files a/data/disks/ProDOS_2_4_2.dsk and /dev/null differ diff --git a/data/disks/ProDOS_2_4_2.po b/data/disks/ProDOS_2_4_2.po new file mode 100644 index 0000000..019e252 Binary files /dev/null and b/data/disks/ProDOS_2_4_2.po differ diff --git a/data/disks/dos33mst.dsk b/data/disks/Super-Mon-2.0.dsk similarity index 51% rename from data/disks/dos33mst.dsk rename to data/disks/Super-Mon-2.0.dsk index ef257aa..badfd82 100644 Binary files a/data/disks/dos33mst.dsk and b/data/disks/Super-Mon-2.0.dsk differ diff --git a/data/disks/dos33mst-incorrect-suffix.po b/data/disks/dos33master.dsk similarity index 100% rename from data/disks/dos33mst-incorrect-suffix.po rename to data/disks/dos33master.dsk diff --git a/disk/dev.go b/disk/dev.go index 5e1205f..4c3f7c0 100644 --- a/disk/dev.go +++ b/disk/dev.go @@ -11,7 +11,7 @@ import ( ) // A ProDOS block. -type Block [256]byte +type Block [512]byte // Dev represents a .po disk image. type Dev struct { diff --git a/disk/disk.go b/disk/disk.go index 74c667d..fde6565 100644 --- a/disk/disk.go +++ b/disk/disk.go @@ -41,6 +41,20 @@ var ProDosPhysicalToLogicalSectorMap = []int{ 0x04, 0x0C, 0x05, 0x0D, 0x06, 0x0E, 0x07, 0x0F, } +// LogicalToPhysicalByName maps from "do" and "po" to the corresponding +// logical-to-physical ordering. +var LogicalToPhysicalByName map[string][]int = map[string][]int{ + "do": Dos33LogicalToPhysicalSectorMap, + "po": ProDOSLogicalToPhysicalSectorMap, +} + +// PhysicalToLogicalByName maps from "do" and "po" to the corresponding +// physical-to-logical ordering. +var PhysicalToLogicalByName map[string][]int = map[string][]int{ + "do": Dos33PhysicalToLogicalSectorMap, + "po": ProDosPhysicalToLogicalSectorMap, +} + // TrackSector is a pair of track/sector bytes. type TrackSector struct { Track byte diff --git a/disk/open.go b/disk/open.go index ba65ce0..34de3c9 100644 --- a/disk/open.go +++ b/disk/open.go @@ -10,36 +10,30 @@ import ( "github.com/zellyn/diskii/types" ) -var diskOrdersByName map[string][]int = map[string][]int{ - "do": Dos33LogicalToPhysicalSectorMap, - "po": ProDOSLogicalToPhysicalSectorMap, - "raw": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}, -} - // OpenImage attempts to open an image on disk, using the provided ordering and system type. -func OpenImage(file *os.File, globals *types.Globals) (types.Operator, string, error) { +func OpenImage(file *os.File, order string, system string, globals *types.Globals) (types.Operator, string, error) { bb, err := io.ReadAll(file) if err != nil { return nil, "", err } if len(bb) == FloppyDiskBytes { - return openDoOrPo(bb, globals, strings.ToLower(path.Ext(file.Name()))) + return openDoOrPo(bb, order, system, globals, strings.ToLower(path.Ext(file.Name()))) } return nil, "", fmt.Errorf("OpenImage not implemented yet for non-disk-sized images") } -func openDoOrPo(diskbytes []byte, globals *types.Globals, ext string) (types.Operator, string, error) { +func openDoOrPo(diskbytes []byte, order string, system string, globals *types.Globals, ext string) (types.Operator, string, error) { var factories []types.OperatorFactory for _, factory := range globals.DiskOperatorFactories { - if globals.System == "auto" || globals.System == factory.Name() { + if system == "auto" || system == factory.Name() { factories = append(factories, factory) } } if len(factories) == 0 { - return nil, "", fmt.Errorf("cannot find disk system with name %q", globals.System) + return nil, "", fmt.Errorf("cannot find disk system with name %q", system) } - orders := []string{globals.Order} - switch globals.Order { + orders := []string{order} + switch order { case "do", "po": // nothing more case "auto": @@ -54,16 +48,16 @@ func openDoOrPo(diskbytes []byte, globals *types.Globals, ext string) (types.Ope return nil, "", fmt.Errorf("unknown disk image extension: %q", ext) } default: - return nil, "", fmt.Errorf("disk order %q invalid for %d-byte disk images", globals.Order, FloppyDiskBytes) + return nil, "", fmt.Errorf("disk order %q invalid for %d-byte disk images", order, FloppyDiskBytes) } for _, order := range orders { - swizzled, err := Swizzle(diskbytes, diskOrdersByName[order]) + swizzled, err := Swizzle(diskbytes, LogicalToPhysicalByName[order]) if err != nil { return nil, "", err } for _, factory := range factories { - if len(orders) == 1 && globals.System != "auto" { + if len(orders) == 1 && system != "auto" { if globals.Debug { fmt.Fprintf(os.Stderr, "Attempting to open with order=%s, system=%s.\n", order, factory.Name()) } @@ -88,14 +82,14 @@ func openDoOrPo(diskbytes []byte, globals *types.Globals, ext string) (types.Ope } } } - return nil, "", fmt.Errorf("openDoOrPo not implemented yet") + return nil, "", fmt.Errorf("unabled to open disk image") } // Swizzle changes the sector ordering according to the order parameter. If // order is nil, it leaves the order unchanged. func Swizzle(diskimage []byte, order []int) ([]byte, error) { if len(diskimage) != FloppyDiskBytes { - return nil, fmt.Errorf("swizzling only works on disk images of %d bytes; got %d", FloppyDiskBytes, len(diskimage)) + return nil, fmt.Errorf("reordering only works on disk images of %d bytes; got %d", FloppyDiskBytes, len(diskimage)) } if err := validateOrder(order); err != nil { return nil, fmt.Errorf("called Swizzle with weird order: %w", err) diff --git a/dos3/dos3.go b/dos3/dos3.go index a0d1e60..f10fcda 100644 --- a/dos3/dos3.go +++ b/dos3/dos3.go @@ -7,7 +7,6 @@ package dos3 import ( "encoding/binary" "fmt" - "os" "strings" "github.com/zellyn/diskii/disk" @@ -500,9 +499,6 @@ func readCatalogSectors(diskbytes []byte, debug bool) ([]CatalogSector, error) { if err := v.Validate(); err != nil { return nil, fmt.Errorf("Invalid VTOC sector: %v", err) } - if debug { - fmt.Fprintf(os.Stderr, "Read VTOC sector: %#v\n", v) - } nextTrack := v.CatalogTrack nextSector := v.CatalogSector @@ -686,9 +682,13 @@ func (of OperatorFactory) Name() string { // SeemsToMatch returns true if the []byte disk image seems to match the // system of this operator. -func (of OperatorFactory) SeemsToMatch(diskbytes []byte, debug bool) bool { +func (of OperatorFactory) SeemsToMatch(rawbytes []byte, debug bool) bool { // For now, just return true if we can run Catalog successfully. - _, _, err := ReadCatalog(diskbytes, debug) + swizzled, err := of.swizzle(rawbytes) + if err != nil { + return false + } + _, _, err = ReadCatalog(swizzled, debug) if err != nil { return false } @@ -696,6 +696,14 @@ func (of OperatorFactory) SeemsToMatch(diskbytes []byte, debug bool) bool { } // Operator returns an Operator for the []byte disk image. -func (of OperatorFactory) Operator(diskbytes []byte, debug bool) (types.Operator, error) { - return operator{data: diskbytes, debug: debug}, nil +func (of OperatorFactory) Operator(rawbytes []byte, debug bool) (types.Operator, error) { + swizzled, err := of.swizzle(rawbytes) + if err != nil { + return nil, err + } + return operator{data: swizzled, debug: debug}, nil +} + +func (of OperatorFactory) swizzle(rawbytes []byte) ([]byte, error) { + return disk.Swizzle(rawbytes, disk.Dos33PhysicalToLogicalSectorMap) } diff --git a/helpers/helpers.go b/helpers/helpers.go index 1b85fe0..eed3847 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -5,7 +5,10 @@ package helpers import ( - "io/ioutil" + "errors" + "fmt" + "io" + "io/fs" "os" ) @@ -13,7 +16,23 @@ import ( // is "-", in which case it reads from stdin. func FileContentsOrStdIn(s string) ([]byte, error) { if s == "-" { - return ioutil.ReadAll(os.Stdin) + return io.ReadAll(os.Stdin) } - return ioutil.ReadFile(s) + return os.ReadFile(s) +} + +func WriteOutput(outfilename string, contents []byte, infilename string, force bool) error { + if outfilename == "" { + outfilename = infilename + } + if outfilename == "-" { + _, err := os.Stdout.Write(contents) + return err + } + if !force { + if _, err := os.Stat(outfilename); !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("cannot overwrite file %q without --force (-f)", outfilename) + } + } + return os.WriteFile(outfilename, contents, 0666) } diff --git a/main.go b/main.go index 2ca0d72..41741db 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,8 @@ package main import ( "github.com/zellyn/diskii/cmd" "github.com/zellyn/diskii/dos3" + "github.com/zellyn/diskii/prodos" + "github.com/zellyn/diskii/supermon" "github.com/zellyn/diskii/types" "fmt" @@ -14,11 +16,10 @@ import ( ) var cli struct { - Debug bool `kong:"short='v',help='Enable debug mode.'"` - Order string `kong:"default='auto',enum='auto,raw,do,po',help='Logical-to-physical sector order.'"` - System string `kong:"default='auto',enum='auto,dos3',help='DOS system used for image.'"` + Debug bool `kong:"short='v',help='Enable debug mode.'"` - Ls cmd.LsCmd `cmd:"" aliases:"cat,catalog" help:"List paths."` + Ls cmd.LsCmd `cmd:"" aliases:"cat,catalog" help:"List paths."` + Reorder cmd.ReorderCmd `cmd:"" help:"Reorder disk images."` } func run() error { @@ -34,13 +35,11 @@ func run() error { ) globals := &types.Globals{ - Debug: cli.Debug, - Order: cli.Order, - System: cli.System, + Debug: cli.Debug, DiskOperatorFactories: []types.OperatorFactory{ dos3.OperatorFactory{}, - // supermon.OperatorFactory, - // prodos.DiskOperatorFactory, + supermon.OperatorFactory{}, + prodos.OperatorFactory{}, }, } // Call the Run() method of the selected parsed command. diff --git a/notes.org b/notes.org index 12bb8d5..6343ecb 100644 --- a/notes.org +++ b/notes.org @@ -85,3 +85,8 @@ F8 CLD echo -n -e '\x20\x40\x03\x6D\x01\xDC\x2C\x02\xDF\x2C\x00\x60\xF8\x4C\x00\x60' | diskii put -f ./lib/supermon/testdata/chacha20.dsk DF01:FHELLO - echo -n -e '\x20\x58\xFC\xA2\x00\xBD\x13\x60\xF0\x06\x20\xED\xFD\xE8\xD0\xF5\x4C\x10\x60\xC8\xC5\xCC\xCC\xCF\xAC\xA0\xD7\xCF\xD2\xCC\xC4\x00' | diskii put -f ./lib/supermon/testdata/chacha20.dsk DF02:FWORLD - + +* Sources + +** ProDOS +[[https://www.apple.asimov.net/documentation/source_code/Apple%20ProDOS%20Boot%20Source.pdf][ProDOS boot source]] diff --git a/prodos/prodos.go b/prodos/prodos.go index a893fcf..7671c26 100644 --- a/prodos/prodos.go +++ b/prodos/prodos.go @@ -7,9 +7,9 @@ package prodos import ( "encoding/binary" "fmt" - "io" "github.com/zellyn/diskii/disk" + "github.com/zellyn/diskii/types" ) // Storage types. @@ -104,11 +104,11 @@ func (vbm VolumeBitMap) IsFree(block uint16) bool { // readVolumeBitMap reads the entire volume bitmap from a block // device. -func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, error) { - blocks := bd.Blocks() / 4096 +func readVolumeBitMap(devicebytes []byte, startBlock uint16) (VolumeBitMap, error) { + blocks := uint16(len(devicebytes) / 512 / 4096) vbm := NewVolumeBitMap(startBlock, blocks) for i := 0; i < len(vbm); i++ { - if err := disk.UnmarshalBlock(bd, &vbm[i], vbm[i].GetBlock()); err != nil { + if err := disk.UnmarshalBlock(devicebytes, &vbm[i], vbm[i].GetBlock()); err != nil { return nil, fmt.Errorf("cannot read block %d (device block %d) of Volume Bit Map: %v", i, vbm[i].GetBlock(), err) } } @@ -116,9 +116,9 @@ func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, err } // Write writes the Volume Bit Map to a block device. -func (vbm VolumeBitMap) Write(bd disk.BlockDevice) error { +func (vbm VolumeBitMap) Write(devicebytes []byte) error { for i, bp := range vbm { - if err := disk.MarshalBlock(bd, bp); err != nil { + if err := disk.MarshalBlock(devicebytes, bp); err != nil { return fmt.Errorf("cannot write block %d (device block %d) of Volume Bit Map: %v", i, bp.GetBlock(), err) } } @@ -351,14 +351,14 @@ type FileDescriptor struct { HeaderPointer uint16 // Block number of the key block for the directory which describes this file. } -// descriptor returns a disk.Descriptor for a FileDescriptor. -func (fd FileDescriptor) descriptor() disk.Descriptor { - desc := disk.Descriptor{ +// descriptor returns a types.Descriptor for a FileDescriptor. +func (fd FileDescriptor) descriptor() types.Descriptor { + desc := types.Descriptor{ Name: fd.Name(), Blocks: int(fd.BlocksUsed), Length: int(fd.Eof[0]) + int(fd.Eof[1])<<8 + int(fd.Eof[2])<<16, - Locked: false, // TODO(zellyn): use prodos-style access in disk.Descriptor - Type: disk.Filetype(fd.FileType), + Locked: false, // TODO(zellyn): use prodos-style access in types.Descriptor + Type: types.Filetype(fd.FileType), } return desc } @@ -652,18 +652,18 @@ func (v Volume) subdirDescriptors() []FileDescriptor { // readVolume reads the entire volume and subdirectories from a device // into memory. -func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) { +func readVolume(devicebytes []byte, keyBlock uint16) (Volume, error) { v := Volume{ keyBlock: &VolumeDirectoryKeyBlock{}, subdirsByBlock: make(map[uint16]*Subdirectory), subdirsByName: make(map[string]*Subdirectory), } - if err := disk.UnmarshalBlock(bd, v.keyBlock, keyBlock); err != nil { + if err := disk.UnmarshalBlock(devicebytes, v.keyBlock, keyBlock); err != nil { return v, fmt.Errorf("cannot read first block of volume directory (block %d): %v", keyBlock, err) } - if vbm, err := readVolumeBitMap(bd, v.keyBlock.Header.BitMapPointer); err != nil { + if vbm, err := readVolumeBitMap(devicebytes, v.keyBlock.Header.BitMapPointer); err != nil { return v, err } else { v.bitmap = &vbm @@ -671,7 +671,7 @@ func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) { for block := v.keyBlock.Next; block != 0; block = v.blocks[len(v.blocks)-1].Next { vdb := VolumeDirectoryBlock{} - if err := disk.UnmarshalBlock(bd, &vdb, block); err != nil { + if err := disk.UnmarshalBlock(devicebytes, &vdb, block); err != nil { return v, err } v.blocks = append(v.blocks, &vdb) @@ -681,7 +681,7 @@ func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) { for i := 0; i < len(sdds); i++ { sdd := sdds[i] - sub, err := readSubdirectory(bd, sdd) + sub, err := readSubdirectory(devicebytes, sdd) if err != nil { return v, err } @@ -751,18 +751,18 @@ func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[u // readSubdirectory reads a single subdirectory from a device into // memory. -func readSubdirectory(bd disk.BlockDevice, fd FileDescriptor) (Subdirectory, error) { +func readSubdirectory(devicebytes []byte, fd FileDescriptor) (Subdirectory, error) { s := Subdirectory{ keyBlock: &SubdirectoryKeyBlock{}, } - if err := disk.UnmarshalBlock(bd, s.keyBlock, fd.KeyPointer); err != nil { + if err := disk.UnmarshalBlock(devicebytes, s.keyBlock, fd.KeyPointer); err != nil { return s, fmt.Errorf("cannot read first block of subdirectory %q (block %d): %v", fd.Name(), fd.KeyPointer, err) } for block := s.keyBlock.Next; block != 0; block = s.blocks[len(s.blocks)-1].Next { sdb := SubdirectoryBlock{} - if err := disk.UnmarshalBlock(bd, &sdb, block); err != nil { + if err := disk.UnmarshalBlock(devicebytes, &sdb, block); err != nil { return s, err } s.blocks = append(s.blocks, &sdb) @@ -783,10 +783,11 @@ func copyBytes(dst, src []byte) int { // operator is a disk.Operator - an interface for performing // high-level operations on files and directories. type operator struct { - dev disk.BlockDevice + data []byte + debug bool } -var _ disk.Operator = operator{} +var _ types.Operator = operator{} // operatorName is the keyword name for the operator that undestands // prodos disks/devices. @@ -797,11 +798,6 @@ func (o operator) Name() string { return operatorName } -// Order returns the sector or block order of the underlying storage. -func (o operator) Order() string { - return o.dev.Order() -} - // HasSubdirs returns true if the underlying operating system on the // disk allows subdirectories. func (o operator) HasSubdirs() bool { @@ -810,14 +806,14 @@ func (o operator) HasSubdirs() bool { // 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) { +func (o operator) Catalog(subdir string) ([]types.Descriptor, error) { - vol, err := readVolume(o.dev, 2) + vol, err := readVolume(o.data, 2) if err != nil { return nil, err } - var result []disk.Descriptor + var result []types.Descriptor if subdir == "" { for _, desc := range vol.descriptors() { @@ -842,8 +838,8 @@ func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) { } // GetFile retrieves a file by name. -func (o operator) GetFile(filename string) (disk.FileInfo, error) { - return disk.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName) +func (o operator) GetFile(filename string) (types.FileInfo, error) { + return types.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName) } // Delete deletes a file by name. It returns true if the file was @@ -855,37 +851,49 @@ func (o operator) Delete(filename string) (bool, error) { // PutFile writes a file by name. If the file exists and overwrite // is false, it returns with an error. Otherwise it returns true if // an existing file was overwritten. -func (o operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) { +func (o operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) { return false, fmt.Errorf("%s doesn't implement PutFile yet", operatorName) } -// Write writes the underlying device blocks to the given writer. -func (o operator) Write(w io.Writer) (int, error) { - return o.dev.Write(w) +// OperatorFactory is a types.OperatorFactory for ProDos disks. +type OperatorFactory struct { } -// deviceOperatorFactory is the factory that returns prodos operators -// given device images. -func deviceOperatorFactory(bd disk.BlockDevice) (disk.Operator, error) { - op := operator{dev: bd} - _, err := op.Catalog("") - if err != nil { - return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err) +// Name returns the name of the operator. +func (of OperatorFactory) Name() string { + return operatorName +} + +// SeemsToMatch returns true if the []byte disk image seems to match the +// system of this operator. +func (of OperatorFactory) SeemsToMatch(devicebytes []byte, debug bool) bool { + // For now, just return true if we can run Catalog successfully. + if len(devicebytes) == disk.FloppyDiskBytes { + swizzled, err := of.swizzle(devicebytes) + if err != nil { + return false + } + devicebytes = swizzled } - return op, nil -} - -// DiskOperatorFactory is the factory that returns dos3 operators -// given disk images. -func DiskOperatorFactory(sd disk.SectorDisk) (disk.Operator, error) { - bd, err := disk.BlockDeviceFromSectorDisk(sd) + _, err := readVolume(devicebytes, 2) if err != nil { - return nil, err + return false } - return deviceOperatorFactory(bd) + return true } -func init() { - disk.RegisterDeviceOperatorFactory(operatorName, deviceOperatorFactory) - disk.RegisterDiskOperatorFactory(operatorName, DiskOperatorFactory) +// Operator returns an Operator for the []byte disk image. +func (of OperatorFactory) Operator(devicebytes []byte, debug bool) (types.Operator, error) { + if len(devicebytes) == disk.FloppyDiskBytes { + swizzled, err := of.swizzle(devicebytes) + if err != nil { + return nil, err + } + devicebytes = swizzled + } + return operator{data: devicebytes, debug: debug}, nil +} + +func (of OperatorFactory) swizzle(rawbytes []byte) ([]byte, error) { + return disk.Swizzle(rawbytes, disk.ProDosPhysicalToLogicalSectorMap) } diff --git a/supermon/supermon.go b/supermon/supermon.go index dcd1e47..a62028b 100644 --- a/supermon/supermon.go +++ b/supermon/supermon.go @@ -12,6 +12,7 @@ import ( "github.com/zellyn/diskii/disk" "github.com/zellyn/diskii/errors" + "github.com/zellyn/diskii/types" ) const ( @@ -253,14 +254,14 @@ func decodeSymbol(five []byte, extra byte) string { value := uint64(five[0]) + uint64(five[1])<<8 + uint64(five[2])<<16 + uint64(five[3])<<24 + uint64(five[4])<<32 + uint64(extra)<<40 for value&0x1f > 0 { if value&0x1f < 27 { - result = result + string(value&0x1f+'@') + result = result + string(rune(value&0x1f+'@')) value >>= 5 continue } if value&0x20 == 0 { - result = result + string((value&0x1f)-0x1b+'0') + result = result + string(rune((value&0x1f)-0x1b+'0')) } else { - result = result + string((value&0x1f)-0x1b+'5') + result = result + string(rune((value&0x1f)-0x1b+'5')) } value >>= 6 } @@ -639,74 +640,75 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named return numFile, namedFile, parts[1], nil } -// Operator is a disk.Operator - an interface for performing +// operator is a disk.Operator - an interface for performing // high-level operations on files and directories. -type Operator struct { - data []byte - SM SectorMap - ST SymbolTable +type operator struct { + data []byte + SM SectorMap + ST SymbolTable + debug bool } -var _ disk.Operator = Operator{} +var _ types.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 { +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 { +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 +func (o operator) Catalog(subdir string) ([]types.Descriptor, error) { + var descs []types.Descriptor sectorsByFile := o.SM.SectorsByFile() for file := byte(1); file < FileReserved; file++ { l := len(sectorsByFile[file]) if l == 0 { continue } - descs = append(descs, disk.Descriptor{ + descs = append(descs, types.Descriptor{ Name: NameForFile(file, o.ST), Fullname: FullnameForFile(file, o.ST), Sectors: l, Length: l * 256, Locked: false, - Type: disk.FiletypeBinary, + Type: types.FiletypeBinary, }) } return descs, nil } // GetFile retrieves a file by name. -func (o Operator) GetFile(filename string) (disk.FileInfo, error) { +func (o operator) GetFile(filename string) (types.FileInfo, error) { file, err := o.ST.FileForName(filename) if err != nil { - return disk.FileInfo{}, err + return types.FileInfo{}, err } data, err := o.SM.ReadFile(o.data, file) if err != nil { - return disk.FileInfo{}, fmt.Errorf("error reading file DF%02x: %v", file, err) + return types.FileInfo{}, fmt.Errorf("error reading file DF%02x: %v", file, err) } if len(data) == 0 { - return disk.FileInfo{}, fmt.Errorf("file DF%02x not fount", file) + return types.FileInfo{}, fmt.Errorf("file DF%02x not fount", file) } - desc := disk.Descriptor{ + desc := types.Descriptor{ Name: NameForFile(file, o.ST), Sectors: len(data) / 256, Length: len(data), Locked: false, - Type: disk.FiletypeBinary, + Type: types.FiletypeBinary, } - fi := disk.FileInfo{ + fi := types.FileInfo{ Descriptor: desc, Data: data, } @@ -718,7 +720,7 @@ func (o Operator) GetFile(filename string) (disk.FileInfo, error) { // 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) { +func (o operator) Delete(filename string) (bool, error) { file, err := o.ST.FileForName(filename) if err != nil { return false, err @@ -742,8 +744,8 @@ func (o Operator) Delete(filename string) (bool, error) { // PutFile writes a file by name. If the file exists and overwrite // is false, it returns with an error. Otherwise it returns true if // an existing file was overwritten. -func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) { - if fileInfo.Descriptor.Type != disk.FiletypeBinary { +func (o operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) { + if fileInfo.Descriptor.Type != types.FiletypeBinary { return false, fmt.Errorf("%s: only binary file type supported", operatorName) } if fileInfo.Descriptor.Length != len(fileInfo.Data) { @@ -783,9 +785,9 @@ func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, return existed, nil } -// OperatorFactory is the factory that returns supermon operators +// XOperatorFactory is the factory that returns supermon operators // given disk images. -func OperatorFactory(diskbytes []byte) (disk.Operator, error) { +func XOperatorFactory(diskbytes []byte) (types.Operator, error) { sm, err := LoadSectorMap(diskbytes) if err != nil { return nil, err @@ -794,7 +796,50 @@ func OperatorFactory(diskbytes []byte) (disk.Operator, error) { return nil, err } - op := Operator{data: diskbytes, SM: sm} + op := operator{data: diskbytes, SM: sm} + + st, err := sm.ReadSymbolTable(diskbytes) + if err == nil { + op.ST = st + } + + return op, nil +} + +// OperatorFactory is a types.OperatorFactory for DOS 3.3 disks. +type OperatorFactory struct { +} + +// Name returns the name of the operator. +func (of OperatorFactory) Name() string { + return operatorName +} + +// SeemsToMatch returns true if the []byte disk image seems to match the +// system of this operator. +func (of OperatorFactory) SeemsToMatch(diskbytes []byte, debug bool) bool { + // For now, just return true if we can run Catalog successfully. + sm, err := LoadSectorMap(diskbytes) + if err != nil { + return false + } + if err := sm.Verify(); err != nil { + return false + } + return true +} + +// Operator returns an Operator for the []byte disk image. +func (of OperatorFactory) Operator(diskbytes []byte, debug bool) (types.Operator, error) { + sm, err := LoadSectorMap(diskbytes) + if err != nil { + return nil, err + } + if err := sm.Verify(); err != nil { + return nil, err + } + + op := operator{data: diskbytes, SM: sm, debug: debug} st, err := sm.ReadSymbolTable(diskbytes) if err == nil { diff --git a/supermon/supermon_test.go b/supermon/supermon_test.go index e0c495a..eaeec0c 100644 --- a/supermon/supermon_test.go +++ b/supermon/supermon_test.go @@ -3,12 +3,14 @@ package supermon import ( + "os" "reflect" "strings" "testing" "github.com/kr/pretty" "github.com/zellyn/diskii/disk" + "github.com/zellyn/diskii/types" ) const testDisk = "testdata/chacha20.dsk" @@ -27,16 +29,36 @@ No more; and by a sleep, to say we end // loadSectorMap loads a sector map for the disk image contained in // filename. It returns the sector map and a sector disk. -func loadSectorMap(filename string) (SectorMap, disk.SectorDisk, error) { - sd, err := disk.LoadDSK(filename) +func loadSectorMap(filename string) (SectorMap, []byte, error) { + rawbytes, err := os.ReadFile(filename) if err != nil { return nil, nil, err } - sm, err := LoadSectorMap(sd) + diskbytes, err := disk.Swizzle(rawbytes, disk.Dos33LogicalToPhysicalSectorMap) if err != nil { return nil, nil, err } - return sm, sd, nil + sm, err := LoadSectorMap(diskbytes) + if err != nil { + return nil, nil, err + } + return sm, diskbytes, nil +} + +// getOperator gets a types.Operator for the given NakedOS disk, assumed to be +// in "do" order. +func getOperator(filename string) (types.Operator, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + op, _, err := disk.OpenImage(f, "do", "nakedos", &types.Globals{ + DiskOperatorFactories: []types.OperatorFactory{OperatorFactory{}}, + }) + if err != nil { + return nil, err + } + return op, nil } // TestReadSectorMap tests the reading of the sector map of a test @@ -126,11 +148,7 @@ func TestReadSymbolTable(t *testing.T) { // TestGetFile tests the retrieval of a file's contents, using the // Operator interface. func TestGetFile(t *testing.T) { - sd, err := disk.OpenDisk(testDisk) - if err != nil { - t.Fatal(err) - } - op, err := disk.OperatorForDisk(sd) + op, err := getOperator(testDisk) if err != nil { t.Fatal(err) } @@ -213,20 +231,16 @@ func TestReadWriteSymbolTable(t *testing.T) { // TestPutFile tests the creation of a file, using the Operator // interface. func TestPutFile(t *testing.T) { - sd, err := disk.OpenDisk(testDisk) - if err != nil { - t.Fatal(err) - } - op, err := disk.OperatorForDisk(sd) + op, err := getOperator(testDisk) if err != nil { t.Fatal(err) } contents := []byte(cities) - fileInfo := disk.FileInfo{ - Descriptor: disk.Descriptor{ + fileInfo := types.FileInfo{ + Descriptor: types.Descriptor{ Name: "FNEWFILE", Length: len(contents), - Type: disk.FiletypeBinary, + Type: types.FiletypeBinary, }, Data: contents, } diff --git a/types/types.go b/types/types.go index eccc3e2..ddf1a8f 100644 --- a/types/types.go +++ b/types/types.go @@ -2,9 +2,7 @@ package types // Globals holds flags and configuration that are shared globally. type Globals struct { - Debug bool - Order string //Logical-to-physical sector order - System string // DOS system used for image + Debug bool DiskOperatorFactories []OperatorFactory } diff --git a/woz/woz_test.go b/woz/woz_test.go index 082a12d..6e717d1 100644 --- a/woz/woz_test.go +++ b/woz/woz_test.go @@ -9,8 +9,7 @@ import ( ) func TestBasicLoad(t *testing.T) { - bb := data.MustAsset("data/disks/dos33master.woz") - wz, err := woz.Decode(bytes.NewReader(bb)) + wz, err := woz.Decode(bytes.NewReader(data.DOS33master_woz)) if err != nil { t.Fatal(err) }