Merge pull request #2 from zellyn/refactor

Refactor to use kong instead of cobra
This commit is contained in:
Zellyn Hunter 2021-07-31 22:12:28 -04:00 committed by GitHub
commit 7d036244af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1658 additions and 1755 deletions

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"filewatcher.commands": [
{
"match": "\\.go",
"cmd": "echo '${file} changed'",
"event": "onFileChange",
},
]
}

View File

@ -28,11 +28,34 @@ diskii's major disadvantage is that it mostly doesn't exist yet.
It rhymes with “whiskey”. It rhymes with “whiskey”.
Discussion/support is in Discussion/support is on the
[#apple2 on the retrocomputing Slack](https://retrocomputing.slack.com/messages/apple2/) [apple2infinitum Slack](https://apple2infinitum.slack.com/)
(invites [here](https://retrocomputing.herokuapp.com)). (invites [here](http://apple2.gs:3000/)).
### Goals # Examples
Get a listing of files on a DOS 3.3 disk image:
```
diskii ls dos33master.dsk
```
… or a ProDOS disk image:
```
diskii ls ProDOS_2_4_2.po
```
… or a Super-Mon disk image:
```
diskii ls Super-Mon-2.0.dsk
```
Reorder the sectors in a disk image:
```
diskii reorder ProDOS_2_4_2.dsk ProDOS_2_4_2.po
```
# Goals
Eventually, it aims to be a comprehensive disk image manipulation Eventually, it aims to be a comprehensive disk image manipulation
tool, but for now only some parts work. tool, but for now only some parts work.
@ -47,8 +70,8 @@ Current disk operations supported:
| ---------------- | -------- | ------ | ------------------ | | ---------------- | -------- | ------ | ------------------ |
| basic structures | ✓ | ✓ | ✓ | | basic structures | ✓ | ✓ | ✓ |
| ls | ✓ | ✓ | ✓ | | ls | ✓ | ✓ | ✓ |
| dump | ✓ | ✗ | ✓ | | dump | ✗ | ✗ | ✗ |
| put | ✗ | ✗ | | | put | ✗ | ✗ | |
| dumptext | ✗ | ✗ | ✗ | | dumptext | ✗ | ✗ | ✗ |
| delete | ✗ | ✗ | ✗ | | delete | ✗ | ✗ | ✗ |
| rename | ✗ | ✗ | ✗ | | rename | ✗ | ✗ | ✗ |
@ -59,7 +82,7 @@ Current disk operations supported:
| init | ✗ | ✗ | ✗ | | init | ✗ | ✗ | ✗ |
| defrag | ✗ | ✗ | ✗ | | defrag | ✗ | ✗ | ✗ |
### Installing/updating # Installing/updating
Assuming you have Go installed, run `go get -u github.com/zellyn/diskii` Assuming you have Go installed, run `go get -u github.com/zellyn/diskii`
You can also download automatically-built binaries from the You can also download automatically-built binaries from the
@ -68,25 +91,24 @@ page](https://github.com/zellyn/diskii/releases/latest). If you
need binaries for a different architecture, please send a pull need binaries for a different architecture, please send a pull
request or open an issue. request or open an issue.
### Short-term TODOs/roadmap/easy ways to contribute # Short-term TODOs/roadmap/easy ways to contribute
My rough TODO list (apart from anything marked (✗) in the disk My rough TODO list (apart from anything marked (✗) in the disk
operations matrix is listed below. Anything that an actual user needs operations matrix is listed below. Anything that an actual user needs
will be likely to get priority. will be likely to get priority.
- [ ] Make `put` accept load address for appropriate filetypes.
- [ ] Implement `GetFile` for prodos
- [x] Build per-platform binaries for Linux, MacOS, Windows. - [x] Build per-platform binaries for Linux, MacOS, Windows.
- [x] Implement `GetFile` for DOS 3.3 - [x] Implement `GetFile` for DOS 3.3
- [ ] Add and implement the `-l` flag for `ls` - [ ] Add and implement the `-l` flag for `ls`
- [x] Add `Delete` to the `disk.Operator` interface - [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 - [ ] Implement it for DOS 3.3
- [ ] Make 13-sector DOS disks work - [ ] Add ProDOS support for all commands
- [ ] Read/write nybble formats - [x] Make `filetypes` command use a tabwriter to write as a table
- [ ] Read/write gzipped files
- [ ] Add basic ProDOS structures
- [ ] Add ProDOS support
### Related tools # Related tools
- http://a2ciderpress.com/ - the great grandaddy of them all. Windows only, unless you Wine - http://a2ciderpress.com/ - the great grandaddy of them all. Windows only, unless you Wine
- http://retrocomputingaustralia.com/rca-downloads/ Michael Mulhern's MacOS package of CiderPress - http://retrocomputingaustralia.com/rca-downloads/ Michael Mulhern's MacOS package of CiderPress
@ -107,3 +129,48 @@ will be likely to get priority.
- https://github.com/slotek/apple2-disk-util - ruby - https://github.com/slotek/apple2-disk-util - ruby
- https://github.com/slotek/dsk2nib - C - https://github.com/slotek/dsk2nib - C
- https://github.com/robmcmullen/atrcopy - dos3.3, python - https://github.com/robmcmullen/atrcopy - dos3.3, python
# Notes
## Disk formats
- `.do`
- `.po`
- `.dsk` - could be DO or PO. When in doubt, assume DO.
| 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:
`00 0D 0B 09 07 05 03 01 0E 0C 0A 08 06 04 02 0F`
So if you write to "T0S1" with DOS RWTS, it ends up in physical sector 0D.
## Commandline examples for thinking about how it should work
diskii ls dos33.dsk
diskii --order=do ls dos33.dsk
diskii --order=do --system=nakedos ls nakedos.dsk

View File

@ -5,7 +5,7 @@ package applesoft
import ( import (
"testing" "testing"
"github.com/zellyn/diskii/lib/basic" "github.com/zellyn/diskii/basic"
) )
// helloBinary is a simple basic program used for testing. Listing // helloBinary is a simple basic program used for testing. Listing

View File

@ -5,7 +5,7 @@ package integer
import ( import (
"testing" "testing"
"github.com/zellyn/diskii/lib/basic" "github.com/zellyn/diskii/basic"
) )
// helloBinary is a simple basic program used for testing. Listing // helloBinary is a simple basic program used for testing. Listing

57
build Executable file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
export ACME="$HOME/gh/acme/ACME_Lib"
ACME_BIN="$HOME/gh/acme/acme"
$ACME_BIN -o writetest.o -r writetest.lst writetest.asm
# cp data/disks/dos33mst.dsk writetest.dsk
# diskii put -f writetest.dsk writetest writetest.o
# Also run mame? (set MAMEDIR and MAMEBIN to your local variant)
[[ -z "${MAMEDIR-}" ]] && MAMEDIR="/Users/zellyn/Library/Application Support/Ample"
[[ -z "${MAMEBIN-}" ]] && MAMEBIN="/Applications/Ample.app/Contents/MacOS/mame64"
# Write audit.o into an OpenEmulator config?
[[ -z "${TMPLS-}" ]] && TMPLS=~/gh/OpenEmulator-OSX/modules/libemulation/res/templates
DSK=$(realpath ./audit.dsk)
case "${1-none}" in
"2ee")
# mame64 apple2ee -skip_gameinfo -nosamples -window -resolution 1120x840 -flop1 /Users/zellyn/gh/a2audit/audit/audit.dsk
(cd "$MAMEDIR"; "$MAMEBIN" apple2ee -window -flop1 "$DSK" -skip_gameinfo)
;;
"2e")
(cd "$MAMEDIR"; "$MAMEBIN" apple2e -window -flop1 "$DSK" -skip_gameinfo)
;;
"2p")
(cd "$MAMEDIR"; "$MAMEBIN" apple2p -window -flop1 "$DSK" -skip_gameinfo)
;;
"2")
(cd "$MAMEDIR"; "$MAMEBIN" apple2 -window -flop1 "$DSK" -skip_gameinfo)
;;
"2ee-d")
(cd "$MAMEDIR"; "$MAMEBIN" apple2ee -window -flop1 "$DSK" -skip_gameinfo -debug)
;;
"2e-d")
(cd "$MAMEDIR"; "$MAMEBIN" apple2e -window -flop1 "$DSK" -skip_gameinfo -debug)
;;
"2p-d")
(cd "$MAMEDIR"; "$MAMEBIN" apple2p -window -flop1 "$DSK" -skip_gameinfo -debug)
;;
"2-d")
(cd "$MAMEDIR"; "$MAMEBIN" apple2 -window -flop1 "$DSK" -skip_gameinfo -debug)
;;
"oe")
(head -c 24576 /dev/zero; cat audit.o; head -c 65536 /dev/zero) | head -c 65536 > $TMPLS/Apple\ II/Apple\ IIe-test.emulation/appleIIe.mainRam.bin
sed -e 's|<property name="pc" value="0x...."/>|<property name="pc" value="0x6000"/>|' $TMPLS/Apple\ II/Apple\ IIe.xml > $TMPLS/Apple\ II/Apple\ IIe-test.emulation/info.xml
;;
"none")
;;
*)
echo Options: 2ee, 2e, 2p, 2, 2ee-d, 2e-d, 2p-d, 2-d
esac
true # Signal success (since we had a bunch of conditionals that can return false status).

View File

@ -3,87 +3,41 @@
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"github.com/spf13/cobra" "github.com/zellyn/diskii/basic"
"github.com/zellyn/diskii/lib/basic" "github.com/zellyn/diskii/basic/applesoft"
"github.com/zellyn/diskii/lib/basic/applesoft" "github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/lib/helpers" "github.com/zellyn/diskii/types"
) )
// applesoftCmd represents the applesoft command type ApplesoftCmd struct {
var applesoftCmd = &cobra.Command{ Decode DecodeCmd `kong:"cmd,help='Convert a binary Applesoft program to a text LISTing.'"`
Use: "applesoft",
Short: "work with applesoft programs",
Long: `diskii applesoft contains the subcommands useful for working
with Applesoft programs.`,
} }
func init() { type DecodeCmd struct {
RootCmd.AddCommand(applesoftCmd) Filename string `kong:"arg,default='-',type='existingfile',help='Binary Applesoft file to read, or “-” for stdin.'"`
// Here you will define your flags and configuration settings. Location uint16 `kong:"type='anybaseuint16',default='0x801',help='Starting program location in memory.'"`
Raw bool `kong:"short='r',help='Print raw control codes (no escaping)'"`
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// applesoftCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// applesoftCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
} }
// ----- applesoft decode command ------------------------------------------- func (d DecodeCmd) Help() string {
return `Examples:
var location uint16 // flag for starting location in memory # Dump the contents of HELLO and then decode it.
var rawControlCodes bool // flag for whether to skip escaping control codes diskii dump dos33master.dsk HELLO | diskii applesoft decode -`
// decodeCmd represents the decode command
var decodeCmd = &cobra.Command{
Use: "decode filename",
Short: "convert a binary applesoft program to a LISTing",
Long: `
decode converts a binary Applesoft program to a text LISTing.
Examples:
decode filename # read filename
decode - # read stdin`,
Run: func(cmd *cobra.Command, args []string) {
if err := runDecode(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(-1)
}
},
} }
func init() { func (d *DecodeCmd) Run(globals *types.Globals) error {
applesoftCmd.AddCommand(decodeCmd) contents, err := helpers.FileContentsOrStdIn(d.Filename)
// 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 {
return fmt.Errorf("decode expects one argument: the filename (or - for stdin)")
}
contents, err := helpers.FileContentsOrStdIn(args[0])
if err != nil { if err != nil {
return err return err
} }
listing, err := applesoft.Decode(contents, location) listing, err := applesoft.Decode(contents, d.Location)
if err != nil { if err != nil {
return err return err
} }
if rawControlCodes { if d.Raw {
os.Stdout.WriteString(listing.String()) os.Stdout.WriteString(listing.String())
} else { } else {
os.Stdout.WriteString(basic.ChevronControlCodes(listing.String())) os.Stdout.WriteString(basic.ChevronControlCodes(listing.String()))

View File

@ -6,59 +6,39 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/spf13/cobra" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/types"
) )
var shortnames bool // flag for whether to print short filenames type LsCmd struct {
var debug bool Order types.DiskOrder `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.'"`
// catalogCmd represents the cat command, used to catalog a disk or ShortNames bool `kong:"short='s',help='Whether to print short filenames (only makes a difference on Super-Mon disks).'"`
// directory. Image *os.File `kong:"arg,required,help='Disk/device image to read.'"`
var catalogCmd = &cobra.Command{ Directory string `kong:"arg,optional,help='Directory to list (ProDOS only).'"`
Use: "catalog",
Aliases: []string{"cat", "ls"},
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() { func (l *LsCmd) Run(globals *types.Globals) error {
RootCmd.AddCommand(catalogCmd) op, order, err := disk.OpenFile(l.Image, l.Order, l.System, globals.DiskOperatorFactories, globals.Debug)
catalogCmd.Flags().BoolVarP(&shortnames, "shortnames", "s", false, "whether to print short filenames (only makes a difference on Super-Mon disks)")
catalogCmd.Flags().BoolVarP(&debug, "debug", "d", false, "pring debug information")
}
// 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")
}
op, err := disk.Open(args[0])
if err != nil { if err != nil {
return err return fmt.Errorf("%w: %s", err, l.Image.Name())
} }
if debug { if globals.Debug {
fmt.Printf("Got disk of type %q with underlying sector/block order %q.\n", op.Name(), op.Order()) fmt.Fprintf(os.Stderr, "Opened disk with order %q, system %q\n", order, op.Name())
} }
subdir := ""
if len(args) == 2 { if l.Directory != "" {
if !op.HasSubdirs() { if !op.HasSubdirs() {
return fmt.Errorf("Disks of type %q cannot have subdirectories", op.Name()) return fmt.Errorf("Disks of type %q cannot have subdirectories", op.Name())
} }
subdir = args[1]
} }
fds, err := op.Catalog(subdir) fds, err := op.Catalog(l.Directory)
if err != nil { if err != nil {
return err return err
} }
for _, fd := range fds { for _, fd := range fds {
if !shortnames && fd.Fullname != "" { if !l.ShortNames && fd.Fullname != "" {
fmt.Println(fd.Fullname) fmt.Println(fd.Fullname)
} else { } else {
fmt.Println(fd.Name) fmt.Println(fd.Name)

View File

@ -4,61 +4,38 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/types"
) )
var missingok bool // flag for whether to consider deleting a nonexistent file okay type DeleteCmd struct {
Order types.DiskOrder `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.'"`
MissingOk bool `kong:"short='f',help='Overwrite existing file?'"`
// deleteCmd represents the delete command, used to delete a file. DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
var deleteCmd = &cobra.Command{ Filename string `kong:"arg,required,help='Filename to use on disk.'"`
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() { func (d DeleteCmd) Help() string {
RootCmd.AddCommand(deleteCmd) return `Examples:
deleteCmd.Flags().BoolVarP(&missingok, "missingok", "f", false, "if true, don't consider deleting a nonexistent file an error") # Delete file GREMLINS on disk image games.dsk.
diskii rm games.dsk GREMLINS`
} }
// runDelete performs the actual delete logic. func (d *DeleteCmd) Run(globals *types.Globals) error {
func runDelete(args []string) error { op, order, err := disk.OpenFilename(d.DiskImage, d.Order, d.System, globals.DiskOperatorFactories, globals.Debug)
if len(args) != 2 {
return fmt.Errorf("delete expects a disk image filename, and a filename")
}
op, err := disk.Open(args[0])
if err != nil { if err != nil {
return err return err
} }
deleted, err := op.Delete(args[1])
deleted, err := op.Delete(d.Filename)
if err != nil { if err != nil {
return err return err
} }
if !deleted && !missingok { if !deleted && !d.MissingOk {
return fmt.Errorf("file %q not found", args[1]) return fmt.Errorf("file %q not found (use -f to prevent this being an error)", d.Filename)
} }
f, err := os.Create(args[0]) return disk.WriteBack(d.DiskImage, op, order, true)
if err != nil {
return err
}
_, err = op.Write(f)
if err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return nil
} }

View File

@ -3,48 +3,39 @@
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"github.com/spf13/cobra" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/types"
) )
// dumpCmd represents the dump command, used to dump the raw contents type DumpCmd struct {
// of a file. Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
var dumpCmd = &cobra.Command{ System string `kong:"default='auto',enum='auto,dos3',help='DOS system used for image.'"`
Use: "dump",
Short: "dump the raw contents of a file",
Long: `Dump the raw contents of a file.
dump disk-image.dsk HELLO DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
`, Filename string `kong:"arg,required,help='Filename to use on disk.'"`
Run: func(cmd *cobra.Command, args []string) {
if err := runDump(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(-1)
}
},
} }
func init() { func (d DumpCmd) Help() string {
RootCmd.AddCommand(dumpCmd) return `Examples:
# Dump file GREMLINS on disk image games.dsk.
diskii dump games.dsk GREMLINS`
} }
// runDump performs the actual dump logic. func (d *DumpCmd) Run(globals *types.Globals) error {
func runDump(args []string) error { op, _, err := disk.OpenFilename(d.DiskImage, d.Order, d.System, globals.DiskOperatorFactories, globals.Debug)
if len(args) != 2 {
return fmt.Errorf("dump expects a disk image filename, and a filename")
}
op, err := disk.Open(args[0])
if err != nil { if err != nil {
return err return err
} }
file, err := op.GetFile(args[1])
file, err := op.GetFile(d.Filename)
if err != nil {
return err
}
_, err = os.Stdout.Write(file.Data)
if err != nil { if err != nil {
return err return err
} }
// TODO(zellyn): allow writing to files
os.Stdout.Write(file.Data)
return nil return nil
} }

