Compare commits

...

34 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
Zellyn Hunter
05fdbcd0ba prodos: basic catalog now works 2017-04-12 23:23:40 -04:00
Zellyn Hunter
c244ec5257 prodos: working on loading Volume 2017-04-11 22:36:06 -04:00
Zellyn Hunter
1758fc0f32 prodos: make structures BlockSources + BlockSinks 2017-03-29 22:27:27 -04:00
Zellyn Hunter
1d7be34b5c prodos: working on load/save of free block bitmap 2017-03-24 21:52:57 -04:00
Zellyn Hunter
df80529449 Add (NOP) prodos factory and DiskBlockDevice
The prodos operator factory functions just return errors for now,
until Catalog is implemented.
2017-03-22 22:27:27 -04:00
Zellyn Hunter
118944b512 Add data/ directory and go-bindata generate cmds 2017-03-19 21:55:32 -04:00
Zellyn Hunter
dc35096652 Add WineBottler CiderPress 2017-03-17 23:21:26 -04:00
Zellyn Hunter
9fed43061d README tweaks 2017-03-17 23:17:17 -04:00
Zellyn Hunter
91a65df106 update README 2017-03-17 22:26:43 -04:00
Zellyn Hunter
2d0d2773a4 refactor to make more disk/device agnostic 2017-03-17 22:26:15 -04:00
Zellyn Hunter
5bd50043b9 Add logo 2017-03-16 23:12:07 -04:00
Zellyn Hunter
570bf171ac prodos: test struct marshal/unmarshal roundtrips 2017-03-14 21:28:02 -04:00
Zellyn Hunter
4b4612c31f Add (untested) ProDOS marshaling and unmarshaling 2017-03-13 22:20:29 -04:00
Zellyn Hunter
209d326cb8 Added DiskBrowser to the README 2017-03-13 22:19:59 -04:00
Zellyn Hunter
40d39fe78f Starting work on ProDOS data structures 2017-03-12 17:47:18 -04:00
Zellyn Hunter
7a03a28621 Update README 2017-03-08 23:00:05 -05:00
Zellyn Hunter
ecd20f8715 Add Cadius to the "related tools" section 2017-02-24 21:20:25 -05:00
Zellyn Hunter
2121a93655 add godoc link 2017-01-26 21:56:28 -06:00
Zellyn Hunter
3a55e2fd91 travis: use default go version 2017-01-26 21:55:58 -06:00
71 changed files with 2836 additions and 1103 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

View File

@ -1,6 +1,4 @@
language: go
go:
- 1.7
deploy:
provider: releases
api_key:

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

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

152
README.md
View File

@ -5,12 +5,17 @@ diskii
functionality, but I'm still experimenting with the command syntax and
organization, so don't get too comfy with it.
![Seagull Srs Micro Software](img/seagull-srs.png)
diskii-the-tool is a commandline tool for working with Apple II disk
images. Given that
[AppleCommander](http://applecommander.sourceforge.net/) already does
everything, it's not terribly necessary. It is, however, mine. Minor
benefits (right now) are binaries you can copy around (no Java
needed), and support for Super-Mon symbol tables on NakedOS disks.
needed), support for Super-Mon symbol tables on NakedOS disks, and
creation of
"[Standard Delivery](https://github.com/peterferrie/standard-delivery)"
disk images.
diskii-the-library is probably more useful: a library of
disk-image-manipulation code that can be used by other Go programs.
@ -19,18 +24,41 @@ diskii's major disadvantage is that it mostly doesn't exist yet.
[![Build Status](https://travis-ci.org/zellyn/diskii.svg?branch=master)](https://travis-ci.org/zellyn/diskii)
[![Report Card](https://goreportcard.com/badge/github.com/zellyn/diskii)](https://goreportcard.com/report/github.com/zellyn/diskii)
[![GoDoc](https://godoc.org/github.com/zellyn/diskii/lib?status.svg)](https://godoc.org/github.com/zellyn/diskii/lib)
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 the `applesoft decode` command works.
tool, but for now only some parts work.
The library code aims (a) to support the commandline tool operations,
and (b) to replace the "read and write disk images" code of the
@ -38,23 +66,23 @@ and (b) to replace the "read and write disk images" code of the
Current disk operations supported:
| Feature | DOS 3.3 | ProDOS | NakedOS/Super-Mon |
| ---------------- | ------------------ | ------ | ------------------ |
| basic structures | :white_check_mark: | :x: | :white_check_mark: |
| ls | :white_check_mark: | :x: | :white_check_mark: |
| dump | :white_check_mark: | :x: | :white_check_mark: |
| put | :x: | :x: | :white_check_mark: |
| dumptext | :x: | :x: | :x: |
| delete | :x: | :x: | :x: |
| rename | :x: | :x: | :x: |
| put | :x: | :x: | :x: |
| puttext | :x: | :x: | :x: |
| extract (all) | :x: | :x: | :x: |
| lock/unlock | :x: | :x: | :x: |
| init | :x: | :x: | :x: |
| defrag | :x: | :x: | :x: |
| Feature | DOS 3.3 | ProDOS | NakedOS/Super-Mon |
| ---------------- | -------- | ------ | ------------------ |
| basic structures | ✓ | ✓ | ✓ |
| ls | ✓ | ✓ | ✓ |
| dump | ✗ | ✗ | ✗ |
| put | ✗ | ✗ | ✗ |
| dumptext | ✗ | ✗ | ✗ |
| delete | ✗ | ✗ | ✗ |
| rename | ✗ | ✗ | ✗ |
| put | ✗ | ✗ | ✗ |
| puttext | ✗ | ✗ | ✗ |
| extract (all) | ✗ | ✗ | ✗ |
| lock/unlock | ✗ | ✗ | ✗ |
| 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
@ -63,27 +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 (:x:) in the disk
My rough TODO list (apart from anything marked () in the disk
operations matrix is listed below. Anything that an actual user needs
will be likely to get priority.
- [x] Build per-platform binaries for Linux, MacOS, Windows.
- [ ] 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 ProDOS support (add `lib/prodos/prodos.go` and register a ProDOS operator factory)
- [ ] 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
@ -94,3 +127,52 @@ will be likely to get priority.
- https://github.com/datajerk/c2d
- 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,58 +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")
}
sd, 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())
}
op, err := disk.OperatorFor(sd)
if err != nil {
return err
if globals.Debug > 0 {
fmt.Fprintf(os.Stderr, "Opened disk with order %q, system %q\n", order, op.Name())
}
subdir := ""
if len(args) == 2 {
if l.Directory != "" {
if !op.HasSubdirs() {
return fmt.Errorf("Disks of type %q cannot have subdirectories", op.Name())
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,65 +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")
}
sd, 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
}
op, err := disk.OperatorFor(sd)
deleted, err := op.Delete(d.Filename)
if err != nil {
return err
}
deleted, err := op.Delete(args[1])
if err != nil {
return err
if !deleted && !d.MissingOk {
return fmt.Errorf("file %q not found (use -f to prevent this being an error)", d.Filename)
}
if !deleted && !missingok {
return fmt.Errorf("file %q not found", args[1])
}
f, err := os.Create(args[0])
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 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,52 +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")
}
sd, 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
}
op, err := disk.OperatorFor(sd)
file, err := op.GetFile(d.Filename)
if err != nil {
return err
}
file, err := op.GetFile(args[1])
_, 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,91 +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)
}
sd, err := disk.Open(args[0])
if err != nil {
return err
}
op, err := disk.OperatorFor(sd)
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 {
@ -98,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 = sd.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,84 +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>")
}
sd, err := disk.Open(args[0])
if err != nil {
return err
}
op, err := disk.OperatorFor(sd)
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 = sd.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)
}

12
data/README.md Normal file
View File

@ -0,0 +1,12 @@
# Embedded data files for diskii
## List of files
- ProDOS_2_4_1.dsk
John Brooks' update to ProDOS, released September 2016.
[Announcement on Call-A.P.P.L.E](http://www.callapple.org/vintage-apple-computers/apple-ii/announcing-prodos-2-4-1-for-all-apple-ii-and-compatible-computers/)
- prodos-old-boot0.bin
The old ProDOS sector 0, used before the IIGS System 4.0 system disk.
- prodos-new-boot0.bin
The new ProDOS sector 0, used on and after the IIGS System 4.0
system disk. Understands sparse PRODOS.SYSTEM files.

Binary file not shown.

Binary file not shown.

26
data/data.go Normal file
View File

@ -0,0 +1,26 @@
// Package data is a bunch of go:embed embedded files.
package data
import _ "embed" // Mark this file as using embeds.
// DOS33masterDSK is a DOS 3.3 Master Disk image.
//go:embed disks/dos33master.dsk
var DOS33masterDSK []byte
// DOS33masterWOZ is a DOS 3.3 Master Disk, as a .woz file.
//go:embed disks/dos33master.woz
var DOS33masterWOZ []byte
// ProDOS242PO is John Brooks' update to ProDOS.
// Website: https://prodos8.com
// Announcements: https://www.callapple.org/author/jbrooks/
//go:embed disks/ProDOS_2_4_2.po
var ProDOS242PO []byte
// ProDOSNewBootSector0 is the new ProDOS sector 0, used on and after the IIGS System 4.0. Understands sparse PRODOS.SYSTEM files.
//go:embed boot/prodos-new-boot0.bin
var ProDOSNewBootSector0 []byte
// ProDOSOldBootSector0 is the old ProDOS sector 0, used before the IIGS System 4.0 system disk.
//go:embed boot/prodos-old-boot0.bin
var ProDOSOldBootSector0 []byte

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

@ -9,7 +9,8 @@ import (
"fmt"
"strings"
"github.com/zellyn/diskii/lib/disk"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/types"
)
const (
@ -31,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
}
@ -41,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
}
@ -355,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,
@ -367,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
@ -396,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)
@ -425,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
}
@ -489,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
@ -515,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
}
@ -527,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
}
@ -548,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.
@ -573,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())
}
@ -588,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
}
@ -597,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,
}
@ -653,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
@ -665,24 +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)
}
// 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
// DiskOrder returns the Physical-to-Logical mapping order.
func (o operator) DiskOrder() types.DiskOrder {
return types.DiskOrderDO
}
func init() {
disk.RegisterOperatorFactory(operatorName, operatorFactory)
// 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.
_, _, 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)
}

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

BIN
img/seagull-srs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@ -1,153 +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,
}
// TrackSector is a pair of track/sector bytes.
type TrackSector struct {
Track byte
Sector byte
}
// SectorDisk is the interface use 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)
}
// Open opens a disk image by filename.
func Open(filename string) (SectorDisk, error) {
ext := strings.ToLower(path.Ext(filename))
switch ext {
case ".dsk":
return LoadDSK(filename)
}
return nil, fmt.Errorf("Unimplemented/unknown disk file extension %q", ext)
}

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,54 +0,0 @@
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
// marshal.go contains helpers for marshaling sector structs to/from
// disk.
package disk
// 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)
}

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

90
main.go
View File

