mirror of https://github.com/zellyn/diskii.git
Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
Zellyn Hunter | 0b85248dde | |
Zellyn Hunter | b264fe8e96 | |
Zellyn Hunter | c2dd6362de | |
Zellyn Hunter | 23c9b1edcf | |
Zellyn Hunter | 116f3781b5 | |
Zellyn Hunter | 7d036244af | |
Zellyn Hunter | 09ee1c6262 | |
Zellyn Hunter | 80aa964915 | |
Zellyn Hunter | bbf7d696db | |
Zellyn Hunter | ef9115dcaf | |
Zellyn Hunter | 9c66e2c5e6 | |
Zellyn Hunter | f09e8f47f1 | |
Zellyn Hunter | 21a4d76ff5 | |
Zellyn Hunter | 6d57f2de51 | |
Zellyn Hunter | 510a7d2ac8 |
|
@ -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
|
|
@ -0,0 +1,17 @@
|
|||
issues:
|
||||
exclude-use-default: false
|
||||
linters:
|
||||
enable:
|
||||
- gocritic
|
||||
- godot
|
||||
- gofmt
|
||||
- gosec
|
||||
- ifshort
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
- predeclared
|
||||
- revive
|
||||
- stylecheck
|
||||
- unconvert
|
||||
- wastedassign
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"filewatcher.commands": [
|
||||
{
|
||||
"match": "\\.go",
|
||||
"cmd": "echo '${file} changed'",
|
||||
"event": "onFileChange",
|
||||
},
|
||||
]
|
||||
}
|
111
README.md
111
README.md
|
@ -28,11 +28,34 @@ diskii's major disadvantage is that it mostly doesn't exist yet.
|
|||
|
||||
It rhymes with “whiskey”.
|
||||
|
||||
Discussion/support is in
|
||||
[#apple2 on the retrocomputing Slack](https://retrocomputing.slack.com/messages/apple2/)
|
||||
(invites [here](https://retrocomputing.herokuapp.com)).
|
||||
Discussion/support is on the
|
||||
[apple2infinitum Slack](https://apple2infinitum.slack.com/)
|
||||
(invites [here](http://apple2.gs:3000/)).
|
||||
|
||||
### Goals
|
||||
# Examples
|
||||
|
||||
Get a listing of files on a DOS 3.3 disk image:
|
||||
```
|
||||
diskii ls dos33master.dsk
|
||||
```
|
||||
|
||||
… or a ProDOS disk image:
|
||||
```
|
||||
diskii ls ProDOS_2_4_2.po
|
||||
```
|
||||
|
||||
… or a Super-Mon disk image:
|
||||
```
|
||||
diskii ls Super-Mon-2.0.dsk
|
||||
```
|
||||
|
||||
Reorder the sectors in a disk image:
|
||||
```
|
||||
diskii reorder ProDOS_2_4_2.dsk ProDOS_2_4_2.po
|
||||
```
|
||||
|
||||
|
||||
# Goals
|
||||
|
||||
Eventually, it aims to be a comprehensive disk image manipulation
|
||||
tool, but for now only some parts work.
|
||||
|
@ -47,8 +70,8 @@ Current disk operations supported:
|
|||
| ---------------- | -------- | ------ | ------------------ |
|
||||
| basic structures | ✓ | ✓ | ✓ |
|
||||
| ls | ✓ | ✓ | ✓ |
|
||||
| dump | ✓ | ✗ | ✓ |
|
||||
| put | ✗ | ✗ | ✓ |
|
||||
| dump | ✗ | ✗ | ✗ |
|
||||
| put | ✗ | ✗ | ✗ |
|
||||
| dumptext | ✗ | ✗ | ✗ |
|
||||
| delete | ✗ | ✗ | ✗ |
|
||||
| rename | ✗ | ✗ | ✗ |
|
||||
|
@ -59,7 +82,7 @@ Current disk operations supported:
|
|||
| init | ✗ | ✗ | ✗ |
|
||||
| defrag | ✗ | ✗ | ✗ |
|
||||
|
||||
### Installing/updating
|
||||
# Installing/updating
|
||||
Assuming you have Go installed, run `go get -u github.com/zellyn/diskii`
|
||||
|
||||
You can also download automatically-built binaries from the
|
||||
|
@ -68,30 +91,32 @@ page](https://github.com/zellyn/diskii/releases/latest). If you
|
|||
need binaries for a different architecture, please send a pull
|
||||
request or open an issue.
|
||||
|
||||
### Short-term TODOs/roadmap/easy ways to contribute
|
||||
# Short-term TODOs/roadmap/easy ways to contribute
|
||||
|
||||
My rough TODO list (apart from anything marked (✗) in the disk
|
||||
operations matrix is listed below. Anything that an actual user needs
|
||||
will be likely to get priority.
|
||||
|
||||
- [x] Build per-platform binaries for Linux, MacOS, Windows.
|
||||
- [x] Implement `GetFile` for DOS 3.3
|
||||
- [x] Make `put` accept load address for appropriate filetypes.
|
||||
- [x] Fix `golint` errors
|
||||
- [ ] Implement `GetFile` for prodos
|
||||
- [ ] Implement `PutFile` for prodos
|
||||
- [ ] Implement `Delete` for Super-Mon
|
||||
- [ ] Implement `Delete` for DOS 3.3
|
||||
- [ ] Implement `Delete` for ProDOS
|
||||
- [ ] Add and implement the `-l` flag for `ls`
|
||||
- [x] Add `Delete` to the `disk.Operator` interface
|
||||
- [x] Implement it for Super-Mon
|
||||
- [ ] Implement it for DOS 3.3
|
||||
- [ ] Make 13-sector DOS disks work
|
||||
- [ ] Read/write nybble formats
|
||||
- [ ] Read/write gzipped files
|
||||
- [ ] Add basic ProDOS structures
|
||||
- [ ] Add ProDOS support
|
||||
- [ ] Make `OperatorFactory.SeemsToMatch` more sophisticated for ProDOS
|
||||
- [ ] Make `OperatorFactory.SeemsToMatch` more sophisticated for DOS 3.3
|
||||
- [ ] Make `OperatorFactory.SeemsToMatch` more sophisticated for NakedOS
|
||||
- [x] Build per-platform binaries for Linux, MacOS, Windows.
|
||||
|
||||
### Related tools
|
||||
# Related tools
|
||||
|
||||
- http://a2ciderpress.com/ - the great grandaddy of them all. Windows only, unless you Wine
|
||||
- http://retrocomputingaustralia.com/rca-downloads/ Michael Mulhern's MacOS package of CiderPress
|
||||
- http://applecommander.sourceforge.net/ - the commandline, cross-platform alternative to CiderPress
|
||||
- http://brutaldeluxe.fr/products/crossdevtools/cadius/index.html - Brutal Deluxe's commandline tools
|
||||
- https://github.com/paleotronic/dskalyzer - cross-platform disk analysis tool (also written in Go!) from the folks who brought you [Octalyzer](http://octalyzer.com/).
|
||||
- https://github.com/cybernesto/dsktool.rb
|
||||
- https://github.com/cmosher01/Apple-II-Disk-Tools
|
||||
- https://github.com/madsen/perl-libA2
|
||||
|
@ -103,3 +128,51 @@ will be likely to get priority.
|
|||
- https://github.com/thecompu/Driv3rs - A Python Script to work with Apple III SOS DSK files
|
||||
- http://www.callapple.org/software/an-a-p-p-l-e-review-shink-fit-x-for-mac-os-x
|
||||
- https://github.com/dmolony/DiskBrowser - graphical (Java) disk browser that knows how to interpret and display many file formats
|
||||
- https://github.com/slotek/apple2-disk-util - ruby
|
||||
- https://github.com/slotek/dsk2nib - C
|
||||
- https://github.com/robmcmullen/atrcopy - dos3.3, python
|
||||
|
||||
# Notes
|
||||
|
||||
## Disk formats
|
||||
|
||||
- `.do`
|
||||
- `.po`
|
||||
- `.dsk` - could be DO or PO. When in doubt, assume DO.
|
||||
|
||||
| Physical Sectors | DOS 3.2 Logical | DOS 3.3 Logical | ProDOS/Pascal Logical | CP/M Logical |
|
||||
|------------------|-----------------|-----------------|-----------------------|------------- |
|
||||
| 0 | 0 | 0 | 0.0 | 0.0 |
|
||||
| 1 | 1 | 7 | 4.0 | 2.3 |
|
||||
| 2 | 2 | E | 0.1 | 1.2 |
|
||||
| 3 | 3 | 6 | 4.1 | 0.1 |
|
||||
| 4 | 4 | D | 1.0 | 3.0 |
|
||||
| 5 | 5 | 5 | 5.0 | 1.3 |
|
||||
| 6 | 6 | C | 1.1 | 0.2 |
|
||||
| 7 | 7 | 4 | 5.1 | 3.1 |
|
||||
| 8 | 8 | B | 2.0 | 2.0 |
|
||||
| 9 | 9 | 3 | 6.0 | 0.3 |
|
||||
| A | A | A | 2.1 | 3.2 |
|
||||
| B | B | 2 | 6.1 | 2.1 |
|
||||
| C | C | 9 | 3.0 | 1.0 |
|
||||
| D | | 1 | 7.0 | 3.3 |
|
||||
| E | | 8 | 3.1 | 2.2 |
|
||||
| F | | F | 7.1 | 1.1 |
|
||||
|
||||
_Note: DOS 3.2 rearranged the physical sectors on disk to achieve interleaving._
|
||||
### RWTS - DOS
|
||||
|
||||
Sector mapping:
|
||||
http://www.textfiles.com/apple/ANATOMY/rwts.s.txt and search for INTRLEAV
|
||||
|
||||
Mapping from specified sector to physical sector:
|
||||
|
||||
`00 0D 0B 09 07 05 03 01 0E 0C 0A 08 06 04 02 0F`
|
||||
|
||||
So if you write to "T0S1" with DOS RWTS, it ends up in physical sector 0D.
|
||||
|
||||
## Commandline examples for thinking about how it should work
|
||||
|
||||
diskii ls dos33.dsk
|
||||
diskii --order=do ls dos33.dsk
|
||||
diskii --order=do --system=nakedos ls nakedos.dsk
|
||||
|
|
|
@ -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 {
|
|
@ -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
|
|
@ -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('?')
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
hermit
|
|
@ -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.
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
.golangci-lint-1.41.1.pkg
|
|
@ -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,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).
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -6,54 +6,50 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
var shortnames bool // flag for whether to print short filenames
|
||||
// LsCmd is the kong `ls` command.
|
||||
type LsCmd struct {
|
||||
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
|
||||
System string `kong:"default='auto',enum='auto,dos3,prodos,nakedos',help='DOS system used for image.'"`
|
||||
|
||||
// catalogCmd represents the cat command, used to catalog a disk or
|
||||
// directory.
|
||||
var catalogCmd = &cobra.Command{
|
||||
Use: "catalog",
|
||||
Aliases: []string{"cat", "ls"},
|
||||
Short: "print a list of files",
|
||||
Long: `Catalog a disk or subdirectory.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runCat(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
},
|
||||
ShortNames bool `kong:"short='s',help='Whether to print short filenames (only makes a difference on Super-Mon disks).'"`
|
||||
Image *os.File `kong:"arg,required,help='Disk/device image to read.'"`
|
||||
Directory string `kong:"arg,optional,help='Directory to list (ProDOS only).'"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(catalogCmd)
|
||||
catalogCmd.Flags().BoolVarP(&shortnames, "shortnames", "s", false, "whether to print short filenames (only makes a difference on Super-Mon disks)")
|
||||
// Help displays extended help and examples.
|
||||
func (l LsCmd) Help() string {
|
||||
return `Examples:
|
||||
# Simple ls of a disk image
|
||||
diskii ls games.dsk
|
||||
# Get really explicit about disk order and system
|
||||
diskii ls --order do --system nakedos Super-Mon-2.0.dsk`
|
||||
}
|
||||
|
||||
// runCat performs the actual catalog logic.
|
||||
func runCat(args []string) error {
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
return fmt.Errorf("cat expects a disk image filename, and an optional subdirectory")
|
||||
}
|
||||
op, err := disk.Open(args[0])
|
||||
// Run the `ls` command.
|
||||
func (l *LsCmd) Run(globals *types.Globals) error {
|
||||
op, order, err := disk.OpenFile(l.Image, l.Order, l.System, globals.DiskOperatorFactories, globals.Debug)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w: %s", err, l.Image.Name())
|
||||
}
|
||||
subdir := ""
|
||||
if len(args) == 2 {
|
||||
if globals.Debug > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Opened disk with order %q, system %q\n", order, op.Name())
|
||||
}
|
||||
|
||||
if l.Directory != "" {
|
||||
if !op.HasSubdirs() {
|
||||
return fmt.Errorf("Disks of type %q cannot have subdirectories", op.Name())
|
||||
return fmt.Errorf("disks of type %q cannot have subdirectories", op.Name())
|
||||
}
|
||||
subdir = args[1]
|
||||
}
|
||||
fds, err := op.Catalog(subdir)
|
||||
fds, err := op.Catalog(l.Directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fd := range fds {
|
||||
if !shortnames && fd.Fullname != "" {
|
||||
if !l.ShortNames && fd.Fullname != "" {
|
||||
fmt.Println(fd.Fullname)
|
||||
} else {
|
||||
fmt.Println(fd.Name)
|
||||
|
|
|
@ -4,61 +4,41 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
var missingok bool // flag for whether to consider deleting a nonexistent file okay
|
||||
// DeleteCmd is the kong `delete` command.
|
||||
type DeleteCmd struct {
|
||||
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
|
||||
System string `kong:"default='auto',enum='auto,dos3',help='DOS system used for image.'"`
|
||||
MissingOk bool `kong:"short='f',help='Overwrite existing file?'"`
|
||||
|
||||
// deleteCmd represents the delete command, used to delete a file.
|
||||
var deleteCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete a file",
|
||||
Long: `Delete a file.
|
||||
|
||||
delete disk-image.dsk HELLO
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runDelete(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
},
|
||||
DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
|
||||
Filename string `kong:"arg,required,help='Filename to use on disk.'"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(deleteCmd)
|
||||
deleteCmd.Flags().BoolVarP(&missingok, "missingok", "f", false, "if true, don't consider deleting a nonexistent file an error")
|
||||
// Help displays extended help and examples.
|
||||
func (d DeleteCmd) Help() string {
|
||||
return `Examples:
|
||||
# Delete file GREMLINS on disk image games.dsk.
|
||||
diskii rm games.dsk GREMLINS`
|
||||
}
|
||||
|
||||
// runDelete performs the actual delete logic.
|
||||
func runDelete(args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("delete expects a disk image filename, and a filename")
|
||||
}
|
||||
op, err := disk.Open(args[0])
|
||||
// Run the `delete` command.
|
||||
func (d *DeleteCmd) Run(globals *types.Globals) error {
|
||||
op, order, err := disk.OpenFilename(d.DiskImage, d.Order, d.System, globals.DiskOperatorFactories, globals.Debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleted, err := op.Delete(args[1])
|
||||
|
||||
deleted, err := op.Delete(d.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !deleted && !missingok {
|
||||
return fmt.Errorf("file %q not found", args[1])
|
||||
if !deleted && !d.MissingOk {
|
||||
return fmt.Errorf("file %q not found (use -f to prevent this being an error)", d.Filename)
|
||||
}
|
||||
f, err := os.Create(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = op.Write(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return disk.WriteBack(d.DiskImage, op, order, true)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
// Package cmd contains the actual command implementations.
|
||||
package cmd
|
50
cmd/dump.go
50
cmd/dump.go
|
@ -3,48 +3,42 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
// dumpCmd represents the dump command, used to dump the raw contents
|
||||
// of a file.
|
||||
var dumpCmd = &cobra.Command{
|
||||
Use: "dump",
|
||||
Short: "dump the raw contents of a file",
|
||||
Long: `Dump the raw contents of a file.
|
||||
// DumpCmd is the kong `dump` command.
|
||||
type DumpCmd struct {
|
||||
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
|
||||
System string `kong:"default='auto',enum='auto,dos3',help='DOS system used for image.'"`
|
||||
|
||||
dump disk-image.dsk HELLO
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runDump(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
},
|
||||
DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
|
||||
Filename string `kong:"arg,required,help='Filename to use on disk.'"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(dumpCmd)
|
||||
// Help displays extended help and examples.
|
||||
func (d DumpCmd) Help() string {
|
||||
return `Examples:
|
||||
# Dump file GREMLINS on disk image games.dsk.
|
||||
diskii dump games.dsk GREMLINS`
|
||||
}
|
||||
|
||||
// runDump performs the actual dump logic.
|
||||
func runDump(args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("dump expects a disk image filename, and a filename")
|
||||
}
|
||||
op, err := disk.Open(args[0])
|
||||
// Run the `dump` command.
|
||||
func (d *DumpCmd) Run(globals *types.Globals) error {
|
||||
op, _, err := disk.OpenFilename(d.DiskImage, d.Order, d.System, globals.DiskOperatorFactories, globals.Debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := op.GetFile(args[1])
|
||||
|
||||
file, err := op.GetFile(d.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(file.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(zellyn): allow writing to files
|
||||
os.Stdout.Write(file.Data)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
133
cmd/nakedos.go
133
cmd/nakedos.go
|
@ -4,87 +4,80 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/lib/supermon"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/supermon"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
// nakedosCmd represents the nakedos command
|
||||
var nakedosCmd = &cobra.Command{
|
||||
Use: "nakedos",
|
||||
Short: "work with NakedOS disks",
|
||||
Long: `diskii nakedos contains the subcommands useful for working
|
||||
with NakedOS (and Super-Mon) disks`,
|
||||
Aliases: []string{"supermon"},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(nakedosCmd)
|
||||
}
|
||||
|
||||
// ----- mkhello command ----------------------------------------------------
|
||||
|
||||
var address uint16 // flag for address to load at
|
||||
var start uint16 // flag for address to start execution at
|
||||
const helloName = "FHELLO" // filename to use (if Super-Mon)
|
||||
|
||||
// mkhelloCmd represents the mkhello command
|
||||
var mkhelloCmd = &cobra.Command{
|
||||
Use: "mkhello <disk-image> filename",
|
||||
Short: "create an FHELLO program that loads and runs another file",
|
||||
Long: `
|
||||
mkhello creates file DF01:FHELLO that loads and runs another program at a specific address.
|
||||
// NakedOSCmd is the kong `nakedos` sub-command.
|
||||
type NakedOSCmd struct {
|
||||
Mkhello MkHelloCmd `kong:"cmd,help='Create an FHELLO program that loads and runs another file.'"`
|
||||
}
|
||||
|
||||
// Help shows extended help on NakedOS/Super-Mon.
|
||||
func (n NakedOSCmd) Help() string {
|
||||
return `NakedOS and Super-Mon were created by the amazing Martin Haye. For more information see:
|
||||
Source/docs: https://bitbucket.org/martin.haye/super-mon/
|
||||
Presentation: https://www.kansasfest.org/2012/08/2010-haye-nakedos/`
|
||||
}
|
||||
|
||||
// MkHelloCmd is the kong `mkhello` command.
|
||||
type MkHelloCmd struct {
|
||||
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
|
||||
|
||||
DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
|
||||
Filename string `kong:"arg,required,help='Name of NakedOS file to load.'"`
|
||||
|
||||
Address uint16 `kong:"type='anybaseuint16',default='0x6000',help='Address to load the code at.'"`
|
||||
Start uint16 `kong:"type='anybaseuint16',default='0xFFFF',help='Address to jump to. Defaults to 0xFFFF, which means “same as address flag”'"`
|
||||
}
|
||||
|
||||
// Help displays extended help and examples.
|
||||
func (m MkHelloCmd) Help() string {
|
||||
return `This command creates a very short DF01:FHELLO program that simply loads another program of your choice.
|
||||
|
||||
Examples:
|
||||
mkhello test.dsk FDEMO # load and run FDEMO at the default address, then jump to the start of the loaded code.
|
||||
mkhello test.dsk --address 0x2000 --start 0x2100 DF06 # load and run file DF06 at address 0x2000, and jump to 0x2100.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runMkhello(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
},
|
||||
# Load and run FDEMO at the default address, then jump to the start of the loaded code.
|
||||
mkhello test.dsk FDEMO
|
||||
|
||||
# Load and run file DF06 at address 0x2000, and jump to 0x2100.
|
||||
mkhello test.dsk --address 0x2000 --start 0x2100 DF06`
|
||||
}
|
||||
|
||||
func init() {
|
||||
nakedosCmd.AddCommand(mkhelloCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
mkhelloCmd.Flags().Uint16VarP(&address, "address", "a", 0x6000, "memory location to load code at")
|
||||
mkhelloCmd.Flags().Uint16VarP(&start, "start", "s", 0x6000, "memory location to jump to")
|
||||
}
|
||||
|
||||
// runMkhello performs the actual mkhello logic.
|
||||
func runMkhello(args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("usage: diskii mkhello <disk image> <file-to-load>")
|
||||
// Run the `mkhello` command.
|
||||
func (m *MkHelloCmd) Run(globals *types.Globals) error {
|
||||
if m.Start == 0xFFFF {
|
||||
m.Start = m.Address
|
||||
}
|
||||
if address%256 != 0 {
|
||||
return fmt.Errorf("address %d (%04X) not on a page boundary", address, address)
|
||||
|
||||
if m.Address%256 != 0 {
|
||||
return fmt.Errorf("address %d (%04X) not on a page boundary", m.Address, m.Address)
|
||||
}
|
||||
if start < address {
|
||||
return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", start, start, address, address)
|
||||
if m.Start < m.Address {
|
||||
return fmt.Errorf("start address %d (%04X) < load address %d (%04X)", m.Start, m.Start, m.Address, m.Address)
|
||||
}
|
||||
op, err := disk.Open(args[0])
|
||||
|
||||
op, order, err := disk.OpenFilename(m.DiskImage, m.Order, "auto", globals.DiskOperatorFactories, globals.Debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if op.Name() != "nakedos" {
|
||||
return fmt.Errorf("mkhello only works on disks of type %q; got %q", "nakedos", op.Name())
|
||||
}
|
||||
nakOp, ok := op.(supermon.Operator)
|
||||
if !ok {
|
||||
return fmt.Errorf("internal error: cannot cast to expected supermon.Operator type")
|
||||
return fmt.Errorf("internal error: cannot cast to expected supermon.Operator type (got %T)", op)
|
||||
}
|
||||
addr, symbolAddr, _, err := nakOp.ST.FilesForCompoundName(args[1])
|
||||
addr, symbolAddr, _, err := nakOp.ST.FilesForCompoundName(m.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if addr == 0 && symbolAddr == 0 {
|
||||
return fmt.Errorf("cannot parse %q as valid filename", args[1])
|
||||
return fmt.Errorf("cannot parse %q as valid filename", m.Filename)
|
||||
}
|
||||
toLoad := addr
|
||||
if addr == 0 {
|
||||
|
@ -94,32 +87,24 @@ func runMkhello(args []string) error {
|
|||
0x20, 0x40, 0x03, // JSR NAKEDOS
|
||||
0x6D, 0x01, 0xDC, // ADC NKRDFILE
|
||||
0x2C, toLoad, 0xDF, // BIT ${file number to load}
|
||||
0x2C, 0x00, byte(address >> 8), // BIT ${target page}
|
||||
0xD8, // CLD
|
||||
0x4C, byte(start), byte(start >> 8), // JMP ${target page}
|
||||
0x2C, 0x00, byte(m.Address >> 8), // BIT ${target page}
|
||||
0xD8, // CLD
|
||||
0x4C, byte(m.Start), byte(m.Start >> 8), // JMP ${target page}
|
||||
}
|
||||
fileInfo := disk.FileInfo{
|
||||
Descriptor: disk.Descriptor{
|
||||
fileInfo := types.FileInfo{
|
||||
Descriptor: types.Descriptor{
|
||||
Name: fmt.Sprintf("DF01:%s", helloName),
|
||||
Length: len(contents),
|
||||
Type: disk.FiletypeBinary,
|
||||
Type: types.FiletypeBinary,
|
||||
},
|
||||
Data: contents,
|
||||
}
|
||||
_ = fileInfo
|
||||
|
||||
_, err = op.PutFile(fileInfo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = op.Write(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
return disk.WriteBack(m.DiskImage, op, order, true)
|
||||
}
|
||||
|
|
94
cmd/put.go
94
cmd/put.go
|
@ -4,80 +4,66 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/lib/helpers"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/helpers"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
var filetypeName string // flag for file type
|
||||
var overwrite bool // flag for whether to overwrite
|
||||
// PutCmd is the kong `put` command.
|
||||
type PutCmd struct {
|
||||
Order types.DiskOrder `kong:"default='auto',enum='auto,do,po',help='Logical-to-physical sector order.'"`
|
||||
System string `kong:"default='auto',enum='auto,dos3',help='DOS system used for image.'"`
|
||||
FiletypeName string `kong:"default='B',help='Type of file (“diskii filetypes” to list).'"`
|
||||
Overwrite bool `kong:"short='f',help='Overwrite existing file?'"`
|
||||
Address uint16 `kong:"type='anybaseuint16',default='0x6000',help='For filetypes where it is appropriate, address to load the code at.'"`
|
||||
|
||||
// putCmd represents the put command, used to put the raw contents
|
||||
// of a file.
|
||||
var putCmd = &cobra.Command{
|
||||
Use: "put",
|
||||
Short: "put the raw contents of a file",
|
||||
Long: `Put the raw contents of a file.
|
||||
DiskImage string `kong:"arg,required,type='existingfile',help='Disk image to modify.'"`
|
||||
TargetFilename string `kong:"arg,required,help='Filename to use on disk.'"`
|
||||
SourceFilename string `kong:"arg,required,type='existingfile',help='Name of file containing data to put.'"`
|
||||
}
|
||||
|
||||
put disk-image.dsk HELLO <name of file with contents>
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runPut(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(-1)
|
||||
// Help displays extended help and examples.
|
||||
func (p PutCmd) Help() string {
|
||||
return `Examples:
|
||||
# Put file gremlins.o onto disk image games.dsk, using the filename GREMLINS.
|
||||
diskii put games.dsk GREMLINS gremlins.o`
|
||||
}
|
||||
|
||||
// Run the `put` command.
|
||||
func (p *PutCmd) Run(globals *types.Globals) error {
|
||||
if p.DiskImage == "-" {
|
||||
if p.SourceFilename == "-" {
|
||||
return fmt.Errorf("cannot get both disk image and file contents from stdin")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(putCmd)
|
||||
putCmd.Flags().StringVarP(&filetypeName, "type", "t", "B", "Type of file (`diskii filetypes` to list)")
|
||||
putCmd.Flags().BoolVarP(&overwrite, "overwrite", "f", false, "whether to overwrite existing files")
|
||||
}
|
||||
|
||||
// runPut performs the actual put logic.
|
||||
func runPut(args []string) error {
|
||||
if len(args) != 3 {
|
||||
return fmt.Errorf("usage: put <disk image> <target filename> <source filename>")
|
||||
}
|
||||
op, err := disk.Open(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contents, err := helpers.FileContentsOrStdIn(args[2])
|
||||
op, order, err := disk.OpenFilename(p.DiskImage, p.Order, p.System, globals.DiskOperatorFactories, globals.Debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filetype, err := disk.FiletypeForName(filetypeName)
|
||||
contents, err := helpers.FileContentsOrStdIn(p.SourceFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileInfo := disk.FileInfo{
|
||||
Descriptor: disk.Descriptor{
|
||||
Name: args[1],
|
||||
filetype, err := types.FiletypeForName(p.FiletypeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileInfo := types.FileInfo{
|
||||
Descriptor: types.Descriptor{
|
||||
Name: p.TargetFilename,
|
||||
Length: len(contents),
|
||||
Type: filetype,
|
||||
},
|
||||
Data: contents,
|
||||
Data: contents,
|
||||
StartAddress: p.Address,
|
||||
}
|
||||
_, err = op.PutFile(fileInfo, overwrite)
|
||||
_, err = op.PutFile(fileInfo, p.Overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = op.Write(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
return disk.WriteBack(p.DiskImage, op, order, true)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
61
cmd/root.go
61
cmd/root.go
|
@ -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
112
cmd/sd.go
|
@ -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)
|
||||
}
|
||||
|
|
297
data/data.go
297
data/data.go
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -7,10 +7,10 @@ package dos3
|
|||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -32,7 +32,7 @@ func (ds DiskSector) GetTrack() byte {
|
|||
}
|
||||
|
||||
// SetTrack sets the track that a DiskSector was loaded from.
|
||||
func (ds DiskSector) SetTrack(track byte) {
|
||||
func (ds *DiskSector) SetTrack(track byte) {
|
||||
ds.Track = track
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ func (ds DiskSector) GetSector() byte {
|
|||
}
|
||||
|
||||
// SetSector sets the sector that a DiskSector was loaded from.
|
||||
func (ds DiskSector) SetSector(sector byte) {
|
||||
func (ds *DiskSector) SetSector(sector byte) {
|
||||
ds.Sector = sector
|
||||
}
|
||||
|
||||
|
@ -356,11 +356,11 @@ func (fd *FileDesc) FilenameString() string {
|
|||
return strings.TrimRight(string(slice), " ")
|
||||
}
|
||||
|
||||
// descriptor returns a disk.Descriptor for a FileDesc, but with the
|
||||
// descriptor returns a types.Descriptor for a FileDesc, but with the
|
||||
// length set to -1, since we can't know it without reading the file
|
||||
// contents.
|
||||
func (fd FileDesc) descriptor() disk.Descriptor {
|
||||
desc := disk.Descriptor{
|
||||
func (fd FileDesc) descriptor() types.Descriptor {
|
||||
desc := types.Descriptor{
|
||||
Name: fd.FilenameString(),
|
||||
Sectors: int(fd.SectorCount),
|
||||
Length: -1,
|
||||
|
@ -368,28 +368,28 @@ func (fd FileDesc) descriptor() disk.Descriptor {
|
|||
}
|
||||
switch fd.Filetype & 0x7f {
|
||||
case FiletypeText: // Text file
|
||||
desc.Type = disk.FiletypeASCIIText
|
||||
desc.Type = types.FiletypeASCIIText
|
||||
case FiletypeInteger: // INTEGER BASIC file
|
||||
desc.Type = disk.FiletypeIntegerBASIC
|
||||
desc.Type = types.FiletypeIntegerBASIC
|
||||
case FiletypeApplesoft: // APPLESOFT BASIC file
|
||||
desc.Type = disk.FiletypeApplesoftBASIC
|
||||
desc.Type = types.FiletypeApplesoftBASIC
|
||||
case FiletypeBinary: // BINARY file
|
||||
desc.Type = disk.FiletypeBinary
|
||||
desc.Type = types.FiletypeBinary
|
||||
case FiletypeS: // S type file
|
||||
desc.Type = disk.FiletypeS
|
||||
desc.Type = types.FiletypeS
|
||||
case FiletypeRelocatable: // RELOCATABLE object module file
|
||||
desc.Type = disk.FiletypeRelocatable
|
||||
desc.Type = types.FiletypeRelocatable
|
||||
case FiletypeA: // A type file
|
||||
desc.Type = disk.FiletypeNewA
|
||||
desc.Type = types.FiletypeNewA
|
||||
case FiletypeB: // B type file
|
||||
desc.Type = disk.FiletypeNewB
|
||||
desc.Type = types.FiletypeNewB
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
// Contents returns the on-disk contents of a file represented by a
|
||||
// FileDesc.
|
||||
func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
|
||||
func (fd *FileDesc) Contents(diskbytes []byte) ([]byte, error) {
|
||||
tsls := []TrackSectorList{}
|
||||
nextTrack := fd.TrackSectorListTrack
|
||||
nextSector := fd.TrackSectorListSector
|
||||
|
@ -397,11 +397,11 @@ func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
|
|||
for nextTrack != 0 || nextSector != 0 {
|
||||
ts := disk.TrackSector{Track: nextTrack, Sector: nextSector}
|
||||
if seen[ts] {
|
||||
return nil, fmt.Errorf("File %q tries to read TrackSector track=%d sector=%d twice", fd.FilenameString(), nextTrack, nextSector)
|
||||
return nil, fmt.Errorf("file %q tries to read TrackSector track=%d sector=%d twice", fd.FilenameString(), nextTrack, nextSector)
|
||||
}
|
||||
seen[ts] = true
|
||||
tsl := TrackSectorList{}
|
||||
if err := disk.UnmarshalLogicalSector(lsd, &tsl, nextTrack, nextSector); err != nil {
|
||||
if err := disk.UnmarshalLogicalSector(diskbytes, &tsl, nextTrack, nextSector); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tsls = append(tsls, tsl)
|
||||
|
@ -426,7 +426,7 @@ func (fd *FileDesc) Contents(lsd disk.LogicalSectorDisk) ([]byte, error) {
|
|||
data = append(data, 0)
|
||||
}
|
||||
} else {
|
||||
contents, err := lsd.ReadLogicalSector(ts.Track, ts.Sector)
|
||||
contents, err := disk.ReadSector(diskbytes, ts.Track, ts.Sector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -490,14 +490,14 @@ func (tsl *TrackSectorList) FromSector(data []byte) error {
|
|||
|
||||
// readCatalogSectors reads the raw CatalogSector structs from a DOS
|
||||
// 3.3 disk.
|
||||
func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) {
|
||||
func readCatalogSectors(diskbytes []byte, debug int) ([]CatalogSector, error) {
|
||||
v := &VTOC{}
|
||||
err := disk.UnmarshalLogicalSector(d, v, VTOCTrack, VTOCSector)
|
||||
err := disk.UnmarshalLogicalSector(diskbytes, v, VTOCTrack, VTOCSector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := v.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("Invalid VTOC sector: %v", err)
|
||||
return nil, fmt.Errorf("invalid VTOC sector: %v", err)
|
||||
}
|
||||
|
||||
nextTrack := v.CatalogTrack
|
||||
|
@ -516,7 +516,7 @@ func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) {
|
|||
return nil, fmt.Errorf("catalog sectors can't be in sector %d: disk only has %d sectors", nextSector, v.NumSectors)
|
||||
}
|
||||
cs := CatalogSector{}
|
||||
err := disk.UnmarshalLogicalSector(d, &cs, nextTrack, nextSector)
|
||||
err := disk.UnmarshalLogicalSector(diskbytes, &cs, nextTrack, nextSector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -528,8 +528,8 @@ func readCatalogSectors(d disk.LogicalSectorDisk) ([]CatalogSector, error) {
|
|||
}
|
||||
|
||||
// ReadCatalog reads the catalog of a DOS 3.3 disk.
|
||||
func ReadCatalog(d disk.LogicalSectorDisk) (files, deleted []FileDesc, err error) {
|
||||
css, err := readCatalogSectors(d)
|
||||
func ReadCatalog(diskbytes []byte, debug int) (files, deleted []FileDesc, err error) {
|
||||
css, err := readCatalogSectors(diskbytes, debug)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -549,13 +549,14 @@ func ReadCatalog(d disk.LogicalSectorDisk) (files, deleted []FileDesc, err error
|
|||
return files, deleted, nil
|
||||
}
|
||||
|
||||
// operator is a disk.Operator - an interface for performing
|
||||
// operator is a types.Operator - an interface for performing
|
||||
// high-level operations on files and directories.
|
||||
type operator struct {
|
||||
lsd disk.LogicalSectorDisk
|
||||
data []byte
|
||||
debug int
|
||||
}
|
||||
|
||||
var _ disk.Operator = operator{}
|
||||
var _ types.Operator = operator{}
|
||||
|
||||
// operatorName is the keyword name for the operator that undestands
|
||||
// dos3 disks.
|
||||
|
@ -574,12 +575,12 @@ func (o operator) HasSubdirs() bool {
|
|||
|
||||
// Catalog returns a catalog of disk entries. subdir should be empty
|
||||
// for operating systems that do not support subdirectories.
|
||||
func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
|
||||
fds, _, err := ReadCatalog(o.lsd)
|
||||
func (o operator) Catalog(subdir string) ([]types.Descriptor, error) {
|
||||
fds, _, err := ReadCatalog(o.data, o.debug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
descs := make([]disk.Descriptor, 0, len(fds))
|
||||
descs := make([]types.Descriptor, 0, len(fds))
|
||||
for _, fd := range fds {
|
||||
descs = append(descs, fd.descriptor())
|
||||
}
|
||||
|
@ -589,7 +590,7 @@ func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
|
|||
// fileForFilename returns the FileDesc corresponding to the given
|
||||
// filename, or an error.
|
||||
func (o operator) fileForFilename(filename string) (FileDesc, error) {
|
||||
fds, _, err := ReadCatalog(o.lsd)
|
||||
fds, _, err := ReadCatalog(o.data, o.debug)
|
||||
if err != nil {
|
||||
return FileDesc{}, err
|
||||
}
|
||||
|
@ -598,22 +599,22 @@ func (o operator) fileForFilename(filename string) (FileDesc, error) {
|
|||
return fd, nil
|
||||
}
|
||||
}
|
||||
return FileDesc{}, fmt.Errorf("Filename %q not found", filename)
|
||||
return FileDesc{}, fmt.Errorf("filename %q not found", filename)
|
||||
}
|
||||
|
||||
// GetFile retrieves a file by name.
|
||||
func (o operator) GetFile(filename string) (disk.FileInfo, error) {
|
||||
func (o operator) GetFile(filename string) (types.FileInfo, error) {
|
||||
fd, err := o.fileForFilename(filename)
|
||||
if err != nil {
|
||||
return disk.FileInfo{}, err
|
||||
return types.FileInfo{}, err
|
||||
}
|
||||
desc := fd.descriptor()
|
||||
data, err := fd.Contents(o.lsd)
|
||||
data, err := fd.Contents(o.data)
|
||||
if err != nil {
|
||||
return disk.FileInfo{}, err
|
||||
return types.FileInfo{}, err
|
||||
}
|
||||
|
||||
fi := disk.FileInfo{
|
||||
fi := types.FileInfo{
|
||||
Descriptor: desc,
|
||||
Data: data,
|
||||
}
|
||||
|
@ -654,7 +655,7 @@ func (o operator) GetFile(filename string) (disk.FileInfo, error) {
|
|||
errType = "B"
|
||||
}
|
||||
|
||||
return disk.FileInfo{}, fmt.Errorf("%s does not yet implement `GetFile` for filetype %s", operatorName, errType)
|
||||
return types.FileInfo{}, fmt.Errorf("%s does not yet implement `GetFile` for filetype %s", operatorName, errType)
|
||||
}
|
||||
|
||||
// Delete deletes a file by name. It returns true if the file was
|
||||
|
@ -666,29 +667,43 @@ func (o operator) Delete(filename string) (bool, error) {
|
|||
// PutFile writes a file by name. If the file exists and overwrite
|
||||
// is false, it returns with an error. Otherwise it returns true if
|
||||
// an existing file was overwritten.
|
||||
func (o operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) {
|
||||
func (o operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) {
|
||||
return false, fmt.Errorf("%s does not implement PutFile yet", operatorName)
|
||||
}
|
||||
|
||||
// Write writes the underlying disk to the given writer.
|
||||
func (o operator) Write(w io.Writer) (int, error) {
|
||||
return o.lsd.Write(w)
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
func (o operator) DiskOrder() types.DiskOrder {
|
||||
return types.DiskOrderDO
|
||||
}
|
||||
|
||||
// operatorFactory is the factory that returns dos3 operators given
|
||||
// disk images.
|
||||
func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
|
||||
lsd, err := disk.NewMappedDisk(sd, disk.Dos33LogicalToPhysicalSectorMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _, err = ReadCatalog(lsd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err)
|
||||
}
|
||||
return operator{lsd: lsd}, nil
|
||||
// GetBytes returns the disk image bytes, in logical order.
|
||||
func (o operator) GetBytes() []byte {
|
||||
return o.data
|
||||
}
|
||||
|
||||
func init() {
|
||||
disk.RegisterDiskOperatorFactory(operatorName, operatorFactory)
|
||||
// OperatorFactory is a types.OperatorFactory for DOS 3.3 disks.
|
||||
type OperatorFactory struct {
|
||||
}
|
||||
|
||||
// Name returns the name of the operator.
|
||||
func (of OperatorFactory) Name() string {
|
||||
return operatorName
|
||||
}
|
||||
|
||||
// SeemsToMatch returns true if the []byte disk image seems to match the
|
||||
// system of this operator.
|
||||
func (of OperatorFactory) SeemsToMatch(diskbytes []byte, debug int) bool {
|
||||
// For now, just return true if we can run Catalog successfully.
|
||||
_, _, err := ReadCatalog(diskbytes, debug)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Operator returns an Operator for the []byte disk image.
|
||||
func (of OperatorFactory) Operator(diskbytes []byte, debug int) (types.Operator, error) {
|
||||
return operator{data: diskbytes, debug: debug}, nil
|
||||
}
|
||||
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
func (of OperatorFactory) DiskOrder() types.DiskOrder {
|
||||
return operator{}.DiskOrder()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// This file contains the list of commands to run to re-generate
|
||||
// generated files.
|
||||
|
||||
// Use go-bindata to embed static assets that we need.
|
||||
//go:generate go-bindata -pkg data -prefix "data/" -o data/data.go data/disks data/boot
|
||||
//go:generate goimports -w data/data.go
|
||||
|
||||
package main
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright © 2017 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// dev.go contains logic for reading ".po" disk images.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// Dev represents a .po disk image.
|
||||
type Dev struct {
|
||||
data []byte // The actual data in the file
|
||||
blocks uint16 // Number of blocks
|
||||
}
|
||||
|
||||
var _ BlockDevice = (*Dev)(nil)
|
||||
|
||||
// LoadDev loads a .po image from a file.
|
||||
func LoadDev(filename string) (Dev, error) {
|
||||
bb, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return Dev{}, err
|
||||
}
|
||||
if len(bb)%512 != 0 {
|
||||
return Dev{}, fmt.Errorf("expected file %q to contain a multiple of 512 bytes, but got %d", filename, len(bb))
|
||||
}
|
||||
return Dev{
|
||||
data: bb,
|
||||
blocks: uint16(len(bb) / 512),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Empty creates a .po image that is all zeros.
|
||||
func EmptyDev(blocks uint16) Dev {
|
||||
return Dev{
|
||||
data: make([]byte, 512*int(blocks)),
|
||||
blocks: blocks,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadBlock reads a single block from the device. It always returns
|
||||
// 512 byes.
|
||||
func (d Dev) ReadBlock(index uint16) (Block, error) {
|
||||
var b Block
|
||||
copy(b[:], d.data[int(index)*512:int(index+1)*512])
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// WriteBlock writes a single block to a device. It expects exactly
|
||||
// 512 bytes.
|
||||
func (d Dev) WriteBlock(index uint16, data Block) error {
|
||||
copy(d.data[int(index)*512:int(index+1)*512], data[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
// Blocks returns the number of blocks in the device.
|
||||
func (d Dev) Blocks() uint16 {
|
||||
return d.blocks
|
||||
}
|
||||
|
||||
// Write writes the device contents to the given file.
|
||||
func (d Dev) Write(w io.Writer) (n int, err error) {
|
||||
return w.Write(d.data)
|
||||
}
|
290
lib/disk/disk.go
290
lib/disk/disk.go
|
@ -1,290 +0,0 @@
|
|||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// Package disk contains routines for reading and writing various disk
|
||||
// file formats.
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Various DOS33 disk characteristics.
|
||||
const (
|
||||
DOS33Tracks = 35
|
||||
DOS33Sectors = 16 // Sectors per track
|
||||
// DOS33DiskBytes is the number of bytes on a DOS 3.3 disk.
|
||||
DOS33DiskBytes = 143360 // 35 tracks * 16 sectors * 256 bytes
|
||||
DOS33TrackBytes = 256 * DOS33Sectors // Bytes per track
|
||||
)
|
||||
|
||||
// Dos33LogicalToPhysicalSectorMap maps logical sector numbers to physical ones.
|
||||
// See [UtA2 9-42 - Read Routines].
|
||||
var Dos33LogicalToPhysicalSectorMap = []byte{
|
||||
0x00, 0x0D, 0x0B, 0x09, 0x07, 0x05, 0x03, 0x01,
|
||||
0x0E, 0x0C, 0x0A, 0x08, 0x06, 0x04, 0x02, 0x0F,
|
||||
}
|
||||
|
||||
// Dos33PhysicalToLogicalSectorMap maps physical sector numbers to logical ones.
|
||||
// See [UtA2 9-42 - Read Routines].
|
||||
var Dos33PhysicalToLogicalSectorMap = []byte{
|
||||
0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04,
|
||||
0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F,
|
||||
}
|
||||
|
||||
// ProDOSLogicalToPhysicalSectorMap maps logical sector numbers to pysical ones.
|
||||
// See [UtA2e 9-43 - Sectors vs. Blocks].
|
||||
var ProDOSLogicalToPhysicalSectorMap = []byte{
|
||||
0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E,
|
||||
0x01, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0D, 0x0F,
|
||||
}
|
||||
|
||||
// ProDosPhysicalToLogicalSectorMap maps physical sector numbers to logical ones.
|
||||
// See [UtA2e 9-43 - Sectors vs. Blocks].
|
||||
var ProDosPhysicalToLogicalSectorMap = []byte{
|
||||
0x00, 0x08, 0x01, 0x09, 0x02, 0x0A, 0x03, 0x0B,
|
||||
0x04, 0x0C, 0x05, 0x0D, 0x06, 0x0E, 0x07, 0x0F,
|
||||
}
|
||||
|
||||
// TrackSector is a pair of track/sector bytes.
|
||||
type TrackSector struct {
|
||||
Track byte
|
||||
Sector byte
|
||||
}
|
||||
|
||||
// SectorDisk is the interface used to read and write disks by
|
||||
// physical (matches sector header) sector number.
|
||||
type SectorDisk interface {
|
||||
// ReadPhysicalSector reads a single physical sector from the disk. It
|
||||
// always returns 256 byes.
|
||||
ReadPhysicalSector(track byte, sector byte) ([]byte, error)
|
||||
// WritePhysicalSector writes a single physical sector to a disk. It
|
||||
// expects exactly 256 bytes.
|
||||
WritePhysicalSector(track byte, sector byte, data []byte) error
|
||||
// Sectors returns the number of sectors on the SectorDisk
|
||||
Sectors() byte
|
||||
// Tracks returns the number of tracks on the SectorDisk
|
||||
Tracks() byte
|
||||
// Write writes the disk contents to the given file.
|
||||
Write(io.Writer) (int, error)
|
||||
}
|
||||
|
||||
// LogicalSectorDisk is the interface used to read and write a disk by
|
||||
// *logical* sector number.
|
||||
type LogicalSectorDisk interface {
|
||||
// ReadLogicalSector reads a single logical sector from the disk. It
|
||||
// always returns 256 byes.
|
||||
ReadLogicalSector(track byte, sector byte) ([]byte, error)
|
||||
// WriteLogicalSector writes a single logical sector to a disk. It
|
||||
// expects exactly 256 bytes.
|
||||
WriteLogicalSector(track byte, sector byte, data []byte) error
|
||||
// Sectors returns the number of sectors on the SectorDisk
|
||||
Sectors() byte
|
||||
// Tracks returns the number of tracks on the SectorDisk
|
||||
Tracks() byte
|
||||
// Write writes the disk contents to the given file.
|
||||
Write(io.Writer) (int, error)
|
||||
}
|
||||
|
||||
// MappedDisk wraps a SectorDisk as a LogicalSectorDisk, handling the
|
||||
// logical-to-physical sector mapping.
|
||||
type MappedDisk struct {
|
||||
sectorDisk SectorDisk // The underlying physical sector disk.
|
||||
logicalToPhysical []byte // The mapping of logical to physical sectors.
|
||||
}
|
||||
|
||||
var _ LogicalSectorDisk = MappedDisk{}
|
||||
|
||||
// NewMappedDisk returns a MappedDisk with the given
|
||||
// logical-to-physical sector mapping.
|
||||
func NewMappedDisk(sd SectorDisk, logicalToPhysical []byte) (MappedDisk, error) {
|
||||
if logicalToPhysical != nil && len(logicalToPhysical) != int(sd.Sectors()) {
|
||||
return MappedDisk{}, fmt.Errorf("NewMappedDisk called on a disk image with %d sectors per track, but a mapping of length %d", sd.Sectors(), len(logicalToPhysical))
|
||||
}
|
||||
if logicalToPhysical == nil {
|
||||
logicalToPhysical = make([]byte, int(sd.Sectors()))
|
||||
for i := range logicalToPhysical {
|
||||
logicalToPhysical[i] = byte(i)
|
||||
}
|
||||
}
|
||||
return MappedDisk{
|
||||
sectorDisk: sd,
|
||||
logicalToPhysical: logicalToPhysical,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadLogicalSector reads a single logical sector from the disk. It
|
||||
// always returns 256 byes.
|
||||
func (md MappedDisk) ReadLogicalSector(track byte, sector byte) ([]byte, error) {
|
||||
if track >= md.sectorDisk.Tracks() {
|
||||
return nil, fmt.Errorf("ReadLogicalSector expected track between 0 and %d; got %d", md.sectorDisk.Tracks()-1, track)
|
||||
}
|
||||
if sector >= md.sectorDisk.Sectors() {
|
||||
return nil, fmt.Errorf("ReadLogicalSector expected sector between 0 and %d; got %d", md.sectorDisk.Sectors()-1, sector)
|
||||
}
|
||||
physicalSector := md.logicalToPhysical[int(sector)]
|
||||
return md.sectorDisk.ReadPhysicalSector(track, physicalSector)
|
||||
}
|
||||
|
||||
// WriteLogicalSector writes a single logical sector to a disk. It
|
||||
// expects exactly 256 bytes.
|
||||
func (md MappedDisk) WriteLogicalSector(track byte, sector byte, data []byte) error {
|
||||
if track >= md.sectorDisk.Tracks() {
|
||||
return fmt.Errorf("WriteLogicalSector expected track between 0 and %d; got %d", md.sectorDisk.Tracks()-1, track)
|
||||
}
|
||||
if sector >= md.sectorDisk.Sectors() {
|
||||
return fmt.Errorf("WriteLogicalSector expected sector between 0 and %d; got %d", md.sectorDisk.Sectors()-1, sector)
|
||||
}
|
||||
physicalSector := md.logicalToPhysical[int(sector)]
|
||||
return md.sectorDisk.WritePhysicalSector(track, physicalSector, data)
|
||||
}
|
||||
|
||||
// Sectors returns the number of sectors in the disk image.
|
||||
func (md MappedDisk) Sectors() byte {
|
||||
return md.sectorDisk.Sectors()
|
||||
}
|
||||
|
||||
// Tracks returns the number of tracks in the disk image.
|
||||
func (md MappedDisk) Tracks() byte {
|
||||
return md.sectorDisk.Tracks()
|
||||
}
|
||||
|
||||
// Write writes the disk contents to the given file.
|
||||
func (md MappedDisk) Write(w io.Writer) (n int, err error) {
|
||||
return md.sectorDisk.Write(w)
|
||||
}
|
||||
|
||||
// OpenDisk opens a disk image by filename.
|
||||
func OpenDisk(filename string) (SectorDisk, error) {
|
||||
ext := strings.ToLower(path.Ext(filename))
|
||||
switch ext {
|
||||
case ".dsk":
|
||||
return LoadDSK(filename)
|
||||
}
|
||||
return nil, fmt.Errorf("Unimplemented/unknown disk file extension %q", ext)
|
||||
}
|
||||
|
||||
// OpenDev opens a device image by filename.
|
||||
func OpenDev(filename string) (BlockDevice, error) {
|
||||
ext := strings.ToLower(path.Ext(filename))
|
||||
switch ext {
|
||||
case ".po":
|
||||
return LoadDev(filename)
|
||||
}
|
||||
return nil, fmt.Errorf("Unimplemented/unknown device file extension %q", ext)
|
||||
}
|
||||
|
||||
// Open opens a disk image by filename, returning an Operator.
|
||||
func Open(filename string) (Operator, error) {
|
||||
sd, err := OpenDisk(filename)
|
||||
if err == nil {
|
||||
var op Operator
|
||||
op, err = OperatorForDisk(sd)
|
||||
if err == nil {
|
||||
return op, nil
|
||||
}
|
||||
}
|
||||
|
||||
dev, err2 := OpenDev(filename)
|
||||
if err2 == nil {
|
||||
var op Operator
|
||||
op, err2 = OperatorForDevice(dev)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
return op, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type DiskBlockDevice struct {
|
||||
lsd LogicalSectorDisk
|
||||
blocks uint16
|
||||
}
|
||||
|
||||
// BlockDeviceFromSectorDisk creates a ProDOS block device from a
|
||||
// SectorDisk. It reads maps ProDOS to physical sectors.
|
||||
func BlockDeviceFromSectorDisk(sd SectorDisk) (BlockDevice, error) {
|
||||
lsd, err := NewMappedDisk(sd, ProDOSLogicalToPhysicalSectorMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DiskBlockDevice{
|
||||
lsd: lsd,
|
||||
blocks: uint16(lsd.Tracks()) / 2 * uint16(lsd.Sectors()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadBlock reads a single block from the device. It always returns
|
||||
// 512 byes.
|
||||
func (dbv DiskBlockDevice) ReadBlock(index uint16) (Block, error) {
|
||||
var b Block
|
||||
if index >= dbv.blocks {
|
||||
return b, fmt.Errorf("device has %d blocks; tried to read block %d (index=%d)", dbv.blocks, index+1, index)
|
||||
}
|
||||
i := int(index) * 2
|
||||
sectors := int(dbv.lsd.Sectors())
|
||||
|
||||
track0 := i / sectors
|
||||
sector0 := i % sectors
|
||||
sector1 := sector0 + 1
|
||||
track1 := track0
|
||||
if sector1 == sectors {
|
||||
sector1 = 0
|
||||
track1++
|
||||
}
|
||||
|
||||
b0, err := dbv.lsd.ReadLogicalSector(byte(track0), byte(sector0))
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("error reading first half of block %d (t:%d s:%d): %v", index, track0, sector0, err)
|
||||
}
|
||||
b1, err := dbv.lsd.ReadLogicalSector(byte(track1), byte(sector1))
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("error reading second half of block %d (t:%d s:%d): %v", index, track1, sector1, err)
|
||||
}
|
||||
copy(b[:256], b0)
|
||||
copy(b[256:], b1)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// WriteBlock writes a single block to a device. It expects exactly
|
||||
// 512 bytes.
|
||||
func (dbv DiskBlockDevice) WriteBlock(index uint16, data Block) error {
|
||||
if index >= dbv.blocks {
|
||||
return fmt.Errorf("device has %d blocks; tried to read block %d (index=%d)", dbv.blocks, index+1, index)
|
||||
}
|
||||
i := int(index) * 2
|
||||
sectors := int(dbv.lsd.Sectors())
|
||||
|
||||
track0 := i / sectors
|
||||
sector0 := i % sectors
|
||||
sector1 := sector0 + 1
|
||||
track1 := track0
|
||||
if sector1 == sectors {
|
||||
sector1 = 0
|
||||
track1++
|
||||
}
|
||||
|
||||
if err := dbv.lsd.WriteLogicalSector(byte(track0), byte(sector0), data[:256]); err != nil {
|
||||
return fmt.Errorf("error writing first half of block %d (t:%d s:%d): %v", index, track0, sector0, err)
|
||||
}
|
||||
if err := dbv.lsd.WriteLogicalSector(byte(track1), byte(sector1), data[256:]); err != nil {
|
||||
return fmt.Errorf("error writing second half of block %d (t:%d s:%d): %v", index, track1, sector1, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Blocks returns the number of blocks on the device.
|
||||
func (dbv DiskBlockDevice) Blocks() uint16 {
|
||||
return dbv.blocks
|
||||
}
|
||||
|
||||
// Write writes the device contents to the given Writer.
|
||||
func (dbv DiskBlockDevice) Write(w io.Writer) (int, error) {
|
||||
return dbv.lsd.Write(w)
|
||||
}
|
103
lib/disk/dsk.go
103
lib/disk/dsk.go
|
@ -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)
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// marshal.go contains helpers for marshaling sector structs to/from
|
||||
// disk and block structs to/from devices.
|
||||
|
||||
package disk
|
||||
|
||||
import "io"
|
||||
|
||||
// A ProDOS block.
|
||||
type Block [512]byte
|
||||
|
||||
// BlockDevice is the interface used to read and write devices by
|
||||
// logical block number.
|
||||
type BlockDevice interface {
|
||||
// ReadBlock reads a single block from the device. It always returns
|
||||
// 512 byes.
|
||||
ReadBlock(index uint16) (Block, error)
|
||||
// WriteBlock writes a single block to a device. It expects exactly
|
||||
// 512 bytes.
|
||||
WriteBlock(index uint16, data Block) error
|
||||
// Blocks returns the number of blocks on the device.
|
||||
Blocks() uint16
|
||||
// Write writes the device contents to the given Writer.
|
||||
Write(io.Writer) (int, error)
|
||||
}
|
||||
|
||||
// SectorSource is the interface for types that can marshal to sectors.
|
||||
type SectorSource interface {
|
||||
// ToSector marshals the sector struct to exactly 256 bytes.
|
||||
ToSector() ([]byte, error)
|
||||
// GetTrack returns the track that a sector struct was loaded from.
|
||||
GetTrack() byte
|
||||
// GetSector returns the sector that a sector struct was loaded from.
|
||||
GetSector() byte
|
||||
}
|
||||
|
||||
// SectorSink is the interface for types that can unmarshal from sectors.
|
||||
type SectorSink interface {
|
||||
// FromSector unmarshals the sector struct from bytes. Input is
|
||||
// expected to be exactly 256 bytes.
|
||||
FromSector(data []byte) error
|
||||
// SetTrack sets the track that a sector struct was loaded from.
|
||||
SetTrack(track byte)
|
||||
// SetSector sets the sector that a sector struct was loaded from.
|
||||
SetSector(sector byte)
|
||||
}
|
||||
|
||||
// UnmarshalLogicalSector reads a sector from a SectorDisk, and
|
||||
// unmarshals it into a SectorSink, setting its track and sector.
|
||||
func UnmarshalLogicalSector(d LogicalSectorDisk, ss SectorSink, track, sector byte) error {
|
||||
bytes, err := d.ReadLogicalSector(track, sector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ss.FromSector(bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
ss.SetTrack(track)
|
||||
ss.SetSector(sector)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalLogicalSector marshals a SectorSource to its sector on a
|
||||
// SectorDisk.
|
||||
func MarshalLogicalSector(d LogicalSectorDisk, ss SectorSource) error {
|
||||
track := ss.GetTrack()
|
||||
sector := ss.GetSector()
|
||||
bytes, err := ss.ToSector()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.WriteLogicalSector(track, sector, bytes)
|
||||
}
|
||||
|
||||
// BlockSource is the interface for types that can marshal to blocks.
|
||||
type BlockSource interface {
|
||||
// ToBlock marshals the block struct to exactly 512 bytes.
|
||||
ToBlock() (Block, error)
|
||||
// GetBlock returns the index that a block struct was loaded from.
|
||||
GetBlock() uint16
|
||||
}
|
||||
|
||||
// BlockSink is the interface for types that can unmarshal from blocks.
|
||||
type BlockSink interface {
|
||||
// FromBlock unmarshals the block struct from a Block. Input is
|
||||
// expected to be exactly 512 bytes.
|
||||
FromBlock(block Block) error
|
||||
// SetBlock sets the index that a block struct was loaded from.
|
||||
SetBlock(index uint16)
|
||||
}
|
||||
|
||||
// UnmarshalBlock reads a block from a BlockDevice, and unmarshals it
|
||||
// into a BlockSink, setting its index.
|
||||
func UnmarshalBlock(d BlockDevice, bs BlockSink, index uint16) error {
|
||||
block, err := d.ReadBlock(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bs.FromBlock(block); err != nil {
|
||||
return err
|
||||
}
|
||||
bs.SetBlock(index)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBlock marshals a BlockSource to its block on a BlockDevice.
|
||||
func MarshalBlock(d BlockDevice, bs BlockSource) error {
|
||||
index := bs.GetBlock()
|
||||
block, err := bs.ToBlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.WriteBlock(index, block)
|
||||
}
|
132
lib/disk/ops.go
132
lib/disk/ops.go
|
@ -1,132 +0,0 @@
|
|||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// ops.go contains the interfaces and helper functions for operating
|
||||
// on disk images logically: catalog, rename, delete, create files,
|
||||
// etc.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Descriptor describes a file's characteristics.
|
||||
type Descriptor struct {
|
||||
Name string
|
||||
Fullname string // If there's a more complete filename (eg. Super-Mon), put it here.
|
||||
Sectors int
|
||||
Blocks int
|
||||
Length int
|
||||
Locked bool
|
||||
Type Filetype
|
||||
}
|
||||
|
||||
// Operator is the interface that can operate on disks.
|
||||
type Operator interface {
|
||||
// Name returns the name of the operator.
|
||||
Name() string
|
||||
// HasSubdirs returns true if the underlying operating system on the
|
||||
// disk allows subdirectories.
|
||||
HasSubdirs() bool
|
||||
// Catalog returns a catalog of disk entries. subdir should be empty
|
||||
// for operating systems that do not support subdirectories.
|
||||
Catalog(subdir string) ([]Descriptor, error)
|
||||
// GetFile retrieves a file by name.
|
||||
GetFile(filename string) (FileInfo, error)
|
||||
// Delete deletes a file by name. It returns true if the file was
|
||||
// deleted, false if it didn't exist.
|
||||
Delete(filename string) (bool, error)
|
||||
// PutFile writes a file by name. If the file exists and overwrite
|
||||
// is false, it returns with an error. Otherwise it returns true if
|
||||
// an existing file was overwritten.
|
||||
PutFile(fileInfo FileInfo, overwrite bool) (existed bool, err error)
|
||||
// Write writes the underlying disk to the given writer.
|
||||
Write(io.Writer) (int, error)
|
||||
}
|
||||
|
||||
// FileInfo represents a file descriptor plus the content.
|
||||
type FileInfo struct {
|
||||
Descriptor Descriptor
|
||||
Data []byte
|
||||
StartAddress uint16
|
||||
}
|
||||
|
||||
// diskOperatorFactory is the type of functions that accept a SectorDisk,
|
||||
// and may return an Operator interface to operate on it.
|
||||
type diskOperatorFactory func(SectorDisk) (Operator, error)
|
||||
|
||||
// diskOperatorFactories is the map of currently-registered disk
|
||||
// operator factories.
|
||||
var diskOperatorFactories map[string]diskOperatorFactory
|
||||
|
||||
func init() {
|
||||
diskOperatorFactories = make(map[string]diskOperatorFactory)
|
||||
}
|
||||
|
||||
// RegisterDiskOperatorFactory registers a disk operator factory with
|
||||
// the given name: a function that accepts a SectorDisk, and may
|
||||
// return an Operator. It doesn't lock diskOperatorFactories: it is
|
||||
// expected to be called only from package `init` functions.
|
||||
func RegisterDiskOperatorFactory(name string, factory diskOperatorFactory) {
|
||||
diskOperatorFactories[name] = factory
|
||||
}
|
||||
|
||||
// OperatorForDisk returns an Operator for the given SectorDisk, if possible.
|
||||
func OperatorForDisk(sd SectorDisk) (Operator, error) {
|
||||
if len(diskOperatorFactories) == 0 {
|
||||
return nil, errors.New("Cannot find an operator matching the given disk image (none registered)")
|
||||
}
|
||||
for _, factory := range diskOperatorFactories {
|
||||
if operator, err := factory(sd); err == nil {
|
||||
return operator, nil
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(diskOperatorFactories))
|
||||
for name := range diskOperatorFactories {
|
||||
names = append(names, `"`+name+`"`)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return nil, fmt.Errorf("Cannot find a disk operator matching the given disk image (tried %s)", strings.Join(names, ", "))
|
||||
}
|
||||
|
||||
// deviceOperatorFactory is the type of functions that accept a BlockDevice,
|
||||
// and may return an Operator interface to operate on it.
|
||||
type deviceOperatorFactory func(BlockDevice) (Operator, error)
|
||||
|
||||
// deviceOperatorFactories is the map of currently-registered device
|
||||
// operator factories.
|
||||
var deviceOperatorFactories map[string]deviceOperatorFactory
|
||||
|
||||
func init() {
|
||||
deviceOperatorFactories = make(map[string]deviceOperatorFactory)
|
||||
}
|
||||
|
||||
// RegisterDeviceOperatorFactory registers a device operator factory with
|
||||
// the given name: a function that accepts a BlockDevice, and may
|
||||
// return an Operator. It doesn't lock deviceOperatorFactories: it is
|
||||
// expected to be called only from package `init` functions.
|
||||
func RegisterDeviceOperatorFactory(name string, factory deviceOperatorFactory) {
|
||||
deviceOperatorFactories[name] = factory
|
||||
}
|
||||
|
||||
// OperatorForDevice returns an Operator for the given BlockDevice, if possible.
|
||||
func OperatorForDevice(sd BlockDevice) (Operator, error) {
|
||||
if len(deviceOperatorFactories) == 0 {
|
||||
return nil, errors.New("Cannot find an operator matching the given device image (none registered)")
|
||||
}
|
||||
for _, factory := range deviceOperatorFactories {
|
||||
if operator, err := factory(sd); err == nil {
|
||||
return operator, nil
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(deviceOperatorFactories))
|
||||
for name := range deviceOperatorFactories {
|
||||
names = append(names, `"`+name+`"`)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return nil, fmt.Errorf("Cannot find a device operator matching the given device image (tried %s)", strings.Join(names, ", "))
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// Package helpers contains various routines used to help cobra
|
||||
// commands stay succinct.
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// FileContentsOrStdIn returns the contents of a file, unless the file
|
||||
// is "-", in which case it reads from stdin.
|
||||
func FileContentsOrStdIn(s string) ([]byte, error) {
|
||||
if s == "-" {
|
||||
return ioutil.ReadAll(os.Stdin)
|
||||
}
|
||||
return ioutil.ReadFile(s)
|
||||
}
|
91
main.go
91
main.go
|
@ -1,16 +1,91 @@
|
|||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
// Copyright ©2021 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/zellyn/diskii/cmd"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
// Import disk operator factories for DOS3 and Super-Mon
|
||||
_ "github.com/zellyn/diskii/lib/dos3"
|
||||
_ "github.com/zellyn/diskii/lib/prodos"
|
||||
_ "github.com/zellyn/diskii/lib/supermon"
|
||||
"github.com/zellyn/diskii/cmd"
|
||||
"github.com/zellyn/diskii/dos3"
|
||||
"github.com/zellyn/diskii/prodos"
|
||||
"github.com/zellyn/diskii/supermon"
|
||||
"github.com/zellyn/diskii/types"
|
||||
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
var cli struct {
|
||||
Debug int `kong:"short='v',type='counter',help='Enable debug mode.'"`
|
||||
|
||||
Ls cmd.LsCmd `cmd:"" aliases:"list,cat,catalog" help:"List files/directories on a disk."`
|
||||
Reorder cmd.ReorderCmd `cmd:"" help:"Convert between DOS-order and ProDOS-order disk images."`
|
||||
Filetypes cmd.FiletypesCmd `cmd:"" help:"Print a list of filetypes understood by diskii."`
|
||||
Put cmd.PutCmd `cmd:"" help:"Put the raw contents of a file onto a disk."`
|
||||
Rm cmd.DeleteCmd `cmd:"" aliases:"delete" help:"Delete a file."`
|
||||
Dump cmd.DumpCmd `cmd:"" help:"Dump the raw contents of a file."`
|
||||
Nakedos cmd.NakedOSCmd `cmd:"" help:"Work with NakedOS-format disks."`
|
||||
Mksd cmd.SDCmd `cmd:"" help:"Create a “Standard Delivery” disk image containing a binary."`
|
||||
Applesoft cmd.ApplesoftCmd `cmd:"" help:"Work with Applesoft BASIC files."`
|
||||
}
|
||||
|
||||
func run() error {
|
||||
ctx := kong.Parse(&cli,
|
||||
kong.Name("diskii"),
|
||||
kong.Description("A commandline tool for working with Apple II disk images."),
|
||||
// kong.UsageOnError(),
|
||||
kong.ConfigureHelp(kong.HelpOptions{
|
||||
Compact: true,
|
||||
Summary: true,
|
||||
}),
|
||||
kong.NamedMapper("anybaseuint16", hexUint16Mapper{}),
|
||||
)
|
||||
|
||||
globals := &types.Globals{
|
||||
Debug: cli.Debug,
|
||||
DiskOperatorFactories: []types.OperatorFactory{
|
||||
dos3.OperatorFactory{},
|
||||
supermon.OperatorFactory{},
|
||||
prodos.OperatorFactory{},
|
||||
},
|
||||
}
|
||||
// Call the Run() method of the selected parsed command.
|
||||
return ctx.Run(globals)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type hexUint16Mapper struct{}
|
||||
|
||||
func (h hexUint16Mapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
|
||||
t, err := ctx.Scan.PopValue("int")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sv string
|
||||
switch v := t.Value.(type) {
|
||||
case string:
|
||||
sv = v
|
||||
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
sv = fmt.Sprintf("%v", v)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("expected an int but got %q (%T)", t, t.Value)
|
||||
}
|
||||
n, err := strconv.ParseUint(sv, 0, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expected a valid %d bit uint but got %q", 16, sv)
|
||||
}
|
||||
target.SetUint(n)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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]]
|
|
@ -7,9 +7,10 @@ package prodos
|
|||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
// Storage types.
|
||||
|
@ -59,8 +60,10 @@ func (bp bitmapPart) ToBlock() (disk.Block, error) {
|
|||
return bp.data, nil
|
||||
}
|
||||
|
||||
// VolumeBitMap represents a volume bitmap.
|
||||
type VolumeBitMap []bitmapPart
|
||||
|
||||
// NewVolumeBitMap returns a volume bitmap of the given size.
|
||||
func NewVolumeBitMap(startBlock uint16, blocks uint16) VolumeBitMap {
|
||||
vbm := VolumeBitMap(make([]bitmapPart, (blocks+(512*8)-1)/(512*8)))
|
||||
for i := range vbm {
|
||||
|
@ -72,10 +75,12 @@ func NewVolumeBitMap(startBlock uint16, blocks uint16) VolumeBitMap {
|
|||
return vbm
|
||||
}
|
||||
|
||||
// MarkUsed marks the given block as used.
|
||||
func (vbm VolumeBitMap) MarkUsed(block uint16) {
|
||||
vbm.mark(block, false)
|
||||
}
|
||||
|
||||
// MarkUnused marks the given block as free.
|
||||
func (vbm VolumeBitMap) MarkUnused(block uint16) {
|
||||
vbm.mark(block, true)
|
||||
}
|
||||
|
@ -104,21 +109,21 @@ func (vbm VolumeBitMap) IsFree(block uint16) bool {
|
|||
|
||||
// readVolumeBitMap reads the entire volume bitmap from a block
|
||||
// device.
|
||||
func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, error) {
|
||||
blocks := bd.Blocks() / 4096
|
||||
func readVolumeBitMap(devicebytes []byte, startBlock uint16) (VolumeBitMap, error) {
|
||||
blocks := uint16(len(devicebytes) / 512 / 4096)
|
||||
vbm := NewVolumeBitMap(startBlock, blocks)
|
||||
for i := 0; i < len(vbm); i++ {
|
||||
if err := disk.UnmarshalBlock(bd, &vbm[i], vbm[i].GetBlock()); err != nil {
|
||||
if err := disk.UnmarshalBlock(devicebytes, &vbm[i], vbm[i].GetBlock()); err != nil {
|
||||
return nil, fmt.Errorf("cannot read block %d (device block %d) of Volume Bit Map: %v", i, vbm[i].GetBlock(), err)
|
||||
}
|
||||
}
|
||||
return VolumeBitMap(vbm), nil
|
||||
return vbm, nil
|
||||
}
|
||||
|
||||
// Write writes the Volume Bit Map to a block device.
|
||||
func (vbm VolumeBitMap) Write(bd disk.BlockDevice) error {
|
||||
func (vbm VolumeBitMap) Write(devicebytes []byte) error {
|
||||
for i, bp := range vbm {
|
||||
if err := disk.MarshalBlock(bd, bp); err != nil {
|
||||
if err := disk.MarshalBlock(devicebytes, bp); err != nil {
|
||||
return fmt.Errorf("cannot write block %d (device block %d) of Volume Bit Map: %v", i, bp.GetBlock(), err)
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +152,7 @@ func (dt *DateTime) fromBytes(b []byte) {
|
|||
dt.HM[1] = b[3]
|
||||
}
|
||||
|
||||
// Validate checks a DateTime for problems, returning a slice of errors
|
||||
// Validate checks a DateTime for problems, returning a slice of errors.
|
||||
func (dt DateTime) Validate(fieldDescription string) (errors []error) {
|
||||
if dt.HM[0] >= 24 {
|
||||
errors = append(errors, fmt.Errorf("%s expects hour<24; got %d", fieldDescription, dt.HM[0]))
|
||||
|
@ -208,7 +213,7 @@ func (vdkb VolumeDirectoryKeyBlock) Validate() (errors []error) {
|
|||
errors = append(errors, desc.Validate()...)
|
||||
}
|
||||
if vdkb.Extra != 0 {
|
||||
errors = append(errors, fmt.Errorf("Expected last byte of Volume Directory Key Block == 0x0; got 0x%02x", vdkb.Extra))
|
||||
errors = append(errors, fmt.Errorf("expected last byte of Volume Directory Key Block == 0x0; got 0x%02x", vdkb.Extra))
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
@ -255,11 +260,12 @@ func (vdb VolumeDirectoryBlock) Validate() (errors []error) {
|
|||
errors = append(errors, desc.Validate()...)
|
||||
}
|
||||
if vdb.Extra != 0 {
|
||||
errors = append(errors, fmt.Errorf("Expected last byte of Volume Directory Block == 0x0; got 0x%02x", vdb.Extra))
|
||||
errors = append(errors, fmt.Errorf("expected last byte of Volume Directory Block == 0x0; got 0x%02x", vdb.Extra))
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// VolumeDirectoryHeader represents a volume directory header.
|
||||
type VolumeDirectoryHeader struct {
|
||||
TypeAndNameLength byte // Storage type (top four bits) and volume name length (lower four).
|
||||
VolumeName [15]byte // Volume name (actual length defined in TypeAndNameLength)
|
||||
|
@ -318,14 +324,20 @@ func (vdh VolumeDirectoryHeader) Validate() (errors []error) {
|
|||
return errors
|
||||
}
|
||||
|
||||
// Access represents a level of file access.
|
||||
type Access byte
|
||||
|
||||
const (
|
||||
AccessReadable Access = 0x01
|
||||
AccessWritable Access = 0x02
|
||||
// AccessReadable denotes a file as readable.
|
||||
AccessReadable Access = 0x01
|
||||
// AccessWritable denotes a file as writable.
|
||||
AccessWritable Access = 0x02
|
||||
// AccessChangedSinceBackup is (I think) always true on real disks.
|
||||
AccessChangedSinceBackup Access = 0x20
|
||||
AccessRenamable Access = 0x40
|
||||
AccessDestroyable Access = 0x80
|
||||
// AccessRenamable denotes a file as renamable.
|
||||
AccessRenamable Access = 0x40
|
||||
// AccessDestroyable denotes a file as deletable.
|
||||
AccessDestroyable Access = 0x80
|
||||
)
|
||||
|
||||
// FileDescriptor is the entry in the volume directory for a file or
|
||||
|
@ -336,7 +348,7 @@ type FileDescriptor struct {
|
|||
FileType byte // ProDOS / SOS filetype
|
||||
KeyPointer uint16 // block number of key block for file
|
||||
BlocksUsed uint16 // Total number of blocks used including index blocks and data blocks. For a subdirectory, the number of directory blocks
|
||||
Eof [3]byte // 3-byte offset of EOF from first byte. For sequential files, just the length
|
||||
EOF [3]byte // 3-byte offset of EOF from first byte. For sequential files, just the length
|
||||
Creation DateTime // Date and time of of file creation
|
||||
Version byte
|
||||
MinVersion byte
|
||||
|
@ -351,14 +363,14 @@ type FileDescriptor struct {
|
|||
HeaderPointer uint16 // Block number of the key block for the directory which describes this file.
|
||||
}
|
||||
|
||||
// descriptor returns a disk.Descriptor for a FileDescriptor.
|
||||
func (fd FileDescriptor) descriptor() disk.Descriptor {
|
||||
desc := disk.Descriptor{
|
||||
// descriptor returns a types.Descriptor for a FileDescriptor.
|
||||
func (fd FileDescriptor) descriptor() types.Descriptor {
|
||||
desc := types.Descriptor{
|
||||
Name: fd.Name(),
|
||||
Blocks: int(fd.BlocksUsed),
|
||||
Length: int(fd.Eof[0]) + int(fd.Eof[1])<<8 + int(fd.Eof[2])<<16,
|
||||
Locked: false, // TODO(zellyn): use prodos-style access in disk.Descriptor
|
||||
Type: disk.Filetype(fd.FileType),
|
||||
Length: int(fd.EOF[0]) + int(fd.EOF[1])<<8 + int(fd.EOF[2])<<16,
|
||||
Locked: false, // TODO(zellyn): use prodos-style access in types.Descriptor
|
||||
Type: types.Filetype(fd.FileType),
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
@ -381,7 +393,7 @@ func (fd FileDescriptor) toBytes() []byte {
|
|||
buf[0x10] = fd.FileType
|
||||
binary.LittleEndian.PutUint16(buf[0x11:0x13], fd.KeyPointer)
|
||||
binary.LittleEndian.PutUint16(buf[0x13:0x15], fd.BlocksUsed)
|
||||
copyBytes(buf[0x15:0x18], fd.Eof[:])
|
||||
copyBytes(buf[0x15:0x18], fd.EOF[:])
|
||||
copyBytes(buf[0x18:0x1c], fd.Creation.toBytes())
|
||||
buf[0x1c] = fd.Version
|
||||
buf[0x1d] = fd.MinVersion
|
||||
|
@ -402,7 +414,7 @@ func (fd *FileDescriptor) fromBytes(buf []byte) {
|
|||
fd.FileType = buf[0x10]
|
||||
fd.KeyPointer = binary.LittleEndian.Uint16(buf[0x11:0x13])
|
||||
fd.BlocksUsed = binary.LittleEndian.Uint16(buf[0x13:0x15])
|
||||
copyBytes(fd.Eof[:], buf[0x15:0x18])
|
||||
copyBytes(fd.EOF[:], buf[0x15:0x18])
|
||||
fd.Creation.fromBytes(buf[0x18:0x1c])
|
||||
fd.Version = buf[0x1c]
|
||||
fd.MinVersion = buf[0x1d]
|
||||
|
@ -419,7 +431,7 @@ func (fd FileDescriptor) Validate() (errors []error) {
|
|||
return errors
|
||||
}
|
||||
|
||||
// An index block contains 256 16-bit block numbers, pointing to other
|
||||
// IndexBlock is an index block, containing 256 16-bit block numbers, pointing to other
|
||||
// blocks. The LSBs are stored in the first half, MSBs in the second.
|
||||
type IndexBlock disk.Block
|
||||
|
||||
|
@ -484,7 +496,7 @@ func (skb SubdirectoryKeyBlock) Validate() (errors []error) {
|
|||
errors = append(errors, desc.Validate()...)
|
||||
}
|
||||
if skb.Extra != 0 {
|
||||
errors = append(errors, fmt.Errorf("Expected last byte of Subdirectory Key Block == 0x0; got 0x%02x", skb.Extra))
|
||||
errors = append(errors, fmt.Errorf("expected last byte of Subdirectory Key Block == 0x0; got 0x%02x", skb.Extra))
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
@ -531,11 +543,12 @@ func (sb SubdirectoryBlock) Validate() (errors []error) {
|
|||
errors = append(errors, desc.Validate()...)
|
||||
}
|
||||
if sb.Extra != 0 {
|
||||
errors = append(errors, fmt.Errorf("Expected last byte of Subdirectory Block == 0x0; got 0x%02x", sb.Extra))
|
||||
errors = append(errors, fmt.Errorf("expected last byte of Subdirectory Block == 0x0; got 0x%02x", sb.Extra))
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// SubdirectoryHeader represents a subdirectory header.
|
||||
type SubdirectoryHeader struct {
|
||||
TypeAndNameLength byte // Storage type (top four bits) and subdirectory name length (lower four).
|
||||
SubdirectoryName [15]byte // Subdirectory name (actual length defined in TypeAndNameLength)
|
||||
|
@ -597,7 +610,7 @@ func (sh *SubdirectoryHeader) fromBytes(buf []byte) {
|
|||
// Validate validates a SubdirectoryHeader for valid values.
|
||||
func (sh SubdirectoryHeader) Validate() (errors []error) {
|
||||
if sh.SeventyFive != 0x75 {
|
||||
errors = append(errors, fmt.Errorf("Byte after subdirectory name %q should be 0x75; got 0x%02x", sh.Name(), sh.SeventyFive))
|
||||
errors = append(errors, fmt.Errorf("byte after subdirectory name %q should be 0x75; got 0x%02x", sh.Name(), sh.SeventyFive))
|
||||
}
|
||||
errors = append(errors, sh.Creation.Validate(fmt.Sprintf("subdirectory %q header creation date/time", sh.Name()))...)
|
||||
return errors
|
||||
|
@ -611,11 +624,12 @@ func (sh SubdirectoryHeader) Name() string {
|
|||
// Volume is the in-memory representation of a device's volume
|
||||
// information.
|
||||
type Volume struct {
|
||||
keyBlock *VolumeDirectoryKeyBlock
|
||||
blocks []*VolumeDirectoryBlock
|
||||
bitmap *VolumeBitMap
|
||||
subdirsByBlock map[uint16]*Subdirectory
|
||||
subdirsByName map[string]*Subdirectory
|
||||
keyBlock *VolumeDirectoryKeyBlock // The key block describing the entire volume
|
||||
blocks []*VolumeDirectoryBlock // The blocks in the top-level volume
|
||||
bitmap *VolumeBitMap // Bitmap of which blocks are free
|
||||
subdirsByBlock map[uint16]*Subdirectory // A mapping of block number to subdirectory object
|
||||
subdirsByName map[string]*Subdirectory // a mapping of string to subdirectory object
|
||||
firstSubdirBlocks map[uint16]uint16 // A mapping of later dir/subdir blocks to the first one in the chain
|
||||
}
|
||||
|
||||
// Subdirectory is the in-memory representation of a single
|
||||
|
@ -652,46 +666,80 @@ func (v Volume) subdirDescriptors() []FileDescriptor {
|
|||
|
||||
// readVolume reads the entire volume and subdirectories from a device
|
||||
// into memory.
|
||||
func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) {
|
||||
func readVolume(devicebytes []byte, keyBlock uint16, debug int) (Volume, error) {
|
||||
v := Volume{
|
||||
keyBlock: &VolumeDirectoryKeyBlock{},
|
||||
subdirsByBlock: make(map[uint16]*Subdirectory),
|
||||
subdirsByName: make(map[string]*Subdirectory),
|
||||
keyBlock: &VolumeDirectoryKeyBlock{},
|
||||
subdirsByBlock: make(map[uint16]*Subdirectory),
|
||||
subdirsByName: make(map[string]*Subdirectory),
|
||||
firstSubdirBlocks: make(map[uint16]uint16),
|
||||
}
|
||||
|
||||
if err := disk.UnmarshalBlock(bd, v.keyBlock, keyBlock); err != nil {
|
||||
if err := disk.UnmarshalBlock(devicebytes, v.keyBlock, keyBlock); err != nil {
|
||||
return v, fmt.Errorf("cannot read first block of volume directory (block %d): %v", keyBlock, err)
|
||||
}
|
||||
// if debug {
|
||||
// fmt.Fprintf(os.Stderr, "keyblock: %#v\n", v.keyBlock)
|
||||
// }
|
||||
|
||||
if vbm, err := readVolumeBitMap(bd, v.keyBlock.Header.BitMapPointer); err != nil {
|
||||
vbm, err := readVolumeBitMap(devicebytes, v.keyBlock.Header.BitMapPointer)
|
||||
if err != nil {
|
||||
return v, err
|
||||
} else {
|
||||
v.bitmap = &vbm
|
||||
}
|
||||
|
||||
v.bitmap = &vbm
|
||||
|
||||
// if debug {
|
||||
// fmt.Fprintf(os.Stderr, "volume bitmap: %#v\n", v.bitmap)
|
||||
// }
|
||||
|
||||
for block := v.keyBlock.Next; block != 0; block = v.blocks[len(v.blocks)-1].Next {
|
||||
vdb := VolumeDirectoryBlock{}
|
||||
if err := disk.UnmarshalBlock(bd, &vdb, block); err != nil {
|
||||
if err := disk.UnmarshalBlock(devicebytes, &vdb, block); err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.blocks = append(v.blocks, &vdb)
|
||||
v.firstSubdirBlocks[block] = keyBlock
|
||||
if debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, " firstSubdirBlocks[%d] → %d\n", block, keyBlock)
|
||||
}
|
||||
// if debug {
|
||||
// fmt.Fprintf(os.Stderr, "block: %#v\n", vdb)
|
||||
// }
|
||||
}
|
||||
|
||||
sdds := v.subdirDescriptors()
|
||||
if debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, "got %d top-level subdir descriptors\n", len(sdds))
|
||||
}
|
||||
|
||||
for i := 0; i < len(sdds); i++ {
|
||||
sdd := sdds[i]
|
||||
sub, err := readSubdirectory(bd, sdd)
|
||||
sub, err := readSubdirectory(devicebytes, sdd)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.subdirsByBlock[sdd.KeyPointer] = &sub
|
||||
if debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, " subdirsByBlock[%d] → %q\n", sdd.KeyPointer, sub.keyBlock.Header.Name())
|
||||
}
|
||||
sdds = append(sdds, sub.subdirDescriptors()...)
|
||||
for _, block := range sub.blocks {
|
||||
v.firstSubdirBlocks[block.block] = sdd.KeyPointer
|
||||
if debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, " firstSubdirBlocks[%d] → %d\n", block.block, sdd.KeyPointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
if debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, "got %d total subdir descriptors\n", len(sdds))
|
||||
}
|
||||
|
||||
for _, sd := range v.subdirsByBlock {
|
||||
name := sd.keyBlock.Header.Name()
|
||||
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, v.subdirsByBlock)
|
||||
if debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, "processing subdir %q\n", name)
|
||||
}
|
||||
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, v.subdirsByBlock, v.firstSubdirBlocks)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
@ -701,6 +749,12 @@ func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) {
|
|||
|
||||
v.subdirsByName[name] = sd
|
||||
}
|
||||
if debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, "subdirsByName:\n")
|
||||
for k := range v.subdirsByName {
|
||||
fmt.Fprintf(os.Stderr, " %s\n", k)
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
|
@ -729,16 +783,23 @@ func (s Subdirectory) subdirDescriptors() []FileDescriptor {
|
|||
return descs
|
||||
}
|
||||
|
||||
// fullDirName returns the full recursive directory name of the given parent directory.
|
||||
func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory) (string, error) {
|
||||
if parentDirectoryBlock == keyBlock {
|
||||
// parentDirName returns the full recursive directory name of the given parent directory.
|
||||
func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory, firstSubdirBlockMap map[uint16]uint16) (string, error) {
|
||||
if parentDirectoryBlock == keyBlock || firstSubdirBlockMap[parentDirectoryBlock] == keyBlock {
|
||||
return "", nil
|
||||
}
|
||||
sd := subdirMap[parentDirectoryBlock]
|
||||
if sd == nil {
|
||||
return "", fmt.Errorf("Unable to find subdirectory for block %d", parentDirectoryBlock)
|
||||
parentFirstBlock, ok := firstSubdirBlockMap[parentDirectoryBlock]
|
||||
if ok {
|
||||
sd = subdirMap[parentFirstBlock]
|
||||
}
|
||||
}
|
||||
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap)
|
||||
if sd == nil {
|
||||
return "", fmt.Errorf("unable to find subdirectory for block %d", parentDirectoryBlock)
|
||||
}
|
||||
|
||||
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap, firstSubdirBlockMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -751,18 +812,18 @@ func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[u
|
|||
|
||||
// readSubdirectory reads a single subdirectory from a device into
|
||||
// memory.
|
||||
func readSubdirectory(bd disk.BlockDevice, fd FileDescriptor) (Subdirectory, error) {
|
||||
func readSubdirectory(devicebytes []byte, fd FileDescriptor) (Subdirectory, error) {
|
||||
s := Subdirectory{
|
||||
keyBlock: &SubdirectoryKeyBlock{},
|
||||
}
|
||||
|
||||
if err := disk.UnmarshalBlock(bd, s.keyBlock, fd.KeyPointer); err != nil {
|
||||
if err := disk.UnmarshalBlock(devicebytes, s.keyBlock, fd.KeyPointer); err != nil {
|
||||
return s, fmt.Errorf("cannot read first block of subdirectory %q (block %d): %v", fd.Name(), fd.KeyPointer, err)
|
||||
}
|
||||
|
||||
for block := s.keyBlock.Next; block != 0; block = s.blocks[len(s.blocks)-1].Next {
|
||||
sdb := SubdirectoryBlock{}
|
||||
if err := disk.UnmarshalBlock(bd, &sdb, block); err != nil {
|
||||
if err := disk.UnmarshalBlock(devicebytes, &sdb, block); err != nil {
|
||||
return s, err
|
||||
}
|
||||
s.blocks = append(s.blocks, &sdb)
|
||||
|
@ -783,10 +844,11 @@ func copyBytes(dst, src []byte) int {
|
|||
// operator is a disk.Operator - an interface for performing
|
||||
// high-level operations on files and directories.
|
||||
type operator struct {
|
||||
dev disk.BlockDevice
|
||||
data []byte
|
||||
debug int
|
||||
}
|
||||
|
||||
var _ disk.Operator = operator{}
|
||||
var _ types.Operator = operator{}
|
||||
|
||||
// operatorName is the keyword name for the operator that undestands
|
||||
// prodos disks/devices.
|
||||
|
@ -805,14 +867,16 @@ func (o operator) HasSubdirs() bool {
|
|||
|
||||
// Catalog returns a catalog of disk entries. subdir should be empty
|
||||
// for operating systems that do not support subdirectories.
|
||||
func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
|
||||
|
||||
vol, err := readVolume(o.dev, 2)
|
||||
func (o operator) Catalog(subdir string) ([]types.Descriptor, error) {
|
||||
if o.debug > 1 {
|
||||
fmt.Fprintf(os.Stderr, "Catalog of %q\n", subdir)
|
||||
}
|
||||
vol, err := readVolume(o.data, 2, o.debug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error reading volume: %w", err)
|
||||
}
|
||||
|
||||
var result []disk.Descriptor
|
||||
var result []types.Descriptor
|
||||
|
||||
if subdir == "" {
|
||||
for _, desc := range vol.descriptors() {
|
||||
|
@ -837,8 +901,8 @@ func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
|
|||
}
|
||||
|
||||
// GetFile retrieves a file by name.
|
||||
func (o operator) GetFile(filename string) (disk.FileInfo, error) {
|
||||
return disk.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName)
|
||||
func (o operator) GetFile(filename string) (types.FileInfo, error) {
|
||||
return types.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName)
|
||||
}
|
||||
|
||||
// Delete deletes a file by name. It returns true if the file was
|
||||
|
@ -850,37 +914,43 @@ func (o operator) Delete(filename string) (bool, error) {
|
|||
// PutFile writes a file by name. If the file exists and overwrite
|
||||
// is false, it returns with an error. Otherwise it returns true if
|
||||
// an existing file was overwritten.
|
||||
func (o operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) {
|
||||
func (o operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) {
|
||||
return false, fmt.Errorf("%s doesn't implement PutFile yet", operatorName)
|
||||
}
|
||||
|
||||
// Write writes the underlying device blocks to the given writer.
|
||||
func (o operator) Write(w io.Writer) (int, error) {
|
||||
return o.dev.Write(w)
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
func (o operator) DiskOrder() types.DiskOrder {
|
||||
return types.DiskOrderPO
|
||||
}
|
||||
|
||||
// deviceOperatorFactory is the factory that returns prodos operators
|
||||
// given device images.
|
||||
func deviceOperatorFactory(bd disk.BlockDevice) (disk.Operator, error) {
|
||||
op := operator{dev: bd}
|
||||
_, err := op.Catalog("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err)
|
||||
}
|
||||
return op, nil
|
||||
// GetBytes returns the disk image bytes, in logical order.
|
||||
func (o operator) GetBytes() []byte {
|
||||
return o.data
|
||||
}
|
||||
|
||||
// diskOperatorFactory is the factory that returns dos3 operators
|
||||
// given disk images.
|
||||
func diskOperatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
|
||||
bd, err := disk.BlockDeviceFromSectorDisk(sd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return deviceOperatorFactory(bd)
|
||||
// OperatorFactory is a types.OperatorFactory for ProDos disks.
|
||||
type OperatorFactory struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
disk.RegisterDeviceOperatorFactory(operatorName, deviceOperatorFactory)
|
||||
disk.RegisterDiskOperatorFactory(operatorName, diskOperatorFactory)
|
||||
// Name returns the name of the operator.
|
||||
func (of OperatorFactory) Name() string {
|
||||
return operatorName
|
||||
}
|
||||
|
||||
// SeemsToMatch returns true if the []byte disk image seems to match the
|
||||
// system of this operator.
|
||||
func (of OperatorFactory) SeemsToMatch(devicebytes []byte, debug int) bool {
|
||||
// For now, just return true if we can run Catalog successfully.
|
||||
_, err := readVolume(devicebytes, 2, debug)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Operator returns an Operator for the []byte disk image.
|
||||
func (of OperatorFactory) Operator(devicebytes []byte, debug int) (types.Operator, error) {
|
||||
return operator{data: devicebytes, debug: debug}, nil
|
||||
}
|
||||
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
func (of OperatorFactory) DiskOrder() types.DiskOrder {
|
||||
return operator{}.DiskOrder()
|
||||
}
|
|
@ -6,12 +6,12 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
)
|
||||
|
||||
func randomBlock() disk.Block {
|
||||
var b1 disk.Block
|
||||
rand.Read(b1[:])
|
||||
_, _ = rand.Read(b1[:])
|
||||
return b1
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,10 @@ func randomBlock() disk.Block {
|
|||
func TestVolumeDirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
|
||||
b1 := randomBlock()
|
||||
vdkb := &VolumeDirectoryKeyBlock{}
|
||||
vdkb.FromBlock(b1)
|
||||
err := vdkb.FromBlock(b1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b2, err := vdkb.ToBlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -28,7 +31,10 @@ func TestVolumeDirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
|
|||
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
|
||||
}
|
||||
vdkb2 := &VolumeDirectoryKeyBlock{}
|
||||
vdkb2.FromBlock(b2)
|
||||
err = vdkb2.FromBlock(b2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *vdkb != *vdkb2 {
|
||||
t.Errorf("Structs differ: %v != %v", vdkb, vdkb2)
|
||||
}
|
||||
|
@ -38,7 +44,10 @@ func TestVolumeDirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
|
|||
func TestVolumeDirectoryBlockMarshalRoundtrip(t *testing.T) {
|
||||
b1 := randomBlock()
|
||||
vdb := &VolumeDirectoryBlock{}
|
||||
vdb.FromBlock(b1)
|
||||
err := vdb.FromBlock(b1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b2, err := vdb.ToBlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -47,7 +56,10 @@ func TestVolumeDirectoryBlockMarshalRoundtrip(t *testing.T) {
|
|||
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
|
||||
}
|
||||
vdb2 := &VolumeDirectoryBlock{}
|
||||
vdb2.FromBlock(b2)
|
||||
err = vdb2.FromBlock(b2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *vdb != *vdb2 {
|
||||
t.Errorf("Structs differ: %v != %v", vdb, vdb2)
|
||||
}
|
||||
|
@ -57,7 +69,10 @@ func TestVolumeDirectoryBlockMarshalRoundtrip(t *testing.T) {
|
|||
func TestSubdirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
|
||||
b1 := randomBlock()
|
||||
skb := &SubdirectoryKeyBlock{}
|
||||
skb.FromBlock(b1)
|
||||
err := skb.FromBlock(b1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b2, err := skb.ToBlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -66,7 +81,10 @@ func TestSubdirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
|
|||
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
|
||||
}
|
||||
skb2 := &SubdirectoryKeyBlock{}
|
||||
skb2.FromBlock(b2)
|
||||
err = skb2.FromBlock(b2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *skb != *skb2 {
|
||||
t.Errorf("Structs differ: %v != %v", skb, skb2)
|
||||
}
|
||||
|
@ -76,7 +94,10 @@ func TestSubdirectoryKeyBlockMarshalRoundtrip(t *testing.T) {
|
|||
func TestSubdirectoryBlockMarshalRoundtrip(t *testing.T) {
|
||||
b1 := randomBlock()
|
||||
sb := &SubdirectoryBlock{}
|
||||
sb.FromBlock(b1)
|
||||
err := sb.FromBlock(b1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b2, err := sb.ToBlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -85,7 +106,10 @@ func TestSubdirectoryBlockMarshalRoundtrip(t *testing.T) {
|
|||
t.Fatalf("Blocks differ: %s", strings.Join(pretty.Diff(b1[:], b2[:]), "; "))
|
||||
}
|
||||
sb2 := &SubdirectoryBlock{}
|
||||
sb2.FromBlock(b2)
|
||||
err = sb2.FromBlock(b2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *sb != *sb2 {
|
||||
t.Errorf("Structs differ: %v != %v", sb, sb2)
|
||||
}
|
|
@ -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",
|
||||
})
|
||||
}
|
|
@ -7,12 +7,12 @@ package supermon
|
|||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/lib/errors"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/errors"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -29,17 +29,17 @@ const (
|
|||
type SectorMap []byte
|
||||
|
||||
// LoadSectorMap loads a NakedOS sector map.
|
||||
func LoadSectorMap(sd disk.SectorDisk) (SectorMap, error) {
|
||||
func LoadSectorMap(diskbytes []byte) (SectorMap, error) {
|
||||
sm := SectorMap(make([]byte, 560))
|
||||
sector09, err := sd.ReadPhysicalSector(0, 9)
|
||||
sector09, err := disk.ReadSector(diskbytes, 0, 9)
|
||||
if err != nil {
|
||||
return sm, err
|
||||
}
|
||||
sector0A, err := sd.ReadPhysicalSector(0, 0xA)
|
||||
sector0A, err := disk.ReadSector(diskbytes, 0, 0xA)
|
||||
if err != nil {
|
||||
return sm, err
|
||||
}
|
||||
sector0B, err := sd.ReadPhysicalSector(0, 0xB)
|
||||
sector0B, err := disk.ReadSector(diskbytes, 0, 0xB)
|
||||
if err != nil {
|
||||
return sm, err
|
||||
}
|
||||
|
@ -61,24 +61,21 @@ func (sm SectorMap) FirstFreeFile() byte {
|
|||
return 0
|
||||
}
|
||||
|
||||
// Persist writes the current contenst of a sector map back back to
|
||||
// Persist writes the current contents of a sector map back back to
|
||||
// disk.
|
||||
func (sm SectorMap) Persist(sd disk.SectorDisk) error {
|
||||
sector09, err := sd.ReadPhysicalSector(0, 9)
|
||||
func (sm SectorMap) Persist(diskbytes []byte) error {
|
||||
sector09, err := disk.ReadSector(diskbytes, 0, 9)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy(sector09[0xd0:], sm[0:0x30])
|
||||
if err := sd.WritePhysicalSector(0, 9, sector09); err != nil {
|
||||
if err := disk.WriteSector(diskbytes, 0, 9, sector09); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sd.WritePhysicalSector(0, 0xA, sm[0x30:0x130]); err != nil {
|
||||
if err := disk.WriteSector(diskbytes, 0, 0xA, sm[0x30:0x130]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sd.WritePhysicalSector(0, 0xB, sm[0x130:0x230]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return disk.WriteSector(diskbytes, 0, 0xB, sm[0x130:0x230])
|
||||
}
|
||||
|
||||
// FreeSectors returns the number of blocks free in a sector map.
|
||||
|
@ -96,7 +93,7 @@ func (sm SectorMap) FreeSectors() int {
|
|||
func (sm SectorMap) Verify() error {
|
||||
for sector := byte(0); sector <= 0xB; sector++ {
|
||||
if file := sm.FileForSector(0, sector); file != FileReserved {
|
||||
return fmt.Errorf("Expected track 0, sectors 0-C to be reserved (0xFE), but got 0x%02X in sector %X", file, sector)
|
||||
return fmt.Errorf("expected track 0, sectors 0-C to be reserved (0xFE), but got 0x%02X in sector %X", file, sector)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +101,7 @@ func (sm SectorMap) Verify() error {
|
|||
for sector := byte(0); sector < 16; sector++ {
|
||||
file := sm.FileForSector(track, sector)
|
||||
if file == FileIllegal {
|
||||
return fmt.Errorf("Found illegal sector map value (%02X), in track %X sector %X", FileIllegal, track, sector)
|
||||
return fmt.Errorf("found illegal sector map value (%02X), in track %X sector %X", FileIllegal, track, sector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,10 +164,10 @@ func (sm SectorMap) SectorsByFile() map[byte][]disk.TrackSector {
|
|||
}
|
||||
|
||||
// ReadFile reads the contents of a file.
|
||||
func (sm SectorMap) ReadFile(sd disk.SectorDisk, file byte) ([]byte, error) {
|
||||
func (sm SectorMap) ReadFile(diskbytes []byte, file byte) ([]byte, error) {
|
||||
var result []byte
|
||||
for _, ts := range sm.SectorsForFile(file) {
|
||||
bytes, err := sd.ReadPhysicalSector(ts.Track, ts.Sector)
|
||||
bytes, err := disk.ReadSector(diskbytes, ts.Track, ts.Sector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -190,7 +187,7 @@ func (sm SectorMap) Delete(file byte) {
|
|||
|
||||
// WriteFile writes the contents of a file. It returns true if the
|
||||
// file already existed.
|
||||
func (sm SectorMap) WriteFile(sd disk.SectorDisk, file byte, contents []byte, overwrite bool) (bool, error) {
|
||||
func (sm SectorMap) WriteFile(diskbytes []byte, file byte, contents []byte, overwrite bool) (bool, error) {
|
||||
sectorsNeeded := (len(contents) + 255) / 256
|
||||
cts := make([]byte, 256*sectorsNeeded)
|
||||
copy(cts, contents)
|
||||
|
@ -209,10 +206,10 @@ func (sm SectorMap) WriteFile(sd disk.SectorDisk, file byte, contents []byte, ov
|
|||
|
||||
i := 0
|
||||
OUTER:
|
||||
for track := byte(0); track < sd.Tracks(); track++ {
|
||||
for sector := byte(0); sector < sd.Sectors(); sector++ {
|
||||
for track := byte(0); track < disk.FloppyTracks; track++ {
|
||||
for sector := byte(0); sector < disk.FloppySectors; sector++ {
|
||||
if sm.FileForSector(track, sector) == FileFree {
|
||||
if err := sd.WritePhysicalSector(track, sector, cts[i*256:(i+1)*256]); err != nil {
|
||||
if err := disk.WriteSector(diskbytes, track, sector, cts[i*256:(i+1)*256]); err != nil {
|
||||
return existed, err
|
||||
}
|
||||
if err := sm.SetFileForSector(track, sector, file); err != nil {
|
||||
|
@ -225,7 +222,7 @@ OUTER:
|
|||
}
|
||||
}
|
||||
}
|
||||
if err := sm.Persist(sd); err != nil {
|
||||
if err := sm.Persist(diskbytes); err != nil {
|
||||
return existed, err
|
||||
}
|
||||
return existed, nil
|
||||
|
@ -254,14 +251,14 @@ func decodeSymbol(five []byte, extra byte) string {
|
|||
value := uint64(five[0]) + uint64(five[1])<<8 + uint64(five[2])<<16 + uint64(five[3])<<24 + uint64(five[4])<<32 + uint64(extra)<<40
|
||||
for value&0x1f > 0 {
|
||||
if value&0x1f < 27 {
|
||||
result = result + string(value&0x1f+'@')
|
||||
result += string(rune(value&0x1f + '@'))
|
||||
value >>= 5
|
||||
continue
|
||||
}
|
||||
if value&0x20 == 0 {
|
||||
result = result + string((value&0x1f)-0x1b+'0')
|
||||
result += string(rune((value & 0x1f) - 0x1b + '0'))
|
||||
} else {
|
||||
result = result + string((value&0x1f)-0x1b+'5')
|
||||
result += string(rune((value & 0x1f) - 0x1b + '5'))
|
||||
}
|
||||
value >>= 6
|
||||
}
|
||||
|
@ -317,16 +314,16 @@ type SymbolTable []Symbol
|
|||
// ReadSymbolTable reads the symbol table from a disk. If there are
|
||||
// problems with the symbol table (like it doesn't exist, or the link
|
||||
// pointers are problematic), it'll return nil and an error.
|
||||
func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) {
|
||||
func (sm SectorMap) ReadSymbolTable(diskbytes []byte) (SymbolTable, error) {
|
||||
table := make(SymbolTable, 0, 819)
|
||||
symtbl1, err := sm.ReadFile(sd, 3)
|
||||
symtbl1, err := sm.ReadFile(diskbytes, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(symtbl1) != 0x1000 {
|
||||
return nil, fmt.Errorf("expected file FSYMTBL1(0x3) to be 0x1000 bytes long; got 0x%04X", len(symtbl1))
|
||||
}
|
||||
symtbl2, err := sm.ReadFile(sd, 4)
|
||||
symtbl2, err := sm.ReadFile(diskbytes, 4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -345,10 +342,10 @@ func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) {
|
|||
link := -1
|
||||
if linkAddr != 0 {
|
||||
if linkAddr < 0xD000 || linkAddr >= 0xDFFF {
|
||||
return nil, fmt.Errorf("Expected symbol table link address between 0xD000 and 0xDFFE; got 0x%04X", linkAddr)
|
||||
return nil, fmt.Errorf("expected symbol table link address between 0xD000 and 0xDFFE; got 0x%04X", linkAddr)
|
||||
}
|
||||
if (linkAddr-0xD000)%5 != 0 {
|
||||
return nil, fmt.Errorf("Expected symbol table link address to 0xD000+5x; got 0x%04X", linkAddr)
|
||||
return nil, fmt.Errorf("expected symbol table link address to 0xD000+5x; got 0x%04X", linkAddr)
|
||||
}
|
||||
link = (int(linkAddr) - 0xD000) / 5
|
||||
}
|
||||
|
@ -380,7 +377,7 @@ func (sm SectorMap) ReadSymbolTable(sd disk.SectorDisk) (SymbolTable, error) {
|
|||
}
|
||||
|
||||
// WriteSymbolTable writes a symbol table to a disk.
|
||||
func (sm SectorMap) WriteSymbolTable(sd disk.SectorDisk, st SymbolTable) error {
|
||||
func (sm SectorMap) WriteSymbolTable(diskbytes []byte, st SymbolTable) error {
|
||||
symtbl1 := make([]byte, 0x1000)
|
||||
symtbl2 := make([]byte, 0x1000)
|
||||
for i, sym := range st {
|
||||
|
@ -400,10 +397,10 @@ func (sm SectorMap) WriteSymbolTable(sd disk.SectorDisk, st SymbolTable) error {
|
|||
symtbl1[offset+4] = six[5]
|
||||
copy(symtbl2[offset:offset+5], six)
|
||||
}
|
||||
if _, err := sm.WriteFile(sd, 3, symtbl1, true); err != nil {
|
||||
if _, err := sm.WriteFile(diskbytes, 3, symtbl1, true); err != nil {
|
||||
return fmt.Errorf("unable to write first half of symbol table: %v", err)
|
||||
}
|
||||
if _, err := sm.WriteFile(sd, 4, symtbl2, true); err != nil {
|
||||
if _, err := sm.WriteFile(diskbytes, 4, symtbl2, true); err != nil {
|
||||
return fmt.Errorf("unable to write first second of symbol table: %v", err)
|
||||
}
|
||||
return nil
|
||||
|
@ -583,6 +580,7 @@ func (st SymbolTable) ParseCompoundSymbol(name string) (address uint16, symAddre
|
|||
}
|
||||
// If it's a valid symbol name, assume that's what it is.
|
||||
if _, err := encodeSymbol(name); err != nil {
|
||||
//nolint:nilerr
|
||||
return 0, 0, name, nil
|
||||
}
|
||||
return 0, 0, "", fmt.Errorf("%q is not a valid symbol name or address", name)
|
||||
|
@ -622,6 +620,7 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named
|
|||
}
|
||||
file, err := st.FileForName(filename)
|
||||
if err != nil {
|
||||
//nolint:nilerr
|
||||
return 0, 0, filename, nil
|
||||
}
|
||||
return file, file, filename, nil
|
||||
|
@ -635,6 +634,7 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named
|
|||
}
|
||||
namedFile, err = st.FileForName(parts[1])
|
||||
if err != nil {
|
||||
//nolint:nilerr
|
||||
return numFile, 0, parts[1], nil
|
||||
}
|
||||
return numFile, namedFile, parts[1], nil
|
||||
|
@ -643,12 +643,13 @@ func (st SymbolTable) FilesForCompoundName(filename string) (numFile byte, named
|
|||
// Operator is a disk.Operator - an interface for performing
|
||||
// high-level operations on files and directories.
|
||||
type Operator struct {
|
||||
SD disk.SectorDisk
|
||||
SM SectorMap
|
||||
ST SymbolTable
|
||||
data []byte
|
||||
SM SectorMap
|
||||
ST SymbolTable
|
||||
debug int
|
||||
}
|
||||
|
||||
var _ disk.Operator = Operator{}
|
||||
var _ types.Operator = Operator{}
|
||||
|
||||
// operatorName is the keyword name for the operator that undestands
|
||||
// NakedOS/Super-Mon disks.
|
||||
|
@ -667,47 +668,47 @@ func (o Operator) HasSubdirs() bool {
|
|||
|
||||
// Catalog returns a catalog of disk entries. subdir should be empty
|
||||
// for operating systems that do not support subdirectories.
|
||||
func (o Operator) Catalog(subdir string) ([]disk.Descriptor, error) {
|
||||
var descs []disk.Descriptor
|
||||
func (o Operator) Catalog(subdir string) ([]types.Descriptor, error) {
|
||||
var descs []types.Descriptor
|
||||
sectorsByFile := o.SM.SectorsByFile()
|
||||
for file := byte(1); file < FileReserved; file++ {
|
||||
l := len(sectorsByFile[file])
|
||||
if l == 0 {
|
||||
continue
|
||||
}
|
||||
descs = append(descs, disk.Descriptor{
|
||||
descs = append(descs, types.Descriptor{
|
||||
Name: NameForFile(file, o.ST),
|
||||
Fullname: FullnameForFile(file, o.ST),
|
||||
Sectors: l,
|
||||
Length: l * 256,
|
||||
Locked: false,
|
||||
Type: disk.FiletypeBinary,
|
||||
Type: types.FiletypeBinary,
|
||||
})
|
||||
}
|
||||
return descs, nil
|
||||
}
|
||||
|
||||
// GetFile retrieves a file by name.
|
||||
func (o Operator) GetFile(filename string) (disk.FileInfo, error) {
|
||||
func (o Operator) GetFile(filename string) (types.FileInfo, error) {
|
||||
file, err := o.ST.FileForName(filename)
|
||||
if err != nil {
|
||||
return disk.FileInfo{}, err
|
||||
return types.FileInfo{}, err
|
||||
}
|
||||
data, err := o.SM.ReadFile(o.SD, file)
|
||||
data, err := o.SM.ReadFile(o.data, file)
|
||||
if err != nil {
|
||||
return disk.FileInfo{}, fmt.Errorf("error reading file DF%02x: %v", file, err)
|
||||
return types.FileInfo{}, fmt.Errorf("error reading file DF%02x: %v", file, err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return disk.FileInfo{}, fmt.Errorf("file DF%02x not fount", file)
|
||||
return types.FileInfo{}, fmt.Errorf("file DF%02x not fount", file)
|
||||
}
|
||||
desc := disk.Descriptor{
|
||||
desc := types.Descriptor{
|
||||
Name: NameForFile(file, o.ST),
|
||||
Sectors: len(data) / 256,
|
||||
Length: len(data),
|
||||
Locked: false,
|
||||
Type: disk.FiletypeBinary,
|
||||
Type: types.FiletypeBinary,
|
||||
}
|
||||
fi := disk.FileInfo{
|
||||
fi := types.FileInfo{
|
||||
Descriptor: desc,
|
||||
Data: data,
|
||||
}
|
||||
|
@ -726,13 +727,13 @@ func (o Operator) Delete(filename string) (bool, error) {
|
|||
}
|
||||
existed := len(o.SM.SectorsForFile(file)) > 0
|
||||
o.SM.Delete(file)
|
||||
if err := o.SM.Persist(o.SD); err != nil {
|
||||
if err := o.SM.Persist(o.data); err != nil {
|
||||
return existed, err
|
||||
}
|
||||
if o.ST != nil {
|
||||
changed := o.ST.DeleteSymbol(filename)
|
||||
if changed {
|
||||
if err := o.SM.WriteSymbolTable(o.SD, o.ST); err != nil {
|
||||
if err := o.SM.WriteSymbolTable(o.data, o.ST); err != nil {
|
||||
return existed, err
|
||||
}
|
||||
}
|
||||
|
@ -743,9 +744,9 @@ func (o Operator) Delete(filename string) (bool, error) {
|
|||
// PutFile writes a file by name. If the file exists and overwrite
|
||||
// is false, it returns with an error. Otherwise it returns true if
|
||||
// an existing file was overwritten.
|
||||
func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) {
|
||||
if fileInfo.Descriptor.Type != disk.FiletypeBinary {
|
||||
return false, fmt.Errorf("%s: only binary file type supported", operatorName)
|
||||
func (o Operator) PutFile(fileInfo types.FileInfo, overwrite bool) (existed bool, err error) {
|
||||
if fileInfo.Descriptor.Type != types.FiletypeBinary {
|
||||
return false, fmt.Errorf("%s: only binary file type supported; got %q", operatorName, fileInfo.Descriptor.Type)
|
||||
}
|
||||
if fileInfo.Descriptor.Length != len(fileInfo.Data) {
|
||||
return false, fmt.Errorf("mismatch between FileInfo.Descriptor.Length (%d) and actual length of FileInfo.Data field (%d)", fileInfo.Descriptor.Length, len(fileInfo.Data))
|
||||
|
@ -769,7 +770,7 @@ func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool,
|
|||
return false, fmt.Errorf("all files already used")
|
||||
}
|
||||
}
|
||||
existed, err = o.SM.WriteFile(o.SD, numFile, fileInfo.Data, overwrite)
|
||||
existed, err = o.SM.WriteFile(o.data, numFile, fileInfo.Data, overwrite)
|
||||
if err != nil {
|
||||
return existed, err
|
||||
}
|
||||
|
@ -777,22 +778,49 @@ func (o Operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool,
|
|||
if err := o.ST.AddSymbol(symbol, 0xDF00+uint16(numFile)); err != nil {
|
||||
return existed, err
|
||||
}
|
||||
if err := o.SM.WriteSymbolTable(o.SD, o.ST); err != nil {
|
||||
if err := o.SM.WriteSymbolTable(o.data, o.ST); err != nil {
|
||||
return existed, err
|
||||
}
|
||||
}
|
||||
return existed, nil
|
||||
}
|
||||
|
||||
// Write writes the underlying disk to the given writer.
|
||||
func (o Operator) Write(w io.Writer) (int, error) {
|
||||
return o.SD.Write(w)
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
func (o Operator) DiskOrder() types.DiskOrder {
|
||||
return types.DiskOrderRaw
|
||||
}
|
||||
|
||||
// operatorFactory is the factory that returns supermon operators
|
||||
// given disk images.
|
||||
func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
|
||||
sm, err := LoadSectorMap(sd)
|
||||
// GetBytes returns the disk image bytes, in logical order.
|
||||
func (o Operator) GetBytes() []byte {
|
||||
return o.data
|
||||
}
|
||||
|
||||
// OperatorFactory is a types.OperatorFactory for DOS 3.3 disks.
|
||||
type OperatorFactory struct {
|
||||
}
|
||||
|
||||
// Name returns the name of the operator.
|
||||
func (of OperatorFactory) Name() string {
|
||||
return operatorName
|
||||
}
|
||||
|
||||
// SeemsToMatch returns true if the []byte disk image seems to match the
|
||||
// system of this operator.
|
||||
func (of OperatorFactory) SeemsToMatch(diskbytes []byte, debug int) bool {
|
||||
// For now, just return true if we can run Catalog successfully.
|
||||
sm, err := LoadSectorMap(diskbytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err := sm.Verify(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Operator returns an Operator for the []byte disk image.
|
||||
func (of OperatorFactory) Operator(diskbytes []byte, debug int) (types.Operator, error) {
|
||||
sm, err := LoadSectorMap(diskbytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -800,9 +828,9 @@ func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
op := Operator{SD: sd, SM: sm}
|
||||
op := Operator{data: diskbytes, SM: sm, debug: debug}
|
||||
|
||||
st, err := sm.ReadSymbolTable(sd)
|
||||
st, err := sm.ReadSymbolTable(diskbytes)
|
||||
if err == nil {
|
||||
op.ST = st
|
||||
}
|
||||
|
@ -810,6 +838,7 @@ func operatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
|
|||
return op, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
disk.RegisterDiskOperatorFactory(operatorName, operatorFactory)
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
func (of OperatorFactory) DiskOrder() types.DiskOrder {
|
||||
return Operator{}.DiskOrder()
|
||||
}
|
|
@ -3,19 +3,21 @@
|
|||
package supermon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/zellyn/diskii/lib/disk"
|
||||
"github.com/zellyn/diskii/disk"
|
||||
"github.com/zellyn/diskii/types"
|
||||
)
|
||||
|
||||
const testDisk = "testdata/chacha20.dsk"
|
||||
|
||||
const cities = `It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way - in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.`
|
||||
|
||||
// The extra newline pads us to 256 bytes…
|
||||
// The extra newline pads us to 256 bytes.
|
||||
const hamlet = `To be, or not to be, that is the question:
|
||||
Whether 'tis Nobler in the mind to suffer
|
||||
The Slings and Arrows of outrageous Fortune,
|
||||
|
@ -27,16 +29,20 @@ No more; and by a sleep, to say we end
|
|||
|
||||
// loadSectorMap loads a sector map for the disk image contained in
|
||||
// filename. It returns the sector map and a sector disk.
|
||||
func loadSectorMap(filename string) (SectorMap, disk.SectorDisk, error) {
|
||||
sd, err := disk.LoadDSK(filename)
|
||||
func loadSectorMap(filename string) (SectorMap, []byte, error) {
|
||||
rawbytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sm, err := LoadSectorMap(sd)
|
||||
diskbytes, err := disk.Swizzle(rawbytes, disk.Dos33LogicalToPhysicalSectorMap)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sm, sd, nil
|
||||
sm, err := LoadSectorMap(diskbytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sm, diskbytes, nil
|
||||
}
|
||||
|
||||
// TestReadSectorMap tests the reading of the sector map of a test
|
||||
|
@ -126,11 +132,7 @@ func TestReadSymbolTable(t *testing.T) {
|
|||
// TestGetFile tests the retrieval of a file's contents, using the
|
||||
// Operator interface.
|
||||
func TestGetFile(t *testing.T) {
|
||||
sd, err := disk.OpenDisk(testDisk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
op, err := disk.OperatorForDisk(sd)
|
||||
op, _, err := disk.OpenFilename(testDisk, types.DiskOrderAuto, "nakedos", []types.OperatorFactory{OperatorFactory{}}, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -138,9 +140,7 @@ func TestGetFile(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(file.Data)
|
||||
want := hamlet
|
||||
if got != want {
|
||||
if want, got := hamlet, string(file.Data); got != want {
|
||||
t.Errorf("Incorrect result for GetFile(\"TOBE\"): want %q; got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
@ -213,20 +213,16 @@ func TestReadWriteSymbolTable(t *testing.T) {
|
|||
// TestPutFile tests the creation of a file, using the Operator
|
||||
// interface.
|
||||
func TestPutFile(t *testing.T) {
|
||||
sd, err := disk.OpenDisk(testDisk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
op, err := disk.OperatorForDisk(sd)
|
||||
op, _, err := disk.OpenFilename(testDisk, types.DiskOrderAuto, "nakedos", []types.OperatorFactory{OperatorFactory{}}, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents := []byte(cities)
|
||||
fileInfo := disk.FileInfo{
|
||||
Descriptor: disk.Descriptor{
|
||||
fileInfo := types.FileInfo{
|
||||
Descriptor: types.Descriptor{
|
||||
Name: "FNEWFILE",
|
||||
Length: len(contents),
|
||||
Type: disk.FiletypeBinary,
|
||||
Type: types.FiletypeBinary,
|
||||
},
|
||||
Data: contents,
|
||||
}
|
||||
|
@ -243,8 +239,7 @@ func TestPutFile(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
last := fds[len(fds)-1]
|
||||
want := "DF0B:FNEWFILE"
|
||||
if got := last.Fullname; got != want {
|
||||
if want, got := "DF0B:FNEWFILE", last.Fullname; got != want {
|
||||
t.Fatalf("Want last file on disk's FullName=%q; got %q", want, got)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# hello world
|
||||
exec cat hello.text
|
||||
stdout 'hello world\n'
|
||||
! stderr .
|
||||
|
||||
-- hello.text --
|
||||
hello world
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// ops.go contains the interfaces and helper functions for operating
|
||||
// on disk images logically: catalog, rename, delete, create files,
|
||||
// etc.
|
||||
|
||||
package types
|
||||
|
||||
// Descriptor describes a file's characteristics.
|
||||
type Descriptor struct {
|
||||
Name string
|
||||
Fullname string // If there's a more complete filename (eg. Super-Mon), put it here.
|
||||
Sectors int
|
||||
Blocks int
|
||||
Length int
|
||||
Locked bool
|
||||
Type Filetype
|
||||
}
|
||||
|
||||
// DiskOrder specifies the logical disk ordering.
|
||||
type DiskOrder string
|
||||
|
||||
const (
|
||||
// DiskOrderDO is the DOS 3.3 logical ordering.
|
||||
DiskOrderDO = DiskOrder("do")
|
||||
// DiskOrderPO is the ProDOS logical ordering.
|
||||
DiskOrderPO = DiskOrder("po")
|
||||
// DiskOrderRaw is the logical ordering that doesn't change anything.
|
||||
DiskOrderRaw = DiskOrder("raw")
|
||||
// DiskOrderAuto is the logical ordering that tells diskii to guess.
|
||||
DiskOrderAuto = DiskOrder("auto")
|
||||
// DiskOrderUnknown is usually an error condition, or a signal that guessing failed.
|
||||
DiskOrderUnknown = DiskOrder("")
|
||||
)
|
||||
|
||||
// OperatorFactory is the interface for getting operators, and finding out a bit
|
||||
// about them before getting them.
|
||||
type OperatorFactory interface {
|
||||
// Name returns the name of the operator.
|
||||
Name() string
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
DiskOrder() DiskOrder
|
||||
// SeemsToMatch returns true if the []byte disk image seems to match the
|
||||
// system of this operator.
|
||||
SeemsToMatch(diskbytes []byte, debug int) bool
|
||||
// Operator returns an Operator for the []byte disk image.
|
||||
Operator(diskbytes []byte, debug int) (Operator, error)
|
||||
}
|
||||
|
||||
// Operator is the interface that can operate on disks.
|
||||
type Operator interface {
|
||||
// Name returns the name of the operator.
|
||||
Name() string
|
||||
// DiskOrder returns the Physical-to-Logical mapping order.
|
||||
DiskOrder() DiskOrder
|
||||
// HasSubdirs returns true if the underlying operating system on the
|
||||
// disk allows subdirectories.
|
||||
HasSubdirs() bool
|
||||
// Catalog returns a catalog of disk entries. subdir should be empty
|
||||
// for operating systems that do not support subdirectories.
|
||||
Catalog(subdir string) ([]Descriptor, error)
|
||||
// GetFile retrieves a file by name.
|
||||
GetFile(filename string) (FileInfo, error)
|
||||
// Delete deletes a file by name. It returns true if the file was
|
||||
// deleted, false if it didn't exist.
|
||||
Delete(filename string) (bool, error)
|
||||
// PutFile writes a file by name. If the file exists and overwrite
|
||||
// is false, it returns with an error. Otherwise it returns true if
|
||||
// an existing file was overwritten.
|
||||
PutFile(fileInfo FileInfo, overwrite bool) (existed bool, err error)
|
||||
// GetBytes returns the disk image bytes, in logical order.
|
||||
GetBytes() []byte
|
||||
}
|
||||
|
||||
// FileInfo represents a file descriptor plus the content.
|
||||
type FileInfo struct {
|
||||
Descriptor Descriptor
|
||||
Data []byte
|
||||
StartAddress uint16
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue