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”.
Discussion/support is in
[#apple2 on the retrocomputing Slack](https://retrocomputing.slack.com/messages/apple2/)
(invites [here](https://retrocomputing.herokuapp.com)).
Discussion/support is on the
[apple2infinitum Slack](https://apple2infinitum.slack.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
tool, but for now only some parts work.
@ -47,8 +70,8 @@ Current disk operations supported:
| ---------------- | -------- | ------ | ------------------ |
| basic structures | ✓ | ✓ | ✓ |
| ls | ✓ | ✓ | ✓ |
| dump | ✓ | ✗ | ✓ |
| put | ✗ | ✗ | |
| dump | ✗ | ✗ | ✗ |
| put | ✗ | ✗ | |
| dumptext | ✗ | ✗ | ✗ |
| delete | ✗ | ✗ | ✗ |
| rename | ✗ | ✗ | ✗ |
@ -59,7 +82,7 @@ Current disk operations supported:
| init | ✗ | ✗ | ✗ |
| defrag | ✗ | ✗ | ✗ |
### Installing/updating
# Installing/updating
Assuming you have Go installed, run `go get -u github.com/zellyn/diskii`
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
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
operations matrix is listed below. Anything that an actual user needs
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] Implement `GetFile` for DOS 3.3
- [ ] Add and implement the `-l` flag for `ls`
- [x] Add `Delete` to the `disk.Operator` interface
- [x] Implement it for Super-Mon
- [ ] Implement it for Super-Mon
- [ ] Implement it for DOS 3.3
- [ ] Make 13-sector DOS disks work
- [ ] Read/write nybble formats
- [ ] Read/write gzipped files
- [ ] Add basic ProDOS structures
- [ ] Add ProDOS support
- [ ] Add ProDOS support for all commands
- [x] Make `filetypes` command use a tabwriter to write as a table
### Related tools
# Related tools
- 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
@ -107,3 +129,48 @@ will be likely to get priority.
- https://github.com/slotek/apple2-disk-util - ruby
- https://github.com/slotek/dsk2nib - C
- 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 (
"testing"
"github.com/zellyn/diskii/lib/basic"
"github.com/zellyn/diskii/basic"
)
// helloBinary is a simple basic program used for testing. Listing

View File

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

View File

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

View File

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

View File

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

View File

@ -5,39 +5,22 @@ package cmd
import (
"fmt"
"os"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/types"
)
var all bool // flag for whether to show all filetypes
// 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)
}
},
type FiletypesCmd struct {
All bool `kong:"help='Display all types, including SOS types and reserved ranges.'"`
}
func init() {
RootCmd.AddCommand(filetypesCmd)
filetypesCmd.Flags().BoolVarP(&all, "all", "a", false, "display all types, including SOS types and reserved ranges")
}
// runFiletypes performs the actual listing of filetypes.
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)
func (f *FiletypesCmd) Run(globals *types.Globals) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
fmt.Fprintln(w, "Description\tName\tThree-letter Name\tOne-letter Name")
fmt.Fprintln(w, "-----------\t----\t-----------------\t---------------")
for _, typ := range types.FiletypeInfos(f.All) {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", typ.Desc, typ.Name, typ.ThreeLetter, typ.OneLetter)
}
w.Flush()
return nil
}

View File