@ -1,15 +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/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]]

956
prodos/prodos.go Normal file
View File

@ -0,0 +1,956 @@
// Copyright © 2017 Zellyn Hunter <zellyn@gmail.com>
// Package prodos contains routines for working with the on-device
// structures of Apple ProDOS.
package prodos
import (
"encoding/binary"
"fmt"
"os"
"github.com/zellyn/diskii/disk"
"github.com/zellyn/diskii/types"
)
// Storage types.
const (
TypeDeleted = 0
TypeSeedling = 0x1
TypeSapling = 0x2
TypeTree = 0x3
TypeSubdirectory = 0xD
TypeSubdirectoryHeader = 0xE
TypeVolumeDirectoryHeader = 0xF
)
// blockBase represents a 512-byte block of data.
type blockBase struct {
block uint16 // Block index this data was loaded from.
}
// GetBlock gets the block index from a blockBase.
func (bb blockBase) GetBlock() uint16 {
return bb.block
}
// SetBlock sets the block index of a blockBase.
func (bb *blockBase) SetBlock(block uint16) {
bb.block = block
}
// A bitmapPart is a single block of a volumeBitMap.
type bitmapPart struct {
blockBase
data disk.Block
}
// Ensure that bitmapPart is valid BlockSource and BlockSink.
var _ disk.BlockSource = (*bitmapPart)(nil)
var _ disk.BlockSink = (*bitmapPart)(nil)
// FromBlock unmarshals a bitmapPart from a Block.
func (bp *bitmapPart) FromBlock(block disk.Block) error {
bp.data = block
return nil
}
// ToBlock marshals a bitmapPart struct to a block.
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 {
vbm[i].SetBlock(startBlock + uint16(i))
}
for b := 0; b < int(blocks); b++ {
vbm.MarkUnused(uint16(b))
}
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)
}
func (vbm VolumeBitMap) mark(block uint16, set bool) {
byteIndex := block >> 3
blockIndex := byteIndex / 512
blockByteIndex := byteIndex % 512
bit := byte(1 << (7 - (block & 7)))
if set {
vbm[blockIndex].data[blockByteIndex] |= bit
} else {
vbm[blockIndex].data[blockByteIndex] &^= bit
}
}
// IsFree returns true if the given block on the device is free,
// according to the VolumeBitMap.
func (vbm VolumeBitMap) IsFree(block uint16) bool {
byteIndex := block >> 3
blockIndex := byteIndex / 512
blockByteIndex := byteIndex % 512
bit := byte(1 << (7 - (block & 7)))
return vbm[blockIndex].data[blockByteIndex]&bit > 0
}
// readVolumeBitMap reads the entire volume bitmap from a block
// device.
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(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 vbm, nil
}
// Write writes the Volume Bit Map to a block device.
func (vbm VolumeBitMap) Write(devicebytes []byte) error {
for i, bp := range vbm {
if err := disk.MarshalBlock(devicebytes, bp); err != nil {
return fmt.Errorf("cannot write block %d (device block %d) of Volume Bit Map: %v", i, bp.GetBlock(), err)
}
}
return nil
}
// DateTime represents the 4-byte ProDOS y/m/d h/m timestamp.
type DateTime struct {
YMD [2]byte
HM [2]byte
}
// toBytes returns a four-byte slice representing a DateTime.
func (dt DateTime) toBytes() []byte {
return []byte{dt.YMD[0], dt.YMD[1], dt.HM[0], dt.HM[1]}
}
// fromBytes turns a slice of four bytes back into a DateTime.
func (dt *DateTime) fromBytes(b []byte) {
if len(b) != 4 {
panic(fmt.Sprintf("DateTime expects 4 bytes; got %d", len(b)))
}
dt.YMD[0] = b[0]
dt.YMD[1] = b[1]
dt.HM[0] = b[2]
dt.HM[1] = b[3]
}
// 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]))
}
if dt.HM[1] >= 60 {
errors = append(errors, fmt.Errorf("%s expects minute<60; got %x", fieldDescription, dt.HM[1]))
}
return errors
}
// VolumeDirectoryKeyBlock is the struct used to hold the ProDOS Volume Directory Key
// Block structure. See page 4-4 of Beneath Apple ProDOS.
type VolumeDirectoryKeyBlock struct {
blockBase
Prev uint16 // Pointer to previous block (always zero: the KeyBlock is the first Volume Directory block
Next uint16 // Pointer to next block in the Volume Directory
Header VolumeDirectoryHeader
Descriptors [12]FileDescriptor
Extra byte // Trailing byte (so we don't lose it)
}
// Ensure that VolumeDirectoryKeyBlock is valid BlockSource and BlockSink.
var _ disk.BlockSource = (*VolumeDirectoryKeyBlock)(nil)
var _ disk.BlockSink = (*VolumeDirectoryKeyBlock)(nil)
// ToBlock marshals the VolumeDirectoryKeyBlock to a Block of bytes.
func (vdkb VolumeDirectoryKeyBlock) ToBlock() (disk.Block, error) {
var block disk.Block
binary.LittleEndian.PutUint16(block[0x0:0x2], vdkb.Prev)
binary.LittleEndian.PutUint16(block[0x2:0x4], vdkb.Next)
copyBytes(block[0x04:0x02b], vdkb.Header.toBytes())
for i, desc := range vdkb.Descriptors {
copyBytes(block[0x2b+i*0x27:0x2b+(i+1)*0x27], desc.toBytes())
}
block[511] = vdkb.Extra
return block, nil
}
// FromBlock unmarshals a Block of bytes into a VolumeDirectoryKeyBlock.
func (vdkb *VolumeDirectoryKeyBlock) FromBlock(block disk.Block) error {
vdkb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
vdkb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
vdkb.Header.fromBytes(block[0x04:0x2b])
for i := range vdkb.Descriptors {
vdkb.Descriptors[i].fromBytes(block[0x2b+i*0x27 : 0x2b+(i+1)*0x27])
}
vdkb.Extra = block[511]
return nil
}
// Validate validates a VolumeDirectoryKeyBlock for valid values.
func (vdkb VolumeDirectoryKeyBlock) Validate() (errors []error) {
if vdkb.Prev != 0 {
errors = append(errors, fmt.Errorf("Volume Directory Key Block should have a `Previous` block of 0, got $%04x", vdkb.Prev))
}
errors = append(errors, vdkb.Header.Validate()...)
for _, desc := range vdkb.Descriptors {
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))
}
return errors
}
// VolumeDirectoryBlock is a normal (non-key) segment in the Volume Directory Header.
type VolumeDirectoryBlock struct {
blockBase
Prev uint16 // Pointer to previous block in the Volume Directory.
Next uint16 // Pointer to next block in the Volume Directory.
Descriptors [13]FileDescriptor
Extra byte // Trailing byte (so we don't lose it)
}
// Ensure that VolumeDirectoryBlock is valid BlockSource and BlockSink.
var _ disk.BlockSource = (*VolumeDirectoryBlock)(nil)
var _ disk.BlockSink = (*VolumeDirectoryBlock)(nil)
// ToBlock marshals a VolumeDirectoryBlock to a Block of bytes.
func (vdb VolumeDirectoryBlock) ToBlock() (disk.Block, error) {
var block disk.Block
binary.LittleEndian.PutUint16(block[0x0:0x2], vdb.Prev)
binary.LittleEndian.PutUint16(block[0x2:0x4], vdb.Next)
for i, desc := range vdb.Descriptors {
copyBytes(block[0x04+i*0x27:0x04+(i+1)*0x27], desc.toBytes())
}
block[511] = vdb.Extra
return block, nil
}
// FromBlock unmarshals a Block of bytes into a VolumeDirectoryBlock.
func (vdb *VolumeDirectoryBlock) FromBlock(block disk.Block) error {
vdb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
vdb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
for i := range vdb.Descriptors {
vdb.Descriptors[i].fromBytes(block[0x4+i*0x27 : 0x4+(i+1)*0x27])
}
vdb.Extra = block[511]
return nil
}
// Validate validates a VolumeDirectoryBlock for valid values.
func (vdb VolumeDirectoryBlock) Validate() (errors []error) {
for _, desc := range vdb.Descriptors {
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))
}
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)
Unused1 [8]byte
Creation DateTime // Date and time volume was formatted
Version byte
MinVersion byte
Access Access
EntryLength byte // Length of each entry in the Volume Directory: usually $27
EntriesPerBlock byte // Usually $0D
FileCount uint16 // Number of active entries in the Volume Directory, not counting the Volume Directory Header
BitMapPointer uint16 // Block number of start of VolumeBitMap. Usually 6
TotalBlocks uint16 // Total number of blocks on the device. $118 (280) for a 35-track diskette.
}
// toBytes converts a VolumeDirectoryHeader to a slice of bytes.
func (vdh VolumeDirectoryHeader) toBytes() []byte {
buf := make([]byte, 0x27)
buf[0] = vdh.TypeAndNameLength
copyBytes(buf[1:0x10], vdh.VolumeName[:])
copyBytes(buf[0x10:0x18], vdh.Unused1[:])
copyBytes(buf[0x18:0x1c], vdh.Creation.toBytes())
buf[0x1c] = vdh.Version
buf[0x1d] = vdh.MinVersion
buf[0x1e] = byte(vdh.Access)
buf[0x1f] = vdh.EntryLength
buf[0x20] = vdh.EntriesPerBlock
binary.LittleEndian.PutUint16(buf[0x21:0x23], vdh.FileCount)
binary.LittleEndian.PutUint16(buf[0x23:0x25], vdh.BitMapPointer)
binary.LittleEndian.PutUint16(buf[0x25:0x27], vdh.TotalBlocks)
return buf
}
// fromBytes unmarshals a slice of bytes into a VolumeDirectoryHeader.
func (vdh *VolumeDirectoryHeader) fromBytes(buf []byte) {
if len(buf) != 0x27 {
panic(fmt.Sprintf("VolumeDirectoryHeader should be 0x27 bytes long; got 0x%02x", len(buf)))
}
vdh.TypeAndNameLength = buf[0]
copyBytes(vdh.VolumeName[:], buf[1:0x10])
copyBytes(vdh.Unused1[:], buf[0x10:0x18])
vdh.Creation.fromBytes(buf[0x18:0x1c])
vdh.Version = buf[0x1c]
vdh.MinVersion = buf[0x1d]
vdh.Access = Access(buf[0x1e])
vdh.EntryLength = buf[0x1f]
vdh.EntriesPerBlock = buf[0x20]
vdh.FileCount = binary.LittleEndian.Uint16(buf[0x21:0x23])
vdh.BitMapPointer = binary.LittleEndian.Uint16(buf[0x23:0x25])
vdh.TotalBlocks = binary.LittleEndian.Uint16(buf[0x25:0x27])
}
// Validate validates a VolumeDirectoryHeader for valid values.
func (vdh VolumeDirectoryHeader) Validate() (errors []error) {
errors = append(errors, vdh.Creation.Validate("creation date/time of VolumeDirectoryHeader")...)
return errors
}
// Access represents a level of file access.
type Access byte
const (
// 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 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
// subdirectory.
type FileDescriptor struct {
TypeAndNameLength byte // Storage type (top four bits) and volume name length (lower four)
FileName [15]byte // Filename (actual length defined in TypeAndNameLength)
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
Creation DateTime // Date and time of of file creation
Version byte
MinVersion byte
Access Access
// For TXT files, random access record length (L from OPEN)
// For BIN files, load address for binary image (A from BSAVE)
// For BAS files, load address for program image (when SAVEd)
// For VAR files, address of compressed variables image (when STOREd)
// For SYS files, load address for system program (usually $2000)
AuxType uint16
LastMod DateTime
HeaderPointer uint16 // Block number of the key block for the directory which describes this file.
}
// 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 types.Descriptor
Type: types.Filetype(fd.FileType),
}
return desc
}
// Name returns the string filename of a file descriptor.
func (fd FileDescriptor) Name() string {
return string(fd.FileName[0 : fd.TypeAndNameLength&0xf])
}
// Type returns the type of a file descriptor.
func (fd FileDescriptor) Type() byte {
return fd.TypeAndNameLength >> 4
}
// toBytes converts a FileDescriptor to a slice of bytes.
func (fd FileDescriptor) toBytes() []byte {
buf := make([]byte, 0x27)
buf[0] = fd.TypeAndNameLength
copyBytes(buf[1:0x10], fd.FileName[:])
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[0x18:0x1c], fd.Creation.toBytes())
buf[0x1c] = fd.Version
buf[0x1d] = fd.MinVersion
buf[0x1e] = byte(fd.Access)
binary.LittleEndian.PutUint16(buf[0x1f:0x21], fd.AuxType)
copyBytes(buf[0x21:0x25], fd.LastMod.toBytes())
binary.LittleEndian.PutUint16(buf[0x25:0x27], fd.HeaderPointer)
return buf
}
// fromBytes unmarshals a slice of bytes into a FileDescriptor.
func (fd *FileDescriptor) fromBytes(buf []byte) {
if len(buf) != 0x27 {
panic(fmt.Sprintf("FileDescriptor should be 0x27 bytes long; got 0x%02x", len(buf)))
}
fd.TypeAndNameLength = buf[0]
copyBytes(fd.FileName[:], buf[1:0x10])
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])
fd.Creation.fromBytes(buf[0x18:0x1c])
fd.Version = buf[0x1c]
fd.MinVersion = buf[0x1d]
fd.Access = Access(buf[0x1e])
fd.AuxType = binary.LittleEndian.Uint16(buf[0x1f:0x21])
fd.LastMod.fromBytes(buf[0x21:0x25])
fd.HeaderPointer = binary.LittleEndian.Uint16(buf[0x25:0x27])
}
// Validate validates a FileDescriptor for valid values.
func (fd FileDescriptor) Validate() (errors []error) {
errors = append(errors, fd.Creation.Validate(fmt.Sprintf("creation date/time of FileDescriptor %q", fd.Name()))...)
errors = append(errors, fd.LastMod.Validate(fmt.Sprintf("last modification date/time of FileDescriptor %q", fd.Name()))...)
return errors
}
// 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
// Get the blockNum'th block number from an index block.
func (i IndexBlock) Get(blockNum byte) uint16 {
return uint16(i[blockNum]) + uint16(i[256+int(blockNum)])<<8
}
// Set the blockNum'th block number in an index block.
func (i IndexBlock) Set(blockNum byte, block uint16) {
i[blockNum] = byte(block)
i[256+int(blockNum)] = byte(block >> 8)
}
// SubdirectoryKeyBlock is the struct used to hold the first entry in
// a subdirectory structure.
type SubdirectoryKeyBlock struct {
blockBase
Prev uint16 // Pointer to previous block (always zero: the KeyBlock is the first Volume Directory block
Next uint16 // Pointer to next block in the Volume Directory
Header SubdirectoryHeader
Descriptors [12]FileDescriptor
Extra byte // Trailing byte (so we don't lose it)
}
// Ensure that SubdirectoryKeyBlock is valid BlockSource and BlockSink.
var _ disk.BlockSource = (*SubdirectoryKeyBlock)(nil)
var _ disk.BlockSink = (*SubdirectoryKeyBlock)(nil)
// ToBlock marshals the SubdirectoryKeyBlock to a Block of bytes.
func (skb SubdirectoryKeyBlock) ToBlock() (disk.Block, error) {
var block disk.Block
binary.LittleEndian.PutUint16(block[0x0:0x2], skb.Prev)
binary.LittleEndian.PutUint16(block[0x2:0x4], skb.Next)
copyBytes(block[0x04:0x02b], skb.Header.toBytes())
for i, desc := range skb.Descriptors {
copyBytes(block[0x2b+i*0x27:0x2b+(i+1)*0x27], desc.toBytes())
}
block[511] = skb.Extra
return block, nil
}
// FromBlock unmarshals a Block of bytes into a SubdirectoryKeyBlock.
func (skb *SubdirectoryKeyBlock) FromBlock(block disk.Block) error {
skb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
skb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
skb.Header.fromBytes(block[0x04:0x2b])
for i := range skb.Descriptors {
skb.Descriptors[i].fromBytes(block[0x2b+i*0x27 : 0x2b+(i+1)*0x27])
}
skb.Extra = block[511]
return nil
}
// Validate validates a SubdirectoryKeyBlock for valid values.
func (skb SubdirectoryKeyBlock) Validate() (errors []error) {
if skb.Prev != 0 {
errors = append(errors, fmt.Errorf("Subdirectory Key Block should have a `Previous` block of 0, got $%04x", skb.Prev))
}
errors = append(errors, skb.Header.Validate()...)
for _, desc := range skb.Descriptors {
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))
}
return errors
}
// SubdirectoryBlock is a normal (non-key) segment in a Subdirectory.
type SubdirectoryBlock struct {
blockBase
Prev uint16 // Pointer to previous block in the Volume Directory.
Next uint16 // Pointer to next block in the Volume Directory.
Descriptors [13]FileDescriptor
Extra byte // Trailing byte (so we don't lose it)
}
// Ensure that SubdirectoryBlock is valid BlockSource and BlockSink.
var _ disk.BlockSource = (*SubdirectoryBlock)(nil)
var _ disk.BlockSink = (*SubdirectoryBlock)(nil)
// ToBlock marshals a SubdirectoryBlock to a Block of bytes.
func (sb SubdirectoryBlock) ToBlock() (disk.Block, error) {
var block disk.Block
binary.LittleEndian.PutUint16(block[0x0:0x2], sb.Prev)
binary.LittleEndian.PutUint16(block[0x2:0x4], sb.Next)
for i, desc := range sb.Descriptors {
copyBytes(block[0x04+i*0x27:0x04+(i+1)*0x27], desc.toBytes())
}
block[511] = sb.Extra
return block, nil
}
// FromBlock unmarshals a Block of bytes into a SubdirectoryBlock.
func (sb *SubdirectoryBlock) FromBlock(block disk.Block) error {
sb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
sb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
for i := range sb.Descriptors {
sb.Descriptors[i].fromBytes(block[0x4+i*0x27 : 0x4+(i+1)*0x27])
}
sb.Extra = block[511]
return nil
}
// Validate validates a SubdirectoryBlock for valid values.
func (sb SubdirectoryBlock) Validate() (errors []error) {
for _, desc := range sb.Descriptors {
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))
}
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)
SeventyFive byte // Must contain $75 (!?)
Unused1 [7]byte
Creation DateTime // Date and time volume was formatted
Version byte
MinVersion byte
Access Access
EntryLength byte // Length of each entry in the Subdirectory: usually $27
EntriesPerBlock byte // Usually $0D
FileCount uint16 // Number of active entries in the Subdirectory, not counting the Subdirectory Header
ParentPointer uint16 // The block number of the key (first) block of the directory that contains the entry that describes this subdirectory
ParentEntry byte // Index in the parent directory for this subdirectory's entry (counting from parent header = 0)
ParentEntryLength byte // Usually $27
}
// toBytes converts a SubdirectoryHeader to a slice of bytes.
func (sh SubdirectoryHeader) toBytes() []byte {
buf := make([]byte, 0x27)
buf[0] = sh.TypeAndNameLength
copyBytes(buf[1:0x10], sh.SubdirectoryName[:])
buf[0x10] = sh.SeventyFive
copyBytes(buf[0x11:0x18], sh.Unused1[:])
copyBytes(buf[0x18:0x1c], sh.Creation.toBytes())
buf[0x1c] = sh.Version
buf[0x1d] = sh.MinVersion
buf[0x1e] = byte(sh.Access)
buf[0x1f] = sh.EntryLength
buf[0x20] = sh.EntriesPerBlock
binary.LittleEndian.PutUint16(buf[0x21:0x23], sh.FileCount)
binary.LittleEndian.PutUint16(buf[0x23:0x25], sh.ParentPointer)
buf[0x25] = sh.ParentEntry
buf[0x26] = sh.ParentEntryLength
return buf
}
// fromBytes unmarshals a slice of bytes into a SubdirectoryHeader.
func (sh *SubdirectoryHeader) fromBytes(buf []byte) {
if len(buf) != 0x27 {
panic(fmt.Sprintf("VolumeDirectoryHeader should be 0x27 bytes long; got 0x%02x", len(buf)))
}
sh.TypeAndNameLength = buf[0]
copyBytes(sh.SubdirectoryName[:], buf[1:0x10])
sh.SeventyFive = buf[0x10]
copyBytes(sh.Unused1[:], buf[0x11:0x18])
sh.Creation.fromBytes(buf[0x18:0x1c])
sh.Version = buf[0x1c]
sh.MinVersion = buf[0x1d]
sh.Access = Access(buf[0x1e])
sh.EntryLength = buf[0x1f]
sh.EntriesPerBlock = buf[0x20]
sh.FileCount = binary.LittleEndian.Uint16(buf[0x21:0x23])
sh.ParentPointer = binary.LittleEndian.Uint16(buf[0x23:0x25])
sh.ParentEntry = buf[0x25]
sh.ParentEntryLength = buf[0x26]
}
// 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, sh.Creation.Validate(fmt.Sprintf("subdirectory %q header creation date/time", sh.Name()))...)
return errors
}
// Name returns the string filename of a subdirectory header.
func (sh SubdirectoryHeader) Name() string {
return string(sh.SubdirectoryName[0 : sh.TypeAndNameLength&0xf])
}
// Volume is the in-memory representation of a device's volume
// information.
type Volume struct {
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
// subdirectory's information.
type Subdirectory struct {
keyBlock *SubdirectoryKeyBlock
blocks []*SubdirectoryBlock
}
// descriptors returns a slice of all top-level file descriptors in a
// volume, deleted or not.
func (v Volume) descriptors() []FileDescriptor {
var descs []FileDescriptor
descs = append(descs, v.keyBlock.Descriptors[:]...)
for _, block := range v.blocks {
descs = append(descs, block.Descriptors[:]...)
}
return descs
}
// subdirDescriptors returns a slice of all top-level file descriptors
// in a volume that are subdirectories.
func (v Volume) subdirDescriptors() []FileDescriptor {
var descs []FileDescriptor
for _, desc := range v.descriptors() {
if desc.Type() == TypeSubdirectory {
descs = append(descs, desc)
}
}
return descs
}
// readVolume reads the entire volume and subdirectories from a device
// into memory.
func readVolume(devicebytes []byte, keyBlock uint16, debug int) (Volume, error) {
v := Volume{
keyBlock: &VolumeDirectoryKeyBlock{},
subdirsByBlock: make(map[uint16]*Subdirectory),
subdirsByName: make(map[string]*Subdirectory),
firstSubdirBlocks: make(map[uint16]uint16),
}
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)
// }
vbm, err := readVolumeBitMap(devicebytes, v.keyBlock.Header.BitMapPointer)
if err != nil {
return v, err
}
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(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(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()
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
}
if parentName != "" {
name = parentName + "/" + name
}
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
}
// descriptors returns a slice of all top-level file descriptors in a
// subdirectory, deleted or not.
func (s Subdirectory) descriptors() []FileDescriptor {
var descs []FileDescriptor
descs = append(descs, s.keyBlock.Descriptors[:]...)
for _, block := range s.blocks {
descs = append(descs, block.Descriptors[:]...)
}
return descs
}
// subdirDescriptors returns a slice of all top-level file descriptors
// in a subdirectory that are subdirectories.
func (s Subdirectory) subdirDescriptors() []FileDescriptor {
var descs []FileDescriptor
for _, desc := range s.descriptors() {
if desc.Type() == TypeSubdirectory {
descs = append(descs, desc)
}
}
return descs
}
// parentDirName returns the full recursive directory name of the given parent directory.
func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory, firstSubdirBlockMap map[uint16]uint16) (string, error) {
if parentDirectoryBlock == keyBlock || firstSubdirBlockMap[parentDirectoryBlock] == keyBlock {
return "", nil
}
sd := subdirMap[parentDirectoryBlock]
if sd == nil {
parentFirstBlock, ok := firstSubdirBlockMap[parentDirectoryBlock]
if ok {
sd = subdirMap[parentFirstBlock]
}
}
if sd == nil {
return "", fmt.Errorf("unable to find subdirectory for block %d", parentDirectoryBlock)
}
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap, firstSubdirBlockMap)
if err != nil {
return "", err
}
if parentName == "" {
return sd.keyBlock.Header.Name(), nil
}
return parentName + "/" + sd.keyBlock.Header.Name(), nil
}
// readSubdirectory reads a single subdirectory from a device into
// memory.
func readSubdirectory(devicebytes []byte, fd FileDescriptor) (Subdirectory, error) {
s := Subdirectory{
keyBlock: &SubdirectoryKeyBlock{},
}
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(devicebytes, &sdb, block); err != nil {
return s, err
}
s.blocks = append(s.blocks, &sdb)
}
return s, nil
}
// copyBytes is just like the builtin copy, but just for byte slices,
// and it checks that dst and src have the same length.
func copyBytes(dst, src []byte) int {
if len(dst) != len(src) {
panic(fmt.Sprintf("copyBytes called with differing lengths %d and %d", len(dst), len(src)))
}
return copy(dst, src)
}
// operator is a disk.Operator - an interface for performing
// high-level operations on files and directories.
type operator struct {
data []byte
debug int
}
var _ types.Operator = operator{}
// operatorName is the keyword name for the operator that undestands
// prodos disks/devices.
const operatorName = "prodos"
// Name returns the name of the operator.
func (o operator) Name() string {
return operatorName
}
// HasSubdirs returns true if the underlying operating system on the
// disk allows subdirectories.
func (o operator) HasSubdirs() bool {
return true
}
// 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) ([]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, fmt.Errorf("error reading volume: %w", err)
}
var result []types.Descriptor
if subdir == "" {
for _, desc := range vol.descriptors() {
if desc.Type() != TypeDeleted {
result = append(result, desc.descriptor())
}
}
return result, nil
}
sd, ok := vol.subdirsByName[subdir]
if !ok {
return nil, fmt.Errorf("subdirectory %q not found", subdir)
}
for _, desc := range sd.descriptors() {
if desc.Type() != TypeDeleted {
result = append(result, desc.descriptor())
}
}
return result, nil
}
// GetFile retrieves a file by name.
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
// deleted, false if it didn't exist.
func (o operator) Delete(filename string) (bool, error) {
return false, fmt.Errorf("%s doesn't implement Delete yet", operatorName)
}
// 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 types.FileInfo, overwrite bool) (existed bool, err error) {
return false, fmt.Errorf("%s doesn't implement PutFile yet", operatorName)
}
// DiskOrder returns the Physical-to-Logical mapping order.
func (o operator) DiskOrder() types.DiskOrder {
return types.DiskOrderPO
}
// GetBytes returns the disk image bytes, in logical order.
func (o operator) GetBytes() []byte {
return o.data
}
// OperatorFactory is a types.OperatorFactory for ProDos disks.
type OperatorFactory struct {
}
// Name returns the name of the operator.
func (of OperatorFactory) Name() string {
return operatorName
}
// SeemsToMatch returns true if the []byte disk image seems to match the
// system of this operator.
func (of OperatorFactory) SeemsToMatch(devicebytes []byte, debug 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()
}

116
prodos/prodos_test.go Normal file
View File

@ -0,0 +1,116 @@
package prodos
import (
"crypto/rand"
"strings"
"testing"
"github.com/kr/pretty"
"github.com/zellyn/diskii/disk"
)
func randomBlock() disk.Block {
var b1 disk.Block
_, _ = rand.Read(b1[:])
return b1
}
// TestVolumeDirectoryKeyBlockMarshalRoundtrip checks a simple roundtrip of VDKB data.
func TestVolumeDirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
vdkb := &VolumeDirectoryKeyBlock{}
err := vdkb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := vdkb.ToBlock()
if err != nil {
t.Fatal(err)
}
if b1 != b2 {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
vdkb2 := &VolumeDirectoryKeyBlock{}
err = vdkb2.FromBlock(b2)
if err != nil {
t.Fatal(err)
}
if *vdkb != *vdkb2 {
t.Errorf("Structs differ: %v != %v", vdkb, vdkb2)
}
}
// TestVolumeDirectoryBlockMarshalRoundtrip checks a simple roundtrip of VDB data.
func TestVolumeDirectoryBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
vdb := &VolumeDirectoryBlock{}
err := vdb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := vdb.ToBlock()
if err != nil {
t.Fatal(err)
}
if b1 != b2 {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
vdb2 := &VolumeDirectoryBlock{}
err = vdb2.FromBlock(b2)
if err != nil {
t.Fatal(err)
}
if *vdb != *vdb2 {
t.Errorf("Structs differ: %v != %v", vdb, vdb2)
}
}
// TestSubdirectoryKeyBlockMarshalRoundtrip checks a simple roundtrip of SKB data.
func TestSubdirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
skb := &SubdirectoryKeyBlock{}
err := skb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := skb.ToBlock()
if err != nil {
t.Fatal(err)
}
if b1 != b2 {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
skb2 := &SubdirectoryKeyBlock{}
err = skb2.FromBlock(b2)
if err != nil {
t.Fatal(err)
}
if *skb != *skb2 {
t.Errorf("Structs differ: %v != %v", skb, skb2)
}
}
// TestSubdirectoryBlockMarshalRoundtrip checks a simple roundtrip of SB data.
func TestSubdirectoryBlockMarshalRoundtrip(t *testing.T) {
b1 := randomBlock()
sb := &SubdirectoryBlock{}
err := sb.FromBlock(b1)
if err != nil {
t.Fatal(err)
}
b2, err := sb.ToBlock()
if err != nil {
t.Fatal(err)
}
if b1 != b2 {
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
}
sb2 := &SubdirectoryBlock{}
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

@ -10,8 +10,9 @@ import (
"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 (
@ -28,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
}
@ -60,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.
@ -95,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)
}
}
@ -103,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)
}
}
}
@ -166,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
}
@ -189,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)
@ -208,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 {
@ -224,7 +222,7 @@ OUTER:
}
}
}
if err := sm.Persist(sd); err != nil {
if err := sm.Persist(diskbytes); err != nil {
return existed, err
}
return existed, nil
@ -253,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
}
@ -316,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
}
@ -344,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
}
@ -379,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 {
@ -399,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
@ -582,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)
@ -621,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
@ -634,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
@ -642,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.
@ -666,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,
}
@ -725,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
}
}
@ -742,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))
@ -768,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
}
@ -776,17 +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
}
// operatorFactory is the factory that returns supermon operators
// given disk images.
func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
sm, err := LoadSectorMap(sd)
// DiskOrder returns the Physical-to-Logical mapping order.
func (o Operator) DiskOrder() types.DiskOrder {
return types.DiskOrderRaw
}
// 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
}
@ -794,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
}
@ -804,6 +838,7 @@ func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
return op, nil
}
func init() {
disk.RegisterOperatorFactory(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.Open(testDisk)
if err != nil {
t.Fatal(err)
}
op, err := disk.OperatorFor(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.Open(testDisk)
if err != nil {
t.Fatal(err)
}
op, err := disk.OperatorFor(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
}

View File

@ -4,29 +4,55 @@
// on disk images logically: catalog, rename, delete, create files,
// etc.
package disk
import (
"errors"
"fmt"
"sort"
"strings"
)
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
@ -42,6 +68,8 @@ type Operator interface {
// 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.
@ -50,41 +78,3 @@ type FileInfo struct {
Data []byte
StartAddress uint16
}
// operatorFactory is the type of functions that accept a SectorDisk,
// and may return an Operator interface to operate on it.
type operatorFactory func(SectorDisk) (Operator, error)
// operatorFactories is the map of currently-registered operator
// factories.
var operatorFactories map[string]operatorFactory
func init() {
operatorFactories = make(map[string]operatorFactory)
}
// RegisterOperatorFactory registers an operator factory with the
// given name: a function that accepts a SectorDisk, and may return an
// Operator. It doesn't lock operatorFactories: it is expected to be
// called only from package `init` functions.
func RegisterOperatorFactory(name string, factory operatorFactory) {
operatorFactories[name] = factory
}
// OperatorFor returns an Operator for the given SectorDisk, if possible.
func OperatorFor(sd SectorDisk) (Operator, error) {
if len(operatorFactories) == 0 {
return nil, errors.New("Cannot find an operator matching the given disk image (none registered)")
}
for _, factory := range operatorFactories {
if operator, err := factory(sd); err == nil {
return operator, nil
}
}
names := make([]string, 0, len(operatorFactories))
for name := range operatorFactories {
names = append(names, `"`+name+`"`)
}
sort.Strings(names)
return nil, fmt.Errorf("Cannot find an operator matching the given disk image (tried %s)", strings.Join(names, ", "))
}

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