mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-06-03 05:29:31 +00:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0aa838ffe1 | ||
|
231197bd8f | ||
|
2477b4a8a3 | ||
|
30f0b03054 | ||
|
ef2b505500 | ||
|
7f8679cdb4 | ||
|
98ab2d4f0a | ||
|
9e6c2fcc22 | ||
|
b80a835c72 | ||
|
10f896399d | ||
|
36c24e60ca | ||
|
286964d3ee | ||
|
1722e7b23a | ||
|
876564d261 | ||
|
ee3d187fb3 | ||
|
ab3b397139 | ||
|
6cc7db13e1 | ||
|
0beb2f41fa | ||
|
cb6b3a25ba | ||
|
8b54fa3235 | ||
|
0ac5bd39d0 | ||
|
592f3a6542 | ||
|
4ed23e0e6f | ||
|
eb438ee9fe | ||
|
f36acac6e6 | ||
|
48aa7a9331 | ||
|
a649647739 | ||
|
6b5045a788 | ||
|
402f39da91 |
|
@ -15,21 +15,28 @@ name: Codacy Security Scan
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '36 19 * * 4'
|
||||
- cron: '38 20 * * 5'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codacy-security-scan:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
name: Codacy Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout the repository to the GitHub Actions runner
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||
- name: Run Codacy Analysis CLI
|
||||
|
@ -49,6 +56,6 @@ jobs:
|
|||
|
||||
# Upload the SARIF file generated in the previous step
|
||||
- name: Upload SARIF results file
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
|
@ -13,12 +13,12 @@ name: "CodeQL"
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '30 10 * * 5'
|
||||
- cron: '44 11 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
@ -34,37 +34,43 @@ jobs:
|
|||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
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).
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, 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
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# ✏️ 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
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
51
README.md
51
README.md
|
@ -5,20 +5,13 @@ This project is just starting but is intended to be both a command line tool and
|
|||
Being a work in progress, be warned that this code is likely to corrupt drive images so be sure to have backups. Also, command line parameters are likely to change significantly in the future.
|
||||
|
||||
## How to get it
|
||||
There are binaries here:
|
||||
- [Windows (64 bit)](binaries/windows/intel/ProDOS-Utilities.exe)
|
||||
- [macOS (Apple Silicon)](binaries/macos/apple-silicon/ProDOS-Utilities)
|
||||
- [macOS (Intel)](binaries/macos/intel/ProDOS-Utilities)
|
||||
- [Linux (Intel)](binaries/linux/intel/ProDOS-Utilities)
|
||||
- [Linux (ARM64)](binaries/linux/arm64/ProDOS-Utilities)
|
||||
- [Linux (ARM32 for Raspberry Pi 1,2, Zero, Zero W, Zero W 2)](binaries/linux/arm32/ProDOS-Utilities)
|
||||
There are binaries [here](https://github.com/tjboldt/ProDOS-Utilities/releases/latest)
|
||||
|
||||
## Current TODO list
|
||||
1. Allow > 128 KB file support
|
||||
2. Create/Delete directories
|
||||
3. Add file/directory/basic tests
|
||||
4. Add rename
|
||||
5. Add in-place file/directory moves
|
||||
1. Delete directories
|
||||
2. Add file/directory tests
|
||||
3. Add rename
|
||||
4. Add in-place file/directory moves
|
||||
|
||||
## Example commands and output
|
||||
|
||||
|
@ -83,6 +76,11 @@ BLOCKS FREE: 15 BLOCKS USED: 65520 TOTAL BLOCKS: 65535
|
|||
ProDOS-Utilities -d new.hdv -c create -b 65535
|
||||
```
|
||||
|
||||
### Add all files from a host directory
|
||||
```
|
||||
ProDOS-Utilities -d new.hdv -c putall -i .
|
||||
```
|
||||
|
||||
### Hex dump a block with command readblock and block number (both decimal and hexadecimal input work)
|
||||
```
|
||||
ProDOS-Utilities -d new.hdv -c readblock -b 0
|
||||
|
@ -124,31 +122,6 @@ Block 0x0000 (0):
|
|||
|
||||
### Export files (using .bas file extension coverts Applesoft to text file)
|
||||
```
|
||||
ProDOS-Utilities -d ../Apple2-IO-RPi/RaspberryPi/Apple2-IO-RPi.hdv -c get -o Update.Firmware.bas -p /APPLE2.IO.RPI/UPDATE.FIRMWARE; cat Update.Firmware.bas
|
||||
10 HOME
|
||||
100 PRINT CHR$ (4)"BLOAD AT28C64B.BIN,A$2000"
|
||||
200 PRINT "Program the firmare in slot #"
|
||||
300 INPUT SL
|
||||
400 FW = 8192 + 256 * SL: REM Firmware source
|
||||
500 PS = 49287 + SL * 16: REM Firmware page selection
|
||||
600 EP = 49152 + SL * 256: REM EPROM location
|
||||
900 HOME
|
||||
1000 FOR PG = 0 TO 3
|
||||
1004 PRINT : PRINT
|
||||
1005 PRINT "Writing page "PG" to slot "SL"
|
||||
1006 PRINT "_______________________________________"
|
||||
1007 PRINT "_______________________________________";
|
||||
1008 HTAB 1
|
||||
1010 POKE PS,PG * 16 + 15: REM Set firmware page
|
||||
1020 FOR X = 0 TO 255
|
||||
1030 A = PEEK (FW + PG * 2048 + X)
|
||||
1040 POKE EP + X,A
|
||||
1041 B = PEEK (EP + X)
|
||||
1042 IF (B < > A) THEN GOTO 1041
|
||||
1045 HTAB (X / 256) * 39 + 1
|
||||
1046 INVERSE : PRINT " ";: NORMAL
|
||||
1050 NEXT X
|
||||
1060 NEXT PG
|
||||
1900 PRINT
|
||||
2000 PRINT "Firmware Update Complete"
|
||||
ProDOS-Utilities -d example.hdv -c get -o Startup.bas -p /EXAMPLE/STARTUP; cat Startup.bas
|
||||
10 PRINT "HELLO WORLD"
|
||||
```
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2
go.mod
2
go.mod
|
@ -1,3 +1,5 @@
|
|||
module github.com/tjboldt/ProDOS-Utilities
|
||||
|
||||
go 1.16
|
||||
|
||||
require golang.org/x/image v0.15.0
|
||||
|
|
33
go.sum
Normal file
33
go.sum
Normal file
|
@ -0,0 +1,33 @@
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
328
main.go
328
main.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/tjboldt/ProDOS-Utilities/prodos"
|
||||
)
|
||||
|
||||
const version = "0.3.0"
|
||||
const version = "0.4.9"
|
||||
|
||||
func main() {
|
||||
var fileName string
|
||||
|
@ -32,14 +32,14 @@ func main() {
|
|||
var auxType int
|
||||
flag.StringVar(&fileName, "d", "", "A ProDOS format drive image")
|
||||
flag.StringVar(&pathName, "p", "", "Path name in ProDOS drive image (default is root of volume)")
|
||||
flag.StringVar(&command, "c", "ls", "Command to execute: ls, get, put, rm, mkdir, readblock, writeblock, create")
|
||||
flag.StringVar(&command, "c", "ls", "Command to execute: ls, get, put, rm, mkdir, readblock, writeblock, create, putall, putallrecursive")
|
||||
flag.StringVar(&outFileName, "o", "", "Name of file to write")
|
||||
flag.StringVar(&inFileName, "i", "", "Name of file to read")
|
||||
flag.IntVar(&volumeSize, "s", 65535, "Number of blocks to create the volume with (default 65535, 64 to 65535, 0x0040 to 0xFFFF hex input accepted)")
|
||||
flag.StringVar(&volumeName, "v", "NO.NAME", "Specifiy a name for the volume from 1 to 15 characters")
|
||||
flag.IntVar(&blockNumber, "b", 0, "A block number to read/write from 0 to 65535 (0x0000 to 0xFFFF hex input accepted)")
|
||||
flag.IntVar(&fileType, "t", 6, "ProDOS FileType: 0x04 for TXT, 0x06 for BIN, 0xFA for BAS, 0xFF for SYS etc.")
|
||||
flag.IntVar(&auxType, "a", 0x2000, "ProDOS AuxType from 0 to 65535 (0x0000 to 0xFFFF hex input accepted)")
|
||||
flag.IntVar(&fileType, "t", 0, "ProDOS FileType: 0x04 for TXT, 0x06 for BIN, 0xFC for BAS, 0xFF for SYS etc., omit to autodetect")
|
||||
flag.IntVar(&auxType, "a", 0, "ProDOS AuxType from 0 to 65535 (0x0000 to 0xFFFF hex input accepted), omit to autodetect")
|
||||
flag.Parse()
|
||||
|
||||
if len(fileName) == 0 {
|
||||
|
@ -50,123 +50,225 @@ func main() {
|
|||
|
||||
switch command {
|
||||
case "ls":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
pathName = strings.ToUpper(pathName)
|
||||
volumeHeader, _, fileEntries, err := prodos.ReadDirectory(file, pathName)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
}
|
||||
if len(pathName) == 0 {
|
||||
pathName = "/" + volumeHeader.VolumeName
|
||||
}
|
||||
volumeBitmap, err := prodos.ReadVolumeBitmap(file)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
freeBlocks := prodos.GetFreeBlockCount(volumeBitmap, volumeHeader.TotalBlocks)
|
||||
prodos.DumpDirectory(freeBlocks, volumeHeader.TotalBlocks, pathName, fileEntries)
|
||||
ls(fileName, pathName)
|
||||
case "get":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
if len(pathName) == 0 {
|
||||
fmt.Println("Missing pathname")
|
||||
os.Exit(1)
|
||||
}
|
||||
getFile, err := prodos.LoadFile(file, pathName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read file %s: %s\n", pathName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(outFileName) == 0 {
|
||||
_, outFileName = prodos.GetDirectoryAndFileNameFromPath(pathName)
|
||||
}
|
||||
outFile, err := os.Create(outFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create output file %s: %s\n", outFileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(outFileName), ".bas") {
|
||||
fmt.Fprintf(outFile, prodos.ConvertBasicToText(getFile))
|
||||
} else {
|
||||
outFile.Write(getFile)
|
||||
}
|
||||
get(fileName, pathName, outFileName)
|
||||
case "put":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
if len(pathName) == 0 {
|
||||
fmt.Println("Missing pathname")
|
||||
os.Exit(1)
|
||||
}
|
||||
inFile, err := os.ReadFile(inFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open input file %s: %s", inFileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = prodos.WriteFile(file, pathName, fileType, auxType, inFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to write file %s: %s", pathName, err)
|
||||
}
|
||||
put(fileName, pathName, fileType, auxType, inFileName)
|
||||
case "readblock":
|
||||
fmt.Printf("Reading block 0x%04X (%d):\n\n", blockNumber, blockNumber)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
block, err := prodos.ReadBlock(file, blockNumber)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
prodos.DumpBlock(block)
|
||||
readBlock(blockNumber, fileName)
|
||||
case "writeblock":
|
||||
fmt.Printf("Writing block 0x%04X (%d):\n\n", blockNumber, blockNumber)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
inFile, err := os.ReadFile(inFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open input file %s: %s", inFileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
prodos.WriteBlock(file, blockNumber, inFile)
|
||||
writeBlock(blockNumber, fileName, inFileName)
|
||||
case "create":
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create file: %s\n", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
prodos.CreateVolume(file, volumeName, volumeSize)
|
||||
create(fileName, volumeName, volumeSize)
|
||||
case "putall":
|
||||
putall(fileName, inFileName, pathName, false)
|
||||
case "putallrecursive":
|
||||
putall(fileName, inFileName, pathName, true)
|
||||
case "rm":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
prodos.DeleteFile(file, pathName)
|
||||
rm(fileName, pathName)
|
||||
case "mkdir":
|
||||
mkdir(fileName, pathName)
|
||||
case "dumpfile":
|
||||
dumpFile(fileName, pathName)
|
||||
case "dumpdirectory":
|
||||
dumpDirectory(fileName, pathName)
|
||||
default:
|
||||
fmt.Printf("Invalid command: %s\n\n", command)
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func dumpFile(fileName string, pathName string) {
|
||||
checkPathName(pathName)
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
fileEntry, err := prodos.GetFileEntry(file, pathName)
|
||||
prodos.DumpFileEntry(fileEntry)
|
||||
}
|
||||
|
||||
func dumpDirectory(fileName string, pathName string) {
|
||||
checkPathName(pathName)
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
_, directoryheader, _, err := prodos.ReadDirectory(file, pathName)
|
||||
prodos.DumpDirectoryHeader(directoryheader)
|
||||
}
|
||||
|
||||
func mkdir(fileName string, pathName string) {
|
||||
checkPathName(pathName)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
err = prodos.CreateDirectory(file, pathName)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create directory %s: %s\n", pathName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func rm(fileName string, pathName string) {
|
||||
checkPathName(pathName)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
prodos.DeleteFile(file, pathName)
|
||||
}
|
||||
|
||||
func putall(fileName string, inFileName string, pathName string, recursive bool) {
|
||||
if len(inFileName) == 0 {
|
||||
inFileName = "."
|
||||
}
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create file: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
err = prodos.AddFilesFromHostDirectory(file, inFileName, pathName, recursive)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to add host files: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func create(fileName string, volumeName string, volumeSize int) {
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create file: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
prodos.CreateVolume(file, volumeName, volumeSize)
|
||||
}
|
||||
|
||||
func writeBlock(blockNumber int, fileName string, inFileName string) {
|
||||
checkInFileName(inFileName)
|
||||
fmt.Printf("Writing block 0x%04X (%d):\n\n", blockNumber, blockNumber)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
inFile, err := os.ReadFile(inFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open input file %s: %s", inFileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
prodos.WriteBlock(file, blockNumber, inFile)
|
||||
}
|
||||
|
||||
func readBlock(blockNumber int, fileName string) {
|
||||
fmt.Printf("Reading block 0x%04X (%d):\n\n", blockNumber, blockNumber)
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
block, err := prodos.ReadBlock(file, blockNumber)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
prodos.DumpBlock(block)
|
||||
}
|
||||
|
||||
func put(fileName string, pathName string, fileType int, auxType int, inFileName string) {
|
||||
checkPathName(pathName)
|
||||
checkInFileName(inFileName)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
fileInfo, err := os.Stat(fileName)
|
||||
|
||||
err = prodos.WriteFileFromFile(file, pathName, fileType, auxType, fileInfo.ModTime(), inFileName, false)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to write file %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func get(fileName string, pathName string, outFileName string) {
|
||||
checkPathName(pathName)
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
getFile, err := prodos.LoadFile(file, pathName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read file %s: %s\n", pathName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(outFileName) == 0 {
|
||||
_, outFileName = prodos.GetDirectoryAndFileNameFromPath(pathName)
|
||||
}
|
||||
outFile, err := os.Create(outFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create output file %s: %s\n", outFileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(outFileName), ".bas") {
|
||||
fmt.Fprintf(outFile, prodos.ConvertBasicToText(getFile))
|
||||
} else {
|
||||
outFile.Write(getFile)
|
||||
}
|
||||
}
|
||||
|
||||
func ls(fileName string, pathName string) {
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
pathName = strings.ToUpper(pathName)
|
||||
volumeHeader, _, fileEntries, err := prodos.ReadDirectory(file, pathName)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
}
|
||||
if len(pathName) == 0 {
|
||||
pathName = "/" + volumeHeader.VolumeName
|
||||
}
|
||||
volumeBitmap, err := prodos.ReadVolumeBitmap(file)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
freeBlocks := prodos.GetFreeBlockCount(volumeBitmap, volumeHeader.TotalBlocks)
|
||||
prodos.DumpDirectory(freeBlocks, volumeHeader.TotalBlocks, pathName, fileEntries)
|
||||
}
|
||||
|
||||
func checkPathName(pathName string) {
|
||||
if len(pathName) == 0 {
|
||||
fmt.Printf("Missing path name (use -p PATHNAME)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func checkInFileName(inFileName string) {
|
||||
if len(inFileName) == 0 {
|
||||
fmt.Printf("Missing input file name (use -i FILENAME)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
133
prodos/basic.go
133
prodos/basic.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -7,7 +7,10 @@
|
|||
package prodos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -88,7 +91,7 @@ var tokens = map[byte]string{
|
|||
0xC9: "-",
|
||||
0xCA: "*",
|
||||
0xCB: "/",
|
||||
0xCC: ";",
|
||||
//0xCC: ";", // fails if this is there
|
||||
0xCD: "AND",
|
||||
0xCE: "OR",
|
||||
0xCF: ">",
|
||||
|
@ -160,3 +163,129 @@ func ConvertBasicToText(basic []byte) string {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertTextToBasic converts text to AppleSoft BASIC
|
||||
func ConvertTextToBasic(text string) ([]byte, error) {
|
||||
// convert line endings
|
||||
text = strings.Replace(text, "\r\n", "\n", -1)
|
||||
text = strings.Replace(text, "\r", "\n", -1)
|
||||
|
||||
starting := true
|
||||
parsingLineNumber := false
|
||||
parsingData := false
|
||||
parsingString := false
|
||||
parsingRem := false
|
||||
foundToken := false
|
||||
|
||||
currentByte := 0x0801
|
||||
var lineNumberString string
|
||||
|
||||
basicFile := new(bytes.Buffer)
|
||||
basicLine := new(bytes.Buffer)
|
||||
|
||||
skipChars := 0
|
||||
|
||||
// parse character by character
|
||||
for index, c := range text {
|
||||
|
||||
// skip initial whitespace and look for the start of a line number
|
||||
if starting {
|
||||
if c == '\n' { // skip blank lines
|
||||
continue
|
||||
}
|
||||
if c == ' ' {
|
||||
continue
|
||||
}
|
||||
if c >= '0' && c <= '9' {
|
||||
starting = false
|
||||
parsingLineNumber = true
|
||||
} else {
|
||||
return nil, errors.New("unexpected character before line number")
|
||||
}
|
||||
}
|
||||
|
||||
if skipChars > 0 && c != '\n' {
|
||||
skipChars--
|
||||
continue
|
||||
}
|
||||
|
||||
// parse line number
|
||||
if parsingLineNumber {
|
||||
if c >= '0' && c <= '9' {
|
||||
lineNumberString += string(c)
|
||||
} else {
|
||||
lineNumber, err := strconv.ParseUint(lineNumberString, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
basicLine.WriteByte(byte(lineNumber % 256)) // low byte
|
||||
basicLine.WriteByte(byte(lineNumber / 256)) // high byte
|
||||
parsingLineNumber = false
|
||||
lineNumberString = ""
|
||||
}
|
||||
}
|
||||
|
||||
if !parsingLineNumber {
|
||||
if c == '\n' {
|
||||
starting = true
|
||||
parsingLineNumber = false
|
||||
parsingData = false
|
||||
parsingRem = false
|
||||
parsingString = false
|
||||
foundToken = false
|
||||
skipChars = 0
|
||||
currentByte += basicLine.Len()
|
||||
currentByte += 3
|
||||
// write address of next line
|
||||
basicFile.WriteByte(byte(currentByte % 256))
|
||||
basicFile.WriteByte(byte(currentByte / 256))
|
||||
// write the line
|
||||
basicFile.Write(basicLine.Bytes())
|
||||
basicFile.WriteByte(0x00)
|
||||
basicLine.Reset()
|
||||
} else if parsingData {
|
||||
basicLine.WriteByte(byte(c))
|
||||
} else if parsingRem {
|
||||
basicLine.WriteByte(byte(c))
|
||||
} else if parsingString {
|
||||
basicLine.WriteByte(byte(c))
|
||||
if c == '"' {
|
||||
parsingString = false
|
||||
}
|
||||
} else if c == '"' {
|
||||
parsingString = true
|
||||
basicLine.WriteByte(byte(c))
|
||||
} else {
|
||||
if c == ' ' {
|
||||
continue
|
||||
}
|
||||
|
||||
for key, token := range tokens {
|
||||
if index < len(text)-len(token) {
|
||||
if text[index:index+len(token)] == token {
|
||||
basicLine.WriteByte(byte(key))
|
||||
skipChars = len(token)
|
||||
foundToken = true
|
||||
if key == 0x83 {
|
||||
parsingData = true
|
||||
}
|
||||
if key == 0xB2 {
|
||||
parsingRem = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if foundToken {
|
||||
foundToken = false
|
||||
} else {
|
||||
basicLine.WriteByte(byte(c))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
basicFile.WriteByte(0x00)
|
||||
basicFile.WriteByte(0x00)
|
||||
|
||||
return basicFile.Bytes(), nil
|
||||
}
|
||||
|
|
82
prodos/basic_test.go
Normal file
82
prodos/basic_test.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
// Copyright Terence J. Boldt (c)2022-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides tests for conversion between BASIC and text
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConvertBasicToText(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
basic []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"Simple",
|
||||
[]byte{
|
||||
0x14, 0x08, 0x0A, 0x00, 0xBA, 0x22, 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x22, 0x00,
|
||||
0x1A, 0x08, 0x14, 0x00, 0x80, 0x00,
|
||||
0x00, 0x00},
|
||||
"10 PRINT \"HELLO WORLD\"\n20 END \n"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
testname := tt.name
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
text := ConvertBasicToText(tt.basic)
|
||||
if text != tt.want {
|
||||
t.Errorf("%s\ngot '%#v'\nwant '%#v'\n", testname, []byte(text), []byte(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTextToBasic(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
basicText string
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
"Hello world",
|
||||
"10 PRINT \"HELLO WORLD\"\n20 END \n",
|
||||
[]byte{
|
||||
0x14, 0x08, 0x0A, 0x00, 0xBA, 0x22, 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x22, 0x00,
|
||||
0x1A, 0x08, 0x14, 0x00, 0x80, 0x00,
|
||||
0x00, 0x00}},
|
||||
{
|
||||
"Variables",
|
||||
"10 A = 1\n",
|
||||
[]byte{
|
||||
0x09, 0x08, 0x0A, 0x00, 0x41, 0xD0, 0x31, 0x00,
|
||||
0x00, 0x00}},
|
||||
{
|
||||
"Rem",
|
||||
"10 REM x y z\n",
|
||||
[]byte{
|
||||
0x0C, 0x08, 0x0A, 0x00, 0xB2, 0x78, 0x20, 0x79, 0x20, 0x7A, 0x00,
|
||||
0x00, 0x00}},
|
||||
{
|
||||
"Punctuation",
|
||||
"1 PRINT ;: PRINT\n",
|
||||
[]byte{
|
||||
0x0A, 0x08, 0x01, 0x00, 0xBA, 0x3B, 0x3A, 0xBA, 0x00,
|
||||
0x00, 0x00}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
testname := tt.name
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
basic, _ := ConvertTextToBasic(tt.basicText)
|
||||
if bytes.Compare(basic, tt.want) != 0 {
|
||||
t.Errorf("%s\ngot '%#v'\nwant '%#v'\n", testname, basic, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -64,11 +64,35 @@ func writeVolumeBitmap(readerWriter ReaderWriterAt, bitmap []byte) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
volumeHeader := parseVolumeHeader(headerBlock)
|
||||
|
||||
for i := 0; i < len(bitmap)/512; i++ {
|
||||
WriteBlock(readerWriter, volumeHeader.BitmapStartBlock+i, bitmap[i*512:i*512+512])
|
||||
volumeHeader := parseVolumeHeader(headerBlock)
|
||||
totalBitmapBytes := volumeHeader.TotalBlocks / 8
|
||||
if volumeHeader.TotalBlocks%8 > 0 {
|
||||
totalBitmapBytes++
|
||||
}
|
||||
|
||||
totalBitmapBlocks := totalBitmapBytes / 512
|
||||
|
||||
if totalBitmapBytes%512 > 0 {
|
||||
totalBitmapBlocks++
|
||||
}
|
||||
|
||||
for i := 0; i < totalBitmapBlocks; i++ {
|
||||
bitmapBlock, err := ReadBlock(readerWriter, i+volumeHeader.BitmapStartBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for j := 0; j < 512 && i*512+j < totalBitmapBytes; j++ {
|
||||
bitmapBlock[j] = bitmap[i*512+j]
|
||||
}
|
||||
|
||||
err = WriteBlock(readerWriter, volumeHeader.BitmapStartBlock+i, bitmapBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -107,7 +131,6 @@ func createVolumeBitmap(numberOfBlocks int) []byte {
|
|||
markBlockInVolumeBitmap(volumeBitmap, i)
|
||||
}
|
||||
}
|
||||
//DumpBlock(volumeBitmap)
|
||||
|
||||
return volumeBitmap
|
||||
}
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides tests for access to volum bitmap on
|
||||
// a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
|
@ -53,3 +60,32 @@ func TestCheckFreeBlockInVolumeBitmap(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkBlockInVolumeBitmap(t *testing.T) {
|
||||
var tests = []struct {
|
||||
blocks int
|
||||
want bool
|
||||
}{
|
||||
{0, false}, // boot block
|
||||
{1, false}, // SOS boot block
|
||||
{2, false}, // volume root
|
||||
{21, false}, // end of volume bitmap
|
||||
{22, true}, // beginning of free space
|
||||
{999, false}, // end of volume bitmap
|
||||
{8192, true}, // more free space
|
||||
{65534, true}, // last free block
|
||||
{65535, false}, // can't use last block because volume size is 0xFFFF, not 0x10000
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("%d", tt.blocks)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
volumeBitMap := createVolumeBitmap(65535)
|
||||
markBlockInVolumeBitmap(volumeBitMap, 999)
|
||||
ans := checkFreeBlockInVolumeBitmap(volumeBitMap, tt.blocks)
|
||||
if ans != tt.want {
|
||||
t.Errorf("got %t, want %t", ans, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -8,6 +8,8 @@
|
|||
package prodos
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
|
@ -16,6 +18,10 @@ func ReadBlock(reader io.ReaderAt, block int) ([]byte, error) {
|
|||
buffer := make([]byte, 512)
|
||||
|
||||
_, err := reader.ReadAt(buffer, int64(block)*512)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to read block %04X: %s", block, err.Error())
|
||||
err = errors.New(errString)
|
||||
}
|
||||
|
||||
return buffer, err
|
||||
}
|
||||
|
@ -23,5 +29,10 @@ func ReadBlock(reader io.ReaderAt, block int) ([]byte, error) {
|
|||
// WriteBlock writes a block to a ProDOS volume from a byte array
|
||||
func WriteBlock(writer io.WriterAt, block int, buffer []byte) error {
|
||||
_, err := writer.WriteAt(buffer, int64(block)*512)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to write block %04X: %s", block, err.Error())
|
||||
err = errors.New(errString)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -31,11 +31,21 @@ type VolumeHeader struct {
|
|||
|
||||
// DirectoryHeader from ProDOS
|
||||
type DirectoryHeader struct {
|
||||
Name string
|
||||
ActiveFileCount int
|
||||
StartingBlock int
|
||||
PreviousBlock int
|
||||
NextBlock int
|
||||
PreviousBlock int
|
||||
NextBlock int
|
||||
IsSubDirectory bool
|
||||
Name string
|
||||
CreationTime time.Time
|
||||
Version int
|
||||
MinVersion int
|
||||
Access int
|
||||
EntryLength int
|
||||
EntriesPerBlock int
|
||||
ActiveFileCount int
|
||||
StartingBlock int
|
||||
ParentBlock int
|
||||
ParentEntry int
|
||||
ParentEntryLength int
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -55,19 +65,20 @@ const (
|
|||
|
||||
// FileEntry from ProDOS
|
||||
type FileEntry struct {
|
||||
StorageType int
|
||||
FileName string
|
||||
FileType int
|
||||
CreationTime time.Time
|
||||
KeyPointer int
|
||||
Version int
|
||||
MinVersion int
|
||||
BlocksUsed int
|
||||
EndOfFile int
|
||||
Access int
|
||||
AuxType int
|
||||
ModifiedTime time.Time
|
||||
HeaderPointer int
|
||||
StorageType int
|
||||
FileName string
|
||||
FileType int
|
||||
CreationTime time.Time
|
||||
KeyPointer int
|
||||
Version int
|
||||
MinVersion int
|
||||
BlocksUsed int
|
||||
EndOfFile int
|
||||
Access int
|
||||
AuxType int
|
||||
ModifiedTime time.Time
|
||||
HeaderPointer int
|
||||
|
||||
DirectoryBlock int
|
||||
DirectoryOffset int
|
||||
}
|
||||
|
@ -86,6 +97,11 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
|
|||
path = fmt.Sprintf("/%s", volumeHeader.VolumeName)
|
||||
}
|
||||
|
||||
// add volume name if not full path
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
|
||||
}
|
||||
|
||||
path = strings.ToUpper(path)
|
||||
paths := strings.Split(path, "/")
|
||||
|
||||
|
@ -97,14 +113,109 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
|
|||
return volumeHeader, directoryHeader, fileEntries, nil
|
||||
}
|
||||
|
||||
func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntry, error) {
|
||||
_, directoryHeader, _, err := ReadDirectory(reader, directory)
|
||||
// CreateDirectory creates a directory information of a specified path
|
||||
// on a ProDOS image
|
||||
func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
|
||||
if len(path) == 0 {
|
||||
return errors.New("cannot create directory with path")
|
||||
}
|
||||
|
||||
// add volume name if not full path
|
||||
path, err := makeFullPath(path, readerWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parentPath, newDirectory := GetDirectoryAndFileNameFromPath(path)
|
||||
|
||||
existingFileEntry, _ := GetFileEntry(readerWriter, path)
|
||||
if existingFileEntry.StorageType != StorageDeleted {
|
||||
return errors.New("directory already exists")
|
||||
}
|
||||
|
||||
fileEntry, err := getFreeFileEntryInDirectory(readerWriter, parentPath)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
// get list of blocks to write file to
|
||||
blockList, err := createBlockList(readerWriter, 512)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
updateVolumeBitmap(readerWriter, blockList)
|
||||
|
||||
fileEntry.FileName = newDirectory
|
||||
fileEntry.BlocksUsed = 1
|
||||
fileEntry.CreationTime = time.Now()
|
||||
fileEntry.ModifiedTime = time.Now()
|
||||
fileEntry.AuxType = 0
|
||||
fileEntry.EndOfFile = 0x200
|
||||
fileEntry.FileType = 0x0F
|
||||
fileEntry.KeyPointer = blockList[0]
|
||||
fileEntry.Access = 0b11100011
|
||||
fileEntry.StorageType = StorageDirectory
|
||||
fileEntry.Version = 0x24
|
||||
fileEntry.MinVersion = 0x00
|
||||
|
||||
writeFileEntry(readerWriter, fileEntry)
|
||||
|
||||
err = incrementFileCount(readerWriter, fileEntry)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
directoryEntry := DirectoryHeader{
|
||||
PreviousBlock: 0,
|
||||
NextBlock: 0,
|
||||
IsSubDirectory: true,
|
||||
Name: newDirectory,
|
||||
CreationTime: time.Now(),
|
||||
Version: 0x24,
|
||||
MinVersion: 0,
|
||||
Access: 0xE3,
|
||||
EntryLength: 0x27,
|
||||
EntriesPerBlock: 0x0D,
|
||||
ActiveFileCount: 0,
|
||||
StartingBlock: blockList[0],
|
||||
ParentBlock: fileEntry.DirectoryBlock,
|
||||
ParentEntry: (fileEntry.DirectoryOffset - 0x04) / 0x27,
|
||||
ParentEntryLength: 0x27,
|
||||
}
|
||||
|
||||
err = writeDirectoryHeader(readerWriter, directoryEntry)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeFullPath(path string, reader io.ReaderAt) (string, error) {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
buffer, err := ReadBlock(reader, 0x0002)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
volumeHeader := parseVolumeHeader(buffer)
|
||||
path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func getFreeFileEntryInDirectory(readerWriter ReaderWriterAt, directory string) (FileEntry, error) {
|
||||
_, directoryHeader, _, err := ReadDirectory(readerWriter, directory)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
//DumpDirectoryHeader(directoryHeader)
|
||||
blockNumber := directoryHeader.StartingBlock
|
||||
buffer, err := ReadBlock(reader, blockNumber)
|
||||
buffer, err := ReadBlock(readerWriter, blockNumber)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
|
@ -113,23 +224,31 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
|
|||
|
||||
for {
|
||||
if entryNumber > 13 {
|
||||
blockNumber = int(buffer[2]) + int(buffer[3])*256
|
||||
// if we ran out of blocks in the directory, return empty
|
||||
// TODO: expand the directory to add more entries
|
||||
if blockNumber == 0 {
|
||||
return FileEntry{}, errors.New("No free file entries found")
|
||||
nextBlockNumber := int(buffer[2]) + int(buffer[3])*256
|
||||
// if we ran out of blocks in the directory, expand directory or fail
|
||||
if nextBlockNumber == 0 {
|
||||
if !directoryHeader.IsSubDirectory {
|
||||
return FileEntry{}, errors.New("no free file entries found")
|
||||
}
|
||||
nextBlockNumber, err = expandDirectory(readerWriter, nextBlockNumber, buffer, blockNumber, directoryHeader)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
}
|
||||
blockNumber = nextBlockNumber
|
||||
// else read the next block in the directory
|
||||
buffer, err = ReadBlock(reader, blockNumber)
|
||||
buffer, err = ReadBlock(readerWriter, blockNumber)
|
||||
if err != nil {
|
||||
return FileEntry{}, nil
|
||||
}
|
||||
|
||||
entryOffset = 4
|
||||
entryNumber = 1
|
||||
}
|
||||
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
|
||||
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+0x28], blockNumber, entryOffset)
|
||||
|
||||
if fileEntry.StorageType == StorageDeleted {
|
||||
fileEntry = FileEntry{}
|
||||
fileEntry.DirectoryBlock = blockNumber
|
||||
fileEntry.DirectoryOffset = entryOffset
|
||||
fileEntry.HeaderPointer = directoryHeader.StartingBlock
|
||||
|
@ -141,6 +260,51 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
|
|||
}
|
||||
}
|
||||
|
||||
func expandDirectory(readerWriter ReaderWriterAt, nextBlockNumber int, buffer []byte, blockNumber int, directoryHeader DirectoryHeader) (int, error) {
|
||||
volumeBitMap, err := ReadVolumeBitmap(readerWriter)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to get volume bitmap to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
blockList := findFreeBlocks(volumeBitMap, 1)
|
||||
if len(blockList) != 1 {
|
||||
return 0, errors.New("failed to get free block to expand directory")
|
||||
}
|
||||
|
||||
nextBlockNumber = blockList[0]
|
||||
buffer[0x02] = byte(nextBlockNumber & 0x00FF)
|
||||
buffer[0x03] = byte(nextBlockNumber >> 8)
|
||||
WriteBlock(readerWriter, blockNumber, buffer)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to write block to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
|
||||
buffer = make([]byte, 0x200)
|
||||
buffer[0x00] = byte(blockNumber & 0x00FF)
|
||||
buffer[0x01] = byte(blockNumber >> 8)
|
||||
err = WriteBlock(readerWriter, nextBlockNumber, buffer)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to write new block to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
|
||||
updateVolumeBitmap(readerWriter, blockList)
|
||||
|
||||
buffer, err = ReadBlock(readerWriter, directoryHeader.ParentBlock)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to read parent block to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
directoryEntryOffset := directoryHeader.ParentEntry*directoryHeader.EntryLength + 0x04
|
||||
directoryFileEntry := parseFileEntry(buffer[directoryEntryOffset:directoryEntryOffset+0x28], directoryHeader.ParentBlock, directoryHeader.ParentEntry*directoryHeader.EntryLength+0x04)
|
||||
directoryFileEntry.BlocksUsed++
|
||||
directoryFileEntry.EndOfFile += 0x200
|
||||
writeFileEntry(readerWriter, directoryFileEntry)
|
||||
|
||||
return nextBlockNumber, nil
|
||||
}
|
||||
|
||||
func getFileEntriesInDirectory(reader io.ReaderAt, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry, error) {
|
||||
buffer, err := ReadBlock(reader, blockNumber)
|
||||
if err != nil {
|
||||
|
@ -255,13 +419,14 @@ func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) {
|
|||
buffer[0x1E] = byte(fileEntry.Access)
|
||||
buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF)
|
||||
buffer[0x20] = byte(fileEntry.AuxType >> 8)
|
||||
modifiedTime := DateTimeToProDOS(fileEntry.CreationTime)
|
||||
modifiedTime := DateTimeToProDOS(fileEntry.ModifiedTime)
|
||||
for i := 0; i < 4; i++ {
|
||||
buffer[0x21+i] = modifiedTime[i]
|
||||
}
|
||||
buffer[0x25] = byte(fileEntry.HeaderPointer & 0x00FF)
|
||||
buffer[0x26] = byte(fileEntry.HeaderPointer >> 8)
|
||||
|
||||
//fmt.Printf("Writing file entry at block: %04X offset: %04X\n", fileEntry.DirectoryBlock, fileEntry.DirectoryOffset)
|
||||
_, err := writer.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset))
|
||||
if err != nil {
|
||||
|
||||
|
@ -281,7 +446,7 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
|
|||
bitmapBlock := int(buffer[39]) + int(buffer[40])*256
|
||||
totalBlocks := int(buffer[41]) + int(buffer[42])*256
|
||||
|
||||
if version > 0 || minVersion > 0 {
|
||||
if minVersion > 0 {
|
||||
panic("Unsupported ProDOS version")
|
||||
}
|
||||
|
||||
|
@ -303,22 +468,43 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
|
|||
func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
|
||||
previousBlock := int(buffer[0x00]) + int(buffer[0x01])*256
|
||||
nextBlock := int(buffer[0x02]) + int(buffer[0x03])*256
|
||||
filenameLength := buffer[0x04] & 15
|
||||
isSubDirectory := (buffer[0x04] & 0xF0) == 0xE0
|
||||
filenameLength := buffer[0x04] & 0x0F
|
||||
name := string(buffer[0x05 : filenameLength+0x05])
|
||||
creationTime := DateTimeFromProDOS(buffer[0x1C:0x20])
|
||||
version := int(buffer[0x20])
|
||||
minVersion := int(buffer[0x21])
|
||||
access := int(buffer[0x22])
|
||||
entryLength := int(buffer[0x23])
|
||||
entriesPerBlock := int(buffer[0x24])
|
||||
fileCount := int(buffer[0x25]) + int(buffer[0x26])*256
|
||||
parentBlock := int(buffer[0x27]) + int(buffer[0x28])*256
|
||||
parentEntry := int(buffer[0x29])
|
||||
parentEntryLength := int(buffer[0x2A])
|
||||
|
||||
directoryEntry := DirectoryHeader{
|
||||
PreviousBlock: previousBlock,
|
||||
NextBlock: nextBlock,
|
||||
StartingBlock: blockNumber,
|
||||
Name: name,
|
||||
ActiveFileCount: fileCount,
|
||||
PreviousBlock: previousBlock,
|
||||
NextBlock: nextBlock,
|
||||
StartingBlock: blockNumber,
|
||||
IsSubDirectory: isSubDirectory,
|
||||
Name: name,
|
||||
CreationTime: creationTime,
|
||||
Version: version,
|
||||
MinVersion: minVersion,
|
||||
Access: access,
|
||||
EntryLength: entryLength,
|
||||
EntriesPerBlock: entriesPerBlock,
|
||||
ActiveFileCount: fileCount,
|
||||
ParentBlock: parentBlock,
|
||||
ParentEntry: parentEntry,
|
||||
ParentEntryLength: parentEntryLength,
|
||||
}
|
||||
|
||||
return directoryEntry
|
||||
}
|
||||
|
||||
func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader DirectoryHeader) error {
|
||||
// Reading back the block preserves values including reserved fields
|
||||
buffer, err := ReadBlock(readerWriter, directoryHeader.StartingBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -327,12 +513,40 @@ func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader Directory
|
|||
buffer[0x01] = byte(directoryHeader.PreviousBlock >> 8)
|
||||
buffer[0x02] = byte(directoryHeader.NextBlock & 0x00FF)
|
||||
buffer[0x03] = byte(directoryHeader.NextBlock >> 8)
|
||||
if directoryHeader.IsSubDirectory {
|
||||
buffer[0x04] = 0xE0
|
||||
} else {
|
||||
buffer[0x04] = 0xF0
|
||||
}
|
||||
buffer[0x04] = buffer[0x04] | byte(len(directoryHeader.Name))
|
||||
for i := 0; i < len(directoryHeader.Name); i++ {
|
||||
buffer[0x05+i] = directoryHeader.Name[i]
|
||||
}
|
||||
creationTime := DateTimeToProDOS(directoryHeader.CreationTime)
|
||||
for i := 0; i < 4; i++ {
|
||||
buffer[0x1C+i] = creationTime[i]
|
||||
}
|
||||
// Without these reserved bytes, reading the directory causes I/O ERROR
|
||||
buffer[0x14] = 0x75
|
||||
buffer[0x15] = byte(directoryHeader.Version)
|
||||
buffer[0x16] = byte(directoryHeader.MinVersion)
|
||||
buffer[0x17] = 0xC3
|
||||
buffer[0x18] = 0x0D
|
||||
buffer[0x19] = 0x27
|
||||
buffer[0x1A] = 0x00
|
||||
buffer[0x1B] = 0x00
|
||||
|
||||
buffer[0x20] = byte(directoryHeader.Version)
|
||||
buffer[0x21] = byte(directoryHeader.MinVersion)
|
||||
buffer[0x22] = byte(directoryHeader.Access)
|
||||
buffer[0x23] = byte(directoryHeader.EntryLength)
|
||||
buffer[0x24] = byte(directoryHeader.EntriesPerBlock)
|
||||
buffer[0x25] = byte(directoryHeader.ActiveFileCount & 0x00FF)
|
||||
buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8)
|
||||
buffer[0x27] = byte(directoryHeader.ParentBlock & 0x00FF)
|
||||
buffer[0x28] = byte(directoryHeader.ParentBlock >> 8)
|
||||
buffer[0x29] = byte(directoryHeader.ParentEntry)
|
||||
buffer[0x2A] = byte(directoryHeader.ParentEntryLength)
|
||||
WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer)
|
||||
|
||||
return nil
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
|
222
prodos/file.go
222
prodos/file.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -9,6 +9,7 @@ package prodos
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -16,7 +17,7 @@ import (
|
|||
|
||||
// LoadFile loads in a file from a ProDOS volume into a byte array
|
||||
func LoadFile(reader io.ReaderAt, path string) ([]byte, error) {
|
||||
fileEntry, err := getFileEntry(reader, path)
|
||||
fileEntry, err := GetFileEntry(reader, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -42,12 +43,16 @@ func LoadFile(reader io.ReaderAt, path string) ([]byte, error) {
|
|||
}
|
||||
|
||||
// WriteFile writes a file to a ProDOS volume from a byte array
|
||||
func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, buffer []byte) error {
|
||||
func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, createdTime time.Time, modifiedTime time.Time, buffer []byte) error {
|
||||
directory, fileName := GetDirectoryAndFileNameFromPath(path)
|
||||
|
||||
existingFileEntry, _ := getFileEntry(readerWriter, path)
|
||||
if len(fileName) > 15 {
|
||||
return errors.New("filename too long")
|
||||
}
|
||||
|
||||
existingFileEntry, _ := GetFileEntry(readerWriter, path)
|
||||
if existingFileEntry.StorageType != StorageDeleted {
|
||||
DeleteFile(readerWriter, path)
|
||||
return errors.New(("file already exists"))
|
||||
}
|
||||
|
||||
// get list of blocks to write file to
|
||||
|
@ -58,7 +63,7 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
|
||||
// seedling file
|
||||
if len(buffer) <= 0x200 {
|
||||
WriteBlock(readerWriter, blockList[0], buffer)
|
||||
writeSeedlingFile(readerWriter, buffer, blockList)
|
||||
}
|
||||
|
||||
// sapling file needs index block
|
||||
|
@ -66,9 +71,13 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
writeSaplingFile(readerWriter, buffer, blockList)
|
||||
}
|
||||
|
||||
// TODO: add tree file
|
||||
if len(buffer) > 0x20000 {
|
||||
return errors.New("files > 128KB not supported yet")
|
||||
// tree file needs master index and index blocks
|
||||
if len(buffer) > 0x20000 && len(buffer) <= 0x1000000 {
|
||||
writeTreeFile(readerWriter, buffer, blockList)
|
||||
}
|
||||
|
||||
if len(buffer) > 0x1000000 {
|
||||
return errors.New("files > 16MB not supported by ProDOS")
|
||||
}
|
||||
|
||||
updateVolumeBitmap(readerWriter, blockList)
|
||||
|
@ -80,12 +89,14 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
}
|
||||
fileEntry.FileName = fileName
|
||||
fileEntry.BlocksUsed = len(blockList)
|
||||
fileEntry.CreationTime = time.Now()
|
||||
fileEntry.ModifiedTime = time.Now()
|
||||
fileEntry.CreationTime = createdTime
|
||||
fileEntry.ModifiedTime = modifiedTime
|
||||
fileEntry.AuxType = auxType
|
||||
fileEntry.EndOfFile = len(buffer)
|
||||
fileEntry.FileType = fileType
|
||||
fileEntry.KeyPointer = blockList[0]
|
||||
fileEntry.Version = 0x24
|
||||
fileEntry.MinVersion = 0x00
|
||||
fileEntry.Access = 0b11100011
|
||||
if len(blockList) == 1 {
|
||||
fileEntry.StorageType = StorageSeedling
|
||||
|
@ -97,7 +108,10 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
|
||||
writeFileEntry(readerWriter, fileEntry)
|
||||
|
||||
// increment file count
|
||||
return incrementFileCount(readerWriter, fileEntry)
|
||||
}
|
||||
|
||||
func incrementFileCount(readerWriter ReaderWriterAt, fileEntry FileEntry) error {
|
||||
directoryHeaderBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -111,22 +125,27 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
|
||||
// DeleteFile deletes a file from a ProDOS volume
|
||||
func DeleteFile(readerWriter ReaderWriterAt, path string) error {
|
||||
fileEntry, err := getFileEntry(readerWriter, path)
|
||||
fileEntry, err := GetFileEntry(readerWriter, path)
|
||||
// DumpFileEntry(fileEntry)
|
||||
// oldDirectoryBlock, _ := ReadBlock(readerWriter, fileEntry.DirectoryBlock)
|
||||
// DumpBlock(oldDirectoryBlock)
|
||||
|
||||
if err != nil {
|
||||
return errors.New("File not found")
|
||||
return errors.New("file not found")
|
||||
}
|
||||
if fileEntry.StorageType == StorageDeleted {
|
||||
return errors.New("File already deleted")
|
||||
return errors.New("file already deleted")
|
||||
}
|
||||
if fileEntry.StorageType == StorageDirectory {
|
||||
return errors.New("Directory deletion not supported")
|
||||
return errors.New("directory deletion not supported")
|
||||
}
|
||||
|
||||
// free the blocks
|
||||
blocks, err := getBlocklist(readerWriter, fileEntry)
|
||||
blocks, err := getAllBlockList(readerWriter, fileEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumeBitmap, err := ReadVolumeBitmap(readerWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -154,6 +173,12 @@ func DeleteFile(readerWriter ReaderWriterAt, path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// FileExists return true if the file exists
|
||||
func FileExists(reader io.ReaderAt, path string) (bool, error) {
|
||||
fileEntry, _ := GetFileEntry(reader, path)
|
||||
return fileEntry.StorageType != StorageDeleted, nil
|
||||
}
|
||||
|
||||
// GetDirectoryAndFileNameFromPath gets the directory and filename from a path
|
||||
func GetDirectoryAndFileNameFromPath(path string) (string, string) {
|
||||
path = strings.ToUpper(path)
|
||||
|
@ -173,15 +198,20 @@ func GetDirectoryAndFileNameFromPath(path string) (string, string) {
|
|||
}
|
||||
|
||||
func updateVolumeBitmap(readerWriter ReaderWriterAt, blockList []int) error {
|
||||
|
||||
volumeBitmap, err := ReadVolumeBitmap(readerWriter)
|
||||
if err != nil {
|
||||
fmt.Printf("%s", err)
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(blockList); i++ {
|
||||
markBlockInVolumeBitmap(volumeBitmap, blockList[i])
|
||||
}
|
||||
writeVolumeBitmap(readerWriter, volumeBitmap)
|
||||
return nil
|
||||
return writeVolumeBitmap(readerWriter, volumeBitmap)
|
||||
}
|
||||
|
||||
func writeSeedlingFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
||||
WriteBlock(writer, blockList[0], buffer)
|
||||
}
|
||||
|
||||
func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
||||
|
@ -191,6 +221,9 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
|||
if i < len(blockList)-1 {
|
||||
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
|
||||
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
|
||||
} else {
|
||||
indexBuffer[i] = 0
|
||||
indexBuffer[i+256] = 0
|
||||
}
|
||||
}
|
||||
WriteBlock(writer, blockList[0], indexBuffer)
|
||||
|
@ -216,8 +249,70 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
|||
}
|
||||
}
|
||||
|
||||
func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
||||
// write master index block with pointers to index blocks
|
||||
indexBuffer := make([]byte, 512)
|
||||
numberOfIndexBlocks := len(blockList)/256 + 1
|
||||
if len(blockList)%256 == 0 {
|
||||
numberOfIndexBlocks--
|
||||
}
|
||||
for i := 0; i < 256; i++ {
|
||||
if i < numberOfIndexBlocks {
|
||||
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
|
||||
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
|
||||
} else {
|
||||
indexBuffer[i] = 0
|
||||
indexBuffer[i+256] = 0
|
||||
}
|
||||
}
|
||||
WriteBlock(writer, blockList[0], indexBuffer)
|
||||
numberOfIndexBlocks++
|
||||
|
||||
// write index blocks
|
||||
for i := 0; i < len(blockList)/256+1; i++ {
|
||||
for j := 0; j < 256; j++ {
|
||||
if i*256+j < len(blockList)-numberOfIndexBlocks {
|
||||
indexBuffer[j] = byte(blockList[i*256+numberOfIndexBlocks+j] & 0x00FF)
|
||||
indexBuffer[j+256] = byte(blockList[i*256+j+2] >> 8)
|
||||
} else {
|
||||
indexBuffer[j] = 0
|
||||
indexBuffer[j+256] = 0
|
||||
}
|
||||
}
|
||||
WriteBlock(writer, blockList[i+1], indexBuffer)
|
||||
}
|
||||
|
||||
// write all data blocks
|
||||
blockBuffer := make([]byte, 512)
|
||||
blockPointer := 0
|
||||
blockIndexNumber := numberOfIndexBlocks
|
||||
for i := 0; i < len(buffer); i++ {
|
||||
blockBuffer[blockPointer] = buffer[i]
|
||||
if blockPointer == 511 {
|
||||
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
|
||||
blockPointer = 0
|
||||
blockIndexNumber++
|
||||
} else if i == len(buffer)-1 {
|
||||
for j := blockPointer; j < 512; j++ {
|
||||
blockBuffer[j] = 0
|
||||
}
|
||||
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
|
||||
} else {
|
||||
blockPointer++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
return getBlocklist(reader, fileEntry, true)
|
||||
}
|
||||
|
||||
func getAllBlockList(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
return getBlocklist(reader, fileEntry, false)
|
||||
}
|
||||
|
||||
// Returns all blocks, including index blocks
|
||||
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int, error) {
|
||||
blocks := make([]int, fileEntry.BlocksUsed)
|
||||
|
||||
switch fileEntry.StorageType {
|
||||
|
@ -229,83 +324,98 @@ func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockOffset := 1
|
||||
blocks[0] = fileEntry.KeyPointer
|
||||
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
|
||||
blocks[i+1] = int(index[i]) + int(index[i+256])*256
|
||||
blocks[i+blockOffset] = int(index[i]) + int(index[i+256])*256
|
||||
}
|
||||
if dataOnly {
|
||||
return blocks[1:], nil
|
||||
}
|
||||
return blocks, nil
|
||||
case StorageTree:
|
||||
// this is actually too large
|
||||
dataBlocks := make([]int, fileEntry.BlocksUsed)
|
||||
// this is also actually too large
|
||||
numberOfIndexBlocks := fileEntry.BlocksUsed/256 + 2
|
||||
indexBlocks := make([]int, numberOfIndexBlocks)
|
||||
masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks[0] = fileEntry.KeyPointer
|
||||
numberOfDataBlocks := 0
|
||||
|
||||
indexBlocks[0] = fileEntry.KeyPointer
|
||||
indexBlockCount := 1
|
||||
|
||||
for i := 0; i < 128; i++ {
|
||||
index, err := ReadBlock(reader, int(masterIndex[i])+int(masterIndex[i+256])*256)
|
||||
indexBlock := int(masterIndex[i]) + int(masterIndex[i+256])*256
|
||||
if indexBlock == 0 {
|
||||
break
|
||||
}
|
||||
indexBlocks[indexBlockCount] = indexBlock
|
||||
indexBlockCount++
|
||||
index, err := ReadBlock(reader, indexBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for j := 0; j < 256 && i*256+j < fileEntry.BlocksUsed; j++ {
|
||||
if (int(index[j]) + int(index[j+256])*256) == 0 {
|
||||
return blocks, nil
|
||||
break
|
||||
}
|
||||
blocks[i*256+j] = int(index[j]) + int(index[j+256])*256
|
||||
numberOfDataBlocks++
|
||||
dataBlocks[i*256+j] = int(index[j]) + int(index[j+256])*256
|
||||
}
|
||||
}
|
||||
|
||||
if dataOnly {
|
||||
return dataBlocks, nil
|
||||
}
|
||||
|
||||
blocks = append(indexBlocks[0:numberOfIndexBlocks], dataBlocks[0:numberOfDataBlocks]...)
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported file storage type")
|
||||
}
|
||||
|
||||
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
switch fileEntry.StorageType {
|
||||
case StorageSeedling:
|
||||
blocks := make([]int, 1)
|
||||
blocks[0] = fileEntry.KeyPointer
|
||||
return blocks, nil
|
||||
case StorageSapling:
|
||||
blocks := make([]int, fileEntry.BlocksUsed-1)
|
||||
index, err := ReadBlock(reader, fileEntry.KeyPointer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
|
||||
blocks[i] = int(index[i]) + int(index[i+256])*256
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Unsupported file storage type")
|
||||
}
|
||||
|
||||
func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
|
||||
numberOfBlocks := fileSize / 512
|
||||
|
||||
if fileSize%512 > 0 {
|
||||
numberOfBlocks++
|
||||
}
|
||||
|
||||
if fileSize > 0x200 && fileSize <= 0x20000 {
|
||||
numberOfBlocks++ // add index block
|
||||
}
|
||||
if fileSize > 0x20000 {
|
||||
// add master index block
|
||||
numberOfBlocks++
|
||||
// add index blocks for each 128 blocks
|
||||
numberOfBlocks += numberOfBlocks / 128
|
||||
|
||||
if fileSize > 0x20000 && fileSize <= 0x1000000 {
|
||||
// add index blocks for each 256 blocks
|
||||
numberOfBlocks += numberOfBlocks / 256
|
||||
// add index block for any remaining blocks
|
||||
if numberOfBlocks%128 > 0 {
|
||||
if numberOfBlocks%256 > 0 {
|
||||
numberOfBlocks++
|
||||
}
|
||||
// add master index block
|
||||
numberOfBlocks++
|
||||
}
|
||||
if fileSize > 0x1000000 {
|
||||
return nil, errors.New("file size too large")
|
||||
}
|
||||
|
||||
volumeBitmap, err := ReadVolumeBitmap(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockList := findFreeBlocks(volumeBitmap, numberOfBlocks)
|
||||
|
||||
return blockList, nil
|
||||
return blockList[0:numberOfBlocks], nil
|
||||
}
|
||||
|
||||
func getFileEntry(reader io.ReaderAt, path string) (FileEntry, error) {
|
||||
// GetFileEntry returns a file entry for the given path
|
||||
func GetFileEntry(reader io.ReaderAt, path string) (FileEntry, error) {
|
||||
directory, fileName := GetDirectoryAndFileNameFromPath(path)
|
||||
_, _, fileEntries, err := ReadDirectory(reader, directory)
|
||||
if err != nil {
|
||||
|
@ -313,7 +423,7 @@ func getFileEntry(reader io.ReaderAt, path string) (FileEntry, error) {
|
|||
}
|
||||
|
||||
if fileEntries == nil || len(fileEntries) == 0 {
|
||||
return FileEntry{}, errors.New("File entry not found")
|
||||
return FileEntry{}, errors.New("file entry not found")
|
||||
}
|
||||
|
||||
var fileEntry FileEntry
|
||||
|
@ -325,7 +435,7 @@ func getFileEntry(reader io.ReaderAt, path string) (FileEntry, error) {
|
|||
}
|
||||
|
||||
if fileEntry.StorageType == StorageDeleted {
|
||||
return FileEntry{}, errors.New("File not found")
|
||||
return FileEntry{}, errors.New("file not found")
|
||||
}
|
||||
|
||||
return fileEntry, nil
|
||||
|
|
64
prodos/file_test.go
Normal file
64
prodos/file_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateBlocklist(t *testing.T) {
|
||||
var tests = []struct {
|
||||
fileSize int
|
||||
wantBlocks int
|
||||
}{
|
||||
{1, 1},
|
||||
{512, 1},
|
||||
{513, 3},
|
||||
{2048, 5},
|
||||
{2049, 6},
|
||||
{17128, 35},
|
||||
}
|
||||
|
||||
virtualDisk := NewMemoryFile(0x2000000)
|
||||
CreateVolume(virtualDisk, "VIRTUAL.DISK", 0xFFFE)
|
||||
|
||||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("%d", tt.fileSize)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
blockList, err := createBlockList(virtualDisk, tt.fileSize)
|
||||
|
||||
if err != nil {
|
||||
t.Error("got error, want nil")
|
||||
}
|
||||
if len(blockList) != tt.wantBlocks {
|
||||
t.Errorf("got %d blocks, want %d", len(blockList), tt.wantBlocks)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateVolumeBitmap(t *testing.T) {
|
||||
blockList := []int{10, 11, 12, 100, 120}
|
||||
|
||||
virtualDisk := NewMemoryFile(0x2000000)
|
||||
CreateVolume(virtualDisk, "VIRTUAL.DISK", 0xFFFE)
|
||||
updateVolumeBitmap(virtualDisk, blockList)
|
||||
|
||||
for _, tt := range blockList {
|
||||
testname := fmt.Sprintf("%d", tt)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
|
||||
volumeBitmap, err := ReadVolumeBitmap(virtualDisk)
|
||||
if err != nil {
|
||||
t.Error("got error, want nil")
|
||||
}
|
||||
free := checkFreeBlockInVolumeBitmap(volumeBitmap, tt)
|
||||
if free {
|
||||
t.Errorf("got true, want false")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides tests for access to format a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
|
|
203
prodos/host.go
Normal file
203
prodos/host.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Copyright Terence J. Boldt (c)2022-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides access to generate a ProDOS drive image from a host directory
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AddFilesFromHostDirectory fills the root volume with files
|
||||
// from the specified host directory
|
||||
func AddFilesFromHostDirectory(
|
||||
readerWriter ReaderWriterAt,
|
||||
directory string,
|
||||
path string,
|
||||
recursive bool,
|
||||
) error {
|
||||
|
||||
path, err := makeFullPath(path, readerWriter)
|
||||
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path = path + "/"
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if file.Name()[0] != '.' && !file.IsDir() && info.Size() > 0 && info.Size() <= 0x1000000 {
|
||||
err = WriteFileFromFile(readerWriter, path, 0, 0, info.ModTime(), filepath.Join(directory, file.Name()), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if file.Name()[0] != '.' && recursive && file.IsDir() {
|
||||
newPath := file.Name()
|
||||
if len(newPath) > 15 {
|
||||
newPath = newPath[0:15]
|
||||
}
|
||||
newFullPath := strings.ToUpper(path + newPath)
|
||||
|
||||
newHostDirectory := filepath.Join(directory, file.Name())
|
||||
err = CreateDirectory(readerWriter, newFullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = AddFilesFromHostDirectory(readerWriter, newHostDirectory, newFullPath+"/", recursive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFileFromFile writes a file to a ProDOS volume from a host file
|
||||
func WriteFileFromFile(
|
||||
readerWriter ReaderWriterAt,
|
||||
pathName string,
|
||||
fileType int,
|
||||
auxType int,
|
||||
modifiedTime time.Time,
|
||||
inFileName string,
|
||||
ignoreDuplicates bool,
|
||||
) error {
|
||||
|
||||
inFile, err := os.ReadFile(inFileName)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("write from file failed: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
if auxType == 0 && fileType == 0 {
|
||||
auxType, fileType, inFile, err = convertFileByType(inFileName, inFile)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to convert file: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
}
|
||||
|
||||
trimExtensions := false
|
||||
if len(pathName) == 0 {
|
||||
_, pathName = filepath.Split(inFileName)
|
||||
pathName = strings.ToUpper(pathName)
|
||||
trimExtensions = true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(pathName, "/") {
|
||||
trimExtensions = true
|
||||
_, fileName := filepath.Split(inFileName)
|
||||
pathName = strings.ToUpper(pathName + fileName)
|
||||
}
|
||||
|
||||
if trimExtensions {
|
||||
ext := filepath.Ext(pathName)
|
||||
|
||||
if len(ext) > 0 {
|
||||
switch ext {
|
||||
case ".SYS", ".TXT", ".BAS", ".BIN":
|
||||
pathName = strings.TrimSuffix(pathName, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths := strings.SplitAfter(pathName, "/")
|
||||
if len(paths[len(paths)-1]) > 15 {
|
||||
paths[len(paths)-1] = paths[len(paths)-1][0:15]
|
||||
pathName = strings.Join(paths, "")
|
||||
}
|
||||
|
||||
// skip if file already exists and ignoring duplicates
|
||||
if ignoreDuplicates {
|
||||
exists, err := FileExists(readerWriter, pathName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return WriteFile(readerWriter, pathName, fileType, auxType, time.Now(), modifiedTime, inFile)
|
||||
}
|
||||
|
||||
func convertFileByType(inFileName string, inFile []byte) (int, int, []byte, error) {
|
||||
fileType := 0x06 // default to BIN
|
||||
auxType := 0x2000 // default to $2000
|
||||
|
||||
var err error
|
||||
|
||||
// Check for an AppleSingle file as produced by cc65
|
||||
if // Magic number
|
||||
binary.BigEndian.Uint32(inFile[0x00:]) == 0x00051600 &&
|
||||
// Version number
|
||||
binary.BigEndian.Uint32(inFile[0x04:]) == 0x00020000 &&
|
||||
// Number of entries
|
||||
binary.BigEndian.Uint16(inFile[0x18:]) == 0x0002 &&
|
||||
// Data Fork ID
|
||||
binary.BigEndian.Uint32(inFile[0x1A:]) == 0x00000001 &&
|
||||
// Offset
|
||||
binary.BigEndian.Uint32(inFile[0x1E:]) == 0x0000003A &&
|
||||
// Length
|
||||
binary.BigEndian.Uint32(inFile[0x22:]) == uint32(len(inFile))-0x3A &&
|
||||
// ProDOS File Info ID
|
||||
binary.BigEndian.Uint32(inFile[0x26:]) == 0x0000000B &&
|
||||
// Offset
|
||||
binary.BigEndian.Uint32(inFile[0x2A:]) == 0x00000032 &&
|
||||
// Length
|
||||
binary.BigEndian.Uint32(inFile[0x2E:]) == 0x00000008 {
|
||||
|
||||
fileType = int(binary.BigEndian.Uint16(inFile[0x34:]))
|
||||
auxType = int(binary.BigEndian.Uint32(inFile[0x36:]))
|
||||
inFile = inFile[0x3A:]
|
||||
} else {
|
||||
// use extension to determine file type
|
||||
ext := strings.ToUpper(filepath.Ext(inFileName))
|
||||
|
||||
switch ext {
|
||||
case ".BAS":
|
||||
inFile, err = ConvertTextToBasic(string(inFile))
|
||||
fileType = 0xFC
|
||||
auxType = 0x0801
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
case ".SYS":
|
||||
fileType = 0xFF
|
||||
auxType = 0x2000
|
||||
case ".BIN":
|
||||
fileType = 0x06
|
||||
auxType = 0x2000
|
||||
case ".TXT":
|
||||
inFile = []byte(strings.ReplaceAll(strings.ReplaceAll(string(inFile), "\r\n", "r"), "\n", "\r"))
|
||||
fileType = 0x04
|
||||
auxType = 0x0000
|
||||
case ".JPG", ".PNG":
|
||||
inFile = ConvertImageToHiResMonochrome(inFile)
|
||||
fileType = 0x06
|
||||
auxType = 0x2000
|
||||
}
|
||||
}
|
||||
|
||||
return auxType, fileType, inFile, err
|
||||
}
|
224
prodos/image.go
Normal file
224
prodos/image.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides access to read, write and delete
|
||||
// files on a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
// force import jpeg support by init only
|
||||
_ "image/jpeg"
|
||||
// force import png support by init only
|
||||
_ "image/png"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// 10 PRINT CHR$ (4)"open offsets"
|
||||
// 20 PRINT CHR$ (4)"write offsets"
|
||||
// 30 PRINT "offsets := [192]int{";
|
||||
// 40 FOR Y = 0 TO 191
|
||||
// 50 HPLOT 0,Y
|
||||
// 60 PRINT ( PEEK (39) * 256 + PEEK (38)) - 8192;
|
||||
// 70 IF Y < 191 THEN PRINT ", ";
|
||||
// 80 NEXT
|
||||
// 90 PRINT "}"
|
||||
// 100 PRINT CHR$ (4)"close offsets"
|
||||
var offsets = []int{0, 1024, 2048, 3072, 4096, 5120, 6144, 7168, 128, 1152, 2176, 3200, 4224, 5248, 6272, 7296, 256, 1280, 2304, 3328, 4352, 5376, 6400, 7424, 384, 1408, 2432, 3456, 4480, 5504, 6528, 7552, 512, 1536, 2560, 3584, 4608, 5632, 6656, 7680, 640, 1664, 2688, 3712, 4736, 5760, 6784, 7808, 768, 1792, 2816, 3840, 4864, 5888, 6912, 7936, 896, 1920, 2944, 3968, 4992, 6016, 7040, 8064, 40, 1064, 2088, 3112, 4136, 5160, 6184, 7208, 168, 1192, 2216, 3240, 4264, 5288, 6312, 7336, 296, 1320, 2344, 3368, 4392, 5416, 6440, 7464, 424, 1448, 2472, 3496, 4520, 5544, 6568, 7592, 552, 1576, 2600, 3624, 4648, 5672, 6696, 7720, 680, 1704, 2728, 3752, 4776, 5800, 6824, 7848, 808, 1832, 2856, 3880, 4904, 5928, 6952, 7976, 936, 1960, 2984, 4008, 5032, 6056, 7080, 8104, 80, 1104, 2128, 3152, 4176, 5200, 6224, 7248, 208, 1232, 2256, 3280, 4304, 5328, 6352, 7376, 336, 1360, 2384, 3408, 4432, 5456, 6480, 7504, 464, 1488, 2512, 3536, 4560, 5584, 6608, 7632, 592, 1616, 2640, 3664, 4688, 5712, 6736, 7760, 720, 1744, 2768, 3792, 4816, 5840, 6864, 7888, 848, 1872, 2896, 3920, 4944, 5968, 6992, 8016, 976, 2000, 3024, 4048, 5072, 6096, 7120, 8144}
|
||||
var pixel = []byte{1, 2, 4, 8, 16, 32, 64}
|
||||
|
||||
// ConvertImageToHiResMonochrome converts jpeg and png images to Apple II hi-res monochrome
|
||||
func ConvertImageToHiResMonochrome(imageBytes []byte) []byte {
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
a2imgSize := image.Rect(0, 0, 280, 192)
|
||||
|
||||
a2img := image.NewPaletted(a2imgSize, []color.Color{
|
||||
color.Black,
|
||||
color.White,
|
||||
})
|
||||
|
||||
a2monoImgSize := image.Rect(0, 0, 280, 192)
|
||||
|
||||
scaledImg := image.NewRGBA(a2monoImgSize)
|
||||
draw.BiLinear.Scale(scaledImg, a2monoImgSize, img, img.Bounds(), draw.Over, nil)
|
||||
draw.FloydSteinberg.Draw(a2img, a2monoImgSize, scaledImg, image.Point{})
|
||||
|
||||
hires := make([]byte, 8192)
|
||||
|
||||
for y := a2img.Bounds().Min.Y; y < a2img.Bounds().Max.Y; y++ {
|
||||
for x := a2img.Bounds().Min.X; x < a2img.Bounds().Max.X; x++ {
|
||||
if a2img.At(x, y) == color.White {
|
||||
hires[offsets[y]+x/7] |= pixel[x%7] | 128
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hires
|
||||
}
|
||||
|
||||
// ConvertImageToHiResColour converts jpeg and png images to Apple II hi-res colour
|
||||
func ConvertImageToHiResColour(imageBytes []byte) []byte {
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
a2colourImgSize := image.Rect(0, 0, 140, 192)
|
||||
|
||||
black := color.NRGBA{0, 0, 0, 255}
|
||||
green := color.NRGBA{20, 245, 60, 255}
|
||||
purple := color.NRGBA{255, 68, 253, 255}
|
||||
white := color.NRGBA{255, 255, 255, 255}
|
||||
orange := color.NRGBA{255, 106, 60, 255}
|
||||
blue := color.NRGBA{20, 207, 253, 255}
|
||||
|
||||
a2img := image.NewPaletted(a2colourImgSize, []color.Color{
|
||||
black,
|
||||
green,
|
||||
purple,
|
||||
white,
|
||||
orange,
|
||||
blue,
|
||||
})
|
||||
|
||||
scaledImg := image.NewRGBA(a2colourImgSize)
|
||||
draw.BiLinear.Scale(scaledImg, a2colourImgSize, img, img.Bounds(), draw.Over, nil)
|
||||
draw.FloydSteinberg.Draw(a2img, a2colourImgSize, scaledImg, image.Point{})
|
||||
|
||||
hires := make([]byte, 8192)
|
||||
|
||||
for y := a2img.Bounds().Min.Y; y < a2img.Bounds().Max.Y; y++ {
|
||||
for x7 := a2img.Bounds().Min.X; x7 < a2img.Bounds().Max.X; x7 += 7 {
|
||||
switch a2img.At(x7, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7] = 2
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] = 1
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7] = 2
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] = 1
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] = 3
|
||||
}
|
||||
switch a2img.At(x7+1, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7] |= 8
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] |= 4
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7] |= 8
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] |= 4
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] |= 12
|
||||
}
|
||||
switch a2img.At(x7+2, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7] |= 32
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] |= 16
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7] |= 32
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] |= 16
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] |= 48
|
||||
}
|
||||
switch a2img.At(x7+3, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 1
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] |= 64
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 1
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] |= 64
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] |= 64
|
||||
hires[offsets[y]+x7*2/7+1] |= 1
|
||||
}
|
||||
switch a2img.At(x7+4, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 4
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7+1] |= 2
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 4
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7+1] |= 2
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7+1] |= 6
|
||||
}
|
||||
switch a2img.At(x7+5, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 16
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7+1] |= 8
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 16
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7+1] |= 8
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7+1] |= 24
|
||||
}
|
||||
switch a2img.At(x7+6, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 64
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7+1] |= 32
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 64
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7+1] |= 32
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7+1] |= 96
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hires
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -12,6 +12,7 @@ type MemoryFile struct {
|
|||
size int
|
||||
}
|
||||
|
||||
// ReaderWriterAt is an interface for both ReaderAt and WriterAt combined
|
||||
type ReaderWriterAt interface {
|
||||
ReadAt(data []byte, offset int64) (int, error)
|
||||
WriteAt(data []byte, offset int64) (int, error)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -99,6 +99,9 @@ func DumpFileEntry(fileEntry FileEntry) {
|
|||
fmt.Printf("File type: %02X\n", fileEntry.FileType)
|
||||
fmt.Printf("Storage type: %02X\n", fileEntry.StorageType)
|
||||
fmt.Printf("Header pointer: %04X\n", fileEntry.HeaderPointer)
|
||||
fmt.Printf("Access: %04X\n", fileEntry.Access)
|
||||
fmt.Printf("Directory block: %04X\n", fileEntry.DirectoryBlock)
|
||||
fmt.Printf("Directory offset: %04X\n", fileEntry.DirectoryOffset)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
|
@ -118,11 +121,22 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
|
|||
|
||||
// DumpDirectoryHeader dumps the directory header as text
|
||||
func DumpDirectoryHeader(directoryHeader DirectoryHeader) {
|
||||
fmt.Printf("Name: %s\n", directoryHeader.Name)
|
||||
fmt.Printf("File count: %d\n", directoryHeader.ActiveFileCount)
|
||||
fmt.Printf("Starting block: %04X\n", directoryHeader.StartingBlock)
|
||||
fmt.Printf("Previous block: %04X\n", directoryHeader.PreviousBlock)
|
||||
fmt.Printf("Next block: %04X\n", directoryHeader.NextBlock)
|
||||
fmt.Printf("Is subdirectory: %t\n", directoryHeader.IsSubDirectory)
|
||||
fmt.Printf("Name: %s\n", directoryHeader.Name)
|
||||
fmt.Printf("Creation time: %s\n", TimeToString(directoryHeader.CreationTime))
|
||||
fmt.Printf("Version: %02X\n", directoryHeader.Version)
|
||||
fmt.Printf("MinVersion: %02X\n", directoryHeader.MinVersion)
|
||||
fmt.Printf("Access: %02X\n", directoryHeader.Access)
|
||||
fmt.Printf("Entry length: %02X\n", directoryHeader.EntryLength)
|
||||
fmt.Printf("Entries per block: %02X\n", directoryHeader.EntriesPerBlock)
|
||||
fmt.Printf("File count: %d\n", directoryHeader.ActiveFileCount)
|
||||
fmt.Printf("Active file count: %04X\n", directoryHeader.ActiveFileCount)
|
||||
fmt.Printf("Parent block: %04X\n", directoryHeader.ParentBlock)
|
||||
fmt.Printf("Parent entry: %02X\n", directoryHeader.ParentEntry)
|
||||
fmt.Printf("Parent entry length: %02X\n", directoryHeader.ParentEntryLength)
|
||||
}
|
||||
|
||||
// DumpBlock dumps the block as hexadecimal and text
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -11,19 +11,20 @@ import (
|
|||
)
|
||||
|
||||
// DateTimeToProDOS converts Time to ProDOS date time
|
||||
// 49041 ($BF91) 49040 ($BF90)
|
||||
//
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// DATE: | year | month | day |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// 49041 ($BF91) 49040 ($BF90)
|
||||
//
|
||||
// 49043 ($BF93) 49042 ($BF92)
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// DATE: | year | month | day |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
//
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// TIME: | hour | | minute |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// 49043 ($BF93) 49042 ($BF92)
|
||||
//
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// TIME: | hour | | minute |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
func DateTimeToProDOS(dateTime time.Time) []byte {
|
||||
year := dateTime.Year() % 100
|
||||
month := dateTime.Month()
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides tests for conversion to and from ProDOS time format
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -14,7 +14,7 @@ func printReadme() {
|
|||
fmt.Println(`
|
||||
MIT License
|
||||
|
||||
Copyright (c)2021-2022 Terence Boldt
|
||||
Copyright (c)2021-2023 Terence Boldt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
Loading…
Reference in New Issue
Block a user