View File

@ -5,39 +5,22 @@ package cmd
import ( import (
"fmt" "fmt"
"os" "os"
"text/tabwriter"
"github.com/spf13/cobra" "github.com/zellyn/diskii/types"
"github.com/zellyn/diskii/lib/disk"
) )
var all bool // flag for whether to show all filetypes type FiletypesCmd struct {
All bool `kong:"help='Display all types, including SOS types and reserved ranges.'"`
// filetypesCmd represents the filetypes command, used to display
// valid filetypes recognized by diskii.
var filetypesCmd = &cobra.Command{
Use: "filetypes",
Short: "print a list of filetypes",
Long: `Print a list of filetypes understood by diskii`,
Run: func(cmd *cobra.Command, args []string) {
if err := runFiletypes(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(-1)
}
},
} }
func init() { func (f *FiletypesCmd) Run(globals *types.Globals) error {
RootCmd.AddCommand(filetypesCmd) w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
filetypesCmd.Flags().BoolVarP(&all, "all", "a", false, "display all types, including SOS types and reserved ranges") fmt.Fprintln(w, "Description\tName\tThree-letter Name\tOne-letter Name")
} fmt.Fprintln(w, "-----------\t----\t-----------------\t---------------")
for _, typ := range types.FiletypeInfos(f.All) {
// runFiletypes performs the actual listing of filetypes. fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", typ.Desc, typ.Name, typ.ThreeLetter, typ.OneLetter)
func runFiletypes(args []string) error {
if len(args) != 0 {
return fmt.Errorf("filetypes expects no arguments")
}
for _, typ := range disk.FiletypeNames(all) {
fmt.Println(typ)
} }
w.Flush()
return nil return nil
} }

View File

@ -4,87 +4,76 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/supermon"
"github.com/zellyn/diskii/lib/supermon" "github.com/zellyn/diskii/types"
) )
// nakedosCmd represents the nakedos command
var nakedosCmd = &cobra.Command{
Use: "nakedos",
Short: "work with NakedOS disks",
Long: `diskii nakedos contains the subcommands useful for working
with NakedOS (and Super-Mon) disks`,
Aliases: []string{"supermon"},
}
func init() {
RootCmd.AddCommand(nakedosCmd)
}
// ----- mkhello command ----------------------------------------------------
var address uint16 // flag for address to load at
var start uint16 // flag for address to start execution at
const helloName = "FHELLO" // filename to use (if Super-Mon) const helloName = "FHELLO" // filename to use (if Super-Mon)
// mkhelloCmd represents the mkhello command type NakedOSCmd struct {
var mkhelloCmd = &cobra.Command{ Mkhello MkHelloCmd `kong:"cmd,help='Create an FHELLO program that loads and runs another file.'"`
Use: "mkhello <disk-image> filename", }
Short: "create an FHELLO program that loads and runs another file",
Long: `
mkhello creates file DF01:FHELLO that loads and runs another program at a specific address.
// Help shows extended help on NakedOS/Super-Mon.
func (n NakedOSCmd) Help() string {
return `NakedOS and Super-Mon were created by the amazing Martin Haye. For more information see:
Source/docs: https://bitbucket.org/martin.haye/super-mon/
Presentation: https://www.kansasfest.org/2012/08/2010-haye-nakedos/`
}
type MkHelloCmd struct {
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
Filename string `kong:"arg,required,help='Name of NakedOS file to load.'"`
Address uint16 `kong:"type='anybaseuint16',default='0x6000',help='Address to load the code at.'"`
Start uint16 `kong:"type='anybaseuint16',default='0xFFFF',help='Address to jump to. Defaults to 0xFFFF, which means “same as address flag”'"`
}
func (m MkHelloCmd) Help() string {
return `This command creates a very short DF01:FHELLO program that simply loads another program of your choice.
Examples: Examples:
mkhello test.dsk FDEMO # load and run FDEMO at the default address, then jump to the start of the loaded code. # Load and run FDEMO at the default address, then jump to the start of the loaded code.
mkhello test.dsk --address 0x2000 --start 0x2100 DF06 # load and run file DF06 at address 0x2000, and jump to 0x2100.`, mkhello test.dsk FDEMO
Run: func(cmd *cobra.Command, args []string) {
if err := runMkhello(args); err != nil { # Load and run file DF06 at address 0x2000, and jump to 0x2100.
fmt.Fprintln(os.Stderr, err.Error()) mkhello test.dsk --address 0x2000 --start 0x2100 DF06`
os.Exit(-1)
}
},
} }
func init() { func (m *MkHelloCmd) Run(globals *types.Globals) error {
nakedosCmd.AddCommand(mkhelloCmd) if m.Start == 0xFFFF {
m.Start = m.Address
// Here you will define your flags and configuration settings.
mkhelloCmd.Flags().Uint16VarP(&address, "address", "a", 0x6000, "memory location to load code at")
mkhelloCmd.Flags().Uint16VarP(&start, "start", "s", 0x6000, "memory location to jump to")
}
// runMkhello performs the actual mkhello logic.
func runMkhello(args []string) error {
if len(args) != 2 {
return fmt.Errorf("usage: diskii mkhello <disk image> <file-to-load>")
} }
if address%256 != 0 {
return fmt.Errorf("address %d (%04X) not on a page boundary", address, address) if m.Address%256 != 0 {
return fmt.Errorf("address %d (%04X) not on a page boundary", m.Address, m.Address)
} }
if start < address { if m.Start < m.Address {
return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", start, start, address, address) return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", m.Start, m.Start, m.Address, m.Address)
} }
op, err := disk.Open(args[0])
op, order, err := disk.OpenFilename(m.DiskImage, m.Order, "auto", globals.DiskOperatorFactories, globals.Debug)
if err != nil { if err != nil {
return err return err
} }
if op.Name() != "nakedos" { if op.Name() != "nakedos" {
return fmt.Errorf("mkhello only works on disks of type %q; got %q", "nakedos", op.Name()) return fmt.Errorf("mkhello only works on disks of type %q; got %q", "nakedos", op.Name())
} }
nakOp, ok := op.(supermon.Operator) nakOp, ok := op.(supermon.Operator)
if !ok { if !ok {
return fmt.Errorf("internal error: cannot cast to expected supermon.Operator type") return fmt.Errorf("internal error: cannot cast to expected supermon.Operator type (got %T)", op)
} }
addr, symbolAddr, _, err := nakOp.ST.FilesForCompoundName(args[1]) addr, symbolAddr, _, err := nakOp.ST.FilesForCompoundName(m.Filename)
if err != nil { if err != nil {
return err return err
} }
if addr == 0 && symbolAddr == 0 { if addr == 0 && symbolAddr == 0 {
return fmt.Errorf("cannot parse %q as valid filename", args[1]) return fmt.Errorf("cannot parse %q as valid filename", m.Filename)
} }
toLoad := addr toLoad := addr
if addr == 0 { if addr == 0 {
@ -94,32 +83,24 @@ func runMkhello(args []string) error {
0x20, 0x40, 0x03, // JSR NAKEDOS 0x20, 0x40, 0x03, // JSR NAKEDOS
0x6D, 0x01, 0xDC, // ADC NKRDFILE 0x6D, 0x01, 0xDC, // ADC NKRDFILE
0x2C, toLoad, 0xDF, // BIT ${file number to load} 0x2C, toLoad, 0xDF, // BIT ${file number to load}
0x2C, 0x00, byte(address >> 8), // BIT ${target page} 0x2C, 0x00, byte(m.Address >> 8), // BIT ${target page}
0xD8, // CLD 0xD8, // CLD
0x4C, byte(start), byte(start >> 8), // JMP ${target page} 0x4C, byte(m.Start), byte(m.Start >> 8), // JMP ${target page}
} }
fileInfo := disk.FileInfo{ fileInfo := types.FileInfo{
Descriptor: disk.Descriptor{ Descriptor: types.Descriptor{
Name: fmt.Sprintf("DF01:%s", helloName), Name: fmt.Sprintf("DF01:%s", helloName),
Length: len(contents), Length: len(contents),
Type: disk.FiletypeBinary, Type: types.FiletypeBinary,
}, },
Data: contents, Data: contents,
} }
_ = fileInfo
_, err = op.PutFile(fileInfo, true) _, err = op.PutFile(fileInfo, true)
if err != nil { if err != nil {
return err return err
} }
f, err := os.Create(args[0])
if err != nil { return disk.WriteBack(m.DiskImage, op, order, true)
return err
}
_, err = op.Write(f)
if err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return nil
} }

View File

@ -4,80 +4,61 @@ package cmd
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/lib/helpers" "github.com/zellyn/diskii/types"
) )
var filetypeName string // flag for file type type PutCmd struct {
var overwrite bool // flag for whether to overwrite Order types.DiskOrder `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.'"`
FiletypeName string `kong:"default='B',help='Type of file (“diskii filetypes” to list).'"`
Overwrite bool `kong:"short='f',help='Overwrite existing file?'"`
// putCmd represents the put command, used to put the raw contents DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
// of a file. TargetFilename string `kong:"arg,required,help='Filename to use on disk.'"`
var putCmd = &cobra.Command{ SourceFilename string `kong:"arg,required,type='existingfile',help='Name of file containing data to put.'"`
Use: "put", }
Short: "put the raw contents of a file",
Long: `Put the raw contents of a file.
put disk-image.dsk HELLO <name of file with contents> func (p PutCmd) Help() string {
`, return `Examples:
Run: func(cmd *cobra.Command, args []string) { # Put file gremlins.o onto disk image games.dsk, using the filename GREMLINS.
if err := runPut(args); err != nil { diskii put games.dsk GREMLINS gremlins.o`
fmt.Fprintln(os.Stderr, err.Error()) }
os.Exit(-1)
func (p *PutCmd) Run(globals *types.Globals) error {
if p.DiskImage == "-" {
if p.SourceFilename == "-" {
return fmt.Errorf("cannot get both disk image and file contents from stdin")
} }
},
}
func init() {
RootCmd.AddCommand(putCmd)
putCmd.Flags().StringVarP(&filetypeName, "type", "t", "B", "Type of file (`diskii filetypes` to list)")
putCmd.Flags().BoolVarP(&overwrite, "overwrite", "f", false, "whether to overwrite existing files")
}
// runPut performs the actual put logic.
func runPut(args []string) error {
if len(args) != 3 {
return fmt.Errorf("usage: put <disk image> <target filename> <source filename>")
} }
op, err := disk.Open(args[0]) op, order, err := disk.OpenFilename(p.DiskImage, p.Order, p.System, globals.DiskOperatorFactories, globals.Debug)
if err != nil {
return err
}
contents, err := helpers.FileContentsOrStdIn(args[2])
if err != nil { if err != nil {
return err return err
} }
filetype, err := disk.FiletypeForName(filetypeName) contents, err := helpers.FileContentsOrStdIn(p.SourceFilename)
if err != nil { if err != nil {
return err return err
} }
fileInfo := disk.FileInfo{ filetype, err := types.FiletypeForName(p.FiletypeName)
Descriptor: disk.Descriptor{ if err != nil {
Name: args[1], return err
}
fileInfo := types.FileInfo{
Descriptor: types.Descriptor{
Name: p.TargetFilename,
Length: len(contents), Length: len(contents),
Type: filetype, Type: filetype,
}, },
Data: contents, Data: contents,
} }
_, err = op.PutFile(fileInfo, overwrite) _, err = op.PutFile(fileInfo, p.Overwrite)
if err != nil { if err != nil {
return err return err
} }
f, err := os.Create(args[0])
if err != nil { return disk.WriteBack(p.DiskImage, op, order, true)
return err
}
_, err = op.Write(f)
if err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return nil
} }

93
cmd/reorder.go Normal file
View File

@ -0,0 +1,93 @@
package cmd
import (
"fmt"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/types"
)
type ReorderCmd struct {
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
NewOrder types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='New Logical-to-physical sector order.'"`
Overwrite bool `kong:"short='f',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
}
overwrite := r.Overwrite
filename := r.NewDiskImage
if filename == "" {
filename = r.DiskImage
overwrite = true
}
return helpers.WriteOutput(filename, tobytes, overwrite)
}
// getOrders returns the input order, and the output order.
func getOrders(inFilename string, inOrder types.DiskOrder, outFilename string, outOrder types.DiskOrder) (types.DiskOrder, types.DiskOrder, 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 := disk.OrderFromFilename(inFilename, types.DiskOrderUnknown), disk.OrderFromFilename(outFilename, types.DiskOrderUnknown)
if inGuess == outGuess {
if inGuess == types.DiskOrderUnknown {
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 == types.DiskOrderUnknown {
return oppositeOrder(outGuess), outGuess, nil
}
if outGuess == types.DiskOrderUnknown {
return inGuess, oppositeOrder(inGuess), nil
}
return inGuess, outGuess, nil
}
// oppositeOrder returns the opposite order from the input.
func oppositeOrder(order types.DiskOrder) types.DiskOrder {
if order == types.DiskOrderDO {
return types.DiskOrderPO
}
return types.DiskOrderDO
}

View File

@ -1,61 +0,0 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "diskii",
Short: "Operate on Apple II disk images and their contents",
Long: `diskii is a commandline tool for working with Apple II disk
images.
Eventually, it aims to be a comprehensive disk image manipulation tool.`,
}
// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags, which, if defined here,
// will be global for your application.
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.diskii.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
// RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" { // enable ability to specify config file via flag
viper.SetConfigFile(cfgFile)
}
viper.SetConfigName(".diskii") // name of config file (without extension)
viper.AddConfigPath("$HOME") // adding home directory as first search path
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

109
cmd/sd.go
View File

@ -4,68 +4,62 @@ package cmd
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"github.com/spf13/cobra" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/lib/helpers" "github.com/zellyn/diskii/types"
) )
var sdAddress uint16 // flag for address to load at type SDCmd struct {
var sdStart uint16 // flag for address to start execution at Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
// mksdCmd represents the mksd command DiskImage string `kong:"arg,required,type='path',help='Disk image to write.'"`
var mksdCmd = &cobra.Command{ Binary *os.File `kong:"arg,required,help='Binary file to write to the disk.'"`
Use: "mksd",
Short: "create a Standard-Delivery disk image", Address uint16 `kong:"type='anybaseuint16',default='0x6000',help='Address to load the code at.'"`
Long: `diskii mksd creates a "Standard Delivery" disk image containing a binary. Start uint16 `kong:"type='anybaseuint16',default='0xFFFF',help='Address to jump to. Defaults to 0xFFFF, which means “same as address flag”'"`
}
func (s SDCmd) Help() string {
return `
See https://github.com/peterferrie/standard-delivery for details. See https://github.com/peterferrie/standard-delivery for details.
Examples: Examples:
mksd test.dsk foo.o # load and run foo.o at the default address, then jump to the start of the loaded code. # Load and run foo.o at the default address, then jump to the start of the loaded code.
mksd test.dsk foo.o --address 0x2000 --start 0x2100 # load foo.o at address 0x2000, then jump to 0x2100.`, diskii mksd test.dsk foo.o
Run: func(cmd *cobra.Command, args []string) {
if err := runMkSd(args); err != nil { # Load foo.o at address 0x2000, then jump to 0x2100.
fmt.Fprintln(os.Stderr, err.Error()) diskii mksd test.dsk foo.o --address 0x2000 --start 0x2100`
os.Exit(-1)
}
},
} }
func init() { func (s *SDCmd) Run(globals *types.Globals) error {
RootCmd.AddCommand(mksdCmd) if s.Start == 0xFFFF {
mksdCmd.Flags().Uint16VarP(&sdAddress, "address", "a", 0x6000, "memory location to load code at") s.Start = s.Address
mksdCmd.Flags().Uint16VarP(&sdStart, "start", "s", 0x6000, "memory location to jump to")
}
// ----- mksd command -------------------------------------------------------
// runMkSd performs the actual mksd logic.
func runMkSd(args []string) error {
if len(args) != 2 {
return fmt.Errorf("usage: diskii mksd <disk image> <file-to-load>")
} }
contents, err := helpers.FileContentsOrStdIn(args[1])
contents, err := io.ReadAll(s.Binary)
if err != nil { if err != nil {
return err return err
} }
if sdAddress%256 != 0 { if s.Address%256 != 0 {
return fmt.Errorf("address %d (%04X) not on a page boundary", sdAddress, sdAddress) return fmt.Errorf("address %d (%04X) not on a page boundary", s.Address, s.Address)
} }
if sdStart < sdAddress { if s.Start < s.Address {
return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", sdStart, sdStart, sdAddress, sdAddress) return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", s.Start, s.Start, s.Address, s.Address)
} }
if int(sdStart) >= int(sdAddress)+len(contents) { if int(s.Start) >= int(s.Address)+len(contents) {
end := int(sdAddress) + len(contents) end := int(s.Address) + len(contents)
return fmt.Errorf("start address %d (%04X) is beyond load address %d (%04X) + file length = %d (%04X)", return fmt.Errorf("start address %d (%04X) is beyond load address %d (%04X) + file length = %d (%04X)",
sdStart, sdStart, sdAddress, sdAddress, end, end) s.Start, s.Start, s.Address, s.Address, end, end)
} }
if int(sdStart)+len(contents) > 0xC000 { if int(s.Start)+len(contents) > 0xC000 {
end := int(sdStart) + len(contents) end := int(s.Start) + len(contents)
return fmt.Errorf("start address %d (%04X) + file length %d (%04X) = %d (%04X), but we can't load past page 0xBF00", return fmt.Errorf("start address %d (%04X) + file length %d (%04X) = %d (%04X), but we can't load past page 0xBF00",
sdStart, sdStart, len(contents), len(contents), end, end) s.Start, s.Start, len(contents), len(contents), end, end)
} }
sectors := (len(contents) + 255) / 256 sectors := (len(contents) + 255) / 256
@ -76,35 +70,35 @@ func runMkSd(args []string) error {
0xc8, 0xa5, 0x27, 0xf0, 0xdf, 0x8a, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x48, 0xa9, 0x5b, 0xc8, 0xa5, 0x27, 0xf0, 0xdf, 0x8a, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x48, 0xa9, 0x5b,
0x48, 0x60, 0xe6, 0x41, 0x06, 0x40, 0x20, 0x37, 0x08, 0x18, 0x20, 0x3c, 0x08, 0xe6, 0x40, 0x48, 0x60, 0xe6, 0x41, 0x06, 0x40, 0x20, 0x37, 0x08, 0x18, 0x20, 0x3c, 0x08, 0xe6, 0x40,
0xa5, 0x40, 0x29, 0x03, 0x2a, 0x05, 0x2b, 0xa8, 0xb9, 0x80, 0xc0, 0xa9, 0x30, 0x4c, 0xa8, 0xa5, 0x40, 0x29, 0x03, 0x2a, 0x05, 0x2b, 0xa8, 0xb9, 0x80, 0xc0, 0xa9, 0x30, 0x4c, 0xa8,
0xfc, 0x4c, byte(sdStart), byte(sdStart >> 8), 0xfc, 0x4c, byte(s.Start), byte(s.Start >> 8),
} }
if len(loader)+sectors+1 > 256 { if len(loader)+sectors+1 > 256 {
return fmt.Errorf("file %q is %d bytes long, max is %d", args[1], len(contents), (255-len(loader))*256) return fmt.Errorf("file %q is %d bytes long, max is %d", s.Binary.Name(), len(contents), (255-len(loader))*256)
} }
for len(contents)%256 != 0 { for len(contents)%256 != 0 {
contents = append(contents, 0) contents = append(contents, 0)
} }
sd := disk.Empty() diskbytes := make([]byte, disk.FloppyDiskBytes)
var track, sector byte var track, sector byte
for i := 0; i < len(contents); i += 256 { for i := 0; i < len(contents); i += 256 {
sector += 2 sector += 2
if sector >= sd.Sectors() { if sector >= disk.FloppySectors {
sector = (sd.Sectors() + 1) - sector sector = (disk.FloppySectors + 1) - sector
if sector == 0 { if sector == 0 {
track++ track++
if track >= sd.Tracks() { if track >= disk.FloppyTracks {
return fmt.Errorf("ran out of tracks") return fmt.Errorf("ran out of tracks")
} }
} }
} }
address := int(sdAddress) + i address := int(s.Address) + i
loader = append(loader, byte(address>>8)) loader = append(loader, byte(address>>8))
if err := sd.WritePhysicalSector(track, sector, contents[i:i+256]); err != nil { if err := disk.WriteSector(diskbytes, track, sector, contents[i:i+256]); err != nil {
return err return err
} }
} }
@ -114,20 +108,17 @@ func runMkSd(args []string) error {
loader = append(loader, 0) loader = append(loader, 0)
} }
if err := sd.WritePhysicalSector(0, 0, loader); err != nil { if err := disk.WriteSector(diskbytes, 0, 0, loader); err != nil {
return err return err
} }
f, err := os.Create(args[0]) order := s.Order
if order == types.DiskOrderAuto {
order = disk.OrderFromFilename(s.DiskImage, types.DiskOrderDO)
}
rawBytes, err := disk.Swizzle(diskbytes, disk.PhysicalToLogicalByName[order])
if err != nil { if err != nil {
return err return err
} }
_, err = sd.Write(f) return helpers.WriteOutput(s.DiskImage, rawBytes, true)
if err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return nil
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
data/disks/ProDOS_2_4_2.po Normal file

Binary file not shown.

Binary file not shown.

1
data/disks/blank.dsk Normal file

File diff suppressed because one or more lines are too long

BIN
data/disks/dos33master.dsk Normal file

Binary file not shown.

Binary file not shown.

View File

@ -10,14 +10,15 @@ import (
"io/ioutil" "io/ioutil"
) )
// A ProDOS block.
type Block [512]byte
// Dev represents a .po disk image. // Dev represents a .po disk image.
type Dev struct { type Dev struct {
data []byte // The actual data in the file data []byte // The actual data in the file
blocks uint16 // Number of blocks blocks uint16 // Number of blocks
} }
var _ BlockDevice = (*Dev)(nil)
// LoadDev loads a .po image from a file. // LoadDev loads a .po image from a file.
func LoadDev(filename string) (Dev, error) { func LoadDev(filename string) (Dev, error) {
bb, err := ioutil.ReadFile(filename) bb, err := ioutil.ReadFile(filename)

67
disk/disk.go Normal file
View File

@ -0,0 +1,67 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// Package disk contains routines for reading and writing various disk
// file formats.
package disk
import "github.com/zellyn/diskii/types"
// Various DOS33 disk characteristics.
const (
FloppyTracks = 35
FloppySectors = 16 // Sectors per track
// FloppyDiskBytes is the number of bytes on a DOS 3.3 disk.
FloppyDiskBytes = 143360 // 35 tracks * 16 sectors * 256 bytes
FloppyTrackBytes = 256 * FloppySectors // Bytes per track
FloppyDiskBytes13Sector = 35 * 13 * 256
)
// Dos33LogicalToPhysicalSectorMap maps logical sector numbers to physical ones.
// See [UtA2 9-42 - Read Routines].
var Dos33LogicalToPhysicalSectorMap = []int{
0x00, 0x0D, 0x0B, 0x09, 0x07, 0x05, 0x03, 0x01,
0x0E, 0x0C, 0x0A, 0x08, 0x06, 0x04, 0x02, 0x0F,
}
// Dos33PhysicalToLogicalSectorMap maps physical sector numbers to logical ones.
// See [UtA2 9-42 - Read Routines].
var Dos33PhysicalToLogicalSectorMap = []int{
0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04,
0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F,
}
// ProDOSLogicalToPhysicalSectorMap maps logical sector numbers to pysical ones.
// See [UtA2e 9-43 - Sectors vs. Blocks].
var ProDOSLogicalToPhysicalSectorMap = []int{
0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E,
0x01, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0D, 0x0F,
}
// ProDosPhysicalToLogicalSectorMap maps physical sector numbers to logical ones.
// See [UtA2e 9-43 - Sectors vs. Blocks].
var ProDosPhysicalToLogicalSectorMap = []int{
0x00, 0x08, 0x01, 0x09, 0x02, 0x0A, 0x03, 0x0B,
0x04, 0x0C, 0x05, 0x0D, 0x06, 0x0E, 0x07, 0x0F,
}
// LogicalToPhysicalByName maps from "do" and "po" to the corresponding
// logical-to-physical ordering.
var LogicalToPhysicalByName map[types.DiskOrder][]int = map[types.DiskOrder][]int{
types.DiskOrderDO: Dos33LogicalToPhysicalSectorMap,
types.DiskOrderPO: ProDOSLogicalToPhysicalSectorMap,
types.DiskOrderRaw: {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
}
// PhysicalToLogicalByName maps from "do" and "po" to the corresponding
// physical-to-logical ordering.
var PhysicalToLogicalByName map[types.DiskOrder][]int = map[types.DiskOrder][]int{
types.DiskOrderDO: Dos33PhysicalToLogicalSectorMap,
types.DiskOrderPO: ProDosPhysicalToLogicalSectorMap,
types.DiskOrderRaw: {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
}
// TrackSector is a pair of track/sector bytes.
type TrackSector struct {
Track byte
Sector byte
}

135
disk/marshal.go Normal file
View File

@ -0,0 +1,135 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// marshal.go contains helpers for marshaling sector structs to/from
// disk and block structs to/from devices.
package disk
import "fmt"
// BlockDevice is the interface used to read and write devices by
// logical block number.
// SectorSource is the interface for types that can marshal to sectors.
type SectorSource interface {
// ToSector marshals the sector struct to exactly 256 bytes.
ToSector() ([]byte, error)
// GetTrack returns the track that a sector struct was loaded from.
GetTrack() byte
// GetSector returns the sector that a sector struct was loaded from.
GetSector() byte
}
// SectorSink is the interface for types that can unmarshal from sectors.
type SectorSink interface {
// FromSector unmarshals the sector struct from bytes. Input is
// expected to be exactly 256 bytes.
FromSector(data []byte) error
// SetTrack sets the track that a sector struct was loaded from.
SetTrack(track byte)
// SetSector sets the sector that a sector struct was loaded from.
SetSector(sector byte)
}
// UnmarshalLogicalSector reads a sector from a disk image, and unmarshals it
// into a SectorSink, setting its track and sector.
func UnmarshalLogicalSector(diskbytes []byte, ss SectorSink, track, sector byte) error {
bytes, err := ReadSector(diskbytes, track, sector)
if err != nil {
return err
}
if err := ss.FromSector(bytes); err != nil {
return err
}
ss.SetTrack(track)
ss.SetSector(sector)
return nil
}
// ReadSector just reads 256 bytes from the given track and sector.
func ReadSector(diskbytes []byte, track, sector byte) ([]byte, error) {
start := int(track)*FloppyTrackBytes + int(sector)*256
end := start + 256
if len(diskbytes) < end {
return nil, fmt.Errorf("cannot read track %d/sector %d (bytes %d-%d) from disk of length %d", track, sector, start, end, len(diskbytes))
}
bytes := make([]byte, 256)
copy(bytes, diskbytes[start:end])
return bytes, nil
}
// MarshalLogicalSector marshals a SectorSource to its track/sector on a disk
// image.
func MarshalLogicalSector(diskbytes []byte, ss SectorSource) error {
track := ss.GetTrack()
sector := ss.GetSector()
bytes, err := ss.ToSector()
if err != nil {
return err
}
return WriteSector(diskbytes, track, sector, bytes)
}
// WriteSector writes 256 bytes to the given track and sector.
func WriteSector(diskbytes []byte, track, sector byte, data []byte) error {
if len(data) != 256 {
return fmt.Errorf("call to writeSector with len(data)==%d; want 256", len(data))
}
start := int(track)*FloppyTrackBytes + int(sector)*256
end := start + 256
if len(diskbytes) < end {
return fmt.Errorf("cannot write track %d/sector %d (bytes %d-%d) to disk of length %d", track, sector, start, end, len(diskbytes))
}
copy(diskbytes[start:end], data)
return nil
}
// BlockSource is the interface for types that can marshal to blocks.
type BlockSource interface {
// ToBlock marshals the block struct to exactly 512 bytes.
ToBlock() (Block, error)
// GetBlock returns the index that a block struct was loaded from.
GetBlock() uint16
}
// BlockSink is the interface for types that can unmarshal from blocks.
type BlockSink interface {
// FromBlock unmarshals the block struct from a Block. Input is
// expected to be exactly 512 bytes.
FromBlock(block Block) error
// SetBlock sets the index that a block struct was loaded from.
SetBlock(index uint16)
}
// UnmarshalBlock reads a block from a block device, and unmarshals it into a
// BlockSink, setting its index.
func UnmarshalBlock(diskbytes []byte, bs BlockSink, index uint16) error {
start := int(index) * 512
end := start + 512
if len(diskbytes) < end {
return fmt.Errorf("device too small to read block %d", index)
}
var block Block
copy(block[:], diskbytes[start:end])
if err := bs.FromBlock(block); err != nil {
return err
}
bs.SetBlock(index)
return nil
}
// MarshalBlock marshals a BlockSource to its block on a block device.
func MarshalBlock(diskbytes []byte, bs BlockSource) error {
index := bs.GetBlock()
block, err := bs.ToBlock()
if err != nil {
return err
}
start := int(index) * 512
end := start + 512
if len(diskbytes) < end {
return fmt.Errorf("device too small to write block %d", index)
}
copy(diskbytes[start:end], block[:])
return nil
}

240
disk/open.go Normal file
View File

@ -0,0 +1,240 @@
package disk
import (
"fmt"
"io"
"os"
"path"
"strings"
"github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/types"
)
// OpenFilename attempts to open a disk or device image, using the provided ordering and system type.
func OpenFilename(filename string, order types.DiskOrder, system string, operatorFactories []types.OperatorFactory, debug bool) (types.Operator, types.DiskOrder, error) {
if filename == "-" {
return OpenFile(os.Stdin, order, system, operatorFactories, debug)
}
file, err := os.Open(filename)
if err != nil {
return nil, "", err
}
return OpenFile(file, order, system, operatorFactories, debug)
}
// OpenImage attempts to open a disk or device image, using the provided ordering and system type.
// OpenImage will close the file.
func OpenFile(file *os.File, order types.DiskOrder, system string, operatorFactories []types.OperatorFactory, debug bool) (types.Operator, types.DiskOrder, error) {
bb, err := io.ReadAll(file)
if err != nil {
return nil, "", err
}
if err := file.Close(); err != nil {
return nil, "", err
}
return OpenImage(bb, file.Name(), order, system, operatorFactories, debug)
}
// OpenImage attempts to open a disk or device image, using the provided ordering and system type.
func OpenImage(filebytes []byte, filename string, order types.DiskOrder, system string, operatorFactories []types.OperatorFactory, debug bool) (types.Operator, types.DiskOrder, error) {
ext := strings.ToLower(path.Ext(filename))
size := len(filebytes)
if size == FloppyDiskBytes {
return openDoOrPo(filebytes, order, system, ext, operatorFactories, debug)
}
if size == FloppyDiskBytes13Sector {
return nil, "", fmt.Errorf("cannot open 13-sector disk images (yet)")
}
if ext == ".hdv" {
return openHDV(filebytes, order, system, operatorFactories, debug)
}
return nil, "", fmt.Errorf("can only open disk-sized images and .hdv files")
}
func openHDV(rawbytes []byte, order types.DiskOrder, system string, operatorFactories []types.OperatorFactory, debug bool) (types.Operator, types.DiskOrder, error) {
size := len(rawbytes)
if size%512 > 0 {
return nil, "", fmt.Errorf("can only open .hdv files that are a multiple of 512 bytes: %d %% 512 == %d", size, size%512)
}
if size/512 > 65536 {
return nil, "", fmt.Errorf("can only open .hdv up to size 32MiB (%d); got %d", 65536*512, size)
}
if order != "auto" && order != types.DiskOrderPO {
return nil, "", fmt.Errorf("cannot open .hdv file in %q order", order)
}
if system != "auto" && system != "prodos" {
return nil, "", fmt.Errorf("cannot open .hdv file with %q system", system)
}
for _, factory := range operatorFactories {
if factory.Name() == "prodos" {
op, err := factory.Operator(rawbytes, debug)
if err != nil {
return nil, "", err
}
return op, types.DiskOrderPO, nil
}
}
return nil, "", fmt.Errorf("unable to find prodos module to open .hdv file") // Should not happen.
}
func openDoOrPo(rawbytes []byte, order types.DiskOrder, system string, ext string, operatorFactories []types.OperatorFactory, debug bool) (types.Operator, types.DiskOrder, error) {
var factories []types.OperatorFactory
for _, factory := range operatorFactories {
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", system)
}
orders := []types.DiskOrder{order}
switch order {
case types.DiskOrderDO, types.DiskOrderPO:
// nothing more
case types.DiskOrderAuto:
switch ext {
case ".po":
orders = []types.DiskOrder{types.DiskOrderPO}
case ".do":
orders = []types.DiskOrder{types.DiskOrderDO}
case ".dsk", "":
orders = []types.DiskOrder{types.DiskOrderDO, types.DiskOrderPO}
default:
return nil, "", fmt.Errorf("unknown disk image extension: %q", ext)
}
default:
return nil, "", fmt.Errorf("disk order %q invalid for %d-byte disk images", order, FloppyDiskBytes)
}
for _, order := range orders {
swizzled, err := Swizzle(rawbytes, LogicalToPhysicalByName[order])
if err != nil {
return nil, "", err
}
for _, factory := range factories {
diskbytes, err := Swizzle(swizzled, PhysicalToLogicalByName[factory.DiskOrder()])
if err != nil {
return nil, "", err
}
if len(orders) == 1 && system != "auto" {
if debug {
fmt.Fprintf(os.Stderr, "Attempting to open with order=%s, system=%s.\n", order, factory.Name())
}
op, err := factory.Operator(diskbytes, debug)
if err != nil {
return nil, "", err
}
return op, order, nil
}
if debug {
fmt.Fprintf(os.Stderr, "Testing whether order=%s, system=%s seems to match.\n", order, factory.Name())
}
if factory.SeemsToMatch(diskbytes, debug) {
op, err := factory.Operator(diskbytes, debug)
if err == nil {
return op, order, nil
}
if debug {
fmt.Fprintf(os.Stderr, "Got error opening with order=%s, system=%s: %v\n", order, factory.Name(), err)
}
}
}
}
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("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)
}
result := make([]byte, FloppyDiskBytes)
for track := 0; track < FloppyTracks; track++ {
for sector := 0; sector < FloppySectors; sector++ {
data, err := ReadSector(diskimage, byte(track), byte(sector))
if err != nil {
return nil, err
}
err = WriteSector(result, byte(track), byte(order[sector]), data)
if err != nil {
return nil, err
}
}
}
return result, nil
}
func UnSwizzle(diskimage []byte, order []int) ([]byte, error) {
if err := validateOrder(order); err != nil {
return nil, fmt.Errorf("called UnSwizzle with weird order: %w", err)
}
reverseOrder := make([]int, FloppySectors)
for index, mapping := range order {
reverseOrder[mapping] = index
}
return Swizzle(diskimage, reverseOrder)
}
// validateOrder validates that an order mapping is valid, and maps [0,15] onto
// [0,15] without repeats.
func validateOrder(order []int) error {
if len(order) != FloppySectors {
return fmt.Errorf("len=%d; want %d: %v", len(order), FloppySectors, order)
}
seen := make(map[int]bool)
for i, mapping := range order {
if mapping < 0 || mapping > 15 {
return fmt.Errorf("mapping %d:%d is not in [0,15]: %v", i, mapping, order)
}
if seen[mapping] {
return fmt.Errorf("mapping %d:%d is a repeat: %v", i, mapping, order)
}
seen[mapping] = true
}
return nil
}
// OrderFromFilename tries to guess the disk order from the filename, using the extension.
func OrderFromFilename(filename string, defaultOrder types.DiskOrder) types.DiskOrder {
ext := strings.ToLower(path.Ext(filename))
switch ext {
case ".dsk", ".do":
return types.DiskOrderDO
case ".po":
return types.DiskOrderPO
default:
return defaultOrder
}
}
// WriteBack writes a disk image back out.
func WriteBack(filename string, op types.Operator, diskFileOrder types.DiskOrder, overwrite bool) error {
logicalBytes := op.GetBytes()
// If it's not floppy-sized, we don't swizzle at all.
if len(logicalBytes) != FloppyDiskBytes {
return helpers.WriteOutput(filename, logicalBytes, overwrite)
}
// Go from logical sectors for the operator back to physical sectors.
physicalBytes, err := Swizzle(logicalBytes, LogicalToPhysicalByName[op.DiskOrder()])
if err != nil {
return err
}
// Go from physical sectors to the disk order (DO or PO)
diskBytes, err := Swizzle(physicalBytes, PhysicalToLogicalByName[diskFileOrder])
if err != nil {
return err
}
return helpers.WriteOutput(filename, diskBytes, overwrite)
}

View File

@ -7,10 +7,10 @@ package dos3
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io"
"strings" "strings"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/types"
) )
const ( const (
@ -356,11 +356,11 @@ func (fd *FileDesc) FilenameString() string {
return strings.TrimRight(string(slice), " ") return strings.TrimRight(string(slice), " ")
} }
// descriptor returns a disk.Descriptor for a FileDesc, but with the // descriptor returns a types.Descriptor for a FileDesc, but with the
// length set to -1, since we can't know it without reading the file // length set to -1, since we can't know it without reading the file
// contents. // contents.
func (fd FileDesc) descriptor() disk.Descriptor { func (fd FileDesc) descriptor() types.Descriptor {
desc := disk.Descriptor{ desc := types.Descriptor{
Name: fd.FilenameString(), Name: fd.FilenameString(),
Sectors: int(fd.SectorCount), Sectors: int(fd.SectorCount),
Length: -1, Length: -1,
@ -368,28 +368,28 @@ func (fd FileDesc) descriptor() disk.Descriptor {
} }
switch fd.Filetype & 0x7f { switch fd.Filetype & 0x7f {
case FiletypeText: // Text file case FiletypeText: // Text file
desc.Type = disk.FiletypeASCIIText desc.Type = types.FiletypeASCIIText
case FiletypeInteger: // INTEGER BASIC file case FiletypeInteger: // INTEGER BASIC file
desc.Type = disk.FiletypeIntegerBASIC desc.Type = types.FiletypeIntegerBASIC
case FiletypeApplesoft: // APPLESOFT BASIC file case FiletypeApplesoft: // APPLESOFT BASIC file
desc.Type = disk.FiletypeApplesoftBASIC desc.Type = types.FiletypeApplesoftBASIC
case FiletypeBinary: // BINARY file case FiletypeBinary: // BINARY file
desc.Type = disk.FiletypeBinary desc.Type = types.FiletypeBinary
case FiletypeS: // S type file case FiletypeS: // S type file
desc.Type = disk.FiletypeS desc.Type = types.FiletypeS
case FiletypeRelocatable: // RELOCATABLE object module file case FiletypeRelocatable: // RELOCATABLE object module file
desc.Type = disk.FiletypeRelocatable desc.Type = types.FiletypeRelocatable
case FiletypeA: // A type file case FiletypeA: // A type file
desc.Type = disk.FiletypeNewA desc.Type = types.FiletypeNewA
case FiletypeB: // B type file case FiletypeB: // B type file
desc.Type = disk.FiletypeNewB desc.Type = types.FiletypeNewB
} }
return desc return desc
} }
// Contents returns the on-disk contents of a file represented by a // Contents returns the on-disk contents of a file represented by a
// FileDesc. // FileDesc.
func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) { func (fd *FileDesc) Contents(diskbytes []byte) ([]byte, error) {
tsls := []TrackSectorList{} tsls := []TrackSectorList{}
nextTrack := fd.TrackSectorListTrack nextTrack := fd.TrackSectorListTrack
nextSector := fd.TrackSectorListSector nextSector := fd.TrackSectorListSector
@ -401,7 +401,7 @@ func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
} }
seen[ts] = true seen[ts] = true
tsl := TrackSectorList{} tsl := TrackSectorList{}
if err := disk.UnmarshalLogicalSector(lsd, &tsl, nextTrack, nextSector); err != nil { if err := disk.UnmarshalLogicalSector(diskbytes, &tsl, nextTrack, nextSector); err != nil {
return nil, err return nil, err
} }
tsls = append(tsls, tsl) tsls = append(tsls, tsl)
@ -426,7 +426,7 @@ func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
data = append(data, 0) data = append(data, 0)
} }
} else { } else {
contents, err := lsd.ReadLogicalSector(ts.Track, ts.Sector) contents, err := disk.ReadSector(diskbytes, ts.Track, ts.Sector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -490,9 +490,9 @@ func (tsl *TrackSectorList) FromSector(data []byte) error {
// readCatalogSectors reads the raw CatalogSector structs from a DOS // readCatalogSectors reads the raw CatalogSector structs from a DOS
// 3.3 disk. // 3.3 disk.
func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) { func readCatalogSectors(diskbytes []byte, debug bool) ([]CatalogSector, error) {
v := &VTOC{} v := &VTOC{}
err := disk.UnmarshalLogicalSector(d, v, VTOCTrack, VTOCSector) err := disk.UnmarshalLogicalSector(diskbytes, v, VTOCTrack, VTOCSector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -516,7 +516,7 @@ func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) {
return nil, fmt.Errorf("catalog sectors can't be in sector %d: disk only has %d sectors", nextSector, v.NumSectors) return nil, fmt.Errorf("catalog sectors can't be in sector %d: disk only has %d sectors", nextSector, v.NumSectors)
} }
cs := CatalogSector{} cs := CatalogSector{}
err := disk.UnmarshalLogicalSector(d, &cs, nextTrack, nextSector) err := disk.UnmarshalLogicalSector(diskbytes, &cs, nextTrack, nextSector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -528,8 +528,8 @@ func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) {
} }
// ReadCatalog reads the catalog of a DOS 3.3 disk. // ReadCatalog reads the catalog of a DOS 3.3 disk.
func ReadCatalog(d disk.LogicalSectorDisk) (files, deleted []FileDesc, err error) { func ReadCatalog(diskbytes []byte, debug bool) (files, deleted []FileDesc, err error) {
css, err := readCatalogSectors(d) css, err := readCatalogSectors(diskbytes, debug)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -549,13 +549,14 @@ func ReadCatalog(d disk.LogicalSectorDisk) (files, deleted []FileDesc, err error
return files, deleted, nil return files, deleted, nil
} }
// operator is a disk.Operator - an interface for performing // operator is a types.Operator - an interface for performing
// high-level operations on files and directories. // high-level operations on files and directories.
type operator struct { type operator struct {
lsd disk.LogicalSectorDisk data []byte
debug bool
} }
var _ disk.Operator = operator{} var _ types.Operator = operator{}
// operatorName is the keyword name for the operator that undestands // operatorName is the keyword name for the operator that undestands
// dos3 disks. // dos3 disks.
@ -566,11 +567,6 @@ func (o operator) Name() string {
return operatorName return operatorName
} }
// Order returns the sector or block order of the underlying storage.
func (o operator) Order() string {
return o.lsd.Order()
}
// HasSubdirs returns true if the underlying operating system on the // HasSubdirs returns true if the underlying operating system on the
// disk allows subdirectories. // disk allows subdirectories.
func (o operator) HasSubdirs() bool { func (o operator) HasSubdirs() bool {
@ -579,12 +575,12 @@ func (o operator) HasSubdirs() bool {
// Catalog returns a catalog of disk entries. subdir should be empty // Catalog returns a catalog of disk entries. subdir should be empty
// for operating systems that do not support subdirectories. // 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) {
fds, _, err := ReadCatalog(o.lsd) fds, _, err := ReadCatalog(o.data, o.debug)
if err != nil { if err != nil {
return nil, err return nil, err
} }
descs := make([]disk.Descriptor, 0, len(fds)) descs := make([]types.Descriptor, 0, len(fds))
for _, fd := range fds { for _, fd := range fds {
descs = append(descs, fd.descriptor()) descs = append(descs, fd.descriptor())
} }
@ -594,7 +590,7 @@ func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
// fileForFilename returns the FileDesc corresponding to the given // fileForFilename returns the FileDesc corresponding to the given
// filename, or an error. // filename, or an error.
func (o operator) fileForFilename(filename string) (FileDesc, error) { func (o operator) fileForFilename(filename string) (FileDesc, error) {
fds, _, err := ReadCatalog(o.lsd) fds, _, err := ReadCatalog(o.data, o.debug)
if err != nil { if err != nil {
return FileDesc{}, err return FileDesc{}, err
} }
@ -607,18 +603,18 @@ func (o operator) fileForFilename(filename string) (FileDesc, error) {
} }
// GetFile retrieves a file by name. // GetFile retrieves a file by name.
func (o operator) GetFile(filename string) (disk.FileInfo, error) { func (o operator) GetFile(filename string) (types.FileInfo, error) {
fd, err := o.fileForFilename(filename) fd, err := o.fileForFilename(filename)
if err != nil { if err != nil {
return disk.FileInfo{}, err return types.FileInfo{}, err
} }
desc := fd.descriptor() desc := fd.descriptor()
data, err := fd.Contents(o.lsd) data, err := fd.Contents(o.data)
if err != nil { if err != nil {
return disk.FileInfo{}, err return types.FileInfo{}, err
} }
fi := disk.FileInfo{ fi := types.FileInfo{
Descriptor: desc, Descriptor: desc,
Data: data, Data: data,
} }
@ -659,7 +655,7 @@ func (o operator) GetFile(filename string) (disk.FileInfo, error) {
errType = "B" errType = "B"
} }
return disk.FileInfo{}, fmt.Errorf("%s does not yet implement `GetFile` for filetype %s", operatorName, errType) return types.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 // Delete deletes a file by name. It returns true if the file was
@ -671,29 +667,46 @@ func (o operator) Delete(filename string) (bool, error) {
// PutFile writes a file by name. If the file exists and overwrite // PutFile writes a file by name. If the file exists and overwrite
// is false, it returns with an error. Otherwise it returns true if // is false, it returns with an error. Otherwise it returns true if
// an existing file was overwritten. // 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 does not implement PutFile yet", operatorName) return false, fmt.Errorf("%s does not implement PutFile yet", operatorName)
} }
// Write writes the underlying disk to the given writer. // DiskOrder returns the Physical-to-Logical mapping order.
func (o operator) Write(w io.Writer) (int, error) { func (o operator) DiskOrder() types.DiskOrder {
return o.lsd.Write(w) return types.DiskOrderDO
} }
// operatorFactory is the factory that returns dos3 operators given // GetBytes returns the disk image bytes, in logical order.
// disk images. func (o operator) GetBytes() []byte {
func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) { return o.data
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() { // OperatorFactory is a types.OperatorFactory for DOS 3.3 disks.
disk.RegisterDiskOperatorFactory(operatorName, operatorFactory) 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.
_, _, err := ReadCatalog(diskbytes, debug)
if 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) {
return operator{data: diskbytes, debug: debug}, nil
}
// DiskOrder returns the Physical-to-Logical mapping order.
func (of OperatorFactory) DiskOrder() types.DiskOrder {
return operator{}.DiskOrder()
} }

View File

@ -2,10 +2,9 @@ package dos3
import ( import (
"crypto/rand" "crypto/rand"
"os"
"reflect" "reflect"
"testing" "testing"
"github.com/zellyn/diskii/lib/disk"
) )
// TestVTOCMarshalRoundtrip checks a simple roundtrip of VTOC data. // TestVTOCMarshalRoundtrip checks a simple roundtrip of VTOC data.
@ -76,15 +75,11 @@ func TestTrackSectorListMarshalRoundtrip(t *testing.T) {
// TestReadCatalog tests the reading of the catalog of a test disk. // TestReadCatalog tests the reading of the catalog of a test disk.
func TestReadCatalog(t *testing.T) { func TestReadCatalog(t *testing.T) {
sd, err := disk.LoadDSK("testdata/dos33test.dsk") diskbytes, err := os.ReadFile("testdata/dos33test.dsk")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
dsk, err := disk.NewMappedDisk(sd, disk.Dos33LogicalToPhysicalSectorMap) fds, deleted, err := ReadCatalog(diskbytes, false)
if err != nil {
t.Fatal(err)
}
fds, deleted, err := ReadCatalog(dsk)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,8 +0,0 @@
// This file contains the list of commands to run to re-generate
// generated files.
// Use go-bindata to embed static assets that we need.
//go:generate go-bindata -pkg data -prefix "data/" -o data/data.go data/disks data/boot
//go:generate goimports -w data/data.go
package main

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/zellyn/diskii
go 1.16
require (
github.com/alecthomas/kong v0.2.17
github.com/kr/pretty v0.2.1
github.com/rogpeppe/go-internal v1.8.0
)

30
go.sum Normal file
View File

@ -0,0 +1,30 @@
github.com/alecthomas/kong v0.2.17 h1:URDISCI96MIgcIlQyoCAlhOmrSw6pZScBNkctg8r0W0=
github.com/alecthomas/kong v0.2.17/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

35
helpers/helpers.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// Package helpers contains helper routines for reading and writing files,
// allowing `-` to mean stdin/stdout.
package helpers
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
)
// FileContentsOrStdIn returns the contents of a file, unless the file
// is "-", in which case it reads from stdin.
func FileContentsOrStdIn(s string) ([]byte, error) {
if s == "-" {
return io.ReadAll(os.Stdin)
}
return os.ReadFile(s)
}
func WriteOutput(filename string, contents []byte, force bool) error {
if filename == "-" {
_, err := os.Stdout.Write(contents)
return err
}
if !force {
if _, err := os.Stat(filename); !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("cannot overwrite file %q without --force (-f)", filename)
}
}
return os.WriteFile(filename, contents, 0666)
}

View File

@ -1,304 +0,0 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// Package disk contains routines for reading and writing various disk
// file formats.
package disk
import (
"fmt"
"io"
"path"
"strings"
)
// Various DOS33 disk characteristics.
const (
DOS33Tracks = 35
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.
// See [UtA2 9-42 - Read Routines].
var Dos33LogicalToPhysicalSectorMap = []byte{
0x00, 0x0D, 0x0B, 0x09, 0x07, 0x05, 0x03, 0x01,
0x0E, 0x0C, 0x0A, 0x08, 0x06, 0x04, 0x02, 0x0F,
}
// Dos33PhysicalToLogicalSectorMap maps physical sector numbers to logical ones.
// See [UtA2 9-42 - Read Routines].
var Dos33PhysicalToLogicalSectorMap = []byte{
0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04,
0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F,
}
// ProDOSLogicalToPhysicalSectorMap maps logical sector numbers to pysical ones.
// See [UtA2e 9-43 - Sectors vs. Blocks].
var ProDOSLogicalToPhysicalSectorMap = []byte{
0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E,
0x01, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0D, 0x0F,
}
// ProDosPhysicalToLogicalSectorMap maps physical sector numbers to logical ones.
// See [UtA2e 9-43 - Sectors vs. Blocks].
var ProDosPhysicalToLogicalSectorMap = []byte{
0x00, 0x08, 0x01, 0x09, 0x02, 0x0A, 0x03, 0x0B,
0x04, 0x0C, 0x05, 0x0D, 0x06, 0x0E, 0x07, 0x0F,
}
// TrackSector is a pair of track/sector bytes.
type TrackSector struct {
Track byte
Sector byte
}
// SectorDisk is the interface used to read and write disks by
// physical (matches sector header) sector number.
type SectorDisk interface {
// ReadPhysicalSector reads a single physical sector from the disk. It
// always returns 256 byes.
ReadPhysicalSector(track byte, sector byte) ([]byte, error)
// WritePhysicalSector writes a single physical sector to a disk. It
// expects exactly 256 bytes.
WritePhysicalSector(track byte, sector byte, data []byte) error
// Sectors returns the number of sectors on the SectorDisk
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)
// Order returns the sector order.
Order() string
}
// LogicalSectorDisk is the interface used to read and write a disk by
// *logical* sector number.
type LogicalSectorDisk interface {
// ReadLogicalSector reads a single logical sector from the disk. It
// always returns 256 byes.
ReadLogicalSector(track byte, sector byte) ([]byte, error)
// WriteLogicalSector writes a single logical sector to a disk. It
// expects exactly 256 bytes.
WriteLogicalSector(track byte, sector byte, data []byte) error
// Sectors returns the number of sectors on the SectorDisk
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)
// Order returns the underlying sector ordering.
Order() string
}
// MappedDisk wraps a SectorDisk as a LogicalSectorDisk, handling the
// logical-to-physical sector mapping.
type MappedDisk struct {
sectorDisk SectorDisk // The underlying physical sector disk.
logicalToPhysical []byte // The mapping of logical to physical sectors.
}
var _ LogicalSectorDisk = MappedDisk{}
// NewMappedDisk returns a MappedDisk with the given
// logical-to-physical sector mapping.
func NewMappedDisk(sd SectorDisk, logicalToPhysical []byte) (MappedDisk, error) {
if logicalToPhysical != nil && len(logicalToPhysical) != int(sd.Sectors()) {
return MappedDisk{}, fmt.Errorf("NewMappedDisk called on a disk image with %d sectors per track, but a mapping of length %d", sd.Sectors(), len(logicalToPhysical))
}
if logicalToPhysical == nil {
logicalToPhysical = make([]byte, int(sd.Sectors()))
for i := range logicalToPhysical {
logicalToPhysical[i] = byte(i)
}
}
return MappedDisk{
sectorDisk: sd,
logicalToPhysical: logicalToPhysical,
}, nil
}
// ReadLogicalSector reads a single logical sector from the disk. It
// always returns 256 byes.
func (md MappedDisk) ReadLogicalSector(track byte, sector byte) ([]byte, error) {
if track >= md.sectorDisk.Tracks() {
return nil, fmt.Errorf("ReadLogicalSector expected track between 0 and %d; got %d", md.sectorDisk.Tracks()-1, track)
}
if sector >= md.sectorDisk.Sectors() {
return nil, fmt.Errorf("ReadLogicalSector expected sector between 0 and %d; got %d", md.sectorDisk.Sectors()-1, sector)
}
physicalSector := md.logicalToPhysical[int(sector)]
return md.sectorDisk.ReadPhysicalSector(track, physicalSector)
}
// WriteLogicalSector writes a single logical sector to a disk. It
// expects exactly 256 bytes.
func (md MappedDisk) WriteLogicalSector(track byte, sector byte, data []byte) error {
if track >= md.sectorDisk.Tracks() {
return fmt.Errorf("WriteLogicalSector expected track between 0 and %d; got %d", md.sectorDisk.Tracks()-1, track)
}
if sector >= md.sectorDisk.Sectors() {
return fmt.Errorf("WriteLogicalSector expected sector between 0 and %d; got %d", md.sectorDisk.Sectors()-1, sector)
}
physicalSector := md.logicalToPhysical[int(sector)]
return md.sectorDisk.WritePhysicalSector(track, physicalSector, data)
}
// 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 in the disk image.
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)
}
// Order returns the sector order of the underlying sector disk.
func (md MappedDisk) Order() string {
return md.sectorDisk.Order()
}
// OpenDisk opens a disk image by filename.
func OpenDisk(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)
}
// OpenDev opens a device image by filename.
func OpenDev(filename string) (BlockDevice, error) {
ext := strings.ToLower(path.Ext(filename))
switch ext {
case ".po":
return LoadDev(filename)
}
return nil, fmt.Errorf("Unimplemented/unknown device file extension %q", ext)
}
// Open opens a disk image by filename, returning an Operator.
func Open(filename string) (Operator, error) {
sd, err := OpenDisk(filename)
if err == nil {
var op Operator
op, err = OperatorForDisk(sd)
if err == nil {
return op, nil
}
}
dev, err2 := OpenDev(filename)
if err2 == nil {
var op Operator
op, err2 = OperatorForDevice(dev)
if err2 != nil {
return nil, err2
}
return op, nil
}
return nil, err
}
type DiskBlockDevice struct {
lsd LogicalSectorDisk
blocks uint16
}
// BlockDeviceFromSectorDisk creates a ProDOS block device from a
// SectorDisk. It reads maps ProDOS to physical sectors.
func BlockDeviceFromSectorDisk(sd SectorDisk) (BlockDevice, error) {
lsd, err := NewMappedDisk(sd, ProDOSLogicalToPhysicalSectorMap)
if err != nil {
return nil, err
}
return DiskBlockDevice{
lsd: lsd,
blocks: uint16(lsd.Tracks()) / 2 * uint16(lsd.Sectors()),
}, nil
}
// ReadBlock reads a single block from the device. It always returns
// 512 byes.
func (dbv DiskBlockDevice) ReadBlock(index uint16) (Block, error) {
var b Block
if index >= dbv.blocks {
return b, fmt.Errorf("device has %d blocks; tried to read block %d (index=%d)", dbv.blocks, index+1, index)
}
i := int(index) * 2
sectors := int(dbv.lsd.Sectors())
track0 := i / sectors
sector0 := i % sectors
sector1 := sector0 + 1
track1 := track0
if sector1 == sectors {
sector1 = 0
track1++
}
b0, err := dbv.lsd.ReadLogicalSector(byte(track0), byte(sector0))
if err != nil {
return b, fmt.Errorf("error reading first half of block %d (t:%d s:%d): %v", index, track0, sector0, err)
}
b1, err := dbv.lsd.ReadLogicalSector(byte(track1), byte(sector1))
if err != nil {
return b, fmt.Errorf("error reading second half of block %d (t:%d s:%d): %v", index, track1, sector1, err)
}
copy(b[:256], b0)
copy(b[256:], b1)
return b, nil
}
// WriteBlock writes a single block to a device. It expects exactly
// 512 bytes.
func (dbv DiskBlockDevice) WriteBlock(index uint16, data Block) error {
if index >= dbv.blocks {
return fmt.Errorf("device has %d blocks; tried to read block %d (index=%d)", dbv.blocks, index+1, index)
}
i := int(index) * 2
sectors := int(dbv.lsd.Sectors())
track0 := i / sectors
sector0 := i % sectors
sector1 := sector0 + 1
track1 := track0
if sector1 == sectors {
sector1 = 0
track1++
}
if err := dbv.lsd.WriteLogicalSector(byte(track0), byte(sector0), data[:256]); err != nil {
return fmt.Errorf("error writing first half of block %d (t:%d s:%d): %v", index, track0, sector0, err)
}
if err := dbv.lsd.WriteLogicalSector(byte(track1), byte(sector1), data[256:]); err != nil {
return fmt.Errorf("error writing second half of block %d (t:%d s:%d): %v", index, track1, sector1, err)
}
return nil
}
// Blocks returns the number of blocks on the device.
func (dbv DiskBlockDevice) Blocks() uint16 {
return dbv.blocks
}
// Order returns the underlying sector or block order of the storage.
func (dbv DiskBlockDevice) Order() string {
return dbv.lsd.Order()
}
// Write writes the device contents to the given Writer.
func (dbv DiskBlockDevice) Write(w io.Writer) (int, error) {
return dbv.lsd.Write(w)
}

View File

@ -1,111 +0,0 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// dsk.go contains logic for reading ".dsk" disk images.
package disk
import (
"fmt"
"io"
"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
order string // Underlying sector order.
}
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,
order: "dos33",
}, nil
}
// Empty creates a .dsk image that is all zeros.
func Empty() DSK {
return DSK{
data: make([]byte, DOS33DiskBytes),
sectors: 16,
physicalToStored: Dos33PhysicalToLogicalSectorMap,
bytesPerTrack: 16 * 256,
tracks: DOS33Tracks,
order: "dos33",
}
}
// 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
}
// Order returns the sector order name.
func (d DSK) Order() string {
return d.order
}
// 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)
}

View File

@ -1,117 +0,0 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// marshal.go contains helpers for marshaling sector structs to/from
// disk and block structs to/from devices.
package disk
import "io"
// A ProDOS block.
type Block [512]byte
// BlockDevice is the interface used to read and write devices by
// logical block number.
type BlockDevice interface {
// ReadBlock reads a single block from the device. It always returns
// 512 byes.
ReadBlock(index uint16) (Block, error)
// WriteBlock writes a single block to a device. It expects exactly
// 512 bytes.
WriteBlock(index uint16, data Block) error
// Blocks returns the number of blocks on the device.
Blocks() uint16
// Write writes the device contents to the given Writer.
Write(io.Writer) (int, error)
// Order returns the sector or block order of the underlying device.
Order() string
}
// SectorSource is the interface for types that can marshal to sectors.
type SectorSource interface {
// ToSector marshals the sector struct to exactly 256 bytes.
ToSector() ([]byte, error)
// GetTrack returns the track that a sector struct was loaded from.
GetTrack() byte
// GetSector returns the sector that a sector struct was loaded from.
GetSector() byte
}
// SectorSink is the interface for types that can unmarshal from sectors.
type SectorSink interface {
// FromSector unmarshals the sector struct from bytes. Input is
// expected to be exactly 256 bytes.
FromSector(data []byte) error
// SetTrack sets the track that a sector struct was loaded from.
SetTrack(track byte)
// SetSector sets the sector that a sector struct was loaded from.
SetSector(sector byte)
}
// UnmarshalLogicalSector reads a sector from a SectorDisk, and
// unmarshals it into a SectorSink, setting its track and sector.
func UnmarshalLogicalSector(d LogicalSectorDisk, ss SectorSink, track, sector byte) error {
bytes, err := d.ReadLogicalSector(track, sector)
if err != nil {
return err
}
if err := ss.FromSector(bytes); err != nil {
return err
}
ss.SetTrack(track)
ss.SetSector(sector)
return nil
}
// MarshalLogicalSector marshals a SectorSource to its sector on a
// SectorDisk.
func MarshalLogicalSector(d LogicalSectorDisk, ss SectorSource) error {
track := ss.GetTrack()
sector := ss.GetSector()
bytes, err := ss.ToSector()
if err != nil {
return err
}
return d.WriteLogicalSector(track, sector, bytes)
}
// BlockSource is the interface for types that can marshal to blocks.
type BlockSource interface {
// ToBlock marshals the block struct to exactly 512 bytes.
ToBlock() (Block, error)
// GetBlock returns the index that a block struct was loaded from.
GetBlock() uint16
}
// BlockSink is the interface for types that can unmarshal from blocks.
type BlockSink interface {
// FromBlock unmarshals the block struct from a Block. Input is
// expected to be exactly 512 bytes.
FromBlock(block Block) error
// SetBlock sets the index that a block struct was loaded from.
SetBlock(index uint16)
}
// UnmarshalBlock reads a block from a BlockDevice, and unmarshals it
// into a BlockSink, setting its index.
func UnmarshalBlock(d BlockDevice, bs BlockSink, index uint16) error {
block, err := d.ReadBlock(index)
if err != nil {
return err
}
if err := bs.FromBlock(block); err != nil {
return err
}
bs.SetBlock(index)
return nil
}
// MarshalBlock marshals a BlockSource to its block on a BlockDevice.
func MarshalBlock(d BlockDevice, bs BlockSource) error {
index := bs.GetBlock()
block, err := bs.ToBlock()
if err != nil {
return err
}
return d.WriteBlock(index, block)
}

View File

@ -1,134 +0,0 @@
// 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"
"io"
"sort"
"strings"
)
// Descriptor describes a file's characteristics.
type Descriptor struct {
Name string
Fullname string // If there's a more complete filename (eg. Super-Mon), put it here.
Sectors int
Blocks int
Length int
Locked bool
Type Filetype
}
// Operator is the interface that can operate on disks.
type Operator interface {
// Name returns the name of the operator.
Name() string
// Order returns the sector or block order name.
Order() 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)
// 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)
// 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.
PutFile(fileInfo FileInfo, overwrite bool) (existed bool, err error)
// Write writes the underlying disk to the given writer.
Write(io.Writer) (int, error)
}
// FileInfo represents a file descriptor plus the content.
type FileInfo struct {
Descriptor Descriptor
Data []byte
StartAddress uint16
}
// diskOperatorFactory is the type of functions that accept a SectorDisk,
// and may return an Operator interface to operate on it.
type diskOperatorFactory func(SectorDisk) (Operator, error)
// diskOperatorFactories is the map of currently-registered disk
// operator factories.
var diskOperatorFactories map[string]diskOperatorFactory
func init() {
diskOperatorFactories = make(map[string]diskOperatorFactory)
}
// RegisterDiskOperatorFactory registers a disk operator factory with
// the given name: a function that accepts a SectorDisk, and may
// return an Operator. It doesn't lock diskOperatorFactories: it is
// expected to be called only from package `init` functions.
func RegisterDiskOperatorFactory(name string, factory diskOperatorFactory) {
diskOperatorFactories[name] = factory
}
// OperatorForDisk returns an Operator for the given SectorDisk, if possible.
func OperatorForDisk(sd SectorDisk) (Operator, error) {
if len(diskOperatorFactories) == 0 {
return nil, errors.New("Cannot find an operator matching the given disk image (none registered)")
}
for _, factory := range diskOperatorFactories {
if operator, err := factory(sd); err == nil {
return operator, nil
}
}
names := make([]string, 0, len(diskOperatorFactories))
for name := range diskOperatorFactories {
names = append(names, `"`+name+`"`)
}
sort.Strings(names)
return nil, fmt.Errorf("Cannot find a disk operator matching the given disk image (tried %s)", strings.Join(names, ", "))
}
// deviceOperatorFactory is the type of functions that accept a BlockDevice,
// and may return an Operator interface to operate on it.
type deviceOperatorFactory func(BlockDevice) (Operator, error)
// deviceOperatorFactories is the map of currently-registered device
// operator factories.
var deviceOperatorFactories map[string]deviceOperatorFactory
func init() {
deviceOperatorFactories = make(map[string]deviceOperatorFactory)
}
// RegisterDeviceOperatorFactory registers a device operator factory with
// the given name: a function that accepts a BlockDevice, and may
// return an Operator. It doesn't lock deviceOperatorFactories: it is
// expected to be called only from package `init` functions.
func RegisterDeviceOperatorFactory(name string, factory deviceOperatorFactory) {
deviceOperatorFactories[name] = factory
}
// OperatorForDevice returns an Operator for the given BlockDevice, if possible.
func OperatorForDevice(sd BlockDevice) (Operator, error) {
if len(deviceOperatorFactories) == 0 {
return nil, errors.New("Cannot find an operator matching the given device image (none registered)")
}
for _, factory := range deviceOperatorFactories {
if operator, err := factory(sd); err == nil {
return operator, nil
}
}
names := make([]string, 0, len(deviceOperatorFactories))
for name := range deviceOperatorFactories {
names = append(names, `"`+name+`"`)
}
sort.Strings(names)
return nil, fmt.Errorf("Cannot find a device operator matching the given device image (tried %s)", strings.Join(names, ", "))
}

View File

@ -1,19 +0,0 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// Package helpers contains various routines used to help cobra
// commands stay succinct.
package helpers
import (
"io/ioutil"
"os"
)
// FileContentsOrStdIn returns the contents of a file, unless the file
// is "-", in which case it reads from stdin.
func FileContentsOrStdIn(s string) ([]byte, error) {
if s == "-" {
return ioutil.ReadAll(os.Stdin)
}
return ioutil.ReadFile(s)
}

92
main.go
View File

@ -1,16 +1,92 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com> // Copyright ©2021 Zellyn Hunter <zellyn@gmail.com>
package main package main
import ( import (
"github.com/zellyn/diskii/cmd" "reflect"
"strconv"
// Import disk operator factories for DOS3 and Super-Mon "github.com/zellyn/diskii/cmd"
_ "github.com/zellyn/diskii/lib/dos3" "github.com/zellyn/diskii/dos3"
_ "github.com/zellyn/diskii/lib/prodos" "github.com/zellyn/diskii/prodos"
_ "github.com/zellyn/diskii/lib/supermon" "github.com/zellyn/diskii/supermon"
"github.com/zellyn/diskii/types"
"fmt"
"os"
"github.com/alecthomas/kong"
) )
func main() { var cli struct {
cmd.Execute() Debug bool `kong:"short='v',help='Enable debug mode.'"`
Ls cmd.LsCmd `cmd:"" aliases:"list,cat,catalog" help:"List files/directories on a disk."`
Reorder cmd.ReorderCmd `cmd:"" help:"Convert between DOS-order and ProDOS-order disk images."`
Filetypes cmd.FiletypesCmd `cmd:"" help:"Print a list of filetypes understood by diskii."`
Put cmd.PutCmd `cmd:"" help:"Put the raw contents of a file onto a disk."`
Rm cmd.DeleteCmd `cmd:"" aliases:"delete" help:"Delete a file."`
Dump cmd.DumpCmd `cmd:"" help:"Dump the raw contents of a file."`
Nakedos cmd.NakedOSCmd `cmd:"" help:"Work with NakedOS-format disks."`
Mksd cmd.SDCmd `cmd:"" help:"Create a “Standard Delivery” disk image containing a binary."`
Applesoft cmd.ApplesoftCmd `cmd:"" help:"Work with Applesoft BASIC files."`
}
func run() error {
ctx := kong.Parse(&cli,
kong.Name("diskii"),
kong.Description("A commandline tool for working with Apple II disk images."),
// kong.UsageOnError(),
kong.ConfigureHelp(kong.HelpOptions{
Compact: true,
Summary: true,
}),
kong.NamedMapper("anybaseuint16", hexUint16Mapper{}),
)
globals := &types.Globals{
Debug: cli.Debug,
DiskOperatorFactories: []types.OperatorFactory{
dos3.OperatorFactory{},
supermon.OperatorFactory{},
prodos.OperatorFactory{},
},
}
// Call the Run() method of the selected parsed command.
return ctx.Run(globals)
}
func main() {
err := run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
type hexUint16Mapper struct{}
func (h hexUint16Mapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
t, err := ctx.Scan.PopValue("int")
if err != nil {
return err
}
var sv string
switch v := t.Value.(type) {
case string:
sv = v
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
sv = fmt.Sprintf("%v", v)
default:
return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
}
n, err := strconv.ParseUint(sv, 0, 16)
if err != nil {
return fmt.Errorf("expected a valid %d bit uint but got %q", 16, sv)
}
target.SetUint(n)
return nil
} }

View File

@ -1,6 +0,0 @@
go-bindata -pkg data -o data/data.go \
data/disks/ProDOS_2_4_1.dsk \
data/disks/dos33master.woz \
data/boot/prodos-new-boot0.bin \
data/boot/prodos-old-boot0.bin
goimports -w data/data.go

6
next Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Quick little script to add failing commands to, so I know what I'm working on next.
set -euo pipefail
set -x
# go run . nakedos mkhello supermon-audit-new.dsk DF02
# go run . put -f supermon-audit-new.dsk DF02:FWORLD audit.o

View File

@ -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\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 - 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]]

View File

@ -7,9 +7,10 @@ package prodos
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "os"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/types"
) )
// Storage types. // Storage types.
@ -104,11 +105,11 @@ func (vbm VolumeBitMap) IsFree(block uint16) bool {
// readVolumeBitMap reads the entire volume bitmap from a block // readVolumeBitMap reads the entire volume bitmap from a block
// device. // device.
func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, error) { func readVolumeBitMap(devicebytes []byte, startBlock uint16) (VolumeBitMap, error) {
blocks := bd.Blocks() / 4096 blocks := uint16(len(devicebytes) / 512 / 4096)
vbm := NewVolumeBitMap(startBlock, blocks) vbm := NewVolumeBitMap(startBlock, blocks)
for i := 0; i < len(vbm); i++ { 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) return nil, fmt.Errorf("cannot read block %d (device block %d) of Volume Bit Map: %v", i, vbm[i].GetBlock(), err)
} }
} }
@ -116,9 +117,9 @@ func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, err
} }
// Write writes the Volume Bit Map to a block device. // 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 { 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) return fmt.Errorf("cannot write block %d (device block %d) of Volume Bit Map: %v", i, bp.GetBlock(), err)
} }
} }
@ -351,14 +352,14 @@ type FileDescriptor struct {
HeaderPointer uint16 // Block number of the key block for the directory which describes this file. HeaderPointer uint16 // Block number of the key block for the directory which describes this file.
} }
// descriptor returns a disk.Descriptor for a FileDescriptor. // descriptor returns a types.Descriptor for a FileDescriptor.
func (fd FileDescriptor) descriptor() disk.Descriptor { func (fd FileDescriptor) descriptor() types.Descriptor {
desc := disk.Descriptor{ desc := types.Descriptor{
Name: fd.Name(), Name: fd.Name(),
Blocks: int(fd.BlocksUsed), Blocks: int(fd.BlocksUsed),
Length: int(fd.Eof[0]) + int(fd.Eof[1])<<8 + int(fd.Eof[2])<<16, 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 Locked: false, // TODO(zellyn): use prodos-style access in types.Descriptor
Type: disk.Filetype(fd.FileType), Type: types.Filetype(fd.FileType),
} }
return desc return desc
} }
@ -611,11 +612,12 @@ func (sh SubdirectoryHeader) Name() string {
// Volume is the in-memory representation of a device's volume // Volume is the in-memory representation of a device's volume
// information. // information.
type Volume struct { type Volume struct {
keyBlock *VolumeDirectoryKeyBlock keyBlock *VolumeDirectoryKeyBlock // The key block describing the entire volume
blocks []*VolumeDirectoryBlock blocks []*VolumeDirectoryBlock // The blocks in the top-level volume
bitmap *VolumeBitMap bitmap *VolumeBitMap // Bitmap of which blocks are free
subdirsByBlock map[uint16]*Subdirectory subdirsByBlock map[uint16]*Subdirectory // A mapping of block number to subdirectory object
subdirsByName map[string]*Subdirectory subdirsByName map[string]*Subdirectory // a mapping of string to subdirectory object
firstSubdirBlocks map[uint16]uint16 // A mapping of later dir/subdir blocks to the first one in the chain
} }
// Subdirectory is the in-memory representation of a single // Subdirectory is the in-memory representation of a single
@ -652,46 +654,79 @@ func (v Volume) subdirDescriptors() []FileDescriptor {
// readVolume reads the entire volume and subdirectories from a device // readVolume reads the entire volume and subdirectories from a device
// into memory. // into memory.
func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) { func readVolume(devicebytes []byte, keyBlock uint16, debug bool) (Volume, error) {
v := Volume{ v := Volume{
keyBlock: &VolumeDirectoryKeyBlock{}, keyBlock: &VolumeDirectoryKeyBlock{},
subdirsByBlock: make(map[uint16]*Subdirectory), subdirsByBlock: make(map[uint16]*Subdirectory),
subdirsByName: make(map[string]*Subdirectory), subdirsByName: make(map[string]*Subdirectory),
firstSubdirBlocks: make(map[uint16]uint16),
} }
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) return v, fmt.Errorf("cannot read first block of volume directory (block %d): %v", keyBlock, err)
} }
// if debug {
// fmt.Fprintf(os.Stderr, "keyblock: %#v\n", v.keyBlock)
// }
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 return v, err
} else { } else {
v.bitmap = &vbm v.bitmap = &vbm
} }
// if debug {
// fmt.Fprintf(os.Stderr, "volume bitmap: %#v\n", v.bitmap)
// }
for block := v.keyBlock.Next; block != 0; block = v.blocks[len(v.blocks)-1].Next { for block := v.keyBlock.Next; block != 0; block = v.blocks[len(v.blocks)-1].Next {
vdb := VolumeDirectoryBlock{} vdb := VolumeDirectoryBlock{}
if err := disk.UnmarshalBlock(bd, &vdb, block); err != nil { if err := disk.UnmarshalBlock(devicebytes, &vdb, block); err != nil {
return v, err return v, err
} }
v.blocks = append(v.blocks, &vdb) v.blocks = append(v.blocks, &vdb)
v.firstSubdirBlocks[block] = keyBlock
if debug {
fmt.Fprintf(os.Stderr, " firstSubdirBlocks[%d] → %d\n", block, keyBlock)
}
// if debug {
// fmt.Fprintf(os.Stderr, "block: %#v\n", vdb)
// }
} }
sdds := v.subdirDescriptors() sdds := v.subdirDescriptors()
if debug {
fmt.Fprintf(os.Stderr, "got %d top-level subdir descriptors\n", len(sdds))
}
for i := 0; i < len(sdds); i++ { for i := 0; i < len(sdds); i++ {
sdd := sdds[i] sdd := sdds[i]
sub, err := readSubdirectory(bd, sdd) sub, err := readSubdirectory(devicebytes, sdd)
if err != nil { if err != nil {
return v, err return v, err
} }
v.subdirsByBlock[sdd.KeyPointer] = &sub v.subdirsByBlock[sdd.KeyPointer] = &sub
if debug {
fmt.Fprintf(os.Stderr, " subdirsByBlock[%d] → %q\n", sdd.KeyPointer, sub.keyBlock.Header.Name())
}
sdds = append(sdds, sub.subdirDescriptors()...) sdds = append(sdds, sub.subdirDescriptors()...)
for _, block := range sub.blocks {
v.firstSubdirBlocks[block.block] = sdd.KeyPointer
if debug {
fmt.Fprintf(os.Stderr, " firstSubdirBlocks[%d] → %d\n", block.block, sdd.KeyPointer)
}
}
}
if debug {
fmt.Fprintf(os.Stderr, "got %d total subdir descriptors\n", len(sdds))
} }
for _, sd := range v.subdirsByBlock { for _, sd := range v.subdirsByBlock {
name := sd.keyBlock.Header.Name() name := sd.keyBlock.Header.Name()
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, v.subdirsByBlock) if debug {
fmt.Fprintf(os.Stderr, "processing subdir %q\n", name)
}
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, v.subdirsByBlock, v.firstSubdirBlocks)
if err != nil { if err != nil {
return v, err return v, err
} }
@ -701,6 +736,9 @@ func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) {
v.subdirsByName[name] = sd v.subdirsByName[name] = sd
} }
if debug {
fmt.Fprintf(os.Stderr, "HERE2\n")
}
return v, nil return v, nil
} }
@ -729,16 +767,23 @@ func (s Subdirectory) subdirDescriptors() []FileDescriptor {
return descs return descs
} }
// fullDirName returns the full recursive directory name of the given parent directory. // parentDirName returns the full recursive directory name of the given parent directory.
func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory) (string, error) { func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory, firstSubdirBlockMap map[uint16]uint16) (string, error) {
if parentDirectoryBlock == keyBlock { if parentDirectoryBlock == keyBlock || firstSubdirBlockMap[parentDirectoryBlock] == keyBlock {
return "", nil return "", nil
} }
sd := subdirMap[parentDirectoryBlock] sd := subdirMap[parentDirectoryBlock]
if sd == nil {
parentFirstBlock, ok := firstSubdirBlockMap[parentDirectoryBlock]
if ok {
sd = subdirMap[parentFirstBlock]
}
}
if sd == nil { if sd == nil {
return "", fmt.Errorf("Unable to find subdirectory for block %d", parentDirectoryBlock) return "", fmt.Errorf("Unable to find subdirectory for block %d", parentDirectoryBlock)
} }
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap)
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap, firstSubdirBlockMap)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -751,18 +796,18 @@ func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[u
// readSubdirectory reads a single subdirectory from a device into // readSubdirectory reads a single subdirectory from a device into
// memory. // memory.
func readSubdirectory(bd disk.BlockDevice, fd FileDescriptor) (Subdirectory, error) { func readSubdirectory(devicebytes []byte, fd FileDescriptor) (Subdirectory, error) {
s := Subdirectory{ s := Subdirectory{
keyBlock: &SubdirectoryKeyBlock{}, 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) 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 { for block := s.keyBlock.Next; block != 0; block = s.blocks[len(s.blocks)-1].Next {
sdb := SubdirectoryBlock{} sdb := SubdirectoryBlock{}
if err := disk.UnmarshalBlock(bd, &sdb, block); err != nil { if err := disk.UnmarshalBlock(devicebytes, &sdb, block); err != nil {
return s, err return s, err
} }
s.blocks = append(s.blocks, &sdb) s.blocks = append(s.blocks, &sdb)
@ -783,10 +828,11 @@ func copyBytes(dst, src []byte) int {
// 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. // high-level operations on files and directories.
type operator struct { 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 // operatorName is the keyword name for the operator that undestands
// prodos disks/devices. // prodos disks/devices.
@ -797,11 +843,6 @@ func (o operator) Name() string {
return operatorName 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 // HasSubdirs returns true if the underlying operating system on the
// disk allows subdirectories. // disk allows subdirectories.
func (o operator) HasSubdirs() bool { func (o operator) HasSubdirs() bool {
@ -810,14 +851,16 @@ func (o operator) HasSubdirs() bool {
// Catalog returns a catalog of disk entries. subdir should be empty // Catalog returns a catalog of disk entries. subdir should be empty
// for operating systems that do not support subdirectories. // 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) {
if o.debug {
vol, err := readVolume(o.dev, 2) fmt.Fprintf(os.Stderr, "Catalog of %q\n", subdir)
}
vol, err := readVolume(o.data, 2, o.debug)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("error reading volume: %w", err)
} }
var result []disk.Descriptor var result []types.Descriptor
if subdir == "" { if subdir == "" {
for _, desc := range vol.descriptors() { for _, desc := range vol.descriptors() {
@ -842,8 +885,8 @@ func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
} }
// GetFile retrieves a file by name. // GetFile retrieves a file by name.
func (o operator) GetFile(filename string) (disk.FileInfo, error) { func (o operator) GetFile(filename string) (types.FileInfo, error) {
return disk.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName) 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 // Delete deletes a file by name. It returns true if the file was
@ -855,37 +898,46 @@ func (o operator) Delete(filename string) (bool, error) {
// PutFile writes a file by name. If the file exists and overwrite // PutFile writes a file by name. If the file exists and overwrite
// is false, it returns with an error. Otherwise it returns true if // is false, it returns with an error. Otherwise it returns true if
// an existing file was overwritten. // 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) return false, fmt.Errorf("%s doesn't implement PutFile yet", operatorName)
} }
// Write writes the underlying device blocks to the given writer. // DiskOrder returns the Physical-to-Logical mapping order.
func (o operator) Write(w io.Writer) (int, error) { func (o operator) DiskOrder() types.DiskOrder {
return o.dev.Write(w) return types.DiskOrderPO
} }
// deviceOperatorFactory is the factory that returns prodos operators // GetBytes returns the disk image bytes, in logical order.
// given device images. func (o operator) GetBytes() []byte {
func deviceOperatorFactory(bd disk.BlockDevice) (disk.Operator, error) { return o.data
op := operator{dev: bd} }
_, err := op.Catalog("")
// OperatorFactory is a types.OperatorFactory for ProDos 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(devicebytes []byte, debug bool) bool {
// For now, just return true if we can run Catalog successfully.
_, err := readVolume(devicebytes, 2, debug)
if err != nil { if err != nil {
return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err) return false
} }
return op, nil return true
} }
// diskOperatorFactory is the factory that returns dos3 operators // Operator returns an Operator for the []byte disk image.
// given disk images. func (of OperatorFactory) Operator(devicebytes []byte, debug bool) (types.Operator, error) {
func diskOperatorFactory(sd disk.SectorDisk) (disk.Operator, error) { return operator{data: devicebytes, debug: debug}, nil
bd, err := disk.BlockDeviceFromSectorDisk(sd)
if err != nil {
return nil, err
}
return deviceOperatorFactory(bd)
} }
func init() { // DiskOrder returns the Physical-to-Logical mapping order.
disk.RegisterDeviceOperatorFactory(operatorName, deviceOperatorFactory) func (of OperatorFactory) DiskOrder() types.DiskOrder {
disk.RegisterDiskOperatorFactory(operatorName, diskOperatorFactory) return operator{}.DiskOrder()
} }

View File

@ -6,7 +6,7 @@ import (
"testing" "testing"
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/disk"
) )
func randomBlock() disk.Block { func randomBlock() disk.Block {

25
script_test.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"os"
"testing"
"github.com/rogpeppe/go-internal/testscript"
)
func testscriptMain() int {
main()
return 0
}
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"diskii": testscriptMain,
}))
}
func TestFoo(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "testdata",
})
}

View File

@ -7,12 +7,12 @@ package supermon
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io"
"strconv" "strconv"
"strings" "strings"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/lib/errors" "github.com/zellyn/diskii/errors"
"github.com/zellyn/diskii/types"
) )
const ( const (
@ -29,17 +29,17 @@ const (
type SectorMap []byte type SectorMap []byte
// LoadSectorMap loads a NakedOS sector map. // LoadSectorMap loads a NakedOS sector map.
func LoadSectorMap(sd disk.SectorDisk) (SectorMap, error) { func LoadSectorMap(diskbytes []byte) (SectorMap, error) {
sm := SectorMap(make([]byte, 560)) sm := SectorMap(make([]byte, 560))
sector09, err := sd.ReadPhysicalSector(0, 9) sector09, err := disk.ReadSector(diskbytes, 0, 9)
if err != nil { if err != nil {
return sm, err return sm, err
} }
sector0A, err := sd.ReadPhysicalSector(0, 0xA) sector0A, err := disk.ReadSector(diskbytes, 0, 0xA)
if err != nil { if err != nil {
return sm, err return sm, err
} }
sector0B, err := sd.ReadPhysicalSector(0, 0xB) sector0B, err := disk.ReadSector(diskbytes, 0, 0xB)
if err != nil { if err != nil {
return sm, err return sm, err
} }
@ -63,19 +63,19 @@ func (sm SectorMap) FirstFreeFile() byte {
// Persist writes the current contenst of a sector map back back to // Persist writes the current contenst of a sector map back back to
// disk. // disk.
func (sm SectorMap) Persist(sd disk.SectorDisk) error { func (sm SectorMap) Persist(diskbytes []byte) error {
sector09, err := sd.ReadPhysicalSector(0, 9) sector09, err := disk.ReadSector(diskbytes, 0, 9)
if err != nil { if err != nil {
return err return err
} }
copy(sector09[0xd0:], sm[0:0x30]) copy(sector09[0xd0:], sm[0:0x30])
if err := sd.WritePhysicalSector(0, 9, sector09); err != nil { if err := disk.WriteSector(diskbytes, 0, 9, sector09); err != nil {
return err return err
} }
if err := sd.WritePhysicalSector(0, 0xA, sm[0x30:0x130]); err != nil { if err := disk.WriteSector(diskbytes, 0, 0xA, sm[0x30:0x130]); err != nil {
return err return err
} }
if err := sd.WritePhysicalSector(0, 0xB, sm[0x130:0x230]); err != nil { if err := disk.WriteSector(diskbytes, 0, 0xB, sm[0x130:0x230]); err != nil {
return err return err
} }
return nil return nil
@ -167,10 +167,10 @@ func (sm SectorMap) SectorsByFile() map[byte][]disk.TrackSector {
} }
// ReadFile reads the contents of a file. // ReadFile reads the contents of a file.
func (sm SectorMap) ReadFile(sd disk.SectorDisk, file byte) ([]byte, error) { func (sm SectorMap) ReadFile(diskbytes []byte, file byte) ([]byte, error) {
var result []byte var result []byte
for _, ts := range sm.SectorsForFile(file) { for _, ts := range sm.SectorsForFile(file) {
bytes, err := sd.ReadPhysicalSector(ts.Track, ts.Sector) bytes, err := disk.ReadSector(diskbytes, ts.Track, ts.Sector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -190,7 +190,7 @@ func (sm SectorMap) Delete(file byte) {
// WriteFile writes the contents of a file. It returns true if the // WriteFile writes the contents of a file. It returns true if the
// file already existed. // file already existed.
func (sm SectorMap) WriteFile(sd disk.SectorDisk, file byte, contents []byte, overwrite bool) (bool, error) { func (sm SectorMap) WriteFile(diskbytes []byte, file byte, contents []byte, overwrite bool) (bool, error) {
sectorsNeeded := (len(contents) + 255) / 256 sectorsNeeded := (len(contents) + 255) / 256
cts := make([]byte, 256*sectorsNeeded) cts := make([]byte, 256*sectorsNeeded)
copy(cts, contents) copy(cts, contents)
@ -209,10 +209,10 @@ func (sm SectorMap) WriteFile(sd disk.SectorDisk, file byte, contents []byte, ov
i := 0 i := 0
OUTER: OUTER:
for track := byte(0); track < sd.Tracks(); track++ { for track := byte(0); track < disk.FloppyTracks; track++ {
for sector := byte(0); sector < sd.Sectors(); sector++ { for sector := byte(0); sector < disk.FloppySectors; sector++ {
if sm.FileForSector(track, sector) == FileFree { if sm.FileForSector(track, sector) == FileFree {
if err := sd.WritePhysicalSector(track, sector, cts[i*256:(i+1)*256]); err != nil { if err := disk.WriteSector(diskbytes, track, sector, cts[i*256:(i+1)*256]); err != nil {
return existed, err return existed, err
} }
if err := sm.SetFileForSector(track, sector, file); err != nil { if err := sm.SetFileForSector(track, sector, file); err != nil {
@ -225,7 +225,7 @@ OUTER:
} }
} }
} }
if err := sm.Persist(sd); err != nil { if err := sm.Persist(diskbytes); err != nil {
return existed, err return existed, err
} }
return existed, nil return existed, nil
@ -254,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 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 { for value&0x1f > 0 {
if value&0x1f < 27 { if value&0x1f < 27 {
result = result + string(value&0x1f+'@') result = result + string(rune(value&0x1f+'@'))
value >>= 5 value >>= 5
continue continue
} }
if value&0x20 == 0 { if value&0x20 == 0 {
result = result + string((value&0x1f)-0x1b+'0') result = result + string(rune((value&0x1f)-0x1b+'0'))
} else { } else {
result = result + string((value&0x1f)-0x1b+'5') result = result + string(rune((value&0x1f)-0x1b+'5'))
} }
value >>= 6 value >>= 6
} }
@ -317,16 +317,16 @@ type SymbolTable []Symbol
// ReadSymbolTable reads the symbol table from a disk. If there are // ReadSymbolTable reads the symbol table from a disk. If there are
// problems with the symbol table (like it doesn't exist, or the link // problems with the symbol table (like it doesn't exist, or the link
// pointers are problematic), it'll return nil and an error. // pointers are problematic), it'll return nil and an error.
func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) { func (sm SectorMap) ReadSymbolTable(diskbytes []byte) (SymbolTable, error) {
table := make(SymbolTable, 0, 819) table := make(SymbolTable, 0, 819)
symtbl1, err := sm.ReadFile(sd, 3) symtbl1, err := sm.ReadFile(diskbytes, 3)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(symtbl1) != 0x1000 { if len(symtbl1) != 0x1000 {
return nil, fmt.Errorf("expected file FSYMTBL1(0x3) to be 0x1000 bytes long; got 0x%04X", len(symtbl1)) return nil, fmt.Errorf("expected file FSYMTBL1(0x3) to be 0x1000 bytes long; got 0x%04X", len(symtbl1))
} }
symtbl2, err := sm.ReadFile(sd, 4) symtbl2, err := sm.ReadFile(diskbytes, 4)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -380,7 +380,7 @@ func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) {
} }
// WriteSymbolTable writes a symbol table to a disk. // WriteSymbolTable writes a symbol table to a disk.
func (sm SectorMap) WriteSymbolTable(sd disk.SectorDisk, st SymbolTable) error { func (sm SectorMap) WriteSymbolTable(diskbytes []byte, st SymbolTable) error {
symtbl1 := make([]byte, 0x1000) symtbl1 := make([]byte, 0x1000)
symtbl2 := make([]byte, 0x1000) symtbl2 := make([]byte, 0x1000)
for i, sym := range st { for i, sym := range st {
@ -400,10 +400,10 @@ func (sm SectorMap) WriteSymbolTable(sd disk.SectorDisk, st SymbolTable) error {
symtbl1[offset+4] = six[5] symtbl1[offset+4] = six[5]
copy(symtbl2[offset:offset+5], six) copy(symtbl2[offset:offset+5], six)
} }
if _, err := sm.WriteFile(sd, 3, symtbl1, true); err != nil { if _, err := sm.WriteFile(diskbytes, 3, symtbl1, true); err != nil {
return fmt.Errorf("unable to write first half of symbol table: %v", err) return fmt.Errorf("unable to write first half of symbol table: %v", err)
} }
if _, err := sm.WriteFile(sd, 4, symtbl2, true); err != nil { if _, err := sm.WriteFile(diskbytes, 4, symtbl2, true); err != nil {
return fmt.Errorf("unable to write first second of symbol table: %v", err) return fmt.Errorf("unable to write first second of symbol table: %v", err)
} }
return nil return nil
@ -643,12 +643,13 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named
// 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. // high-level operations on files and directories.
type Operator struct { type Operator struct {
SD disk.SectorDisk data []byte
SM SectorMap SM SectorMap
ST SymbolTable ST SymbolTable
debug bool
} }
var _ disk.Operator = Operator{} var _ types.Operator = Operator{}
// operatorName is the keyword name for the operator that undestands // operatorName is the keyword name for the operator that undestands
// NakedOS/Super-Mon disks. // NakedOS/Super-Mon disks.
@ -659,11 +660,6 @@ func (o Operator) Name() string {
return operatorName return operatorName
} }
// Order returns the sector or block order of the Operator.
func (o Operator) Order() string {
return o.SD.Order()
}
// HasSubdirs returns true if the underlying operating system on the // HasSubdirs returns true if the underlying operating system on the
// disk allows subdirectories. // disk allows subdirectories.
func (o Operator) HasSubdirs() bool { func (o Operator) HasSubdirs() bool {
@ -672,47 +668,47 @@ func (o Operator) HasSubdirs() bool {
// Catalog returns a catalog of disk entries. subdir should be empty // Catalog returns a catalog of disk entries. subdir should be empty
// for operating systems that do not support subdirectories. // 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) {
var descs []disk.Descriptor var descs []types.Descriptor
sectorsByFile := o.SM.SectorsByFile() sectorsByFile := o.SM.SectorsByFile()
for file := byte(1); file < FileReserved; file++ { for file := byte(1); file < FileReserved; file++ {
l := len(sectorsByFile[file]) l := len(sectorsByFile[file])
if l == 0 { if l == 0 {
continue continue
} }
descs = append(descs, disk.Descriptor{ descs = append(descs, types.Descriptor{
Name: NameForFile(file, o.ST), Name: NameForFile(file, o.ST),
Fullname: FullnameForFile(file, o.ST), Fullname: FullnameForFile(file, o.ST),
Sectors: l, Sectors: l,
Length: l * 256, Length: l * 256,
Locked: false, Locked: false,
Type: disk.FiletypeBinary, Type: types.FiletypeBinary,
}) })
} }
return descs, nil return descs, nil
} }
// GetFile retrieves a file by name. // 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) file, err := o.ST.FileForName(filename)
if err != nil { if err != nil {
return disk.FileInfo{}, err return types.FileInfo{}, err
} }
data, err := o.SM.ReadFile(o.SD, file) data, err := o.SM.ReadFile(o.data, file)
if err != nil { 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 { 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), Name: NameForFile(file, o.ST),
Sectors: len(data) / 256, Sectors: len(data) / 256,
Length: len(data), Length: len(data),
Locked: false, Locked: false,
Type: disk.FiletypeBinary, Type: types.FiletypeBinary,
} }
fi := disk.FileInfo{ fi := types.FileInfo{
Descriptor: desc, Descriptor: desc,
Data: data, Data: data,
} }
@ -731,13 +727,13 @@ func (o Operator) Delete(filename string) (bool, error) {
} }
existed := len(o.SM.SectorsForFile(file)) > 0 existed := len(o.SM.SectorsForFile(file)) > 0
o.SM.Delete(file) o.SM.Delete(file)
if err := o.SM.Persist(o.SD); err != nil { if err := o.SM.Persist(o.data); err != nil {
return existed, err return existed, err
} }
if o.ST != nil { if o.ST != nil {
changed := o.ST.DeleteSymbol(filename) changed := o.ST.DeleteSymbol(filename)
if changed { if changed {
if err := o.SM.WriteSymbolTable(o.SD, o.ST); err != nil { if err := o.SM.WriteSymbolTable(o.data, o.ST); err != nil {
return existed, err return existed, err
} }
} }
@ -748,9 +744,9 @@ func (o Operator) Delete(filename string) (bool, error) {
// PutFile writes a file by name. If the file exists and overwrite // PutFile writes a file by name. If the file exists and overwrite
// is false, it returns with an error. Otherwise it returns true if // is false, it returns with an error. Otherwise it returns true if
// an existing file was overwritten. // 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) {
if fileInfo.Descriptor.Type != disk.FiletypeBinary { if fileInfo.Descriptor.Type != types.FiletypeBinary {
return false, fmt.Errorf("%s: only binary file type supported", operatorName) return false, fmt.Errorf("%s: only binary file type supported; got %q", operatorName, fileInfo.Descriptor.Type)
} }
if fileInfo.Descriptor.Length != len(fileInfo.Data) { if fileInfo.Descriptor.Length != len(fileInfo.Data) {
return false, fmt.Errorf("mismatch between FileInfo.Descriptor.Length (%d) and actual length of FileInfo.Data field (%d)", fileInfo.Descriptor.Length, len(fileInfo.Data)) return false, fmt.Errorf("mismatch between FileInfo.Descriptor.Length (%d) and actual length of FileInfo.Data field (%d)", fileInfo.Descriptor.Length, len(fileInfo.Data))
@ -774,7 +770,7 @@ func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool,
return false, fmt.Errorf("all files already used") return false, fmt.Errorf("all files already used")
} }
} }
existed, err = o.SM.WriteFile(o.SD, numFile, fileInfo.Data, overwrite) existed, err = o.SM.WriteFile(o.data, numFile, fileInfo.Data, overwrite)
if err != nil { if err != nil {
return existed, err return existed, err
} }
@ -782,22 +778,49 @@ func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool,
if err := o.ST.AddSymbol(symbol, 0xDF00+uint16(numFile)); err != nil { if err := o.ST.AddSymbol(symbol, 0xDF00+uint16(numFile)); err != nil {
return existed, err return existed, err
} }
if err := o.SM.WriteSymbolTable(o.SD, o.ST); err != nil { if err := o.SM.WriteSymbolTable(o.data, o.ST); err != nil {
return existed, err return existed, err
} }
} }
return existed, nil return existed, nil
} }
// Write writes the underlying disk to the given writer. // DiskOrder returns the Physical-to-Logical mapping order.
func (o Operator) Write(w io.Writer) (int, error) { func (o Operator) DiskOrder() types.DiskOrder {
return o.SD.Write(w) return types.DiskOrderRaw
} }
// operatorFactory is the factory that returns supermon operators // GetBytes returns the disk image bytes, in logical order.
// given disk images. func (o Operator) GetBytes() []byte {
func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) { return o.data
sm, err := LoadSectorMap(sd) }
// 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 { if err != nil {
return nil, err return nil, err
} }
@ -805,9 +828,9 @@ func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
return nil, err return nil, err
} }
op := Operator{SD: sd, SM: sm} op := Operator{data: diskbytes, SM: sm, debug: debug}
st, err := sm.ReadSymbolTable(sd) st, err := sm.ReadSymbolTable(diskbytes)
if err == nil { if err == nil {
op.ST = st op.ST = st
} }
@ -815,6 +838,7 @@ func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
return op, nil return op, nil
} }
func init() { // DiskOrder returns the Physical-to-Logical mapping order.
disk.RegisterDiskOperatorFactory(operatorName, operatorFactory) func (of OperatorFactory) DiskOrder() types.DiskOrder {
return Operator{}.DiskOrder()
} }

View File

@ -3,12 +3,14 @@
package supermon package supermon
import ( import (
"os"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/zellyn/diskii/lib/disk" "github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/types"
) )
const testDisk = "testdata/chacha20.dsk" const testDisk = "testdata/chacha20.dsk"
@ -27,16 +29,34 @@ No more; and by a sleep, to say we end
// loadSectorMap loads a sector map for the disk image contained in // loadSectorMap loads a sector map for the disk image contained in
// filename. It returns the sector map and a sector disk. // filename. It returns the sector map and a sector disk.
func loadSectorMap(filename string) (SectorMap, disk.SectorDisk, error) { func loadSectorMap(filename string) (SectorMap, []byte, error) {
sd, err := disk.LoadDSK(filename) rawbytes, err := os.ReadFile(filename)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
sm, err := LoadSectorMap(sd) diskbytes, err := disk.Swizzle(rawbytes, disk.Dos33LogicalToPhysicalSectorMap)
if err != nil { if err != nil {
return nil, nil, err 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.OpenFile(f, "do", "nakedos", []types.OperatorFactory{OperatorFactory{}}, false)
if err != nil {
return nil, err
}
return op, nil
} }
// TestReadSectorMap tests the reading of the sector map of a test // TestReadSectorMap tests the reading of the sector map of a test
@ -126,11 +146,7 @@ func TestReadSymbolTable(t *testing.T) {
// TestGetFile tests the retrieval of a file's contents, using the // TestGetFile tests the retrieval of a file's contents, using the
// Operator interface. // Operator interface.
func TestGetFile(t *testing.T) { func TestGetFile(t *testing.T) {
sd, err := disk.OpenDisk(testDisk) op, err := getOperator(testDisk)
if err != nil {
t.Fatal(err)
}
op, err := disk.OperatorForDisk(sd)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -213,20 +229,16 @@ func TestReadWriteSymbolTable(t *testing.T) {
// TestPutFile tests the creation of a file, using the Operator // TestPutFile tests the creation of a file, using the Operator
// interface. // interface.
func TestPutFile(t *testing.T) { func TestPutFile(t *testing.T) {
sd, err := disk.OpenDisk(testDisk) op, err := getOperator(testDisk)
if err != nil {
t.Fatal(err)
}
op, err := disk.OperatorForDisk(sd)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
contents := []byte(cities) contents := []byte(cities)
fileInfo := disk.FileInfo{ fileInfo := types.FileInfo{
Descriptor: disk.Descriptor{ Descriptor: types.Descriptor{
Name: "FNEWFILE", Name: "FNEWFILE",
Length: len(contents), Length: len(contents),
Type: disk.FiletypeBinary, Type: types.FiletypeBinary,
}, },
Data: contents, Data: contents,
} }

7
testdata/cathello.txt vendored Normal file
View File

@ -0,0 +1,7 @@
# hello world
exec cat hello.text
stdout 'hello world\n'
! stderr .
-- hello.text --
hello world

View File

@ -3,7 +3,7 @@
// filetype.go contains the Filetype type, along with routines for // filetype.go contains the Filetype type, along with routines for
// converting to and from strings. // converting to and from strings.
package disk package types
import "fmt" import "fmt"
@ -57,62 +57,62 @@ const (
// | C0-EE | ProDOS | ProDOS reserved for future use // | C0-EE | ProDOS | ProDOS reserved for future use
) )
// filetypeInfo holds name information about filetype constants. // FiletypeInfo holds name information about filetype constants.
type filetypeInfo struct { type FiletypeInfo struct {
Type Filetype // The type itself Type Filetype // The type itself
Name string // The constant name, without the "Filetype" prefix Name string // The constant name, without the "Filetype" prefix
ThreeLetter string // The three-letter abbreviation (ProDOS) ThreeLetter string // The three-letter abbreviation (ProDOS)
OneLetter string // The one-letter abbreviation (DOS 3.x) OneLetter string // The one-letter abbreviation (DOS 3.x)
Desc string // The description of the type Desc string // The description of the type
Stringified string // (Generated) result of calling String() on the Constant
Extra bool // If true, exclude from normal display listing Extra bool // If true, exclude from normal display listing
Stringified string // (Generated) result of calling String() on the Constant
NamesString string // (Generated) the names usable for this filetype.
} }
// names of Filetype constants above // names of Filetype constants above
var filetypeInfos = []filetypeInfo{ var filetypeInfos = []FiletypeInfo{
{FiletypeTypeless, "Typeless", "", "", "Typeless file", "", false}, {Type: FiletypeTypeless, Name: "Typeless", Desc: "Typeless file"},
{FiletypeBadBlocks, "BadBlocks", "", "", "Bad blocks file", "", false}, {Type: FiletypeBadBlocks, Name: "BadBlocks", Desc: "Bad blocks file"},
{FiletypeSOSPascalCode, "SOSPascalCode", "", "", "PASCAL code file", "", true}, {Type: FiletypeSOSPascalCode, Name: "SOSPascalCode", Desc: "PASCAL code file", Extra: true},
{FiletypeSOSPascalText, "SOSPascalText", "", "", "PASCAL text file", "", true}, {Type: FiletypeSOSPascalText, Name: "SOSPascalText", Desc: "PASCAL text file", Extra: true},
{FiletypeASCIIText, "ASCIIText", "T", "TXT", "ASCII text file", "", false}, {Type: FiletypeASCIIText, Name: "ASCIIText", ThreeLetter: "TXT", OneLetter: "T", Desc: "ASCII text file"},
{FiletypeSOSPascalText2, "SOSPascalText2", "", "", "PASCAL text file", "", true}, {Type: FiletypeSOSPascalText2, Name: "SOSPascalText2", Desc: "PASCAL text file", Extra: true},
{FiletypeBinary, "Binary", "B", "BIN", "Binary file", "", false}, {Type: FiletypeBinary, Name: "Binary", ThreeLetter: "BIN", OneLetter: "B", Desc: "Binary file"},
{FiletypeFont, "Font", "", "", "Font file", "", true}, {Type: FiletypeFont, Name: "Font", Desc: "Font file", Extra: true},
{FiletypeGraphicsScreen, "GraphicsScreen", "", "", "Graphics screen file", "", true}, {Type: FiletypeGraphicsScreen, Name: "GraphicsScreen", Desc: "Graphics screen file", Extra: true},
{FiletypeBusinessBASIC, "BusinessBASIC", "", "", "Business BASIC program file", "", true}, {Type: FiletypeBusinessBASIC, Name: "BusinessBASIC", Desc: "Business BASIC program file", Extra: true},
{FiletypeBusinessBASICData, "BusinessBASICData", "", "", "Business BASIC data file", "", true}, {Type: FiletypeBusinessBASICData, Name: "BusinessBASICData", Desc: "Business BASIC data file", Extra: true},
{FiletypeSOSWordProcessor, "SOSWordProcessor", "", "", "Word processor file", "", true}, {Type: FiletypeSOSWordProcessor, Name: "SOSWordProcessor", Desc: "Word processor file", Extra: true},
{FiletypeSOSSystem, "SOSSystem", "", "", "SOS system file", "", true}, {Type: FiletypeSOSSystem, Name: "SOSSystem", Desc: "SOS system file", Extra: true},
{FiletypeDirectory, "Directory", "", "DIR", "Directory file", "", false}, {Type: FiletypeDirectory, Name: "Directory", ThreeLetter: "DIR", OneLetter: "D", Desc: "Directory file"},
{FiletypeRPSData, "RPSData", "", "", "RPS data file", "", true}, {Type: FiletypeRPSData, Name: "RPSData", Desc: "RPS data file", Extra: true},
{FiletypeRPSIndex, "RPSIndex", "", "", "RPS index file", "", true}, {Type: FiletypeRPSIndex, Name: "RPSIndex", Desc: "RPS index file", Extra: true},
{FiletypeAppleWorksDatabase, "AppleWorksDatabase", "", "ADB", "AppleWorks data base file", "", false}, {Type: FiletypeAppleWorksDatabase, Name: "AppleWorksDatabase", ThreeLetter: "ADB", Desc: "AppleWorks data base file"},
{FiletypeAppleWorksWordProcessor, "AppleWorksWordProcessor", "", "AWP", "AppleWorks word processing file", "", false}, {Type: FiletypeAppleWorksWordProcessor, Name: "AppleWorksWordProcessor", ThreeLetter: "AWP", Desc: "AppleWorks word processing file"},
{FiletypeAppleWorksSpreadsheet, "AppleWorksSpreadsheet", "", "ASP", "AppleWorks spreadsheet file", "", false}, {Type: FiletypeAppleWorksSpreadsheet, Name: "AppleWorksSpreadsheet", ThreeLetter: "ASP", Desc: "AppleWorks spreadsheet file"},
{FiletypePascal, "Pascal", "", "PAS", "ProDOS PASCAL file", "", false}, {Type: FiletypePascal, Name: "Pascal", ThreeLetter: "PAS", Desc: "ProDOS PASCAL file"},
{FiletypeCommand, "Command", "", "CMD", "Added command file", "", false}, {Type: FiletypeCommand, Name: "Command", ThreeLetter: "CMD", Desc: "Added command file"},
{FiletypeUserDefinedF1, "UserDefinedF1", "", "", "ProDOS user defined file type F1", "", true}, {Type: FiletypeUserDefinedF1, Name: "UserDefinedF1", Desc: "ProDOS user defined file type F1", Extra: true},
{FiletypeUserDefinedF2, "UserDefinedF2", "", "", "ProDOS user defined file type F2", "", true}, {Type: FiletypeUserDefinedF2, Name: "UserDefinedF2", Desc: "ProDOS user defined file type F2", Extra: true},
{FiletypeUserDefinedF3, "UserDefinedF3", "", "", "ProDOS user defined file type F3", "", true}, {Type: FiletypeUserDefinedF3, Name: "UserDefinedF3", Desc: "ProDOS user defined file type F3", Extra: true},
{FiletypeUserDefinedF4, "UserDefinedF4", "", "", "ProDOS user defined file type F4", "", true}, {Type: FiletypeUserDefinedF4, Name: "UserDefinedF4", Desc: "ProDOS user defined file type F4", Extra: true},
{FiletypeUserDefinedF5, "UserDefinedF5", "", "", "ProDOS user defined file type F5", "", true}, {Type: FiletypeUserDefinedF5, Name: "UserDefinedF5", Desc: "ProDOS user defined file type F5", Extra: true},
{FiletypeUserDefinedF6, "UserDefinedF6", "", "", "ProDOS user defined file type F6", "", true}, {Type: FiletypeUserDefinedF6, Name: "UserDefinedF6", Desc: "ProDOS user defined file type F6", Extra: true},
{FiletypeUserDefinedF7, "UserDefinedF7", "", "", "ProDOS user defined file type F7", "", true}, {Type: FiletypeUserDefinedF7, Name: "UserDefinedF7", Desc: "ProDOS user defined file type F7", Extra: true},
{FiletypeUserDefinedF8, "UserDefinedF8", "", "", "ProDOS user defined file type F8", "", true}, {Type: FiletypeUserDefinedF8, Name: "UserDefinedF8", Desc: "ProDOS user defined file type F8", Extra: true},
{FiletypeIntegerBASIC, "IntegerBASIC", "I", "INT", "Integer BASIC program file", "", false}, {Type: FiletypeIntegerBASIC, Name: "IntegerBASIC", ThreeLetter: "INT", OneLetter: "I", Desc: "Integer BASIC program file"},
{FiletypeIntegerBASICVariables, "IntegerBASICVariables", "", "IVR", "Integer BASIC variables file", "", false}, {Type: FiletypeIntegerBASICVariables, Name: "IntegerBASICVariables", ThreeLetter: "IVR", Desc: "Integer BASIC variables file"},
{FiletypeApplesoftBASIC, "ApplesoftBASIC", "A", "BAS", "Applesoft BASIC program file", "", false}, {Type: FiletypeApplesoftBASIC, Name: "ApplesoftBASIC", ThreeLetter: "BAS", OneLetter: "A", Desc: "Applesoft BASIC program file"},
{FiletypeApplesoftBASICVariables, "ApplesoftBASICVariables", "", "VAR", "Applesoft BASIC variables file", "", false}, {Type: FiletypeApplesoftBASICVariables, Name: "ApplesoftBASICVariables", ThreeLetter: "VAR", Desc: "Applesoft BASIC variables file"},
{FiletypeRelocatable, "Relocatable", "R", "REL", "EDASM relocatable object module file", "", false}, {Type: FiletypeRelocatable, Name: "Relocatable", ThreeLetter: "REL", OneLetter: "R", Desc: "EDASM relocatable object module file"},
{FiletypeSystem, "System", "", "SYS", "System file", "", false}, {Type: FiletypeSystem, Name: "System", ThreeLetter: "SYS", Desc: "System file"},
{FiletypeS, "S", "", "S", `DOS 3.3 Type "S"`, "", false}, {Type: FiletypeS, Name: "S", OneLetter: "S", Desc: `DOS 3.3 Type "S"`},
{FiletypeNewA, "NewA", "", "A", `DOS 3.3 Type "new A"`, "", false}, {Type: FiletypeNewA, Name: "NewA", OneLetter: "A", Desc: `DOS 3.3 Type "new A"`},
{FiletypeNewB, "NewB", "", "B", `DOS 3.3 Type "new B"`, "", false}, {Type: FiletypeNewB, Name: "NewB", OneLetter: "B", Desc: `DOS 3.3 Type "new B"`},
} }
var filetypeInfosMap map[Filetype]filetypeInfo var filetypeInfosMap map[Filetype]FiletypeInfo
var filetypeNames []string
var filetypeNamesExtras []string
func init() { func init() {
sosReserved := []Filetype{0x0D, 0x0E, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18} sosReserved := []Filetype{0x0D, 0x0E, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}
@ -124,7 +124,7 @@ func init() {
prodosReserved = append(prodosReserved, i) prodosReserved = append(prodosReserved, i)
} }
for _, typ := range sosReserved { for _, typ := range sosReserved {
info := filetypeInfo{ info := FiletypeInfo{
Type: typ, Type: typ,
Name: fmt.Sprintf("SOSReserved%02X", int(typ)), Name: fmt.Sprintf("SOSReserved%02X", int(typ)),
ThreeLetter: "", ThreeLetter: "",
@ -135,7 +135,7 @@ func init() {
filetypeInfos = append(filetypeInfos, info) filetypeInfos = append(filetypeInfos, info)
} }
for _, typ := range prodosReserved { for _, typ := range prodosReserved {
info := filetypeInfo{ info := FiletypeInfo{
Type: typ, Type: typ,
Name: fmt.Sprintf("ProDOSReserved%02X", int(typ)), Name: fmt.Sprintf("ProDOSReserved%02X", int(typ)),
ThreeLetter: "", ThreeLetter: "",
@ -147,25 +147,24 @@ func init() {
} }
seen := map[string]bool{} seen := map[string]bool{}
filetypeInfosMap = make(map[Filetype]filetypeInfo, len(filetypeInfos)) filetypeInfosMap = make(map[Filetype]FiletypeInfo, len(filetypeInfos))
for i, info := range filetypeInfos { for i, info := range filetypeInfos {
info.Stringified = info.Desc + " (" + info.Name info.Stringified = info.Desc + " (" + info.Name
info.NamesString = info.Name
if info.ThreeLetter != "" && !seen[info.ThreeLetter] { if info.ThreeLetter != "" && !seen[info.ThreeLetter] {
info.Stringified += "|" + info.ThreeLetter info.Stringified += "|" + info.ThreeLetter
info.NamesString += "|" + info.ThreeLetter
seen[info.ThreeLetter] = true seen[info.ThreeLetter] = true
} }
if info.OneLetter != "" && info.OneLetter != info.Name && !seen[info.OneLetter] { if info.OneLetter != "" && info.OneLetter != info.Name && !seen[info.OneLetter] {
info.Stringified += "|" + info.OneLetter info.Stringified += "|" + info.OneLetter
info.NamesString += "|" + info.OneLetter
seen[info.OneLetter] = true seen[info.OneLetter] = true
} }
info.Stringified += ")" info.Stringified += ")"
filetypeInfos[i] = info filetypeInfos[i] = info
filetypeInfosMap[info.Type] = info filetypeInfosMap[info.Type] = info
filetypeNamesExtras = append(filetypeNamesExtras, info.Stringified)
if !info.Extra {
filetypeNames = append(filetypeNames, info.Stringified)
}
} }
} }
@ -187,10 +186,16 @@ func FiletypeForName(name string) (Filetype, error) {
return 0, fmt.Errorf("Unknown Filetype: %q", name) return 0, fmt.Errorf("Unknown Filetype: %q", name)
} }
// FiletypeNames returns a list of all filetype names. // FiletypeInfos returns a list information on all filetypes.
func FiletypeNames(all bool) []string { func FiletypeInfos(all bool) []FiletypeInfo {
if all { if all {
return filetypeNamesExtras return filetypeInfos
} }
return filetypeNames var result []FiletypeInfo
for _, info := range filetypeInfos {
if !info.Extra {
result = append(result, info)
}
}
return result
} }

74
types/ops.go Normal file
View File

@ -0,0 +1,74 @@
// 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 types
// Descriptor describes a file's characteristics.
type Descriptor struct {
Name string
Fullname string // If there's a more complete filename (eg. Super-Mon), put it here.
Sectors int
Blocks int
Length int
Locked bool
Type Filetype
}
type DiskOrder string
const (
DiskOrderDO = DiskOrder("do")
DiskOrderPO = DiskOrder("po")
DiskOrderRaw = DiskOrder("raw")
DiskOrderAuto = DiskOrder("auto")
DiskOrderUnknown = DiskOrder("")
)
// OperatorFactory is the interface for getting operators, and finding out a bit
// about them before getting them.
type OperatorFactory interface {
// Name returns the name of the operator.
Name() string
// DiskOrder returns the Physical-to-Logical mapping order.
DiskOrder() DiskOrder
// SeemsToMatch returns true if the []byte disk image seems to match the
// system of this operator.
SeemsToMatch(diskbytes []byte, debug bool) bool
// Operator returns an Operator for the []byte disk image.
Operator(diskbytes []byte, debug bool) (Operator, error)
}
// Operator is the interface that can operate on disks.
type Operator interface {
// Name returns the name of the operator.
Name() string
// DiskOrder returns the Physical-to-Logical mapping order.
DiskOrder() DiskOrder
// 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)
// 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)
// 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.
PutFile(fileInfo FileInfo, overwrite bool) (existed bool, err error)
// GetBytes returns the disk image bytes, in logical order.
GetBytes() []byte
}
// FileInfo represents a file descriptor plus the content.
type FileInfo struct {
Descriptor Descriptor
Data []byte
StartAddress uint16
}

8
types/types.go Normal file
View File

@ -0,0 +1,8 @@
package types
// Globals holds flags and configuration that are shared globally.
type Globals struct {
Debug bool
DiskOperatorFactories []OperatorFactory
}

View File

@ -5,12 +5,11 @@ import (
"testing" "testing"
"github.com/zellyn/diskii/data" "github.com/zellyn/diskii/data"
"github.com/zellyn/diskii/lib/woz" "github.com/zellyn/diskii/woz"
) )
func TestBasicLoad(t *testing.T) { func TestBasicLoad(t *testing.T) {
bb := data.MustAsset("data/disks/dos33master.woz") wz, err := woz.Decode(bytes.NewReader(data.DOS33master_woz))
wz, err := woz.Decode(bytes.NewReader(bb))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

64
writetest.asm Normal file
View File

@ -0,0 +1,64 @@
* = $6000
START = *
;; Free addresses: 6, 7, 8, 9
IOB = $6
RWTS = $3D9
GET_IOB = $3E3
BUF = $6100
start:
; set up IOB address
jsr GET_IOB
sta IOB+1
sty IOB
lda #$f
outer:
ldy #0
inner:
sta BUF,Y
iny
bne inner
pha
jsr write
pla
tax
dex
txa
bpl outer
rts
write:
ldy #$5 ; sector number
sta ($6), Y
ldy #$2 ; drive number
lda #$2 ; drive 2
sta ($6), Y
iny ; 3: volume
lda #0 ; any volume
sta ($6), Y
iny ; 4: track
sta ($6), Y
ldy #$8 ; LO of buffer
lda #<BUF
sta ($6), Y
iny ; $9: HI of buffer
lda #>BUF
sta ($6), Y
iny ; $A: unused
iny ; $B: Byte count
lda #0
sta ($6),Y
iny ; $C: command
lda #$02 ; write
sta ($6),Y
lda IOB+1
ldy IOB
jsr RWTS
rts

1
writetest.mon Normal file
View File

@ -0,0 +1 @@
6000:20 e3 03 85 07 84 06 a9 0f a0 00 99 00 61 c8 d0 fa 48 20 1c 60 68 aa ca 8a 10 ee 60 a0 05 91 06 a0 02 a9 02 91 06 c8 a9 00 91 06 c8 91 06 a0 08 a9 00 91 06 c8 a9 61 91 06 c8 c8 a9 00 91 06 c8 a9 02 91 06 a5 07 a4 06 20 d9 03 60