mirror of https://github.com/zellyn/diskii.git
Initial commit
Includes "applesoft decode" command to convert Applesoft bytes to listings.
This commit is contained in:
commit
7aa075b594
|
@ -0,0 +1,4 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.7
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © 2016 Zellyn Hunter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,9 @@
|
||||||
|
diskii
|
||||||
|
======
|
||||||
|
diskii is a commandline tool for working with Apple II disk images.
|
||||||
|
|
||||||
|
Eventually, it aims to be a comprehensive disk image manipulation tool.
|
||||||
|
|
||||||
|
It is pronounced so as to rhyme with "whiskey".
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/zellyn/diskii.svg?branch=master)](https://travis-ci.org/zellyn/diskii)
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
// 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.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(applesoftCmd)
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/zellyn/diskii/lib/applesoft"
|
||||||
|
"github.com/zellyn/diskii/lib/helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDecode performs the actual decode logic.
|
||||||
|
func runDecode(args []string) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("decode expects one argument: the filename (or - for stdin)")
|
||||||
|
}
|
||||||
|
contents, err := helpers.FileContentsOrStdIn(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listing, err := applesoft.Decode(contents, location)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rawControlCodes {
|
||||||
|
os.Stdout.WriteString(listing.String())
|
||||||
|
} else {
|
||||||
|
os.Stdout.WriteString(applesoft.ChevronControlCodes(listing.String()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)")
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||||
|
|
||||||
|
// Package applesoft provides routines for working with Applesoft
|
||||||
|
// files.
|
||||||
|
package applesoft
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokensByCode is a map from byte value to token text.
|
||||||
|
var TokensByCode = map[byte]string{
|
||||||
|
0x80: "END",
|
||||||
|
0x81: "FOR",
|
||||||
|
0x82: "NEXT",
|
||||||
|
0x83: "DATA",
|
||||||
|
0x84: "INPUT",
|
||||||
|
0x85: "DEL",
|
||||||
|
0x86: "DIM",
|
||||||
|
0x87: "READ",
|
||||||
|
0x88: "GR",
|
||||||
|
0x89: "TEXT",
|
||||||
|
0x8A: "PR #",
|
||||||
|
0x8B: "IN #",
|
||||||
|
0x8C: "CALL",
|
||||||
|
0x8D: "PLOT",
|
||||||
|
0x8E: "HLIN",
|
||||||
|
0x8F: "VLIN",
|
||||||
|
0x90: "HGR2",
|
||||||
|
0x91: "HGR",
|
||||||
|
0x92: "HCOLOR=",
|
||||||
|
0x93: "HPLOT",
|
||||||
|
0x94: "DRAW",
|
||||||
|
0x95: "XDRAW",
|
||||||
|
0x96: "HTAB",
|
||||||
|
0x97: "HOME",
|
||||||
|
0x98: "ROT=",
|
||||||
|
0x99: "SCALE=",
|
||||||
|
0x9A: "SHLOAD",
|
||||||
|
0x9B: "TRACE",
|
||||||
|
0x9C: "NOTRACE",
|
||||||
|
0x9D: "NORMAL",
|
||||||
|
0x9E: "INVERSE",
|
||||||
|
0x9F: "FLASH",
|
||||||
|
0xA0: "COLOR=",
|
||||||
|
0xA1: "POP",
|
||||||
|
0xA2: "VTAB",
|
||||||
|
0xA3: "HIMEM:",
|
||||||
|
0xA4: "LOMEM:",
|
||||||
|
0xA5: "ONERR",
|
||||||
|
0xA6: "RESUME",
|
||||||
|
0xA7: "RECALL",
|
||||||
|
0xA8: "STORE",
|
||||||
|
0xA9: "SPEED=",
|
||||||
|
0xAA: "LET",
|
||||||
|
0xAB: "GOTO",
|
||||||
|
0xAC: "RUN",
|
||||||
|
0xAD: "IF",
|
||||||
|
0xAE: "RESTORE",
|
||||||
|
0xAF: "&",
|
||||||
|
0xB0: "GOSUB",
|
||||||
|
0xB1: "RETURN",
|
||||||
|
0xB2: "REM",
|
||||||
|
0xB3: "STOP",
|
||||||
|
0xB4: "ON",
|
||||||
|
0xB5: "WAIT",
|
||||||
|
0xB6: "LOAD",
|
||||||
|
0xB7: "SAVE",
|
||||||
|
0xB8: "DEF FN",
|
||||||
|
0xB9: "POKE",
|
||||||
|
0xBA: "PRINT",
|
||||||
|
0xBB: "CONT",
|
||||||
|
0xBC: "LIST",
|
||||||
|
0xBD: "CLEAR",
|
||||||
|
0xBE: "GET",
|
||||||
|
0xBF: "NEW",
|
||||||
|
0xC0: "TAB",
|
||||||
|
0xC1: "TO",
|
||||||
|
0xC2: "FN",
|
||||||
|
0xC3: "SPC(",
|
||||||
|
0xC4: "THEN",
|
||||||
|
0xC5: "AT",
|
||||||
|
0xC6: "NOT",
|
||||||
|
0xC7: "STEP",
|
||||||
|
0xC8: "+",
|
||||||
|
0xC9: "-",
|
||||||
|
0xCA: "*",
|
||||||
|
0xCB: "/",
|
||||||
|
0xCC: ";",
|
||||||
|
0xCD: "AND",
|
||||||
|
0xCE: "OR",
|
||||||
|
0xCF: ">",
|
||||||
|
0xD0: "=",
|
||||||
|
0xD1: "<",
|
||||||
|
0xD2: "SGN",
|
||||||
|
0xD3: "INT",
|
||||||
|
0xD4: "ABS",
|
||||||
|
0xD5: "USR",
|
||||||
|
0xD6: "FRE",
|
||||||
|
0xD7: "SCRN (",
|
||||||
|
0xD8: "PDL",
|
||||||
|
0xD9: "POS",
|
||||||
|
0xDA: "SQR",
|
||||||
|
0xDB: "RND",
|
||||||
|
0xDC: "LOG",
|
||||||
|
0xDD: "EXP",
|
||||||
|
0xDE: "COS",
|
||||||
|
0xDF: "SIN",
|
||||||
|
0xE0: "TAN",
|
||||||
|
0xE1: "ATN",
|
||||||
|
0xE2: "PEEK",
|
||||||
|
0xE3: "LEN",
|
||||||
|
0xE4: "STR$",
|
||||||
|
0xE5: "VAL",
|
||||||
|
0xE6: "ASC",
|
||||||
|
0xE7: "CHR$",
|
||||||
|
0xE8: "LEFT$",
|
||||||
|
0xE9: "RIGHT$",
|
||||||
|
0xEA: "MID$",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listing holds a listing of an entire BASIC program.
|
||||||
|
type Listing []Line
|
||||||
|
|
||||||
|
// Line holds a single BASIC line, with line number and text.
|
||||||
|
type Line struct {
|
||||||
|
Num int
|
||||||
|
Bytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode turns a raw binary file into a basic program. Location
|
||||||
|
// specifies the program's location in RAM (0x801 for in-ROM Applesoft, 0x3001 for tape-loaded Applesoft).
|
||||||
|
func Decode(raw []byte, location uint16) (Listing, error) {
|
||||||
|
// First two bytes of Applesoft files on disk are length. Let's be
|
||||||
|
// tolerant to getting either format.
|
||||||
|
if len(raw) >= 2 {
|
||||||
|
size := int(raw[0]) + (256 * int(raw[1]))
|
||||||
|
if size == len(raw)-2 || size == len(raw)-3 {
|
||||||
|
raw = raw[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds := fmt.Sprintf("$%X to $%X", location, int(location)+len(raw))
|
||||||
|
|
||||||
|
calcOffset := func(address int) int {
|
||||||
|
return address - int(location)
|
||||||
|
}
|
||||||
|
listing := []Line{}
|
||||||
|
last := 0 // last line number
|
||||||
|
next := int(location)
|
||||||
|
for next != 0 {
|
||||||
|
ofs := calcOffset(next)
|
||||||
|
if ofs < -1 || ofs+1 >= len(raw) {
|
||||||
|
return nil, fmt.Errorf("line %d has next line at $%X, which is outside the input range of %s", last, next, bounds)
|
||||||
|
}
|
||||||
|
next = int(raw[ofs]) + 256*int(raw[ofs+1])
|
||||||
|
ofs += 2
|
||||||
|
if next == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ofs+1 >= len(raw) {
|
||||||
|
if len(listing) == 0 {
|
||||||
|
return nil, fmt.Errorf("ran out of input trying to read the first line number")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("ran out of input trying to read line number of line after %d", last)
|
||||||
|
}
|
||||||
|
line := Line{Num: int(raw[ofs]) + 256*int(raw[ofs+1])}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
char := raw[ofs]
|
||||||
|
if char == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if char < 0x80 {
|
||||||
|
line.Bytes = append(line.Bytes, char)
|
||||||
|
} else {
|
||||||
|
token := TokensByCode[char]
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("unknown token $%X in line %d", char, line.Num)
|
||||||
|
}
|
||||||
|
line.Bytes = append(line.Bytes, char)
|
||||||
|
}
|
||||||
|
ofs++
|
||||||
|
}
|
||||||
|
listing = append(listing, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return listing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Line) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%d ", l.Num)
|
||||||
|
for _, char := range l.Bytes {
|
||||||
|
if char < 0x80 {
|
||||||
|
buf.WriteByte(char)
|
||||||
|
} else {
|
||||||
|
token := TokensByCode[char]
|
||||||
|
buf.WriteString(" " + token + " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Listing) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, line := range l {
|
||||||
|
buf.WriteString(line.String())
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlCharRegexp = regexp.MustCompile(`[\x00-\x1F]`)
|
||||||
|
|
||||||
|
// ChevronControlCodes converts ASCII control characters like chr(4)
|
||||||
|
// to chevron-surrounded codes like «ctrl-D».
|
||||||
|
func ChevronControlCodes(s string) string {
|
||||||
|
return controlCharRegexp.ReplaceAllStringFunc(s, func(s string) string {
|
||||||
|
if s == "\n" || s == "\t" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if s >= "\x01" && s <= "\x1a" {
|
||||||
|
return "«ctrl-" + string('A'-1+s[0]) + "»"
|
||||||
|
}
|
||||||
|
code := "?"
|
||||||
|
switch s[0] {
|
||||||
|
case '\x00':
|
||||||
|
code = "NUL"
|
||||||
|
case '\x1C':
|
||||||
|
code = "FS"
|
||||||
|
case '\x1D':
|
||||||
|
code = "GS"
|
||||||
|
case '\x1E':
|
||||||
|
code = "RS"
|
||||||
|
case '\x1F':
|
||||||
|
code = "US"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "«" + code + "»"
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||||
|
|
||||||
|
package applesoft
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// helloBinary is a simple basic program used for testing. Listing
|
||||||
|
// below.
|
||||||
|
var helloBinary = []byte{
|
||||||
|
0x11, 0x08, 0x0A, 0x00, 0xBA, 0x22, 0x04, 0x43, 0x41, 0x54, 0x41, 0x4C, 0x4F, 0x47, 0x22, 0x00,
|
||||||
|
0x17, 0x08, 0x14, 0x00, 0xBA, 0x00, 0x25, 0x08, 0x1E, 0x00, 0xBA, 0x22, 0x48, 0x45, 0x4C, 0x4C,
|
||||||
|
0x4F, 0x22, 0x3B, 0x00, 0x34, 0x08, 0x28, 0x00, 0xBA, 0x22, 0x2C, 0x20, 0x57, 0x4F, 0x52, 0x4C,
|
||||||
|
0x44, 0x22, 0x00, 0x49, 0x08, 0x32, 0x00, 0x81, 0x49, 0xD0, 0x31, 0xC1, 0x34, 0x30, 0x3A, 0xBA,
|
||||||
|
0x22, 0x2A, 0x22, 0x3B, 0x3A, 0x82, 0x49, 0x00, 0x6B, 0x08, 0x3C, 0x00, 0x81, 0x49, 0xD0, 0x31,
|
||||||
|
0xC1, 0x31, 0x30, 0x3A, 0x81, 0x4A, 0xD0, 0x31, 0xC1, 0x49, 0x3A, 0xBA, 0x22, 0x20, 0x22, 0x3B,
|
||||||
|
0x3A, 0x82, 0x3A, 0xBA, 0x22, 0x2A, 0x22, 0x3A, 0x82, 0x00, 0x98, 0x08, 0x46, 0x00, 0xB2, 0x22,
|
||||||
|
0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x41, 0x20, 0x54, 0x45, 0x53, 0x54, 0x20, 0x4F,
|
||||||
|
0x46, 0x20, 0x41, 0x20, 0x4C, 0x4F, 0x4E, 0x47, 0x20, 0x4C, 0x49, 0x4E, 0x45, 0x20, 0x4F, 0x46,
|
||||||
|
0x20, 0x54, 0x45, 0x58, 0x54, 0x22, 0x00, 0xEC, 0x08, 0x50, 0x00, 0xBA, 0x22, 0x54, 0x48, 0x49,
|
||||||
|
0x53, 0x20, 0x49, 0x53, 0x20, 0x41, 0x20, 0x54, 0x45, 0x53, 0x54, 0x20, 0x4F, 0x46, 0x20, 0x41,
|
||||||
|
0x4E, 0x20, 0x45, 0x56, 0x45, 0x4E, 0x20, 0x4C, 0x4F, 0x4E, 0x47, 0x45, 0x52, 0x20, 0x4C, 0x49,
|
||||||
|
0x4E, 0x45, 0x20, 0x4F, 0x46, 0x20, 0x54, 0x45, 0x58, 0x54, 0x20, 0x54, 0x48, 0x41, 0x54, 0x20,
|
||||||
|
0x49, 0x53, 0x20, 0x45, 0x56, 0x45, 0x4E, 0x20, 0x4D, 0x4F, 0x52, 0x45, 0x20, 0x54, 0x48, 0x41,
|
||||||
|
0x4E, 0x20, 0x38, 0x30, 0x20, 0x43, 0x4F, 0x4C, 0x53, 0x22, 0x00, 0x04, 0x09, 0x5A, 0x00, 0xBA,
|
||||||
|
0x22, 0x41, 0x4C, 0x4C, 0x20, 0x44, 0x4F, 0x4E, 0x45, 0x20, 0x54, 0x45, 0x53, 0x54, 0x49, 0x4E,
|
||||||
|
0x47, 0x22, 0x00, 0x00, 0x00, 0x0A,
|
||||||
|
}
|
||||||
|
|
||||||
|
// helloListing is the text version of the basic program above. Note
|
||||||
|
// that there are trailing newlines on lines 20 and 60.
|
||||||
|
var helloListing = `10 PRINT "«ctrl-D»CATALOG"
|
||||||
|
20 PRINT
|
||||||
|
30 PRINT "HELLO";
|
||||||
|
40 PRINT ", WORLD"
|
||||||
|
50 FOR I = 1 TO 40: PRINT "*";: NEXT I
|
||||||
|
60 FOR I = 1 TO 10: FOR J = 1 TO I: PRINT " ";: NEXT : PRINT "*": NEXT
|
||||||
|
70 REM "THIS IS A TEST OF A LONG LINE OF TEXT"
|
||||||
|
80 PRINT "THIS IS A TEST OF AN EVEN LONGER LINE OF TEXT THAT IS EVEN MORE THAN 80 COLS"
|
||||||
|
90 PRINT "ALL DONE TESTING"
|
||||||
|
`
|
||||||
|
|
||||||
|
// TestParse tests the full parsing and output of a basic program from
|
||||||
|
// bytes.
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
listing, err := Decode(helloBinary, 0x801)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
text := ChevronControlCodes(listing.String())
|
||||||
|
if text != helloListing {
|
||||||
|
t.Fatalf("Wrong listing; want:\n%s\ngot:\n%s", helloListing, text)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
** Program location
|
||||||
|
Defaults to $801 for ROM-based Applesoft. Locations $67/$68 point to
|
||||||
|
the start of the program, and the byte preceding it must be #0.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
- http://www.atarimagazines.com/compute/issue11/36_1_THE_APPLE_GAZETTE_RESOLVING_APPLESOFT_AND_HIRES_GRAPHICS_MEMORY_CONFLICTS.php
|
||||||
|
- http://retrocomputing.stackexchange.com/questions/1604
|
||||||
|
|
||||||
|
** Format
|
||||||
|
DOS stores an additional byte on the end, which should be ignored.
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FileContentsOrStdIn(s string) ([]byte, error) {
|
||||||
|
if s == "-" {
|
||||||
|
return ioutil.ReadAll(os.Stdin)
|
||||||
|
}
|
||||||
|
return ioutil.ReadFile(s)
|
||||||
|
}
|
Loading…
Reference in New Issue