Compare commits

...

46 Commits
v0.1.0 ... main

Author SHA1 Message Date
Terence Boldt
0aa838ffe1 Update version 2024-01-14 08:58:26 -05:00
Terence Boldt
231197bd8f Update dependencies 2024-01-14 08:58:26 -05:00
Chris Pacejo
2477b4a8a3 open files O_RDONLY for read-only operations 2024-01-14 08:46:44 -05:00
Terence Boldt
30f0b03054 Update dependencies 2023-11-07 23:03:44 -05:00
Terence Boldt
ef2b505500
Update dependencies (#31)
* Update dependencies

* Update to 0.4.7
2023-07-06 19:57:02 -04:00
Terence Boldt
7f8679cdb4
Update dependencies (#30) 2023-02-22 21:09:20 -05:00
Terence Boldt
98ab2d4f0a
Fixes #27 basic parser (#28)
* Fix #27 basic parsing

* Update version
2023-01-25 21:25:48 -05:00
Terence Boldt
9e6c2fcc22
Update README.md 2023-01-22 20:12:34 -05:00
Terence Boldt
b80a835c72 Update version to 0.4.4 2023-01-22 20:08:27 -05:00
Terence Boldt
10f896399d
Fix codacy warnings in image (#24) 2023-01-22 12:25:11 -05:00
Terence Boldt
36c24e60ca Ignore duplicate files when recursive 2023-01-22 12:06:43 -05:00
Terence Boldt
286964d3ee Add putallrecursive 2023-01-22 12:06:43 -05:00
Terence Boldt
1722e7b23a Refactor main 2023-01-22 12:06:43 -05:00
Terence Boldt
876564d261 Add directory creation 2023-01-22 12:06:43 -05:00
Terence Boldt
ee3d187fb3 Improve error handling and fix putall command 2023-01-22 12:06:43 -05:00
Terence Boldt
ab3b397139
Add image import (#17)
* Add image import

* Update version
2023-01-13 23:03:43 -05:00
Terence Boldt
6cc7db13e1
Add tree file read support (#16)
* Add tree file read support

* Update copyright year

* Add tree file write support
2023-01-03 23:24:48 -05:00
Terence Boldt
0beb2f41fa
Delete codacy-analysis.yml 2023-01-01 09:45:18 -05:00
Terence Boldt
cb6b3a25ba
Create codacy.yml 2023-01-01 09:45:07 -05:00
Terence Boldt
8b54fa3235
Delete codeql-analysis.yml 2023-01-01 09:44:43 -05:00
Terence Boldt
0ac5bd39d0
Create codeql.yml 2023-01-01 09:44:11 -05:00
Terence Boldt
592f3a6542
Fix putall to not erase volume (#15) 2022-12-31 09:14:27 -05:00
Terence Boldt
4ed23e0e6f
Add putall command (#14) 2022-12-30 06:12:41 -05:00
Terence Boldt
eb438ee9fe
Update README.md 2022-12-27 20:50:36 -05:00
Terence Boldt
f36acac6e6
Remove binaries and update version (#13) 2022-12-27 20:35:28 -05:00
Terence Boldt
48aa7a9331
Add BASIC import (#12)
* Add BASIC import

* Fix text to BASIC when line feed after token
2022-12-27 17:24:25 -05:00
Oliver Schmidt
a649647739
Added minimal AppleSingle support. (#11)
* Added minimal AppleSingle support.

This is just enough AppleSingle support to allow for putting files generated by cc65's default linker configuration into ProDOS images.

* Improved logging.

By the time the file is put into the ProDOS image, it isn't an AppleSingle file anymore.

* Minor comment update.
2022-10-15 14:11:58 -04:00
Terence Boldt
6b5045a788 Update binaries to 0.3.1 2022-03-06 05:31:49 -05:00
Terence Boldt
402f39da91
Fix volume bitmap not writing full blocks (#8) 2022-03-06 05:29:33 -05:00
Terence Boldt
62bb781fca
Add error handling (#5)
* Add error handling

* Update to 0.3.0
2022-03-04 18:08:33 -05:00
Terence Boldt
30a221c177 Fix comments for documentation 2022-01-23 18:57:16 -05:00
Terence Boldt
7f727a6377 Fix comments for documentation 2022-01-23 18:51:44 -05:00
Terence Boldt
e590b82196 Update to version 0.2.0 2022-01-23 18:25:37 -05:00
Terence Boldt
62c594d322 Update binaries 2022-01-23 18:23:54 -05:00
Terence Boldt
819ee47503
Merge pull request #3 from tjboldt/refactor
Change to reader/writer instead of file
2022-01-23 17:33:04 -05:00
Terence Boldt
ea08fd6edc Improve comments for documentation 2022-01-23 17:30:18 -05:00
Terence Boldt
b295288d05 Merge branch 'main' into refactor 2022-01-23 17:05:07 -05:00
Terence Boldt
45c50dcd94
Create go.yaml 2022-01-23 17:04:22 -05:00
Terence Boldt
fb9621ff09 Change to reader/writer instead of file 2022-01-23 16:58:34 -05:00
Terence Boldt
1377fc9020
Create codacy-analysis.yml 2022-01-22 22:41:51 -05:00
Terence Boldt
2a595f0a5c Add binaries 2022-01-04 16:10:53 -05:00
Terence Boldt
4a8be2c327 Fix catalog for 80 column display 2021-12-28 09:30:38 -05:00
Terence Boldt
d53977a1a3 Merge branch 'main' of https://github.com/tjboldt/ProDOS-Utilities 2021-12-12 10:43:37 -05:00
Terence Boldt
2e60d3c09c Add writeblock command 2021-12-12 10:43:30 -05:00
Terence Boldt
790f4f2521
Create codeql-analysis.yml 2021-11-04 05:49:51 -04:00
Terence Boldt
1cbb9fa0d9 Fix DateTimeFromProDOS and tests 2021-10-23 10:06:25 -04:00
26 changed files with 1999 additions and 364 deletions

61
.github/workflows/codacy.yml vendored Normal file
View 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
View 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
View 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 ./...

View File

@ -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
View 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
View File

@ -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
View 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
View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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
View 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

View File

@ -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
View 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")
}
})
}
}

View File

@ -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 {

View File

@ -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
View 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
View 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
View 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
}

View File

@ -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,

View File

@ -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])

View File

@ -1,3 +1,9 @@
// Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// This file provides tests for conversion to and from ProDOS time format
package prodos
import (

View File

@ -1,3 +1,9 @@
// Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// This file provides 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