mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-06-03 05:29:31 +00:00
Compare commits
46 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 | ||
|
62bb781fca | ||
|
30a221c177 | ||
|
7f727a6377 | ||
|
e590b82196 | ||
|
62c594d322 | ||
|
819ee47503 | ||
|
ea08fd6edc | ||
|
b295288d05 | ||
|
45c50dcd94 | ||
|
fb9621ff09 | ||
|
1377fc9020 | ||
|
2a595f0a5c | ||
|
4a8be2c327 | ||
|
d53977a1a3 | ||
|
2e60d3c09c | ||
|
790f4f2521 | ||
|
1cbb9fa0d9 |
61
.github/workflows/codacy.yml
vendored
Normal file
61
.github/workflows/codacy.yml
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# This workflow checks out code, performs a Codacy security scan
|
||||
# and integrates the results with the
|
||||
# GitHub Advanced Security code scanning feature. For more information on
|
||||
# the Codacy security scan action usage and parameters, see
|
||||
# https://github.com/codacy/codacy-analysis-cli-action.
|
||||
# For more information on Codacy Analysis CLI in general, see
|
||||
# https://github.com/codacy/codacy-analysis-cli.
|
||||
|
||||
name: Codacy Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- 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@v3
|
||||
|
||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||
- name: Run Codacy Analysis CLI
|
||||
uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b
|
||||
with:
|
||||
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
|
||||
# You can also omit the token and run the tools that support default configurations
|
||||
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
verbose: true
|
||||
output: results.sarif
|
||||
format: sarif
|
||||
# Adjust severity of non-security issues
|
||||
gh-code-scanning-compat: true
|
||||
# Force 0 exit code to allow SARIF file generation
|
||||
# This will handover control about PR rejection to the GitHub side
|
||||
max-allowed-issues: 2147483647
|
||||
|
||||
# Upload the SARIF file generated in the previous step
|
||||
- name: Upload SARIF results file
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
76
.github/workflows/codeql.yml
vendored
Normal file
76
.github/workflows/codeql.yml
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '44 11 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# 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@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
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.
|
||||
|
||||
# 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@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 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.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
25
.github/workflows/go.yaml
vendored
Normal file
25
.github/workflows/go.yaml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
48
README.md
48
README.md
|
@ -4,12 +4,14 @@ This project is just starting but is intended to be both a command line tool and
|
|||
## DISCLAIMER
|
||||
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](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
|
||||
|
||||
|
@ -74,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
|
||||
|
@ -115,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"
|
||||
```
|
||||
|
|
7
build.sh
Executable file
7
build.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
GOOS=darwin GOARCH=arm64 go build -o binaries/macos/apple-silicon/ProDOS-Utilities
|
||||
GOOS=darwin GOARCH=amd64 go build -o binaries/macos/intel/ProDOS-Utilities
|
||||
GOOS=windows GOARCH=amd64 go build -o binaries/windows/intel/ProDOS-Utilities.exe
|
||||
GOOS=linux GOARCH=amd64 go build -o binaries/linux/intel/ProDOS-Utilities
|
||||
GOOS=linux GOARCH=arm go build -o binaries/linux/arm32/ProDOS-Utilities
|
||||
GOOS=linux GOARCH=arm64 go build -o binaries/linux/arm64/ProDOS-Utilities
|
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=
|
311
main.go
311
main.go
|
@ -1,3 +1,11 @@
|
|||
// 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 a command line utility to read, write and delete
|
||||
// files and directories on a ProDOS drive image as well as format
|
||||
// new volumes
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -9,7 +17,7 @@ import (
|
|||
"github.com/tjboldt/ProDOS-Utilities/prodos"
|
||||
)
|
||||
|
||||
const version = "0.1.0"
|
||||
const version = "0.4.9"
|
||||
|
||||
func main() {
|
||||
var fileName string
|
||||
|
@ -24,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 {
|
||||
|
@ -42,98 +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 := prodos.ReadDirectory(file, pathName)
|
||||
if len(pathName) == 0 {
|
||||
pathName = "/" + volumeHeader.VolumeName
|
||||
}
|
||||
volumeBitmap := prodos.ReadVolumeBitmap(file)
|
||||
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("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 := prodos.ReadBlock(file, blockNumber)
|
||||
prodos.DumpBlock(block)
|
||||
readBlock(blockNumber, fileName)
|
||||
case "writeblock":
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
138
prodos/basic.go
138
prodos/basic.go
|
@ -1,7 +1,16 @@
|
|||
// 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 conversion between BASIC and text
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -82,7 +91,7 @@ var tokens = map[byte]string{
|
|||
0xC9: "-",
|
||||
0xCA: "*",
|
||||
0xCB: "/",
|
||||
0xCC: ";",
|
||||
//0xCC: ";", // fails if this is there
|
||||
0xCD: "AND",
|
||||
0xCE: "OR",
|
||||
0xCF: ">",
|
||||
|
@ -115,6 +124,7 @@ var tokens = map[byte]string{
|
|||
0xEA: "MID$",
|
||||
}
|
||||
|
||||
// ConvertBasicToText converts AppleSoft BASIC to text
|
||||
func ConvertBasicToText(basic []byte) string {
|
||||
var builder strings.Builder
|
||||
|
||||
|
@ -153,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,11 +1,22 @@
|
|||
// 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 volum bitmap on
|
||||
// a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"os"
|
||||
"io"
|
||||
)
|
||||
|
||||
func ReadVolumeBitmap(file *os.File) []byte {
|
||||
headerBlock := ReadBlock(file, 2)
|
||||
// ReadVolumeBitmap reads the volume bitmap from a ProDOS image
|
||||
func ReadVolumeBitmap(reader io.ReaderAt) ([]byte, error) {
|
||||
headerBlock, err := ReadBlock(reader, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
volumeHeader := parseVolumeHeader(headerBlock)
|
||||
|
||||
|
@ -23,24 +34,66 @@ func ReadVolumeBitmap(file *os.File) []byte {
|
|||
}
|
||||
|
||||
for i := 0; i < totalBitmapBlocks; i++ {
|
||||
bitmapBlock := ReadBlock(file, i+volumeHeader.BitmapStartBlock)
|
||||
bitmapBlock, err := ReadBlock(reader, i+volumeHeader.BitmapStartBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for j := 0; j < 512 && i*512+j < totalBitmapBytes; j++ {
|
||||
bitmap[i*512+j] = bitmapBlock[j]
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap
|
||||
return bitmap, nil
|
||||
}
|
||||
|
||||
func writeVolumeBitmap(file *os.File, bitmap []byte) {
|
||||
headerBlock := ReadBlock(file, 2)
|
||||
// GetFreeBlockCount gets the number of free blocks on a ProDOS image
|
||||
func GetFreeBlockCount(volumeBitmap []byte, totalBlocks int) int {
|
||||
freeBlockCount := 0
|
||||
|
||||
for i := 0; i < totalBlocks; i++ {
|
||||
if checkFreeBlockInVolumeBitmap(volumeBitmap, i) {
|
||||
freeBlockCount++
|
||||
}
|
||||
}
|
||||
return freeBlockCount
|
||||
}
|
||||
|
||||
func writeVolumeBitmap(readerWriter ReaderWriterAt, bitmap []byte) error {
|
||||
headerBlock, err := ReadBlock(readerWriter, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumeHeader := parseVolumeHeader(headerBlock)
|
||||
|
||||
for i := 0; i < len(bitmap)/512; i++ {
|
||||
WriteBlock(file, volumeHeader.BitmapStartBlock+i, bitmap[i*512:i*512+512])
|
||||
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
|
||||
}
|
||||
|
||||
func createVolumeBitmap(numberOfBlocks int) []byte {
|
||||
|
@ -78,7 +131,6 @@ func createVolumeBitmap(numberOfBlocks int) []byte {
|
|||
markBlockInVolumeBitmap(volumeBitmap, i)
|
||||
}
|
||||
}
|
||||
//DumpBlock(volumeBitmap)
|
||||
|
||||
return volumeBitmap
|
||||
}
|
||||
|
@ -101,17 +153,6 @@ func findFreeBlocks(volumeBitmap []byte, numberOfBlocks int) []int {
|
|||
return nil
|
||||
}
|
||||
|
||||
func GetFreeBlockCount(volumeBitmap []byte, totalBlocks int) int {
|
||||
freeBlockCount := 0
|
||||
|
||||
for i := 0; i < totalBlocks; i++ {
|
||||
if checkFreeBlockInVolumeBitmap(volumeBitmap, i) {
|
||||
freeBlockCount++
|
||||
}
|
||||
}
|
||||
return freeBlockCount
|
||||
}
|
||||
|
||||
func markBlockInVolumeBitmap(volumeBitmap []byte, blockNumber int) {
|
||||
bitToChange := blockNumber % 8
|
||||
byteToChange := blockNumber / 8
|
||||
|
|
|
@ -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,22 +1,38 @@
|
|||
// 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 and write
|
||||
// blocks on a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"os"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func ReadBlock(file *os.File, block int) []byte {
|
||||
// ReadBlock reads a block from a ProDOS volume into a byte array
|
||||
func ReadBlock(reader io.ReaderAt, block int) ([]byte, error) {
|
||||
buffer := make([]byte, 512)
|
||||
|
||||
file.ReadAt(buffer, int64(block)*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
|
||||
return buffer, err
|
||||
}
|
||||
|
||||
func WriteBlock(file *os.File, block int, buffer []byte) {
|
||||
WriteBlockNoSync(file, block, buffer)
|
||||
file.Sync()
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
func WriteBlockNoSync(file *os.File, block int, buffer []byte) {
|
||||
file.WriteAt(buffer, int64(block)*512)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
// 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, delete
|
||||
// fand parse directories on a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// VolumeHeader from ProDOS
|
||||
type VolumeHeader struct {
|
||||
VolumeName string
|
||||
CreationTime time.Time
|
||||
|
@ -21,43 +29,67 @@ type VolumeHeader struct {
|
|||
Version int
|
||||
}
|
||||
|
||||
// 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 (
|
||||
StorageDeleted = 0
|
||||
StorageSeedling = 1
|
||||
StorageSapling = 2
|
||||
StorageTree = 3
|
||||
StoragePascal = 4
|
||||
// StorageDeleted signifies file is deleted
|
||||
StorageDeleted = 0
|
||||
// StorageSeedling signifies file is <= 512 bytes
|
||||
StorageSeedling = 1
|
||||
// StorageSapling signifies file is > 512 bytes and <= 128 KB
|
||||
StorageSapling = 2
|
||||
// StorageTree signifies file is > 128 KB and <= 16 MB
|
||||
StorageTree = 3
|
||||
// StoragePascal signifies pascal storage area
|
||||
StoragePascal = 4
|
||||
// StorageDirectory signifies directory
|
||||
StorageDirectory = 13
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func ReadDirectory(file *os.File, path string) (VolumeHeader, DirectoryHeader, []FileEntry) {
|
||||
buffer := ReadBlock(file, 2)
|
||||
// ReadDirectory reads the directory information from a specified path
|
||||
// on a ProDOS image
|
||||
func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHeader, []FileEntry, error) {
|
||||
buffer, err := ReadBlock(reader, 2)
|
||||
if err != nil {
|
||||
return VolumeHeader{}, DirectoryHeader{}, nil, err
|
||||
}
|
||||
|
||||
volumeHeader := parseVolumeHeader(buffer)
|
||||
|
||||
|
@ -65,39 +97,158 @@ func ReadDirectory(file *os.File, path string) (VolumeHeader, DirectoryHeader, [
|
|||
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, "/")
|
||||
|
||||
directoryHeader, fileEntries := getFileEntriesInDirectory(file, 2, 1, paths)
|
||||
directoryHeader, fileEntries, err := getFileEntriesInDirectory(reader, 2, 1, paths)
|
||||
if err != nil {
|
||||
return VolumeHeader{}, DirectoryHeader{}, nil, err
|
||||
}
|
||||
|
||||
return volumeHeader, directoryHeader, fileEntries
|
||||
return volumeHeader, directoryHeader, fileEntries, nil
|
||||
}
|
||||
|
||||
func getFreeFileEntryInDirectory(file *os.File, directory string) (FileEntry, error) {
|
||||
_, directoryHeader, _ := ReadDirectory(file, directory)
|
||||
//DumpDirectoryHeader(directoryHeader)
|
||||
blockNumber := directoryHeader.StartingBlock
|
||||
buffer := ReadBlock(file, blockNumber)
|
||||
// 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
|
||||
}
|
||||
blockNumber := directoryHeader.StartingBlock
|
||||
buffer, err := ReadBlock(readerWriter, blockNumber)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
entryOffset := 43 // start at offset after header
|
||||
entryNumber := 2 // header is essentially the first entry so start at 2
|
||||
|
||||
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 = ReadBlock(file, 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
|
||||
|
@ -109,8 +260,56 @@ func getFreeFileEntryInDirectory(file *os.File, directory string) (FileEntry, er
|
|||
}
|
||||
}
|
||||
|
||||
func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry) {
|
||||
buffer := ReadBlock(file, blockNumber)
|
||||
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 {
|
||||
return DirectoryHeader{}, nil, err
|
||||
}
|
||||
|
||||
directoryHeader := parseDirectoryHeader(buffer, blockNumber)
|
||||
|
||||
|
@ -125,7 +324,7 @@ func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int,
|
|||
|
||||
if !matchedDirectory && (currentPath == len(paths)-1) {
|
||||
// path not matched by last path part
|
||||
return DirectoryHeader{}, nil
|
||||
return DirectoryHeader{}, nil, errors.New("path not matched")
|
||||
}
|
||||
|
||||
for {
|
||||
|
@ -133,21 +332,24 @@ func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int,
|
|||
entryOffset = 4
|
||||
entryNumber = 1
|
||||
if blockNumber == 0 {
|
||||
return DirectoryHeader{}, nil
|
||||
return DirectoryHeader{}, nil, nil
|
||||
}
|
||||
buffer, err = ReadBlock(reader, nextBlock)
|
||||
if err != nil {
|
||||
return DirectoryHeader{}, nil, err
|
||||
}
|
||||
buffer = ReadBlock(file, nextBlock)
|
||||
nextBlock = int(buffer[2]) + int(buffer[3])*256
|
||||
}
|
||||
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
|
||||
|
||||
if fileEntry.StorageType != StorageDeleted {
|
||||
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
|
||||
return directoryHeader, fileEntries[0:activeEntries]
|
||||
return directoryHeader, fileEntries[0:activeEntries], nil
|
||||
}
|
||||
if matchedDirectory {
|
||||
fileEntries[activeEntries] = fileEntry
|
||||
} else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName {
|
||||
return getFileEntriesInDirectory(file, fileEntry.KeyPointer, currentPath+1, paths)
|
||||
return getFileEntriesInDirectory(reader, fileEntry.KeyPointer, currentPath+1, paths)
|
||||
}
|
||||
activeEntries++
|
||||
}
|
||||
|
@ -194,7 +396,7 @@ func parseFileEntry(buffer []byte, blockNumber int, entryOffset int) FileEntry {
|
|||
return fileEntry
|
||||
}
|
||||
|
||||
func writeFileEntry(file *os.File, fileEntry FileEntry) {
|
||||
func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) {
|
||||
buffer := make([]byte, 39)
|
||||
buffer[0] = byte(fileEntry.StorageType)<<4 + byte(len(fileEntry.FileName))
|
||||
for i := 0; i < len(fileEntry.FileName); i++ {
|
||||
|
@ -217,14 +419,15 @@ func writeFileEntry(file *os.File, 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)
|
||||
|
||||
_, err := file.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset))
|
||||
//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 {
|
||||
|
||||
}
|
||||
|
@ -243,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")
|
||||
}
|
||||
|
||||
|
@ -265,32 +468,86 @@ 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(file *os.File, directoryHeader DirectoryHeader) {
|
||||
buffer := ReadBlock(file, directoryHeader.StartingBlock)
|
||||
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
|
||||
}
|
||||
buffer[0x00] = byte(directoryHeader.PreviousBlock & 0x00FF)
|
||||
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)
|
||||
WriteBlock(file, directoryHeader.StartingBlock, buffer)
|
||||
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
|
||||
}
|
||||
|
|
11
prodos/doc.go
Normal file
11
prodos/doc.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// 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 provides access to read/write/delete/list files and
|
||||
directories on a ProDOS order drive images.
|
||||
|
||||
*/
|
||||
|
||||
package prodos
|
376
prodos/file.go
376
prodos/file.go
|
@ -1,19 +1,28 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"os"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LoadFile(file *os.File, path string) ([]byte, error) {
|
||||
fileEntry, err := getFileEntry(file, path)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockList, err := getDataBlocklist(file, fileEntry)
|
||||
blockList, err := getDataBlocklist(reader, fileEntry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -21,7 +30,10 @@ func LoadFile(file *os.File, path string) ([]byte, error) {
|
|||
buffer := make([]byte, fileEntry.EndOfFile)
|
||||
|
||||
for i := 0; i < len(blockList); i++ {
|
||||
block := ReadBlock(file, blockList[i])
|
||||
block, err := ReadBlock(reader, blockList[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for j := 0; j < 512 && i*512+j < fileEntry.EndOfFile; j++ {
|
||||
buffer[i*512+j] = block[j]
|
||||
}
|
||||
|
@ -30,47 +42,61 @@ func LoadFile(file *os.File, path string) ([]byte, error) {
|
|||
return buffer, nil
|
||||
}
|
||||
|
||||
func WriteFile(file *os.File, path string, fileType int, auxType int, buffer []byte) error {
|
||||
// WriteFile writes a file to a ProDOS volume from a byte array
|
||||
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(file, path)
|
||||
if len(fileName) > 15 {
|
||||
return errors.New("filename too long")
|
||||
}
|
||||
|
||||
existingFileEntry, _ := GetFileEntry(readerWriter, path)
|
||||
if existingFileEntry.StorageType != StorageDeleted {
|
||||
DeleteFile(file, path)
|
||||
return errors.New(("file already exists"))
|
||||
}
|
||||
|
||||
// get list of blocks to write file to
|
||||
blockList := createBlockList(file, len(buffer))
|
||||
blockList, err := createBlockList(readerWriter, len(buffer))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// seedling file
|
||||
if len(buffer) <= 0x200 {
|
||||
WriteBlock(file, blockList[0], buffer)
|
||||
writeSeedlingFile(readerWriter, buffer, blockList)
|
||||
}
|
||||
|
||||
// sapling file needs index block
|
||||
if len(buffer) > 0x200 && len(buffer) <= 0x20000 {
|
||||
writeSaplingFile(file, buffer, blockList)
|
||||
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)
|
||||
}
|
||||
|
||||
updateVolumeBitmap(file, blockList)
|
||||
if len(buffer) > 0x1000000 {
|
||||
return errors.New("files > 16MB not supported by ProDOS")
|
||||
}
|
||||
|
||||
updateVolumeBitmap(readerWriter, blockList)
|
||||
|
||||
// add file entry to directory
|
||||
fileEntry, err := getFreeFileEntryInDirectory(file, directory)
|
||||
fileEntry, err := getFreeFileEntryInDirectory(readerWriter, directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
|
@ -80,95 +106,80 @@ func WriteFile(file *os.File, path string, fileType int, auxType int, buffer []b
|
|||
fileEntry.StorageType = StorageTree
|
||||
}
|
||||
|
||||
writeFileEntry(file, fileEntry)
|
||||
writeFileEntry(readerWriter, fileEntry)
|
||||
|
||||
// increment file count
|
||||
directoryHeaderBlock := ReadBlock(file, fileEntry.HeaderPointer)
|
||||
return incrementFileCount(readerWriter, fileEntry)
|
||||
}
|
||||
|
||||
func incrementFileCount(readerWriter ReaderWriterAt, fileEntry FileEntry) error {
|
||||
directoryHeaderBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
directoryHeader := parseDirectoryHeader(directoryHeaderBlock, fileEntry.HeaderPointer)
|
||||
directoryHeader.ActiveFileCount++
|
||||
writeDirectoryHeader(file, directoryHeader)
|
||||
writeDirectoryHeader(readerWriter, directoryHeader)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateVolumeBitmap(file *os.File, blockList []int) {
|
||||
volumeBitmap := ReadVolumeBitmap(file)
|
||||
for i := 0; i < len(blockList); i++ {
|
||||
markBlockInVolumeBitmap(volumeBitmap, blockList[i])
|
||||
}
|
||||
writeVolumeBitmap(file, volumeBitmap)
|
||||
}
|
||||
// DeleteFile deletes a file from a ProDOS volume
|
||||
func DeleteFile(readerWriter ReaderWriterAt, path string) error {
|
||||
fileEntry, err := GetFileEntry(readerWriter, path)
|
||||
// DumpFileEntry(fileEntry)
|
||||
// oldDirectoryBlock, _ := ReadBlock(readerWriter, fileEntry.DirectoryBlock)
|
||||
// DumpBlock(oldDirectoryBlock)
|
||||
|
||||
func writeSaplingFile(file *os.File, buffer []byte, blockList []int) {
|
||||
// write index block with pointers to data blocks
|
||||
indexBuffer := make([]byte, 512)
|
||||
for i := 0; i < 256; i++ {
|
||||
if i < len(blockList)-1 {
|
||||
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
|
||||
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
|
||||
}
|
||||
}
|
||||
WriteBlock(file, blockList[0], indexBuffer)
|
||||
|
||||
// write all data blocks
|
||||
blockBuffer := make([]byte, 512)
|
||||
blockPointer := 0
|
||||
blockIndexNumber := 1
|
||||
for i := 0; i < len(buffer); i++ {
|
||||
blockBuffer[blockPointer] = buffer[i]
|
||||
if blockPointer == 511 {
|
||||
WriteBlock(file, blockList[blockIndexNumber], blockBuffer)
|
||||
blockPointer = 0
|
||||
blockIndexNumber++
|
||||
} else if i == len(buffer)-1 {
|
||||
for j := blockPointer; j < 512; j++ {
|
||||
blockBuffer[j] = 0
|
||||
}
|
||||
WriteBlock(file, blockList[blockIndexNumber], blockBuffer)
|
||||
} else {
|
||||
blockPointer++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteFile(file *os.File, path string) error {
|
||||
fileEntry, err := getFileEntry(file, path)
|
||||
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(file, fileEntry)
|
||||
blocks, err := getAllBlockList(readerWriter, fileEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumeBitmap, err := ReadVolumeBitmap(readerWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
volumeBitmap := ReadVolumeBitmap(file)
|
||||
for i := 0; i < len(blocks); i++ {
|
||||
freeBlockInVolumeBitmap(volumeBitmap, blocks[i])
|
||||
}
|
||||
writeVolumeBitmap(file, volumeBitmap)
|
||||
writeVolumeBitmap(readerWriter, volumeBitmap)
|
||||
|
||||
// decrement the directory entry count
|
||||
directoryBlock := ReadBlock(file, fileEntry.HeaderPointer)
|
||||
directoryBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
directoryHeader := parseDirectoryHeader(directoryBlock, fileEntry.HeaderPointer)
|
||||
|
||||
directoryHeader.ActiveFileCount--
|
||||
writeDirectoryHeader(file, directoryHeader)
|
||||
writeDirectoryHeader(readerWriter, directoryHeader)
|
||||
|
||||
// zero out directory entry
|
||||
fileEntry.StorageType = 0
|
||||
fileEntry.FileName = ""
|
||||
writeFileEntry(file, fileEntry)
|
||||
writeFileEntry(readerWriter, fileEntry)
|
||||
|
||||
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)
|
||||
paths := strings.Split(path, "/")
|
||||
|
@ -186,8 +197,122 @@ func GetDirectoryAndFileNameFromPath(path string) (string, string) {
|
|||
return directory, fileName
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
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) {
|
||||
// write index block with pointers to data blocks
|
||||
indexBuffer := make([]byte, 512)
|
||||
for i := 0; i < 256; i++ {
|
||||
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)
|
||||
|
||||
// write all data blocks
|
||||
blockBuffer := make([]byte, 512)
|
||||
blockPointer := 0
|
||||
blockIndexNumber := 1
|
||||
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 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(file *os.File, fileEntry FileEntry) ([]int, error) {
|
||||
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int, error) {
|
||||
blocks := make([]int, fileEntry.BlocksUsed)
|
||||
|
||||
switch fileEntry.StorageType {
|
||||
|
@ -195,77 +320,110 @@ func getBlocklist(file *os.File, fileEntry FileEntry) ([]int, error) {
|
|||
blocks[0] = fileEntry.KeyPointer
|
||||
return blocks, nil
|
||||
case StorageSapling:
|
||||
index := ReadBlock(file, fileEntry.KeyPointer)
|
||||
index, err := ReadBlock(reader, fileEntry.KeyPointer)
|
||||
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:
|
||||
masterIndex := ReadBlock(file, fileEntry.KeyPointer)
|
||||
blocks[0] = fileEntry.KeyPointer
|
||||
// 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
|
||||
}
|
||||
numberOfDataBlocks := 0
|
||||
|
||||
indexBlocks[0] = fileEntry.KeyPointer
|
||||
indexBlockCount := 1
|
||||
|
||||
for i := 0; i < 128; i++ {
|
||||
index := ReadBlock(file, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("Unsupported file storage type")
|
||||
}
|
||||
|
||||
func getDataBlocklist(file *os.File, 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 := ReadBlock(file, fileEntry.KeyPointer)
|
||||
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
|
||||
blocks[i] = int(index[i]) + int(index[i+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")
|
||||
return nil, errors.New("unsupported file storage type")
|
||||
}
|
||||
|
||||
func createBlockList(file *os.File, fileSize int) []int {
|
||||
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++
|
||||
}
|
||||
volumeBitmap := ReadVolumeBitmap(file)
|
||||
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
|
||||
return blockList[0:numberOfBlocks], nil
|
||||
}
|
||||
|
||||
func getFileEntry(file *os.File, 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 := ReadDirectory(file, directory)
|
||||
_, _, fileEntries, err := ReadDirectory(reader, directory)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -277,7 +435,7 @@ func getFileEntry(file *os.File, 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,13 +1,20 @@
|
|||
// 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 format a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CreateVolume(file *os.File, volumeName string, numberOfBlocks int) {
|
||||
// CreateVolume formats a new ProDOS volume including boot block,
|
||||
// volume bitmap and empty directory
|
||||
func CreateVolume(readerWriter ReaderWriterAt, volumeName string, numberOfBlocks int) {
|
||||
if numberOfBlocks > 65535 || numberOfBlocks < 64 {
|
||||
return
|
||||
}
|
||||
|
@ -20,7 +27,7 @@ func CreateVolume(file *os.File, volumeName string, numberOfBlocks int) {
|
|||
|
||||
blankBlock := make([]byte, 512)
|
||||
for i := 0; i < numberOfBlocks; i++ {
|
||||
WriteBlockNoSync(file, i, blankBlock)
|
||||
WriteBlock(readerWriter, i, blankBlock)
|
||||
}
|
||||
|
||||
volumeHeader := [43]byte{}
|
||||
|
@ -55,11 +62,10 @@ func CreateVolume(file *os.File, volumeName string, numberOfBlocks int) {
|
|||
volumeHeader[0x29] = byte(numberOfBlocks & 0xFF)
|
||||
volumeHeader[0x2A] = byte(numberOfBlocks >> 8)
|
||||
|
||||
file.WriteAt(volumeHeader[:], 1024)
|
||||
file.Sync()
|
||||
readerWriter.WriteAt(volumeHeader[:], 1024)
|
||||
|
||||
// boot block 0
|
||||
WriteBlock(file, 0, getBootBlock())
|
||||
WriteBlock(readerWriter, 0, getBootBlock())
|
||||
|
||||
// pointers to volume directory blocks
|
||||
for i := 2; i < 6; i++ {
|
||||
|
@ -76,12 +82,12 @@ func CreateVolume(file *os.File, volumeName string, numberOfBlocks int) {
|
|||
pointers[2] = byte(i + 1)
|
||||
}
|
||||
pointers[3] = 0x00
|
||||
file.WriteAt(pointers, int64(i*512))
|
||||
readerWriter.WriteAt(pointers, int64(i*512))
|
||||
}
|
||||
|
||||
// volume bit map starting at block 6
|
||||
volumeBitmap := createVolumeBitmap(numberOfBlocks)
|
||||
writeVolumeBitmap(file, volumeBitmap)
|
||||
writeVolumeBitmap(readerWriter, volumeBitmap)
|
||||
}
|
||||
|
||||
func getBootBlock() []byte {
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -20,19 +25,11 @@ func TestCreateVolume(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("%d", tt.blocks)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
fileName := os.TempDir() + "test-volume.hdv"
|
||||
defer os.Remove(fileName)
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create file: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
file := NewMemoryFile(0x2000000)
|
||||
|
||||
CreateVolume(file, tt.wantVolumeName, tt.blocks)
|
||||
|
||||
volumeHeader, _, fileEntries := ReadDirectory(file, "")
|
||||
volumeHeader, _, fileEntries, _ := ReadDirectory(file, "")
|
||||
if volumeHeader.VolumeName != tt.wantVolumeName {
|
||||
t.Errorf("got volume name %s, want %s", volumeHeader.VolumeName, tt.wantVolumeName)
|
||||
}
|
||||
|
@ -43,7 +40,7 @@ func TestCreateVolume(t *testing.T) {
|
|||
t.Errorf("got files %d, want 0", len(fileEntries))
|
||||
}
|
||||
|
||||
volumeBitmap := ReadVolumeBitmap(file)
|
||||
volumeBitmap, _ := ReadVolumeBitmap(file)
|
||||
freeBlockCount := GetFreeBlockCount(volumeBitmap, tt.blocks)
|
||||
if freeBlockCount != tt.wantFreeBlocks {
|
||||
t.Errorf("got free blocks: %d, want %d", freeBlockCount, tt.wantFreeBlocks)
|
||||
|
|
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
|
||||
}
|
36
prodos/memfile.go
Normal file
36
prodos/memfile.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
// 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 a file in memory
|
||||
|
||||
package prodos
|
||||
|
||||
// MemoryFile containts file data and size
|
||||
type MemoryFile struct {
|
||||
data []byte
|
||||
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)
|
||||
}
|
||||
|
||||
// NewMemoryFile creates an in-memory file of the specified size in bytes
|
||||
func NewMemoryFile(size int) *MemoryFile {
|
||||
return &MemoryFile{make([]byte, size), size}
|
||||
}
|
||||
|
||||
// WriteAt writes data to the specified offset in the file
|
||||
func (memoryFile *MemoryFile) WriteAt(data []byte, offset int64) (int, error) {
|
||||
copy(memoryFile.data[int(offset):], data)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// ReadAt reads data from the specified offset in the file
|
||||
func (memoryFile *MemoryFile) ReadAt(data []byte, offset int64) (int, error) {
|
||||
copy(data, memoryFile.data[int(offset):])
|
||||
return len(data), nil
|
||||
}
|
|
@ -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 text dumps for directories
|
||||
// and blocks
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
|
@ -6,6 +13,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// TimeToString displays the date and time in ProDOS format
|
||||
func TimeToString(printTime time.Time) string {
|
||||
return fmt.Sprintf("%04d-%s-%02d %02d:%02d",
|
||||
printTime.Year(),
|
||||
|
@ -16,6 +24,7 @@ func TimeToString(printTime time.Time) string {
|
|||
)
|
||||
}
|
||||
|
||||
// FileTypeToString display the file type as a string
|
||||
func FileTypeToString(fileType int) string {
|
||||
switch fileType {
|
||||
case 1:
|
||||
|
@ -78,6 +87,7 @@ func FileTypeToString(fileType int) string {
|
|||
*/
|
||||
}
|
||||
|
||||
// DumpFileEntry dumps the file entry values as text
|
||||
func DumpFileEntry(fileEntry FileEntry) {
|
||||
fmt.Printf("FileName: %s\n", fileEntry.FileName)
|
||||
fmt.Printf("Creation time: %d-%s-%d %02d:%02d\n", fileEntry.CreationTime.Year(), fileEntry.CreationTime.Month(), fileEntry.CreationTime.Day(), fileEntry.CreationTime.Hour(), fileEntry.CreationTime.Minute())
|
||||
|
@ -89,9 +99,13 @@ 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")
|
||||
}
|
||||
|
||||
// DumpVolumeHeader dumps the volume header values as text
|
||||
func DumpVolumeHeader(volumeHeader VolumeHeader) {
|
||||
fmt.Printf("Next block: %d\n", volumeHeader.NextBlock)
|
||||
fmt.Printf("Volume name: %s\n", volumeHeader.VolumeName)
|
||||
|
@ -105,14 +119,27 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
|
|||
fmt.Printf("Total blocks: %d\n", volumeHeader.TotalBlocks)
|
||||
}
|
||||
|
||||
// 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
|
||||
func DumpBlock(buffer []byte) {
|
||||
for i := 0; i < len(buffer); i += 16 {
|
||||
fmt.Printf("%04X: ", i)
|
||||
|
@ -131,9 +158,10 @@ func DumpBlock(buffer []byte) {
|
|||
}
|
||||
}
|
||||
|
||||
// DumpDirectory displays the directory similar to ProDOS catalog
|
||||
func DumpDirectory(blocksFree int, totalBlocks int, path string, fileEntries []FileEntry) {
|
||||
fmt.Printf("%s\n\n", path)
|
||||
fmt.Printf(" NAME TYPE BLOCKS MODIFIED CREATED ENDFILE SUBTYPE\n\n")
|
||||
fmt.Printf("NAME TYPE BLOCKS MODIFIED CREATED ENDFILE SUBTYPE\n\n")
|
||||
|
||||
for i := 0; i < len(fileEntries); i++ {
|
||||
var zeroTime = time.Time{}
|
||||
|
@ -148,7 +176,7 @@ func DumpDirectory(blocksFree int, totalBlocks int, path string, fileEntries []F
|
|||
} else {
|
||||
createdTime = TimeToString(fileEntries[i].CreationTime)
|
||||
}
|
||||
fmt.Printf(" %-15s %s %7d %s %s %8d %8d\n",
|
||||
fmt.Printf("%-15s %s%6d %s %s%8d %8d\n",
|
||||
fileEntries[i].FileName,
|
||||
FileTypeToString(fileEntries[i].FileType),
|
||||
fileEntries[i].BlocksUsed,
|
||||
|
|
|
@ -1,24 +1,30 @@
|
|||
// 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 conversion to and from ProDOS time format
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"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 |
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
|
||||
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
TIME: | hour | | minute |
|
||||
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
|
||||
49043 ($BF93) 49042 ($BF92)
|
||||
*/
|
||||
|
||||
// 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 |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
//
|
||||
// 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()
|
||||
|
@ -35,6 +41,7 @@ func DateTimeToProDOS(dateTime time.Time) []byte {
|
|||
return buffer
|
||||
}
|
||||
|
||||
// DateTimeFromProDOS converts Time from ProDOS date time
|
||||
func DateTimeFromProDOS(buffer []byte) time.Time {
|
||||
if buffer[0] == 0 &&
|
||||
buffer[1] == 0 &&
|
||||
|
@ -50,7 +57,7 @@ func DateTimeFromProDOS(buffer []byte) time.Time {
|
|||
year = 1900 + int(twoDigitYear)
|
||||
}
|
||||
|
||||
month := int(buffer[0]>>5 + buffer[1]&1)
|
||||
month := int(buffer[0]>>5 + (buffer[1]&1)<<3)
|
||||
day := int(buffer[0] & 31)
|
||||
hour := int(buffer[3])
|
||||
minute := int(buffer[2])
|
||||
|
|
|
@ -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,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 readme for command line utility
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
@ -8,7 +14,7 @@ func printReadme() {
|
|||
fmt.Println(`
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 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