@ -4,87 +4,76 @@ package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/lib/supermon"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/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)
// mkhelloCmd represents the mkhello command
var mkhelloCmd = &cobra.Command{
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.
type NakedOSCmd struct {
Mkhello MkHelloCmd `kong:"cmd,help='Create an FHELLO program that loads and runs another file.'"`
}
// 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:
mkhello test.dsk FDEMO # 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.`,
Run: func(cmd *cobra.Command, args []string) {
if err := runMkhello(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(-1)
}
},
# Load and run FDEMO at the default address, then jump to the start of the loaded code.
mkhello test.dsk FDEMO
# Load and run file DF06 at address 0x2000, and jump to 0x2100.
mkhello test.dsk --address 0x2000 --start 0x2100 DF06`
}
func init() {
nakedosCmd.AddCommand(mkhelloCmd)
// 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>")
func (m *MkHelloCmd) Run(globals *types.Globals) error {
if m.Start == 0xFFFF {
m.Start = m.Address
}
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 {
return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", start, start, address, address)
if m.Start < m.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 {
return err
}
if op.Name() != "nakedos" {
return fmt.Errorf("mkhello only works on disks of type %q; got %q", "nakedos", op.Name())
}
nakOp, ok := op.(supermon.Operator)
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 {
return err
}
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
if addr == 0 {
@ -94,32 +83,24 @@ func runMkhello(args []string) error {
0x20, 0x40, 0x03, // JSR NAKEDOS
0x6D, 0x01, 0xDC, // ADC NKRDFILE
0x2C, toLoad, 0xDF, // BIT ${file number to load}
0x2C, 0x00, byte(address >> 8), // BIT ${target page}
0xD8, // CLD
0x4C, byte(start), byte(start >> 8), // JMP ${target page}
0x2C, 0x00, byte(m.Address >> 8), // BIT ${target page}
0xD8, // CLD
0x4C, byte(m.Start), byte(m.Start >> 8), // JMP ${target page}
}
fileInfo := disk.FileInfo{
Descriptor: disk.Descriptor{
fileInfo := types.FileInfo{
Descriptor: types.Descriptor{
Name: fmt.Sprintf("DF01:%s", helloName),
Length: len(contents),
Type: disk.FiletypeBinary,
Type: types.FiletypeBinary,
},
Data: contents,
}
_ = fileInfo
_, err = op.PutFile(fileInfo, true)
if err != nil {
return err
}
f, err := os.Create(args[0])
if err != nil {
return err
}
_, err = op.Write(f)
if err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return nil
return disk.WriteBack(m.DiskImage, op, order, true)
}

View File

@ -4,80 +4,61 @@ package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/lib/helpers"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/types"
)
var filetypeName string // flag for file type
var overwrite bool // flag for whether to overwrite
type PutCmd 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.'"`
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
// of a file.
var putCmd = &cobra.Command{
Use: "put",
Short: "put the raw contents of a file",
Long: `Put the raw contents of a file.
DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
TargetFilename string `kong:"arg,required,help='Filename to use on disk.'"`
SourceFilename string `kong:"arg,required,type='existingfile',help='Name of file containing data to put.'"`
}
put disk-image.dsk HELLO <name of file with contents>
`,
Run: func(cmd *cobra.Command, args []string) {
if err := runPut(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(-1)
func (p PutCmd) Help() string {
return `Examples:
# Put file gremlins.o onto disk image games.dsk, using the filename GREMLINS.
diskii put games.dsk GREMLINS gremlins.o`
}
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])
if err != nil {
return err
}
contents, err := helpers.FileContentsOrStdIn(args[2])
op, order, err := disk.OpenFilename(p.DiskImage, p.Order, p.System, globals.DiskOperatorFactories, globals.Debug)
if err != nil {
return err
}
filetype, err := disk.FiletypeForName(filetypeName)
contents, err := helpers.FileContentsOrStdIn(p.SourceFilename)
if err != nil {
return err
}
fileInfo := disk.FileInfo{
Descriptor: disk.Descriptor{
Name: args[1],
filetype, err := types.FiletypeForName(p.FiletypeName)
if err != nil {
return err
}
fileInfo := types.FileInfo{
Descriptor: types.Descriptor{
Name: p.TargetFilename,
Length: len(contents),
Type: filetype,
},
Data: contents,
}
_, err = op.PutFile(fileInfo, overwrite)
_, err = op.PutFile(fileInfo, p.Overwrite)
if err != nil {
return err
}
f, err := os.Create(args[0])
if err != nil {
return err
}
_, err = op.Write(f)
if err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return nil
return disk.WriteBack(p.DiskImage, op, order, true)
}

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 (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/lib/helpers"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/types"
)
var sdAddress uint16 // flag for address to load at
var sdStart uint16 // flag for address to start execution at
type SDCmd struct {
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
// mksdCmd represents the mksd command
var mksdCmd = &cobra.Command{
Use: "mksd",
Short: "create a Standard-Delivery disk image",
Long: `diskii mksd creates a "Standard Delivery" disk image containing a binary.
DiskImage string `kong:"arg,required,type='path',help='Disk image to write.'"`
Binary *os.File `kong:"arg,required,help='Binary file to write to the disk.'"`
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 (s SDCmd) Help() string {
return `
See https://github.com/peterferrie/standard-delivery for details.
Examples:
mksd test.dsk foo.o # 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.`,
Run: func(cmd *cobra.Command, args []string) {
if err := runMkSd(args); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(-1)
}
},
# Load and run foo.o at the default address, then jump to the start of the loaded code.
diskii mksd test.dsk foo.o
# Load foo.o at address 0x2000, then jump to 0x2100.
diskii mksd test.dsk foo.o --address 0x2000 --start 0x2100`
}
func init() {
RootCmd.AddCommand(mksdCmd)
mksdCmd.Flags().Uint16VarP(&sdAddress, "address", "a", 0x6000, "memory location to load code at")
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>")
func (s *SDCmd) Run(globals *types.Globals) error {
if s.Start == 0xFFFF {
s.Start = s.Address
}
contents, err := helpers.FileContentsOrStdIn(args[1])
contents, err := io.ReadAll(s.Binary)
if err != nil {
return err
}
if sdAddress%256 != 0 {
return fmt.Errorf("address %d (%04X) not on a page boundary", sdAddress, sdAddress)
if s.Address%256 != 0 {
return fmt.Errorf("address %d (%04X) not on a page boundary", s.Address, s.Address)
}
if sdStart < sdAddress {
return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", sdStart, sdStart, sdAddress, sdAddress)
if s.Start < s.Address {
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) {
end := int(sdAddress) + len(contents)
if int(s.Start) >= int(s.Address)+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)",
sdStart, sdStart, sdAddress, sdAddress, end, end)
s.Start, s.Start, s.Address, s.Address, end, end)
}
if int(sdStart)+len(contents) > 0xC000 {
end := int(sdStart) + len(contents)
if int(s.Start)+len(contents) > 0xC000 {
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",
sdStart, sdStart, len(contents), len(contents), end, end)
s.Start, s.Start, len(contents), len(contents), end, end)
}
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,
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,
0xfc, 0x4c, byte(sdStart), byte(sdStart >> 8),
0xfc, 0x4c, byte(s.Start), byte(s.Start >> 8),
}
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 {
contents = append(contents, 0)
}
sd := disk.Empty()
diskbytes := make([]byte, disk.FloppyDiskBytes)
var track, sector byte
for i := 0; i < len(contents); i += 256 {
sector += 2
if sector >= sd.Sectors() {
sector = (sd.Sectors() + 1) - sector
if sector >= disk.FloppySectors {
sector = (disk.FloppySectors + 1) - sector
if sector == 0 {
track++
if track >= sd.Tracks() {
if track >= disk.FloppyTracks {
return fmt.Errorf("ran out of tracks")
}
}
}
address := int(sdAddress) + i
address := int(s.Address) + i
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
}
}
@ -114,20 +108,17 @@ func runMkSd(args []string) error {
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
}
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 {
return err
}
_, err = sd.Write(f)
if err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return nil
return helpers.WriteOutput(s.DiskImage, rawBytes, true)
}

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"
)
// A ProDOS block.
type Block [512]byte
// Dev represents a .po disk image.
type Dev struct {
data []byte // The actual data in the file
blocks uint16 // Number of blocks
}
var _ BlockDevice = (*Dev)(nil)
// LoadDev loads a .po image from a file.
func LoadDev(filename string) (Dev, error) {
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 (
"encoding/binary"
"fmt"
"io"
"strings"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/types"
)
const (
@ -356,11 +356,11 @@ func (fd *FileDesc) FilenameString() string {
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
// contents.
func (fd FileDesc) descriptor() disk.Descriptor {
desc := disk.Descriptor{
func (fd FileDesc) descriptor() types.Descriptor {
desc := types.Descriptor{
Name: fd.FilenameString(),
Sectors: int(fd.SectorCount),
Length: -1,
@ -368,28 +368,28 @@ func (fd FileDesc) descriptor() disk.Descriptor {
}
switch fd.Filetype & 0x7f {
case FiletypeText: // Text file
desc.Type = disk.FiletypeASCIIText
desc.Type = types.FiletypeASCIIText
case FiletypeInteger: // INTEGER BASIC file
desc.Type = disk.FiletypeIntegerBASIC
desc.Type = types.FiletypeIntegerBASIC
case FiletypeApplesoft: // APPLESOFT BASIC file
desc.Type = disk.FiletypeApplesoftBASIC
desc.Type = types.FiletypeApplesoftBASIC
case FiletypeBinary: // BINARY file
desc.Type = disk.FiletypeBinary
desc.Type = types.FiletypeBinary
case FiletypeS: // S type file
desc.Type = disk.FiletypeS
desc.Type = types.FiletypeS
case FiletypeRelocatable: // RELOCATABLE object module file
desc.Type = disk.FiletypeRelocatable
desc.Type = types.FiletypeRelocatable
case FiletypeA: // A type file
desc.Type = disk.FiletypeNewA
desc.Type = types.FiletypeNewA
case FiletypeB: // B type file
desc.Type = disk.FiletypeNewB
desc.Type = types.FiletypeNewB
}
return desc
}
// Contents returns the on-disk contents of a file represented by a
// FileDesc.
func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
func (fd *FileDesc) Contents(diskbytes []byte) ([]byte, error) {
tsls := []TrackSectorList{}
nextTrack := fd.TrackSectorListTrack
nextSector := fd.TrackSectorListSector
@ -401,7 +401,7 @@ func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
}
seen[ts] = true
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
}
tsls = append(tsls, tsl)
@ -426,7 +426,7 @@ func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
data = append(data, 0)
}
} else {
contents, err := lsd.ReadLogicalSector(ts.Track, ts.Sector)
contents, err := disk.ReadSector(diskbytes, ts.Track, ts.Sector)
if err != nil {
return nil, err
}
@ -490,9 +490,9 @@ func (tsl *TrackSectorList) FromSector(data []byte) error {
// readCatalogSectors reads the raw CatalogSector structs from a DOS
// 3.3 disk.
func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) {
func readCatalogSectors(diskbytes []byte, debug bool) ([]CatalogSector, error) {
v := &VTOC{}
err := disk.UnmarshalLogicalSector(d, v, VTOCTrack, VTOCSector)
err := disk.UnmarshalLogicalSector(diskbytes, v, VTOCTrack, VTOCSector)
if err != nil {
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)
}
cs := CatalogSector{}
err := disk.UnmarshalLogicalSector(d, &cs, nextTrack, nextSector)
err := disk.UnmarshalLogicalSector(diskbytes, &cs, nextTrack, nextSector)
if err != nil {
return nil, err
}
@ -528,8 +528,8 @@ func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) {
}
// ReadCatalog reads the catalog of a DOS 3.3 disk.
func ReadCatalog(d disk.LogicalSectorDisk) (files, deleted []FileDesc, err error) {
css, err := readCatalogSectors(d)
func ReadCatalog(diskbytes []byte, debug bool) (files, deleted []FileDesc, err error) {
css, err := readCatalogSectors(diskbytes, debug)
if err != nil {
return nil, nil, err
}
@ -549,13 +549,14 @@ func ReadCatalog(d disk.LogicalSectorDisk) (files, deleted []FileDesc, err error
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.
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
// dos3 disks.
@ -566,11 +567,6 @@ func (o operator) Name() string {
return operatorName
}
// Order returns the sector or block order of the underlying storage.
func (o operator) Order() string {
return o.lsd.Order()
}
// HasSubdirs returns true if the underlying operating system on the
// disk allows subdirectories.
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
// for operating systems that do not support subdirectories.
func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
fds, _, err := ReadCatalog(o.lsd)
func (o operator) Catalog(subdir string) ([]types.Descriptor, error) {
fds, _, err := ReadCatalog(o.data, o.debug)
if err != nil {
return nil, err
}
descs := make([]disk.Descriptor, 0, len(fds))
descs := make([]types.Descriptor, 0, len(fds))
for _, fd := range fds {
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
// filename, or an error.
func (o operator) fileForFilename(filename string) (FileDesc, error) {
fds, _, err := ReadCatalog(o.lsd)
fds, _, err := ReadCatalog(o.data, o.debug)
if err != nil {
return FileDesc{}, err
}
@ -607,18 +603,18 @@ func (o operator) fileForFilename(filename string) (FileDesc, error) {
}
// 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)
if err != nil {
return disk.FileInfo{}, err
return types.FileInfo{}, err
}
desc := fd.descriptor()
data, err := fd.Contents(o.lsd)
data, err := fd.Contents(o.data)
if err != nil {
return disk.FileInfo{}, err
return types.FileInfo{}, err
}
fi := disk.FileInfo{
fi := types.FileInfo{
Descriptor: desc,
Data: data,
}
@ -659,7 +655,7 @@ func (o operator) GetFile(filename string) (disk.FileInfo, error) {
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
@ -671,29 +667,46 @@ func (o operator) Delete(filename string) (bool, error) {
// PutFile writes a file by name. If the file exists and overwrite
// is false, it returns with an error. Otherwise it returns true if
// an existing file was overwritten.
func (o operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) {
func (o operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) {
return false, fmt.Errorf("%s does not implement PutFile yet", operatorName)
}
// Write writes the underlying disk to the given writer.
func (o operator) Write(w io.Writer) (int, error) {
return o.lsd.Write(w)
// DiskOrder returns the Physical-to-Logical mapping order.
func (o operator) DiskOrder() types.DiskOrder {
return types.DiskOrderDO
}
// operatorFactory is the factory that returns dos3 operators given
// disk images.
func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
lsd, err := disk.NewMappedDisk(sd, disk.Dos33LogicalToPhysicalSectorMap)
if err != nil {
return nil, err
}
_, _, err = ReadCatalog(lsd)
if err != nil {
return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err)
}
return operator{lsd: lsd}, nil
// GetBytes returns the disk image bytes, in logical order.
func (o operator) GetBytes() []byte {
return o.data
}
func init() {
disk.RegisterDiskOperatorFactory(operatorName, operatorFactory)
// 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.
_, _, 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 (
"crypto/rand"
"os"
"reflect"
"testing"
"github.com/zellyn/diskii/lib/disk"
)
// 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.
func TestReadCatalog(t *testing.T) {
sd, err := disk.LoadDSK("testdata/dos33test.dsk")
diskbytes, err := os.ReadFile("testdata/dos33test.dsk")
if err != nil {
t.Fatal(err)
}
dsk, err := disk.NewMappedDisk(sd, disk.Dos33LogicalToPhysicalSectorMap)
if err != nil {
t.Fatal(err)
}
fds, deleted, err := ReadCatalog(dsk)
fds, deleted, err := ReadCatalog(diskbytes, false)
if err != nil {
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
import (
"github.com/zellyn/diskii/cmd"
"reflect"
"strconv"
// Import disk operator factories for DOS3 and Super-Mon
_ "github.com/zellyn/diskii/lib/dos3"
_ "github.com/zellyn/diskii/lib/prodos"
_ "github.com/zellyn/diskii/lib/supermon"
"github.com/zellyn/diskii/cmd"
"github.com/zellyn/diskii/dos3"
"github.com/zellyn/diskii/prodos"
"github.com/zellyn/diskii/supermon"
"github.com/zellyn/diskii/types"
"fmt"
"os"
"github.com/alecthomas/kong"
)
func main() {
cmd.Execute()
var cli struct {
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\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 (
"encoding/binary"
"fmt"
"io"
"os"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/types"
)
// Storage types.
@ -104,11 +105,11 @@ func (vbm VolumeBitMap) IsFree(block uint16) bool {
// readVolumeBitMap reads the entire volume bitmap from a block
// device.
func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, error) {
blocks := bd.Blocks() / 4096
func readVolumeBitMap(devicebytes []byte, startBlock uint16) (VolumeBitMap, error) {
blocks := uint16(len(devicebytes) / 512 / 4096)
vbm := NewVolumeBitMap(startBlock, blocks)
for i := 0; i < len(vbm); i++ {
if err := disk.UnmarshalBlock(bd, &vbm[i], vbm[i].GetBlock()); err != nil {
if err := disk.UnmarshalBlock(devicebytes, &vbm[i], vbm[i].GetBlock()); err != nil {
return nil, fmt.Errorf("cannot read block %d (device block %d) of Volume Bit Map: %v", i, vbm[i].GetBlock(), err)
}
}
@ -116,9 +117,9 @@ func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, err
}
// Write writes the Volume Bit Map to a block device.
func (vbm VolumeBitMap) Write(bd disk.BlockDevice) error {
func (vbm VolumeBitMap) Write(devicebytes []byte) error {
for i, bp := range vbm {
if err := disk.MarshalBlock(bd, bp); err != nil {
if err := disk.MarshalBlock(devicebytes, bp); err != nil {
return fmt.Errorf("cannot write block %d (device block %d) of Volume Bit Map: %v", i, bp.GetBlock(), err)
}
}
@ -351,14 +352,14 @@ type FileDescriptor struct {
HeaderPointer uint16 // Block number of the key block for the directory which describes this file.
}
// descriptor returns a disk.Descriptor for a FileDescriptor.
func (fd FileDescriptor) descriptor() disk.Descriptor {
desc := disk.Descriptor{
// descriptor returns a types.Descriptor for a FileDescriptor.
func (fd FileDescriptor) descriptor() types.Descriptor {
desc := types.Descriptor{
Name: fd.Name(),
Blocks: int(fd.BlocksUsed),
Length: int(fd.Eof[0]) + int(fd.Eof[1])<<8 + int(fd.Eof[2])<<16,
Locked: false, // TODO(zellyn): use prodos-style access in disk.Descriptor
Type: disk.Filetype(fd.FileType),
Locked: false, // TODO(zellyn): use prodos-style access in types.Descriptor
Type: types.Filetype(fd.FileType),
}
return desc
}
@ -611,11 +612,12 @@ func (sh SubdirectoryHeader) Name() string {
// Volume is the in-memory representation of a device's volume
// information.
type Volume struct {
keyBlock *VolumeDirectoryKeyBlock
blocks []*VolumeDirectoryBlock
bitmap *VolumeBitMap
subdirsByBlock map[uint16]*Subdirectory
subdirsByName map[string]*Subdirectory
keyBlock *VolumeDirectoryKeyBlock // The key block describing the entire volume
blocks []*VolumeDirectoryBlock // The blocks in the top-level volume
bitmap *VolumeBitMap // Bitmap of which blocks are free
subdirsByBlock map[uint16]*Subdirectory // A mapping of block number to subdirectory object
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
@ -652,46 +654,79 @@ func (v Volume) subdirDescriptors() []FileDescriptor {
// readVolume reads the entire volume and subdirectories from a device
// into memory.
func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) {
func readVolume(devicebytes []byte, keyBlock uint16, debug bool) (Volume, error) {
v := Volume{
keyBlock: &VolumeDirectoryKeyBlock{},
subdirsByBlock: make(map[uint16]*Subdirectory),
subdirsByName: make(map[string]*Subdirectory),
keyBlock: &VolumeDirectoryKeyBlock{},
subdirsByBlock: make(map[uint16]*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)
}
// 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
} else {
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 {
vdb := VolumeDirectoryBlock{}
if err := disk.UnmarshalBlock(bd, &vdb, block); err != nil {
if err := disk.UnmarshalBlock(devicebytes, &vdb, block); err != nil {
return v, err
}
v.blocks = append(v.blocks, &vdb)
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()
if debug {
fmt.Fprintf(os.Stderr, "got %d top-level subdir descriptors\n", len(sdds))
}
for i := 0; i < len(sdds); i++ {
sdd := sdds[i]
sub, err := readSubdirectory(bd, sdd)
sub, err := readSubdirectory(devicebytes, sdd)
if err != nil {
return v, err
}
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()...)
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 {
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 {
return v, err
}
@ -701,6 +736,9 @@ func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) {
v.subdirsByName[name] = sd
}
if debug {
fmt.Fprintf(os.Stderr, "HERE2\n")
}
return v, nil
}
@ -729,16 +767,23 @@ func (s Subdirectory) subdirDescriptors() []FileDescriptor {
return descs
}
// fullDirName returns the full recursive directory name of the given parent directory.
func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory) (string, error) {
if parentDirectoryBlock == keyBlock {
// parentDirName returns the full recursive directory name of the given parent directory.
func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory, firstSubdirBlockMap map[uint16]uint16) (string, error) {
if parentDirectoryBlock == keyBlock || firstSubdirBlockMap[parentDirectoryBlock] == keyBlock {
return "", nil
}
sd := subdirMap[parentDirectoryBlock]
if sd == nil {
parentFirstBlock, ok := firstSubdirBlockMap[parentDirectoryBlock]
if ok {
sd = subdirMap[parentFirstBlock]
}
}
if sd == nil {
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 {
return "", err
}
@ -751,18 +796,18 @@ func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[u
// readSubdirectory reads a single subdirectory from a device into
// memory.
func readSubdirectory(bd disk.BlockDevice, fd FileDescriptor) (Subdirectory, error) {
func readSubdirectory(devicebytes []byte, fd FileDescriptor) (Subdirectory, error) {
s := Subdirectory{
keyBlock: &SubdirectoryKeyBlock{},
}
if err := disk.UnmarshalBlock(bd, s.keyBlock, fd.KeyPointer); err != nil {
if err := disk.UnmarshalBlock(devicebytes, s.keyBlock, fd.KeyPointer); err != nil {
return s, fmt.Errorf("cannot read first block of subdirectory %q (block %d): %v", fd.Name(), fd.KeyPointer, err)
}
for block := s.keyBlock.Next; block != 0; block = s.blocks[len(s.blocks)-1].Next {
sdb := SubdirectoryBlock{}
if err := disk.UnmarshalBlock(bd, &sdb, block); err != nil {
if err := disk.UnmarshalBlock(devicebytes, &sdb, block); err != nil {
return s, err
}
s.blocks = append(s.blocks, &sdb)
@ -783,10 +828,11 @@ func copyBytes(dst, src []byte) int {
// operator is a disk.Operator - an interface for performing
// high-level operations on files and directories.
type operator struct {
dev disk.BlockDevice
data []byte
debug bool
}
var _ disk.Operator = operator{}
var _ types.Operator = operator{}
// operatorName is the keyword name for the operator that undestands
// prodos disks/devices.
@ -797,11 +843,6 @@ func (o operator) Name() string {
return operatorName
}
// Order returns the sector or block order of the underlying storage.
func (o operator) Order() string {
return o.dev.Order()
}
// HasSubdirs returns true if the underlying operating system on the
// disk allows subdirectories.
func (o operator) HasSubdirs() bool {
@ -810,14 +851,16 @@ func (o operator) HasSubdirs() bool {
// Catalog returns a catalog of disk entries. subdir should be empty
// for operating systems that do not support subdirectories.
func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
vol, err := readVolume(o.dev, 2)
func (o operator) Catalog(subdir string) ([]types.Descriptor, error) {
if o.debug {
fmt.Fprintf(os.Stderr, "Catalog of %q\n", subdir)
}
vol, err := readVolume(o.data, 2, o.debug)
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 == "" {
for _, desc := range vol.descriptors() {
@ -842,8 +885,8 @@ func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
}
// GetFile retrieves a file by name.
func (o operator) GetFile(filename string) (disk.FileInfo, error) {
return disk.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName)
func (o operator) GetFile(filename string) (types.FileInfo, error) {
return types.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName)
}
// Delete deletes a file by name. It returns true if the file was
@ -855,37 +898,46 @@ func (o operator) Delete(filename string) (bool, error) {
// PutFile writes a file by name. If the file exists and overwrite
// is false, it returns with an error. Otherwise it returns true if
// an existing file was overwritten.
func (o operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) {
func (o operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) {
return false, fmt.Errorf("%s doesn't implement PutFile yet", operatorName)
}
// Write writes the underlying device blocks to the given writer.
func (o operator) Write(w io.Writer) (int, error) {
return o.dev.Write(w)
// DiskOrder returns the Physical-to-Logical mapping order.
func (o operator) DiskOrder() types.DiskOrder {
return types.DiskOrderPO
}
// deviceOperatorFactory is the factory that returns prodos operators
// given device images.
func deviceOperatorFactory(bd disk.BlockDevice) (disk.Operator, error) {
op := operator{dev: bd}
_, err := op.Catalog("")
// GetBytes returns the disk image bytes, in logical order.
func (o operator) GetBytes() []byte {
return o.data
}
// 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 {
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
// given disk images.
func diskOperatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
bd, err := disk.BlockDeviceFromSectorDisk(sd)
if err != nil {
return nil, err
}
return deviceOperatorFactory(bd)
// Operator returns an Operator for the []byte disk image.
func (of OperatorFactory) Operator(devicebytes []byte, debug bool) (types.Operator, error) {
return operator{data: devicebytes, debug: debug}, nil
}
func init() {
disk.RegisterDeviceOperatorFactory(operatorName, deviceOperatorFactory)
disk.RegisterDiskOperatorFactory(operatorName, diskOperatorFactory)
// DiskOrder returns the Physical-to-Logical mapping order.
func (of OperatorFactory) DiskOrder() types.DiskOrder {
return operator{}.DiskOrder()
}

View File

@ -6,7 +6,7 @@ import (
"testing"
"github.com/kr/pretty"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/disk"
)
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 (
"encoding/binary"
"fmt"
"io"
"strconv"
"strings"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/lib/errors"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/errors"
"github.com/zellyn/diskii/types"
)
const (
@ -29,17 +29,17 @@ const (
type SectorMap []byte
// LoadSectorMap loads a NakedOS sector map.
func LoadSectorMap(sd disk.SectorDisk) (SectorMap, error) {
func LoadSectorMap(diskbytes []byte) (SectorMap, error) {
sm := SectorMap(make([]byte, 560))
sector09, err := sd.ReadPhysicalSector(0, 9)
sector09, err := disk.ReadSector(diskbytes, 0, 9)
if err != nil {
return sm, err
}
sector0A, err := sd.ReadPhysicalSector(0, 0xA)
sector0A, err := disk.ReadSector(diskbytes, 0, 0xA)
if err != nil {
return sm, err
}
sector0B, err := sd.ReadPhysicalSector(0, 0xB)
sector0B, err := disk.ReadSector(diskbytes, 0, 0xB)
if err != nil {
return sm, err
}
@ -63,19 +63,19 @@ func (sm SectorMap) FirstFreeFile() byte {
// Persist writes the current contenst of a sector map back back to
// disk.
func (sm SectorMap) Persist(sd disk.SectorDisk) error {
sector09, err := sd.ReadPhysicalSector(0, 9)
func (sm SectorMap) Persist(diskbytes []byte) error {
sector09, err := disk.ReadSector(diskbytes, 0, 9)
if err != nil {
return err
}
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
}
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
}
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 nil
@ -167,10 +167,10 @@ func (sm SectorMap) SectorsByFile() map[byte][]disk.TrackSector {
}
// 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
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 {
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
// 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
cts := make([]byte, 256*sectorsNeeded)
copy(cts, contents)
@ -209,10 +209,10 @@ func (sm SectorMap) WriteFile(sd disk.SectorDisk, file byte, contents []byte, ov
i := 0
OUTER:
for track := byte(0); track < sd.Tracks(); track++ {
for sector := byte(0); sector < sd.Sectors(); sector++ {
for track := byte(0); track < disk.FloppyTracks; track++ {
for sector := byte(0); sector < disk.FloppySectors; sector++ {
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
}
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, 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
for value&0x1f > 0 {
if value&0x1f < 27 {
result = result + string(value&0x1f+'@')
result = result + string(rune(value&0x1f+'@'))
value >>= 5
continue
}
if value&0x20 == 0 {
result = result + string((value&0x1f)-0x1b+'0')
result = result + string(rune((value&0x1f)-0x1b+'0'))
} else {
result = result + string((value&0x1f)-0x1b+'5')
result = result + string(rune((value&0x1f)-0x1b+'5'))
}
value >>= 6
}
@ -317,16 +317,16 @@ type SymbolTable []Symbol
// ReadSymbolTable reads the symbol table from a disk. If there are
// problems with the symbol table (like it doesn't exist, or the link
// 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)
symtbl1, err := sm.ReadFile(sd, 3)
symtbl1, err := sm.ReadFile(diskbytes, 3)
if err != nil {
return nil, err
}
if len(symtbl1) != 0x1000 {
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 {
return nil, err
}
@ -380,7 +380,7 @@ func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) {
}
// 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)
symtbl2 := make([]byte, 0x1000)
for i, sym := range st {
@ -400,10 +400,10 @@ func (sm SectorMap) WriteSymbolTable(sd disk.SectorDisk, st SymbolTable) error {
symtbl1[offset+4] = six[5]
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)
}
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 nil
@ -643,12 +643,13 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named
// Operator is a disk.Operator - an interface for performing
// high-level operations on files and directories.
type Operator struct {
SD disk.SectorDisk
SM SectorMap
ST SymbolTable
data []byte
SM SectorMap
ST SymbolTable
debug bool
}
var _ disk.Operator = Operator{}
var _ types.Operator = Operator{}
// operatorName is the keyword name for the operator that undestands
// NakedOS/Super-Mon disks.
@ -659,11 +660,6 @@ func (o Operator) Name() string {
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
// disk allows subdirectories.
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
// for operating systems that do not support subdirectories.
func (o Operator) Catalog(subdir string) ([]disk.Descriptor, error) {
var descs []disk.Descriptor
func (o Operator) Catalog(subdir string) ([]types.Descriptor, error) {
var descs []types.Descriptor
sectorsByFile := o.SM.SectorsByFile()
for file := byte(1); file < FileReserved; file++ {
l := len(sectorsByFile[file])
if l == 0 {
continue
}
descs = append(descs, disk.Descriptor{
descs = append(descs, types.Descriptor{
Name: NameForFile(file, o.ST),
Fullname: FullnameForFile(file, o.ST),
Sectors: l,
Length: l * 256,
Locked: false,
Type: disk.FiletypeBinary,
Type: types.FiletypeBinary,
})
}
return descs, nil
}
// GetFile retrieves a file by name.
func (o Operator) GetFile(filename string) (disk.FileInfo, error) {
func (o Operator) GetFile(filename string) (types.FileInfo, error) {
file, err := o.ST.FileForName(filename)
if err != nil {
return disk.FileInfo{}, err
return types.FileInfo{}, err
}
data, err := o.SM.ReadFile(o.SD, file)
data, err := o.SM.ReadFile(o.data, file)
if err != nil {
return disk.FileInfo{}, fmt.Errorf("error reading file DF%02x: %v", file, err)
return types.FileInfo{}, fmt.Errorf("error reading file DF%02x: %v", file, err)
}
if len(data) == 0 {
return disk.FileInfo{}, fmt.Errorf("file DF%02x not fount", file)
return types.FileInfo{}, fmt.Errorf("file DF%02x not fount", file)
}
desc := disk.Descriptor{
desc := types.Descriptor{
Name: NameForFile(file, o.ST),
Sectors: len(data) / 256,
Length: len(data),
Locked: false,
Type: disk.FiletypeBinary,
Type: types.FiletypeBinary,
}
fi := disk.FileInfo{
fi := types.FileInfo{
Descriptor: desc,
Data: data,
}
@ -731,13 +727,13 @@ func (o Operator) Delete(filename string) (bool, error) {
}
existed := len(o.SM.SectorsForFile(file)) > 0
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
}
if o.ST != nil {
changed := o.ST.DeleteSymbol(filename)
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
}
}
@ -748,9 +744,9 @@ func (o Operator) Delete(filename string) (bool, error) {
// PutFile writes a file by name. If the file exists and overwrite
// is false, it returns with an error. Otherwise it returns true if
// an existing file was overwritten.
func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) {
if fileInfo.Descriptor.Type != disk.FiletypeBinary {
return false, fmt.Errorf("%s: only binary file type supported", operatorName)
func (o Operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) {
if fileInfo.Descriptor.Type != types.FiletypeBinary {
return false, fmt.Errorf("%s: only binary file type supported; got %q", operatorName, fileInfo.Descriptor.Type)
}
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))
@ -774,7 +770,7 @@ func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool,
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 {
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 {
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, nil
}
// Write writes the underlying disk to the given writer.
func (o Operator) Write(w io.Writer) (int, error) {
return o.SD.Write(w)
// DiskOrder returns the Physical-to-Logical mapping order.
func (o Operator) DiskOrder() types.DiskOrder {
return types.DiskOrderRaw
}
// operatorFactory is the factory that returns supermon operators
// given disk images.
func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
sm, err := LoadSectorMap(sd)
// GetBytes returns the disk image bytes, in logical order.
func (o Operator) GetBytes() []byte {
return o.data
}
// OperatorFactory is a types.OperatorFactory for DOS 3.3 disks.
type OperatorFactory struct {
}
// Name returns the name of the operator.
func (of OperatorFactory) Name() string {
return operatorName
}
// SeemsToMatch returns true if the []byte disk image seems to match the
// system of this operator.
func (of OperatorFactory) SeemsToMatch(diskbytes []byte, debug bool) bool {
// For now, just return true if we can run Catalog successfully.
sm, err := LoadSectorMap(diskbytes)
if err != nil {
return false
}
if err := sm.Verify(); err != nil {
return false
}
return true
}
// Operator returns an Operator for the []byte disk image.
func (of OperatorFactory) Operator(diskbytes []byte, debug bool) (types.Operator, error) {
sm, err := LoadSectorMap(diskbytes)
if err != nil {
return nil, err
}
@ -805,9 +828,9 @@ func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
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 {
op.ST = st
}
@ -815,6 +838,7 @@ func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
return op, nil
}
func init() {
disk.RegisterDiskOperatorFactory(operatorName, operatorFactory)
// DiskOrder returns the Physical-to-Logical mapping order.
func (of OperatorFactory) DiskOrder() types.DiskOrder {
return Operator{}.DiskOrder()
}

View File

@ -3,12 +3,14 @@
package supermon
import (
"os"
"reflect"
"strings"
"testing"
"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"
@ -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
// filename. It returns the sector map and a sector disk.
func loadSectorMap(filename string) (SectorMap, disk.SectorDisk, error) {
sd, err := disk.LoadDSK(filename)
func loadSectorMap(filename string) (SectorMap, []byte, error) {
rawbytes, err := os.ReadFile(filename)
if err != nil {
return nil, nil, err
}
sm, err := LoadSectorMap(sd)
diskbytes, err := disk.Swizzle(rawbytes, disk.Dos33LogicalToPhysicalSectorMap)
if err != nil {
return nil, nil, err
}
return sm, sd, nil
sm, err := LoadSectorMap(diskbytes)
if err != nil {
return nil, nil, err
}
return sm, diskbytes, nil
}
// getOperator gets a types.Operator for the given NakedOS disk, assumed to be
// in "do" order.
func getOperator(filename string) (types.Operator, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
op, _, err := disk.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
@ -126,11 +146,7 @@ func TestReadSymbolTable(t *testing.T) {
// TestGetFile tests the retrieval of a file's contents, using the
// Operator interface.
func TestGetFile(t *testing.T) {
sd, err := disk.OpenDisk(testDisk)
if err != nil {
t.Fatal(err)
}
op, err := disk.OperatorForDisk(sd)
op, err := getOperator(testDisk)
if err != nil {
t.Fatal(err)
}
@ -213,20 +229,16 @@ func TestReadWriteSymbolTable(t *testing.T) {
// TestPutFile tests the creation of a file, using the Operator
// interface.
func TestPutFile(t *testing.T) {
sd, err := disk.OpenDisk(testDisk)
if err != nil {
t.Fatal(err)
}
op, err := disk.OperatorForDisk(sd)
op, err := getOperator(testDisk)
if err != nil {
t.Fatal(err)
}
contents := []byte(cities)
fileInfo := disk.FileInfo{
Descriptor: disk.Descriptor{
fileInfo := types.FileInfo{
Descriptor: types.Descriptor{
Name: "FNEWFILE",
Length: len(contents),
Type: disk.FiletypeBinary,
Type: types.FiletypeBinary,
},
Data: contents,
}

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

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