Compare commits

...

15 Commits

Author SHA1 Message Date
Zellyn Hunter 0b85248dde debug: made it a counter rather than boolean 2021-08-01 14:19:26 -04:00
Zellyn Hunter b264fe8e96 nits: missed a couple of comments keeping lint happy 2021-08-01 13:22:10 -04:00
Zellyn Hunter c2dd6362de more lints, removed woz 2021-07-31 23:49:22 -04:00
Zellyn Hunter 23c9b1edcf lint fixes 2021-07-31 23:14:55 -04:00
Zellyn Hunter 116f3781b5 todos and lint fixes; added hermit 2021-07-31 22:44:04 -04:00
Zellyn Hunter 7d036244af
Merge pull request #2 from zellyn/refactor
Refactor to use kong instead of cobra
2021-07-31 22:12:28 -04:00
Zellyn Hunter 09ee1c6262 major refactor
Major refactor to use kong instead of cobra. All functionality
that worked before should still be working now.

Added `reorder` command
2021-07-31 22:10:44 -04:00
Zellyn Hunter 80aa964915
Create codeql-analysis.yml 2021-07-23 09:08:34 -04:00
Zellyn Hunter bbf7d696db working on filetypes 2021-07-12 17:02:11 -04:00
Zellyn Hunter ef9115dcaf catalog working for all types, reorder added 2021-07-12 16:27:13 -04:00
Zellyn Hunter 9c66e2c5e6 add a couple disks 2021-07-10 21:09:57 -04:00
Zellyn Hunter f09e8f47f1
README.md: spell Octalyzer with a "t" 😂 2020-09-30 15:01:52 -04:00
Zellyn Hunter 21a4d76ff5 first pass woz parsing done 2018-06-07 23:30:25 -04:00
Zellyn Hunter 6d57f2de51 working on disk formats 2018-06-06 22:27:35 -04:00
Zellyn Hunter 510a7d2ac8 More links to tools 2017-05-03 21:25:25 -04:00
70 changed files with 1953 additions and 1810 deletions

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '37 2 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

17
.golangci.yml Normal file
View File

@ -0,0 +1,17 @@
issues:
exclude-use-default: false
linters:
enable:
- gocritic
- godot
- gofmt
- gosec
- ifshort
- misspell
- nakedret
- nilerr
- predeclared
- revive
- stylecheck
- unconvert
- wastedassign

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

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

111
README.md
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,30 +91,32 @@ 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.
- [x] Build per-platform binaries for Linux, MacOS, Windows.
- [x] Implement `GetFile` for DOS 3.3
- [x] Make `put` accept load address for appropriate filetypes.
- [x] Fix `golint` errors
- [ ] Implement `GetFile` for prodos
- [ ] Implement `PutFile` for prodos
- [ ] Implement `Delete` for Super-Mon
- [ ] Implement `Delete` for DOS 3.3
- [ ] Implement `Delete` for ProDOS
- [ ] 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 DOS 3.3
- [ ] Make 13-sector DOS disks work
- [ ] Read/write nybble formats
- [ ] Read/write gzipped files
- [ ] Add basic ProDOS structures
- [ ] Add ProDOS support
- [ ] Make `OperatorFactory.SeemsToMatch` more sophisticated for ProDOS
- [ ] Make `OperatorFactory.SeemsToMatch` more sophisticated for DOS 3.3
- [ ] Make `OperatorFactory.SeemsToMatch` more sophisticated for NakedOS
- [x] Build per-platform binaries for Linux, MacOS, Windows.
### 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
- http://applecommander.sourceforge.net/ - the commandline, cross-platform alternative to CiderPress
- http://brutaldeluxe.fr/products/crossdevtools/cadius/index.html - Brutal Deluxe's commandline tools
- https://github.com/paleotronic/dskalyzer - cross-platform disk analysis tool (also written in Go!) from the folks who brought you [Octalyzer](http://octalyzer.com/).
- https://github.com/cybernesto/dsktool.rb
- https://github.com/cmosher01/Apple-II-Disk-Tools
- https://github.com/madsen/perl-libA2
@ -103,3 +128,51 @@ will be likely to get priority.
- https://github.com/thecompu/Driv3rs - A Python Script to work with Apple III SOS DSK files
- http://www.callapple.org/software/an-a-p-p-l-e-review-shink-fit-x-for-mac-os-x
- https://github.com/dmolony/DiskBrowser - graphical (Java) disk browser that knows how to interpret and display many file formats
- 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

@ -169,7 +169,7 @@ func Decode(raw []byte, location uint16) (Listing, error) {
ofs += 2
for {
if ofs >= len(raw) {
return nil, fmt.Errorf("Ran out of input at location $%X in line %d", ofs+int(location), line.Num)
return nil, fmt.Errorf("ran out of input at location $%X in line %d", ofs+int(location), line.Num)
}
char := raw[ofs]
if char == 0 {

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

@ -186,6 +186,7 @@ func Decode(raw []byte) (Listing, error) {
return listing, nil
}
/*
const (
tokenREM = 0x5D
tokenUnaryPlus = 0x35
@ -193,6 +194,7 @@ const (
tokenQuoteStart = 0x28
tokenQuoteEnd = 0x29
)
*/
func isalnum(b byte) bool {
switch {
@ -226,7 +228,7 @@ func (l Line) String() string {
break
}
} else {
ch = ch - 0x80
ch -= 0x80
if !lastAN && ch >= '0' && ch <= '9' {
if len(l.Bytes) < i+3 {
buf.WriteByte('?')

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
@ -39,6 +39,7 @@ var helloListing = ` 10 REM THIS IS A COMMENT
// TestParse tests the full parsing and output of a basic program from
// bytes.
func TestParse(t *testing.T) {
t.Skip("ignoring for now")
listing, err := Decode(helloBinary)
if err != nil {
t.Fatal(err)
@ -46,6 +47,6 @@ func TestParse(t *testing.T) {
text := basic.ChevronControlCodes(listing.String())
if text != helloListing {
// TODO(zellyn): actually test, once we understand how adding spaces works.
// t.Fatalf("Wrong listing; want:\n%s\ngot:\n%s", helloListing, text)
t.Fatalf("Wrong listing; want:\n%s\ngot:\n%s", helloListing, text)
}
}

View File

@ -0,0 +1 @@
hermit

7
bin/README.hermit.md Normal file
View File

@ -0,0 +1,7 @@
# Hermit environment
This is a [Hermit](https://github.com/cashapp/hermit) bin directory.
The symlinks in this directory are managed by Hermit and will automatically
download and install Hermit itself as well as packages. These packages are
local to this environment.

19
bin/activate-hermit Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
# This file must be used with "source bin/activate-hermit" from bash or zsh.
# You cannot run it directly
if [ "${BASH_SOURCE-}" = "$0" ]; then
echo "You must source this script: \$ source $0" >&2
exit 33
fi
BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
if "${BIN_DIR}/hermit" noop > /dev/null; then
eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
hash -r 2>/dev/null
fi
echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
fi

1
bin/golangci-lint Symbolic link
View File

@ -0,0 +1 @@
.golangci-lint-1.41.1.pkg

26
bin/hermit Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
set -eo pipefail
if [ -z "${HERMIT_STATE_DIR}" ]; then
case "$(uname -s)" in
Darwin)
export HERMIT_STATE_DIR="${HOME}/Library/Caches/hermit"
;;
Linux)
export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/hermit"
;;
esac
fi
export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://d1abdrezunyhdp.cloudfront.net/square}"
HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
export HERMIT_CHANNEL
export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}
if [ ! -x "${HERMIT_EXE}" ]; then
echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
curl -fsSL "${HERMIT_DIST_URL}/install.sh" | /bin/bash 1>&2
fi
exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"

0
bin/hermit.hcl Normal file
View File

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,90 +3,48 @@
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.`,
// ApplesoftCmd is the kong `applesoft` command.
type ApplesoftCmd struct {
Decode DecodeCmd `kong:"cmd,help='Convert a binary Applesoft program to a text LISTing.'"`
}
func init() {
RootCmd.AddCommand(applesoftCmd)
// DecodeCmd is the kong `decode` command.
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)
}
},
// Help displays extended help and examples.
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])
// Run the `decode` command.
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 {
os.Stdout.WriteString(listing.String())
if d.Raw {
_, _ = os.Stdout.WriteString(listing.String())
} else {
os.Stdout.WriteString(basic.ChevronControlCodes(listing.String()))
_, _ = os.Stdout.WriteString(basic.ChevronControlCodes(listing.String()))
}
return nil
}

View File

@ -6,54 +6,50 @@ 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
// LsCmd is the kong `ls` command.
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,prodos,nakedos',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)")
// Help displays extended help and examples.
func (l LsCmd) Help() string {
return `Examples:
# Simple ls of a disk image
diskii ls games.dsk
# Get really explicit about disk order and system
diskii ls --order do --system nakedos Super-Mon-2.0.dsk`
}
// 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])
// Run the `ls` command.
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())
}
subdir := ""
if len(args) == 2 {
if globals.Debug > 0 {
fmt.Fprintf(os.Stderr, "Opened disk with order %q, system %q\n", order, op.Name())
}
if l.Directory != "" {
if !op.HasSubdirs() {
return fmt.Errorf("Disks of type %q cannot have subdirectories", op.Name())
return fmt.Errorf("disks of type %q cannot have subdirectories", op.Name())
}
subdir = args[1]
}
fds, err := op.Catalog(subdir)
fds, err := op.Catalog(l.Directory)
if err != nil {
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,41 @@ 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
// DeleteCmd is the kong `delete` command.
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")
// Help displays extended help and examples.
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])
// Run the `delete` command.
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)
}

2
cmd/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package cmd contains the actual command implementations.
package cmd

View File

@ -3,48 +3,42 @@
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.
// DumpCmd is the kong `dump` command.
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)
// Help displays extended help and examples.
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])
// Run the `dump` command.
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,24 @@ 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)
}
},
// FiletypesCmd is the kong `filetypes` command.
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)
// Run the `filetypes` command.
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,80 @@ 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.
// NakedOSCmd is the kong `nakedos` sub-command.
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/`
}
// MkHelloCmd is the kong `mkhello` command.
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”'"`
}
// Help displays extended help and examples.
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>")
// Run the `mkhello` command.
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 +87,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,66 @@ 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
// PutCmd is the kong `put` command.
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?'"`
Address uint16 `kong:"type='anybaseuint16',default='0x6000',help='For filetypes where it is appropriate, address to load the code at.'"`
// 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)
// Help displays extended help and examples.
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`
}
// Run the `put` command.
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,
Data: contents,
StartAddress: p.Address,
}
_, 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)
}

95
cmd/reorder.go Normal file
View File

@ -0,0 +1,95 @@
package cmd
import (
"fmt"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/helpers"
"github.com/zellyn/diskii/types"
)
// ReorderCmd is the kong `reorder` command.
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.'"`
}
// Run the `reorder` command.
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())
}
}

112
cmd/sd.go
View File

@ -4,68 +4,65 @@ 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
// SDCmd is the kong `mksd` command.
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”'"`
}
// Help displays extended help and examples.
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>")
// Run the `mksd` command.
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 +73,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 +111,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.

BIN
data/disks/dos33master.woz Normal file

Binary file not shown.

BIN
data/disks/lode-runner-disk-1.dsk Executable file

Binary file not shown.

BIN
data/disks/lode-runner-disk-1.po Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

70
disk/disk.go Normal file
View File

@ -0,0 +1,70 @@
// 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
}
// Block is a ProDOS block: 512 bytes.
type Block [512]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
}

230
disk/open.go Normal file
View File

@ -0,0 +1,230 @@
package disk
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"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 int) (types.Operator, types.DiskOrder, error) {
if filename == "-" {
return OpenFile(os.Stdin, order, system, operatorFactories, debug)
}
file, err := os.Open(filepath.Clean(filename))
if err != nil {
return nil, "", err
}
return OpenFile(file, order, system, operatorFactories, debug)
}
// OpenFile attempts to open a disk or device image, using the provided ordering and system type.
// OpenFile will close the file.
func OpenFile(file *os.File, order types.DiskOrder, system string, operatorFactories []types.OperatorFactory, debug int) (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 int) (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 int) (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 int) (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 > 1 {
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 > 1 {
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 > 1 {
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
}
// 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 (
@ -32,7 +32,7 @@ func (ds DiskSector) GetTrack() byte {
}
// SetTrack sets the track that a DiskSector was loaded from.
func (ds DiskSector) SetTrack(track byte) {
func (ds *DiskSector) SetTrack(track byte) {
ds.Track = track
}
@ -42,7 +42,7 @@ func (ds DiskSector) GetSector() byte {
}
// SetSector sets the sector that a DiskSector was loaded from.
func (ds DiskSector) SetSector(sector byte) {
func (ds *DiskSector) SetSector(sector byte) {
ds.Sector = sector
}
@ -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
@ -397,11 +397,11 @@ func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
for nextTrack != 0 || nextSector != 0 {
ts := disk.TrackSector{Track: nextTrack, Sector: nextSector}
if seen[ts] {
return nil, fmt.Errorf("File %q tries to read TrackSector track=%d sector=%d twice", fd.FilenameString(), nextTrack, nextSector)
return nil, fmt.Errorf("file %q tries to read TrackSector track=%d sector=%d twice", fd.FilenameString(), nextTrack, nextSector)
}
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,14 +490,14 @@ 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 int) ([]CatalogSector, error) {
v := &VTOC{}
err := disk.UnmarshalLogicalSector(d, v, VTOCTrack, VTOCSector)
err := disk.UnmarshalLogicalSector(diskbytes, v, VTOCTrack, VTOCSector)
if err != nil {
return nil, err
}
if err := v.Validate(); err != nil {
return nil, fmt.Errorf("Invalid VTOC sector: %v", err)
return nil, fmt.Errorf("invalid VTOC sector: %v", err)
}
nextTrack := v.CatalogTrack
@ -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 int) (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 int
}
var _ disk.Operator = operator{}
var _ types.Operator = operator{}
// operatorName is the keyword name for the operator that undestands
// dos3 disks.
@ -574,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())
}
@ -589,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
}
@ -598,22 +599,22 @@ func (o operator) fileForFilename(filename string) (FileDesc, error) {
return fd, nil
}
}
return FileDesc{}, fmt.Errorf("Filename %q not found", filename)
return FileDesc{}, fmt.Errorf("filename %q not found", filename)
}
// 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,
}
@ -654,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
@ -666,29 +667,43 @@ 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 int) bool {
// For now, just return true if we can run Catalog successfully.
_, _, err := ReadCatalog(diskbytes, debug)
return err == nil
}
// Operator returns an Operator for the []byte disk image.
func (of OperatorFactory) Operator(diskbytes []byte, debug int) (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,20 +2,22 @@ package dos3
import (
"crypto/rand"
"os"
"reflect"
"testing"
"github.com/zellyn/diskii/lib/disk"
)
// TestVTOCMarshalRoundtrip checks a simple roundtrip of VTOC data.
func TestVTOCMarshalRoundtrip(t *testing.T) {
buf := make([]byte, 256)
rand.Read(buf)
_, _ = rand.Read(buf)
buf1 := make([]byte, 256)
copy(buf1, buf)
vtoc1 := &VTOC{}
vtoc1.FromSector(buf1)
err := vtoc1.FromSector(buf1)
if err != nil {
t.Fatal(err)
}
buf2, err := vtoc1.ToSector()
if err != nil {
t.Fatal(err)
@ -24,7 +26,10 @@ func TestVTOCMarshalRoundtrip(t *testing.T) {
t.Errorf("Buffers differ: %v != %v", buf, buf2)
}
vtoc2 := &VTOC{}
vtoc2.FromSector(buf2)
err = vtoc2.FromSector(buf2)
if err != nil {
t.Fatal(err)
}
if *vtoc1 != *vtoc2 {
t.Errorf("Structs differ: %v != %v", vtoc1, vtoc2)
}
@ -33,11 +38,14 @@ func TestVTOCMarshalRoundtrip(t *testing.T) {
// TestCatalogSectorMarshalRoundtrip checks a simple roundtrip of CatalogSector data.
func TestCatalogSectorMarshalRoundtrip(t *testing.T) {
buf := make([]byte, 256)
rand.Read(buf)
_, _ = rand.Read(buf)
buf1 := make([]byte, 256)
copy(buf1, buf)
cs1 := &CatalogSector{}
cs1.FromSector(buf1)
err := cs1.FromSector(buf1)
if err != nil {
t.Fatal(err)
}
buf2, err := cs1.ToSector()
if err != nil {
t.Fatal(err)
@ -46,7 +54,10 @@ func TestCatalogSectorMarshalRoundtrip(t *testing.T) {
t.Errorf("Buffers differ: %v != %v", buf, buf2)
}
cs2 := &CatalogSector{}
cs2.FromSector(buf2)
err = cs2.FromSector(buf2)
if err != nil {
t.Fatal(err)
}
if *cs1 != *cs2 {
t.Errorf("Structs differ: %v != %v", cs1, cs2)
}
@ -55,11 +66,14 @@ func TestCatalogSectorMarshalRoundtrip(t *testing.T) {
// TestTrackSectorListMarshalRoundtrip checks a simple roundtrip of TrackSectorList data.
func TestTrackSectorListMarshalRoundtrip(t *testing.T) {
buf := make([]byte, 256)
rand.Read(buf)
_, _ = rand.Read(buf)
buf1 := make([]byte, 256)
copy(buf1, buf)
cs1 := &TrackSectorList{}
cs1.FromSector(buf1)
err := cs1.FromSector(buf1)
if err != nil {
t.Fatal(err)
}
buf2, err := cs1.ToSector()
if err != nil {
t.Fatal(err)
@ -68,7 +82,10 @@ func TestTrackSectorListMarshalRoundtrip(t *testing.T) {
t.Errorf("Buffers differ: %v != %v", buf, buf2)
}
cs2 := &TrackSectorList{}
cs2.FromSector(buf2)
err = cs2.FromSector(buf2)
if err != nil {
t.Fatal(err)
}
if *cs1 != *cs2 {
t.Errorf("Structs differ: %v != %v", cs1, cs2)
}
@ -76,15 +93,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, 0)
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=

36
helpers/helpers.go Normal file
View File

@ -0,0 +1,36 @@
// 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)
}
// WriteOutput writes a byte slice to the given filename, using `-` for standard out.
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, 0600)
}

View File

@ -1,67 +0,0 @@
// Copyright © 2017 Zellyn Hunter <zellyn@gmail.com>
// dev.go contains logic for reading ".po" disk images.
package disk
import (
"fmt"
"io"
"io/ioutil"
)
// 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)
if err != nil {
return Dev{}, err
}
if len(bb)%512 != 0 {
return Dev{}, fmt.Errorf("expected file %q to contain a multiple of 512 bytes, but got %d", filename, len(bb))
}
return Dev{
data: bb,
blocks: uint16(len(bb) / 512),
}, nil
}
// Empty creates a .po image that is all zeros.
func EmptyDev(blocks uint16) Dev {
return Dev{
data: make([]byte, 512*int(blocks)),
blocks: blocks,
}
}
// ReadBlock reads a single block from the device. It always returns
// 512 byes.
func (d Dev) ReadBlock(index uint16) (Block, error) {
var b Block
copy(b[:], d.data[int(index)*512:int(index+1)*512])
return b, nil
}
// WriteBlock writes a single block to a device. It expects exactly
// 512 bytes.
func (d Dev) WriteBlock(index uint16, data Block) error {
copy(d.data[int(index)*512:int(index+1)*512], data[:])
return nil
}
// Blocks returns the number of blocks in the device.
func (d Dev) Blocks() uint16 {
return d.blocks
}
// Write writes the device contents to the given file.
func (d Dev) Write(w io.Writer) (n int, err error) {
return w.Write(d.data)
}

View File

@ -1,290 +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)
}
// 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)
}
// 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)
}
// 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
}
// 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,103 +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
}
var _ SectorDisk = (*DSK)(nil)
// LoadDSK loads a .dsk image from a file.
func LoadDSK(filename string) (DSK, error) {
bb, err := ioutil.ReadFile(filename)
if err != nil {
return DSK{}, err
}
// TODO(zellyn): handle 13-sector disks.
if len(bb) != DOS33DiskBytes {
return DSK{}, fmt.Errorf("expected file %q to contain %d bytes, but got %d", filename, DOS33DiskBytes, len(bb))
}
return DSK{
data: bb,
sectors: 16,
physicalToStored: Dos33PhysicalToLogicalSectorMap,
bytesPerTrack: 16 * 256,
tracks: DOS33Tracks,
}, nil
}
// 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,
}
}
// 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
}
// 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,115 +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)
}
// 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,132 +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
// 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)
}

91
main.go
View File

@ -1,16 +1,91 @@
// 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 int `kong:"short='v',type='counter',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() {
if err := run(); 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
}

7
next Executable file
View File

@ -0,0 +1,7 @@
#!/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
go run . dump ./data/disks/ProDOS_2_4_2.po VIEW.README

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.
@ -59,8 +60,10 @@ func (bp bitmapPart) ToBlock() (disk.Block, error) {
return bp.data, nil
}
// VolumeBitMap represents a volume bitmap.
type VolumeBitMap []bitmapPart
// NewVolumeBitMap returns a volume bitmap of the given size.
func NewVolumeBitMap(startBlock uint16, blocks uint16) VolumeBitMap {
vbm := VolumeBitMap(make([]bitmapPart, (blocks+(512*8)-1)/(512*8)))
for i := range vbm {
@ -72,10 +75,12 @@ func NewVolumeBitMap(startBlock uint16, blocks uint16) VolumeBitMap {
return vbm
}
// MarkUsed marks the given block as used.
func (vbm VolumeBitMap) MarkUsed(block uint16) {
vbm.mark(block, false)
}
// MarkUnused marks the given block as free.
func (vbm VolumeBitMap) MarkUnused(block uint16) {
vbm.mark(block, true)
}
@ -104,21 +109,21 @@ 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)
}
}
return VolumeBitMap(vbm), nil
return vbm, nil
}
// 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)
}
}
@ -147,7 +152,7 @@ func (dt *DateTime) fromBytes(b []byte) {
dt.HM[1] = b[3]
}
// Validate checks a DateTime for problems, returning a slice of errors
// Validate checks a DateTime for problems, returning a slice of errors.
func (dt DateTime) Validate(fieldDescription string) (errors []error) {
if dt.HM[0] >= 24 {
errors = append(errors, fmt.Errorf("%s expects hour<24; got %d", fieldDescription, dt.HM[0]))
@ -208,7 +213,7 @@ func (vdkb VolumeDirectoryKeyBlock) Validate() (errors []error) {
errors = append(errors, desc.Validate()...)
}
if vdkb.Extra != 0 {
errors = append(errors, fmt.Errorf("Expected last byte of Volume Directory Key Block == 0x0; got 0x%02x", vdkb.Extra))
errors = append(errors, fmt.Errorf("expected last byte of Volume Directory Key Block == 0x0; got 0x%02x", vdkb.Extra))
}
return errors
}
@ -255,11 +260,12 @@ func (vdb VolumeDirectoryBlock) Validate() (errors []error) {
errors = append(errors, desc.Validate()...)
}
if vdb.Extra != 0 {
errors = append(errors, fmt.Errorf("Expected last byte of Volume Directory Block == 0x0; got 0x%02x", vdb.Extra))
errors = append(errors, fmt.Errorf("expected last byte of Volume Directory Block == 0x0; got 0x%02x", vdb.Extra))
}
return errors
}
// VolumeDirectoryHeader represents a volume directory header.
type VolumeDirectoryHeader struct {
TypeAndNameLength byte // Storage type (top four bits) and volume name length (lower four).
VolumeName [15]byte // Volume name (actual length defined in TypeAndNameLength)
@ -318,14 +324,20 @@ func (vdh VolumeDirectoryHeader) Validate() (errors []error) {
return errors
}
// Access represents a level of file access.
type Access byte
const (
AccessReadable Access = 0x01
AccessWritable Access = 0x02
// AccessReadable denotes a file as readable.
AccessReadable Access = 0x01
// AccessWritable denotes a file as writable.
AccessWritable Access = 0x02
// AccessChangedSinceBackup is (I think) always true on real disks.
AccessChangedSinceBackup Access = 0x20
AccessRenamable Access = 0x40
AccessDestroyable Access = 0x80
// AccessRenamable denotes a file as renamable.
AccessRenamable Access = 0x40
// AccessDestroyable denotes a file as deletable.
AccessDestroyable Access = 0x80
)
// FileDescriptor is the entry in the volume directory for a file or
@ -336,7 +348,7 @@ type FileDescriptor struct {
FileType byte // ProDOS / SOS filetype
KeyPointer uint16 // block number of key block for file
BlocksUsed uint16 // Total number of blocks used including index blocks and data blocks. For a subdirectory, the number of directory blocks
Eof [3]byte // 3-byte offset of EOF from first byte. For sequential files, just the length
EOF [3]byte // 3-byte offset of EOF from first byte. For sequential files, just the length
Creation DateTime // Date and time of of file creation
Version byte
MinVersion byte
@ -351,14 +363,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),
Length: int(fd.EOF[0]) + int(fd.EOF[1])<<8 + int(fd.EOF[2])<<16,
Locked: false, // TODO(zellyn): use prodos-style access in types.Descriptor
Type: types.Filetype(fd.FileType),
}
return desc
}
@ -381,7 +393,7 @@ func (fd FileDescriptor) toBytes() []byte {
buf[0x10] = fd.FileType
binary.LittleEndian.PutUint16(buf[0x11:0x13], fd.KeyPointer)
binary.LittleEndian.PutUint16(buf[0x13:0x15], fd.BlocksUsed)
copyBytes(buf[0x15:0x18], fd.Eof[:])
copyBytes(buf[0x15:0x18], fd.EOF[:])
copyBytes(buf[0x18:0x1c], fd.Creation.toBytes())
buf[0x1c] = fd.Version
buf[0x1d] = fd.MinVersion
@ -402,7 +414,7 @@ func (fd *FileDescriptor) fromBytes(buf []byte) {
fd.FileType = buf[0x10]
fd.KeyPointer = binary.LittleEndian.Uint16(buf[0x11:0x13])
fd.BlocksUsed = binary.LittleEndian.Uint16(buf[0x13:0x15])
copyBytes(fd.Eof[:], buf[0x15:0x18])
copyBytes(fd.EOF[:], buf[0x15:0x18])
fd.Creation.fromBytes(buf[0x18:0x1c])
fd.Version = buf[0x1c]
fd.MinVersion = buf[0x1d]
@ -419,7 +431,7 @@ func (fd FileDescriptor) Validate() (errors []error) {
return errors
}
// An index block contains 256 16-bit block numbers, pointing to other
// IndexBlock is an index block, containing 256 16-bit block numbers, pointing to other
// blocks. The LSBs are stored in the first half, MSBs in the second.
type IndexBlock disk.Block
@ -484,7 +496,7 @@ func (skb SubdirectoryKeyBlock) Validate() (errors []error) {
errors = append(errors, desc.Validate()...)
}
if skb.Extra != 0 {
errors = append(errors, fmt.Errorf("Expected last byte of Subdirectory Key Block == 0x0; got 0x%02x", skb.Extra))
errors = append(errors, fmt.Errorf("expected last byte of Subdirectory Key Block == 0x0; got 0x%02x", skb.Extra))
}
return errors
}
@ -531,11 +543,12 @@ func (sb SubdirectoryBlock) Validate() (errors []error) {
errors = append(errors, desc.Validate()...)
}
if sb.Extra != 0 {
errors = append(errors, fmt.Errorf("Expected last byte of Subdirectory Block == 0x0; got 0x%02x", sb.Extra))
errors = append(errors, fmt.Errorf("expected last byte of Subdirectory Block == 0x0; got 0x%02x", sb.Extra))
}
return errors
}
// SubdirectoryHeader represents a subdirectory header.
type SubdirectoryHeader struct {
TypeAndNameLength byte // Storage type (top four bits) and subdirectory name length (lower four).
SubdirectoryName [15]byte // Subdirectory name (actual length defined in TypeAndNameLength)
@ -597,7 +610,7 @@ func (sh *SubdirectoryHeader) fromBytes(buf []byte) {
// Validate validates a SubdirectoryHeader for valid values.
func (sh SubdirectoryHeader) Validate() (errors []error) {
if sh.SeventyFive != 0x75 {
errors = append(errors, fmt.Errorf("Byte after subdirectory name %q should be 0x75; got 0x%02x", sh.Name(), sh.SeventyFive))
errors = append(errors, fmt.Errorf("byte after subdirectory name %q should be 0x75; got 0x%02x", sh.Name(), sh.SeventyFive))
}
errors = append(errors, sh.Creation.Validate(fmt.Sprintf("subdirectory %q header creation date/time", sh.Name()))...)
return errors
@ -611,11 +624,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 +666,80 @@ 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 int) (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 {
vbm, err := readVolumeBitMap(devicebytes, v.keyBlock.Header.BitMapPointer)
if err != nil {
return v, err
} else {
v.bitmap = &vbm
}
v.bitmap = &vbm
// if debug {
// fmt.Fprintf(os.Stderr, "volume bitmap: %#v\n", v.bitmap)
// }
for block := v.keyBlock.Next; block != 0; block = v.blocks[len(v.blocks)-1].Next {
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 > 1 {
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 > 1 {
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 > 1 {
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 > 1 {
fmt.Fprintf(os.Stderr, " firstSubdirBlocks[%d] → %d\n", block.block, sdd.KeyPointer)
}
}
}
if debug > 1 {
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 > 1 {
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 +749,12 @@ func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) {
v.subdirsByName[name] = sd
}
if debug > 1 {
fmt.Fprintf(os.Stderr, "subdirsByName:\n")
for k := range v.subdirsByName {
fmt.Fprintf(os.Stderr, " %s\n", k)
}
}
return v, nil
}
@ -729,16 +783,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 {
return "", fmt.Errorf("Unable to find subdirectory for block %d", parentDirectoryBlock)
parentFirstBlock, ok := firstSubdirBlockMap[parentDirectoryBlock]
if ok {
sd = subdirMap[parentFirstBlock]
}
}
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap)
if sd == nil {
return "", fmt.Errorf("unable to find subdirectory for block %d", parentDirectoryBlock)
}
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap, firstSubdirBlockMap)
if err != nil {
return "", err
}
@ -751,18 +812,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 +844,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 int
}
var _ disk.Operator = operator{}
var _ types.Operator = operator{}
// operatorName is the keyword name for the operator that undestands
// prodos disks/devices.
@ -805,14 +867,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 > 1 {
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() {
@ -837,8 +901,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
@ -850,37 +914,43 @@ 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("")
if err != nil {
return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err)
}
return op, nil
// GetBytes returns the disk image bytes, in logical order.
func (o operator) GetBytes() []byte {
return o.data
}
// 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)
// OperatorFactory is a types.OperatorFactory for ProDos disks.
type OperatorFactory struct {
}
func init() {
disk.RegisterDeviceOperatorFactory(operatorName, deviceOperatorFactory)
disk.RegisterDiskOperatorFactory(operatorName, diskOperatorFactory)
// 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 int) bool {
// For now, just return true if we can run Catalog successfully.
_, err := readVolume(devicebytes, 2, debug)
return err == nil
}
// Operator returns an Operator for the []byte disk image.
func (of OperatorFactory) Operator(devicebytes []byte, debug int) (types.Operator, error) {
return operator{data: devicebytes, debug: debug}, nil
}
// DiskOrder returns the Physical-to-Logical mapping order.
func (of OperatorFactory) DiskOrder() types.DiskOrder {
return operator{}.DiskOrder()
}

View File

@ -6,12 +6,12 @@ import (
"testing"
"github.com/kr/pretty"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/disk"
)
func randomBlock() disk.Block {
var b1 disk.Block
rand.Read(b1[:])
_, _ = rand.Read(b1[:])
return b1
}
@ -19,7 +19,10 @@ func randomBlock() disk.Block {
func TestVolumeDirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
vdkb := &VolumeDirectoryKeyBlock{}
vdkb.FromBlock(b1)
err := vdkb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := vdkb.ToBlock()
if err != nil {
t.Fatal(err)
@ -28,7 +31,10 @@ func TestVolumeDirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
vdkb2 := &VolumeDirectoryKeyBlock{}
vdkb2.FromBlock(b2)
err = vdkb2.FromBlock(b2)
if err != nil {
t.Fatal(err)
}
if *vdkb != *vdkb2 {
t.Errorf("Structs differ: %v != %v", vdkb, vdkb2)
}
@ -38,7 +44,10 @@ func TestVolumeDirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
func TestVolumeDirectoryBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
vdb := &VolumeDirectoryBlock{}
vdb.FromBlock(b1)
err := vdb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := vdb.ToBlock()
if err != nil {
t.Fatal(err)
@ -47,7 +56,10 @@ func TestVolumeDirectoryBlockMarshalRoundtrip(t *testing.T) {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
vdb2 := &VolumeDirectoryBlock{}
vdb2.FromBlock(b2)
err = vdb2.FromBlock(b2)
if err != nil {
t.Fatal(err)
}
if *vdb != *vdb2 {
t.Errorf("Structs differ: %v != %v", vdb, vdb2)
}
@ -57,7 +69,10 @@ func TestVolumeDirectoryBlockMarshalRoundtrip(t *testing.T) {
func TestSubdirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
skb := &SubdirectoryKeyBlock{}
skb.FromBlock(b1)
err := skb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := skb.ToBlock()
if err != nil {
t.Fatal(err)
@ -66,7 +81,10 @@ func TestSubdirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
skb2 := &SubdirectoryKeyBlock{}
skb2.FromBlock(b2)
err = skb2.FromBlock(b2)
if err != nil {
t.Fatal(err)
}
if *skb != *skb2 {
t.Errorf("Structs differ: %v != %v", skb, skb2)
}
@ -76,7 +94,10 @@ func TestSubdirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
func TestSubdirectoryBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
sb := &SubdirectoryBlock{}
sb.FromBlock(b1)
err := sb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := sb.ToBlock()
if err != nil {
t.Fatal(err)
@ -85,7 +106,10 @@ func TestSubdirectoryBlockMarshalRoundtrip(t *testing.T) {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
sb2 := &SubdirectoryBlock{}
sb2.FromBlock(b2)
err = sb2.FromBlock(b2)
if err != nil {
t.Fatal(err)
}
if *sb != *sb2 {
t.Errorf("Structs differ: %v != %v", sb, sb2)
}

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
}
@ -61,24 +61,21 @@ func (sm SectorMap) FirstFreeFile() byte {
return 0
}
// Persist writes the current contenst of a sector map back back to
// Persist writes the current contents 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 {
return err
}
return nil
return disk.WriteSector(diskbytes, 0, 0xB, sm[0x130:0x230])
}
// FreeSectors returns the number of blocks free in a sector map.
@ -96,7 +93,7 @@ func (sm SectorMap) FreeSectors() int {
func (sm SectorMap) Verify() error {
for sector := byte(0); sector <= 0xB; sector++ {
if file := sm.FileForSector(0, sector); file != FileReserved {
return fmt.Errorf("Expected track 0, sectors 0-C to be reserved (0xFE), but got 0x%02X in sector %X", file, sector)
return fmt.Errorf("expected track 0, sectors 0-C to be reserved (0xFE), but got 0x%02X in sector %X", file, sector)
}
}
@ -104,7 +101,7 @@ func (sm SectorMap) Verify() error {
for sector := byte(0); sector < 16; sector++ {
file := sm.FileForSector(track, sector)
if file == FileIllegal {
return fmt.Errorf("Found illegal sector map value (%02X), in track %X sector %X", FileIllegal, track, sector)
return fmt.Errorf("found illegal sector map value (%02X), in track %X sector %X", FileIllegal, track, sector)
}
}
}
@ -167,10 +164,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 +187,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 +206,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 +222,7 @@ OUTER:
}
}
}
if err := sm.Persist(sd); err != nil {
if err := sm.Persist(diskbytes); err != nil {
return existed, err
}
return existed, nil
@ -254,14 +251,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 += string(rune(value&0x1f + '@'))
value >>= 5
continue
}
if value&0x20 == 0 {
result = result + string((value&0x1f)-0x1b+'0')
result += string(rune((value & 0x1f) - 0x1b + '0'))
} else {
result = result + string((value&0x1f)-0x1b+'5')
result += string(rune((value & 0x1f) - 0x1b + '5'))
}
value >>= 6
}
@ -317,16 +314,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
}
@ -345,10 +342,10 @@ func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) {
link := -1
if linkAddr != 0 {
if linkAddr < 0xD000 || linkAddr >= 0xDFFF {
return nil, fmt.Errorf("Expected symbol table link address between 0xD000 and 0xDFFE; got 0x%04X", linkAddr)
return nil, fmt.Errorf("expected symbol table link address between 0xD000 and 0xDFFE; got 0x%04X", linkAddr)
}
if (linkAddr-0xD000)%5 != 0 {
return nil, fmt.Errorf("Expected symbol table link address to 0xD000+5x; got 0x%04X", linkAddr)
return nil, fmt.Errorf("expected symbol table link address to 0xD000+5x; got 0x%04X", linkAddr)
}
link = (int(linkAddr) - 0xD000) / 5
}
@ -380,7 +377,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 +397,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
@ -583,6 +580,7 @@ func (st SymbolTable) ParseCompoundSymbol(name string) (address uint16, symAddre
}
// If it's a valid symbol name, assume that's what it is.
if _, err := encodeSymbol(name); err != nil {
//nolint:nilerr
return 0, 0, name, nil
}
return 0, 0, "", fmt.Errorf("%q is not a valid symbol name or address", name)
@ -622,6 +620,7 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named
}
file, err := st.FileForName(filename)
if err != nil {
//nolint:nilerr
return 0, 0, filename, nil
}
return file, file, filename, nil
@ -635,6 +634,7 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named
}
namedFile, err = st.FileForName(parts[1])
if err != nil {
//nolint:nilerr
return numFile, 0, parts[1], nil
}
return numFile, namedFile, parts[1], 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 int
}
var _ disk.Operator = Operator{}
var _ types.Operator = Operator{}
// operatorName is the keyword name for the operator that undestands
// NakedOS/Super-Mon disks.
@ -667,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,
}
@ -726,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
}
}
@ -743,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))
@ -769,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
}
@ -777,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 int) 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 int) (types.Operator, error) {
sm, err := LoadSectorMap(diskbytes)
if err != nil {
return nil, err
}
@ -800,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
}
@ -810,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,19 +3,21 @@
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"
const cities = `It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way - in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.`
// The extra newline pads us to 256 bytes
// The extra newline pads us to 256 bytes.
const hamlet = `To be, or not to be, that is the question:
Whether 'tis Nobler in the mind to suffer
The Slings and Arrows of outrageous Fortune,
@ -27,16 +29,20 @@ 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
}
// TestReadSectorMap tests the reading of the sector map of a test
@ -126,11 +132,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 := disk.OpenFilename(testDisk, types.DiskOrderAuto, "nakedos", []types.OperatorFactory{OperatorFactory{}}, 0)
if err != nil {
t.Fatal(err)
}
@ -138,9 +140,7 @@ func TestGetFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
got := string(file.Data)
want := hamlet
if got != want {
if want, got := hamlet, string(file.Data); got != want {
t.Errorf("Incorrect result for GetFile(\"TOBE\"): want %q; got %q", want, got)
}
}
@ -213,20 +213,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 := disk.OpenFilename(testDisk, types.DiskOrderAuto, "nakedos", []types.OperatorFactory{OperatorFactory{}}, 0)
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,
}
@ -243,8 +239,7 @@ func TestPutFile(t *testing.T) {
t.Fatal(err)
}
last := fds[len(fds)-1]
want := "DF0B:FNEWFILE"
if got := last.Fullname; got != want {
if want, got := "DF0B:FNEWFILE", last.Fullname; got != want {
t.Fatalf("Want last file on disk's FullName=%q; got %q", want, got)
}
}

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"
@ -55,64 +55,65 @@ const (
// | 12-18 | SOS | SOS reserved for future use
// | 1C-BF | SOS | SOS reserved for future use
// | C0-EE | ProDOS | ProDOS reserved for future use
// End.
)
// 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},
// Names of Filetype constants above.
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 +125,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 +136,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 +148,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)
}
}
}
@ -184,13 +184,19 @@ func FiletypeForName(name string) (Filetype, error) {
return info.Type, nil
}
}
return 0, fmt.Errorf("Unknown Filetype: %q", name)
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
}

80
types/ops.go Normal file
View File

@ -0,0 +1,80 @@
// 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
}
// DiskOrder specifies the logical disk ordering.
type DiskOrder string
const (
// DiskOrderDO is the DOS 3.3 logical ordering.
DiskOrderDO = DiskOrder("do")
// DiskOrderPO is the ProDOS logical ordering.
DiskOrderPO = DiskOrder("po")
// DiskOrderRaw is the logical ordering that doesn't change anything.
DiskOrderRaw = DiskOrder("raw")
// DiskOrderAuto is the logical ordering that tells diskii to guess.
DiskOrderAuto = DiskOrder("auto")
// DiskOrderUnknown is usually an error condition, or a signal that guessing failed.
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 int) bool
// Operator returns an Operator for the []byte disk image.
Operator(diskbytes []byte, debug int) (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
}

11
types/types.go Normal file
View File

@ -0,0 +1,11 @@
// Package types holds various types that are needed all over the place. They're
// in their own package to avoid circular dependencies.
package types
// Globals holds flags and configuration that are shared globally.
type Globals struct {
// Debug level (0 = no debugging, 1 = normal user debugging, 2+ is mostly for me)
Debug int
// DiskOperatorFactories holds the current list of registered OperatorFactory instances.
DiskOperatorFactories []OperatorFactory
}

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