Compare commits

...

22 Commits
v0.4.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
24 changed files with 1133 additions and 354 deletions

View File

@ -15,21 +15,28 @@ name: Codacy Security Scan
on: on:
push: push:
branches: [ main ] branches: [ "main" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ main ] branches: [ "main" ]
schedule: schedule:
- cron: '36 19 * * 4' - cron: '38 20 * * 5'
permissions:
contents: read
jobs: jobs:
codacy-security-scan: 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 name: Codacy Security Scan
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Checkout the repository to the GitHub Actions runner # Checkout the repository to the GitHub Actions runner
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI - name: Run Codacy Analysis CLI
@ -49,6 +56,6 @@ jobs:
# Upload the SARIF file generated in the previous step # Upload the SARIF file generated in the previous step
- name: Upload SARIF results file - name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v1 uses: github/codeql-action/upload-sarif@v2
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -13,12 +13,12 @@ name: "CodeQL"
on: on:
push: push:
branches: [ main ] branches: [ "main" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ main ] branches: [ "main" ]
schedule: schedule:
- cron: '30 10 * * 5' - cron: '44 11 * * 3'
jobs: jobs:
analyze: analyze:
@ -34,37 +34,43 @@ jobs:
matrix: matrix:
language: [ 'go' ] language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support # Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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. # 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. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # If the Autobuild fails above, remove it and uncomment the following three lines.
# and modify them (or add more) to build your code if your project # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# uses a compiled language
#- run: | # - run: |
# make bootstrap # echo "Run, Build Application using script"
# make release # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@ -8,11 +8,10 @@ Being a work in progress, be warned that this code is likely to corrupt drive im
There are binaries [here](https://github.com/tjboldt/ProDOS-Utilities/releases/latest) There are binaries [here](https://github.com/tjboldt/ProDOS-Utilities/releases/latest)
## Current TODO list ## Current TODO list
1. Allow > 128 KB file support 1. Delete directories
2. Create/Delete directories 2. Add file/directory tests
3. Add file/directory tests 3. Add rename
4. Add rename 4. Add in-place file/directory moves
5. Add in-place file/directory moves
## Example commands and output ## Example commands and output

2
go.mod
View File

@ -1,3 +1,5 @@
module github.com/tjboldt/ProDOS-Utilities module github.com/tjboldt/ProDOS-Utilities
go 1.16 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=

333
main.go
View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -17,7 +17,7 @@ import (
"github.com/tjboldt/ProDOS-Utilities/prodos" "github.com/tjboldt/ProDOS-Utilities/prodos"
) )
const version = "0.4.0" const version = "0.4.9"
func main() { func main() {
var fileName string var fileName string
@ -32,7 +32,7 @@ func main() {
var auxType int var auxType int
flag.StringVar(&fileName, "d", "", "A ProDOS format drive image") 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(&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, putall") 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(&outFileName, "o", "", "Name of file to write")
flag.StringVar(&inFileName, "i", "", "Name of file to read") 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.IntVar(&volumeSize, "s", 65535, "Number of blocks to create the volume with (default 65535, 64 to 65535, 0x0040 to 0xFFFF hex input accepted)")
@ -50,136 +50,225 @@ func main() {
switch command { switch command {
case "ls": case "ls":
file, err := os.OpenFile(fileName, os.O_RDWR, 0755) ls(fileName, pathName)
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)
case "get": case "get":
file, err := os.OpenFile(fileName, os.O_RDWR, 0755) get(fileName, pathName, outFileName)
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)
}
case "put": case "put":
file, err := os.OpenFile(fileName, os.O_RDWR, 0755) put(fileName, pathName, fileType, auxType, inFileName)
if err != nil {
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
os.Exit(1)
}
defer file.Close()
err = prodos.WriteFileFromFile(file, pathName, fileType, auxType, inFileName)
if err != nil {
fmt.Printf("Failed to write file %s", err)
}
case "readblock": case "readblock":
fmt.Printf("Reading block 0x%04X (%d):\n\n", blockNumber, blockNumber) readBlock(blockNumber, fileName)
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
if err != nil {
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
os.Exit(1)
}
defer file.Close()
block, err := prodos.ReadBlock(file, blockNumber)
if err != nil {
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
os.Exit(1)
}
prodos.DumpBlock(block)
case "writeblock": case "writeblock":
fmt.Printf("Writing block 0x%04X (%d):\n\n", blockNumber, blockNumber) writeBlock(blockNumber, fileName, 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()
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)
case "create": case "create":
file, err := os.Create(fileName) create(fileName, volumeName, volumeSize)
if err != nil {
fmt.Printf("failed to create file: %s\n", err)
return
}
defer file.Close()
prodos.CreateVolume(file, volumeName, volumeSize)
case "putall": case "putall":
file, err := os.Create(fileName) putall(fileName, inFileName, pathName, false)
if err != nil { case "putallrecursive":
fmt.Printf("failed to create file: %s\n", err) putall(fileName, inFileName, pathName, true)
return
}
defer file.Close()
err = prodos.AddFilesFromHostDirectory(file, inFileName, volumeName, volumeSize)
if err != nil {
fmt.Printf("failed to add host files: %s\n", err)
return
}
case "rm": case "rm":
file, err := os.OpenFile(fileName, os.O_RDWR, 0755) rm(fileName, pathName)
if err != nil { case "mkdir":
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err) mkdir(fileName, pathName)
os.Exit(1)
}
defer file.Close()
prodos.DeleteFile(file, pathName)
case "dumpfile": case "dumpfile":
file, err := os.OpenFile(fileName, os.O_RDWR, 0755) dumpFile(fileName, pathName)
if err != nil { case "dumpdirectory":
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err) dumpDirectory(fileName, pathName)
os.Exit(1)
}
defer file.Close()
fileEntry, err := prodos.GetFileEntry(file, pathName)
prodos.DumpFileEntry(fileEntry)
default: default:
fmt.Printf("Invalid command: %s\n\n", command) fmt.Printf("Invalid command: %s\n\n", command)
flag.PrintDefaults() flag.PrintDefaults()
os.Exit(1) 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,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -164,6 +164,7 @@ func ConvertBasicToText(basic []byte) string {
} }
} }
// ConvertTextToBasic converts text to AppleSoft BASIC
func ConvertTextToBasic(text string) ([]byte, error) { func ConvertTextToBasic(text string) ([]byte, error) {
// convert line endings // convert line endings
text = strings.Replace(text, "\r\n", "\n", -1) text = strings.Replace(text, "\r\n", "\n", -1)
@ -189,6 +190,9 @@ func ConvertTextToBasic(text string) ([]byte, error) {
// skip initial whitespace and look for the start of a line number // skip initial whitespace and look for the start of a line number
if starting { if starting {
if c == '\n' { // skip blank lines
continue
}
if c == ' ' { if c == ' ' {
continue continue
} }
@ -229,6 +233,7 @@ func ConvertTextToBasic(text string) ([]byte, error) {
parsingRem = false parsingRem = false
parsingString = false parsingString = false
foundToken = false foundToken = false
skipChars = 0
currentByte += basicLine.Len() currentByte += basicLine.Len()
currentByte += 3 currentByte += 3
// write address of next line // write address of next line

View File

@ -1,3 +1,9 @@
// 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 package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -131,7 +131,6 @@ func createVolumeBitmap(numberOfBlocks int) []byte {
markBlockInVolumeBitmap(volumeBitmap, i) markBlockInVolumeBitmap(volumeBitmap, i)
} }
} }
//DumpBlock(volumeBitmap)
return volumeBitmap return volumeBitmap
} }

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 package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -8,6 +8,8 @@
package prodos package prodos
import ( import (
"errors"
"fmt"
"io" "io"
) )
@ -16,6 +18,10 @@ func ReadBlock(reader io.ReaderAt, block int) ([]byte, error) {
buffer := make([]byte, 512) buffer := make([]byte, 512)
_, err := reader.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, err return buffer, err
} }
@ -23,5 +29,10 @@ func ReadBlock(reader io.ReaderAt, block int) ([]byte, error) {
// WriteBlock writes a block to a ProDOS volume from a byte array // WriteBlock writes a block to a ProDOS volume from a byte array
func WriteBlock(writer io.WriterAt, block int, buffer []byte) error { func WriteBlock(writer io.WriterAt, block int, buffer []byte) error {
_, err := writer.WriteAt(buffer, int64(block)*512) _, err := writer.WriteAt(buffer, int64(block)*512)
if err != nil {
errString := fmt.Sprintf("failed to write block %04X: %s", block, err.Error())
err = errors.New(errString)
}
return err return err
} }

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -31,11 +31,21 @@ type VolumeHeader struct {
// DirectoryHeader from ProDOS // DirectoryHeader from ProDOS
type DirectoryHeader struct { type DirectoryHeader struct {
Name string PreviousBlock int
ActiveFileCount int NextBlock int
StartingBlock int IsSubDirectory bool
PreviousBlock int Name string
NextBlock int CreationTime time.Time
Version int
MinVersion int
Access int
EntryLength int
EntriesPerBlock int
ActiveFileCount int
StartingBlock int
ParentBlock int
ParentEntry int
ParentEntryLength int
} }
const ( const (
@ -55,19 +65,20 @@ const (
// FileEntry from ProDOS // FileEntry from ProDOS
type FileEntry struct { type FileEntry struct {
StorageType int StorageType int
FileName string FileName string
FileType int FileType int
CreationTime time.Time CreationTime time.Time
KeyPointer int KeyPointer int
Version int Version int
MinVersion int MinVersion int
BlocksUsed int BlocksUsed int
EndOfFile int EndOfFile int
Access int Access int
AuxType int AuxType int
ModifiedTime time.Time ModifiedTime time.Time
HeaderPointer int HeaderPointer int
DirectoryBlock int DirectoryBlock int
DirectoryOffset int DirectoryOffset int
} }
@ -86,6 +97,11 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
path = fmt.Sprintf("/%s", volumeHeader.VolumeName) 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) path = strings.ToUpper(path)
paths := strings.Split(path, "/") paths := strings.Split(path, "/")
@ -97,14 +113,109 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
return volumeHeader, directoryHeader, fileEntries, nil return volumeHeader, directoryHeader, fileEntries, nil
} }
func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntry, error) { // CreateDirectory creates a directory information of a specified path
_, directoryHeader, _, err := ReadDirectory(reader, directory) // 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 { if err != nil {
return FileEntry{}, err return FileEntry{}, err
} }
//DumpDirectoryHeader(directoryHeader)
blockNumber := directoryHeader.StartingBlock blockNumber := directoryHeader.StartingBlock
buffer, err := ReadBlock(reader, blockNumber) buffer, err := ReadBlock(readerWriter, blockNumber)
if err != nil { if err != nil {
return FileEntry{}, err return FileEntry{}, err
} }
@ -113,23 +224,31 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
for { for {
if entryNumber > 13 { if entryNumber > 13 {
blockNumber = int(buffer[2]) + int(buffer[3])*256 nextBlockNumber := int(buffer[2]) + int(buffer[3])*256
// if we ran out of blocks in the directory, return empty // if we ran out of blocks in the directory, expand directory or fail
// TODO: expand the directory to add more entries if nextBlockNumber == 0 {
if blockNumber == 0 { if !directoryHeader.IsSubDirectory {
return FileEntry{}, errors.New("no free file entries found") 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 // else read the next block in the directory
buffer, err = ReadBlock(reader, blockNumber) buffer, err = ReadBlock(readerWriter, blockNumber)
if err != nil { if err != nil {
return FileEntry{}, nil return FileEntry{}, nil
} }
entryOffset = 4 entryOffset = 4
entryNumber = 1 entryNumber = 1
} }
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset) fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+0x28], blockNumber, entryOffset)
if fileEntry.StorageType == StorageDeleted { if fileEntry.StorageType == StorageDeleted {
fileEntry = FileEntry{}
fileEntry.DirectoryBlock = blockNumber fileEntry.DirectoryBlock = blockNumber
fileEntry.DirectoryOffset = entryOffset fileEntry.DirectoryOffset = entryOffset
fileEntry.HeaderPointer = directoryHeader.StartingBlock fileEntry.HeaderPointer = directoryHeader.StartingBlock
@ -141,6 +260,51 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
} }
} }
func expandDirectory(readerWriter ReaderWriterAt, nextBlockNumber int, buffer []byte, blockNumber int, directoryHeader DirectoryHeader) (int, error) {
volumeBitMap, err := ReadVolumeBitmap(readerWriter)
if err != nil {
errString := fmt.Sprintf("failed to get volume bitmap to expand directory: %s", err)
return 0, errors.New(errString)
}
blockList := findFreeBlocks(volumeBitMap, 1)
if len(blockList) != 1 {
return 0, errors.New("failed to get free block to expand directory")
}
nextBlockNumber = blockList[0]
buffer[0x02] = byte(nextBlockNumber & 0x00FF)
buffer[0x03] = byte(nextBlockNumber >> 8)
WriteBlock(readerWriter, blockNumber, buffer)
if err != nil {
errString := fmt.Sprintf("failed to write block to expand directory: %s", err)
return 0, errors.New(errString)
}
buffer = make([]byte, 0x200)
buffer[0x00] = byte(blockNumber & 0x00FF)
buffer[0x01] = byte(blockNumber >> 8)
err = WriteBlock(readerWriter, nextBlockNumber, buffer)
if err != nil {
errString := fmt.Sprintf("failed to write new block to expand directory: %s", err)
return 0, errors.New(errString)
}
updateVolumeBitmap(readerWriter, blockList)
buffer, err = ReadBlock(readerWriter, directoryHeader.ParentBlock)
if err != nil {
errString := fmt.Sprintf("failed to read parent block to expand directory: %s", err)
return 0, errors.New(errString)
}
directoryEntryOffset := directoryHeader.ParentEntry*directoryHeader.EntryLength + 0x04
directoryFileEntry := parseFileEntry(buffer[directoryEntryOffset:directoryEntryOffset+0x28], directoryHeader.ParentBlock, directoryHeader.ParentEntry*directoryHeader.EntryLength+0x04)
directoryFileEntry.BlocksUsed++
directoryFileEntry.EndOfFile += 0x200
writeFileEntry(readerWriter, directoryFileEntry)
return nextBlockNumber, nil
}
func getFileEntriesInDirectory(reader io.ReaderAt, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry, error) { func getFileEntriesInDirectory(reader io.ReaderAt, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry, error) {
buffer, err := ReadBlock(reader, blockNumber) buffer, err := ReadBlock(reader, blockNumber)
if err != nil { if err != nil {
@ -255,13 +419,14 @@ func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) {
buffer[0x1E] = byte(fileEntry.Access) buffer[0x1E] = byte(fileEntry.Access)
buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF) buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF)
buffer[0x20] = byte(fileEntry.AuxType >> 8) buffer[0x20] = byte(fileEntry.AuxType >> 8)
modifiedTime := DateTimeToProDOS(fileEntry.CreationTime) modifiedTime := DateTimeToProDOS(fileEntry.ModifiedTime)
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
buffer[0x21+i] = modifiedTime[i] buffer[0x21+i] = modifiedTime[i]
} }
buffer[0x25] = byte(fileEntry.HeaderPointer & 0x00FF) buffer[0x25] = byte(fileEntry.HeaderPointer & 0x00FF)
buffer[0x26] = byte(fileEntry.HeaderPointer >> 8) buffer[0x26] = byte(fileEntry.HeaderPointer >> 8)
//fmt.Printf("Writing file entry at block: %04X offset: %04X\n", fileEntry.DirectoryBlock, fileEntry.DirectoryOffset)
_, err := writer.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset)) _, err := writer.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset))
if err != nil { if err != nil {
@ -281,7 +446,7 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
bitmapBlock := int(buffer[39]) + int(buffer[40])*256 bitmapBlock := int(buffer[39]) + int(buffer[40])*256
totalBlocks := int(buffer[41]) + int(buffer[42])*256 totalBlocks := int(buffer[41]) + int(buffer[42])*256
if version > 0 || minVersion > 0 { if minVersion > 0 {
panic("Unsupported ProDOS version") panic("Unsupported ProDOS version")
} }
@ -303,22 +468,43 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader { func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
previousBlock := int(buffer[0x00]) + int(buffer[0x01])*256 previousBlock := int(buffer[0x00]) + int(buffer[0x01])*256
nextBlock := int(buffer[0x02]) + int(buffer[0x03])*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]) 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 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{ directoryEntry := DirectoryHeader{
PreviousBlock: previousBlock, PreviousBlock: previousBlock,
NextBlock: nextBlock, NextBlock: nextBlock,
StartingBlock: blockNumber, StartingBlock: blockNumber,
Name: name, IsSubDirectory: isSubDirectory,
ActiveFileCount: fileCount, Name: name,
CreationTime: creationTime,
Version: version,
MinVersion: minVersion,
Access: access,
EntryLength: entryLength,
EntriesPerBlock: entriesPerBlock,
ActiveFileCount: fileCount,
ParentBlock: parentBlock,
ParentEntry: parentEntry,
ParentEntryLength: parentEntryLength,
} }
return directoryEntry return directoryEntry
} }
func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader DirectoryHeader) error { func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader DirectoryHeader) error {
// Reading back the block preserves values including reserved fields
buffer, err := ReadBlock(readerWriter, directoryHeader.StartingBlock) buffer, err := ReadBlock(readerWriter, directoryHeader.StartingBlock)
if err != nil { if err != nil {
return err return err
@ -327,12 +513,40 @@ func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader Directory
buffer[0x01] = byte(directoryHeader.PreviousBlock >> 8) buffer[0x01] = byte(directoryHeader.PreviousBlock >> 8)
buffer[0x02] = byte(directoryHeader.NextBlock & 0x00FF) buffer[0x02] = byte(directoryHeader.NextBlock & 0x00FF)
buffer[0x03] = byte(directoryHeader.NextBlock >> 8) buffer[0x03] = byte(directoryHeader.NextBlock >> 8)
if directoryHeader.IsSubDirectory {
buffer[0x04] = 0xE0
} else {
buffer[0x04] = 0xF0
}
buffer[0x04] = buffer[0x04] | byte(len(directoryHeader.Name)) buffer[0x04] = buffer[0x04] | byte(len(directoryHeader.Name))
for i := 0; i < len(directoryHeader.Name); i++ { for i := 0; i < len(directoryHeader.Name); i++ {
buffer[0x05+i] = 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[0x25] = byte(directoryHeader.ActiveFileCount & 0x00FF)
buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8) buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8)
buffer[0x27] = byte(directoryHeader.ParentBlock & 0x00FF)
buffer[0x28] = byte(directoryHeader.ParentBlock >> 8)
buffer[0x29] = byte(directoryHeader.ParentEntry)
buffer[0x2A] = byte(directoryHeader.ParentEntryLength)
WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer) WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer)
return nil return nil

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -8,12 +8,9 @@
package prodos package prodos
import ( import (
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath"
"strings" "strings"
"time" "time"
) )
@ -45,100 +42,17 @@ func LoadFile(reader io.ReaderAt, path string) ([]byte, error) {
return buffer, nil return buffer, nil
} }
// WriteFileFromFile writes a file to a ProDOS volume from a host file
func WriteFileFromFile(readerWriter ReaderWriterAt, pathName string, fileType int, auxType int, inFileName string) error {
inFile, err := os.ReadFile(inFileName)
if err != nil {
return err
}
if auxType == 0 && fileType == 0 {
auxType, fileType, inFile, err = convertFileByType(inFileName, inFile)
if err != nil {
return err
}
}
if len(pathName) == 0 {
_, pathName = filepath.Split(inFileName)
pathName = strings.ToUpper(pathName)
ext := filepath.Ext(pathName)
if len(ext) > 0 {
switch ext {
case ".SYS", ".TXT", ".BAS", ".BIN":
pathName = strings.TrimSuffix(pathName, ext)
}
}
}
return WriteFile(readerWriter, pathName, fileType, auxType, 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
}
}
return auxType, fileType, inFile, err
}
// WriteFile writes a file to a ProDOS volume from a byte array // WriteFile writes a file to a ProDOS volume from a byte array
func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, buffer []byte) error { func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType int, createdTime time.Time, modifiedTime time.Time, buffer []byte) error {
directory, fileName := GetDirectoryAndFileNameFromPath(path) directory, fileName := GetDirectoryAndFileNameFromPath(path)
if len(fileName) > 15 {
return errors.New("filename too long")
}
existingFileEntry, _ := GetFileEntry(readerWriter, path) existingFileEntry, _ := GetFileEntry(readerWriter, path)
if existingFileEntry.StorageType != StorageDeleted { if existingFileEntry.StorageType != StorageDeleted {
DeleteFile(readerWriter, path) return errors.New(("file already exists"))
} }
// get list of blocks to write file to // get list of blocks to write file to
@ -149,7 +63,7 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
// seedling file // seedling file
if len(buffer) <= 0x200 { if len(buffer) <= 0x200 {
WriteBlock(readerWriter, blockList[0], buffer) writeSeedlingFile(readerWriter, buffer, blockList)
} }
// sapling file needs index block // sapling file needs index block
@ -175,12 +89,14 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
} }
fileEntry.FileName = fileName fileEntry.FileName = fileName
fileEntry.BlocksUsed = len(blockList) fileEntry.BlocksUsed = len(blockList)
fileEntry.CreationTime = time.Now() fileEntry.CreationTime = createdTime
fileEntry.ModifiedTime = time.Now() fileEntry.ModifiedTime = modifiedTime
fileEntry.AuxType = auxType fileEntry.AuxType = auxType
fileEntry.EndOfFile = len(buffer) fileEntry.EndOfFile = len(buffer)
fileEntry.FileType = fileType fileEntry.FileType = fileType
fileEntry.KeyPointer = blockList[0] fileEntry.KeyPointer = blockList[0]
fileEntry.Version = 0x24
fileEntry.MinVersion = 0x00
fileEntry.Access = 0b11100011 fileEntry.Access = 0b11100011
if len(blockList) == 1 { if len(blockList) == 1 {
fileEntry.StorageType = StorageSeedling fileEntry.StorageType = StorageSeedling
@ -192,7 +108,10 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
writeFileEntry(readerWriter, fileEntry) writeFileEntry(readerWriter, fileEntry)
// increment file count return incrementFileCount(readerWriter, fileEntry)
}
func incrementFileCount(readerWriter ReaderWriterAt, fileEntry FileEntry) error {
directoryHeaderBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer) directoryHeaderBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer)
if err != nil { if err != nil {
return err return err
@ -207,6 +126,10 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
// DeleteFile deletes a file from a ProDOS volume // DeleteFile deletes a file from a ProDOS volume
func DeleteFile(readerWriter ReaderWriterAt, path string) error { func DeleteFile(readerWriter ReaderWriterAt, path string) error {
fileEntry, err := GetFileEntry(readerWriter, path) fileEntry, err := GetFileEntry(readerWriter, path)
// DumpFileEntry(fileEntry)
// oldDirectoryBlock, _ := ReadBlock(readerWriter, fileEntry.DirectoryBlock)
// DumpBlock(oldDirectoryBlock)
if err != nil { if err != nil {
return errors.New("file not found") return errors.New("file not found")
} }
@ -218,10 +141,11 @@ func DeleteFile(readerWriter ReaderWriterAt, path string) error {
} }
// free the blocks // free the blocks
blocks, err := getBlocklist(readerWriter, fileEntry) blocks, err := getAllBlockList(readerWriter, fileEntry)
if err != nil { if err != nil {
return err return err
} }
volumeBitmap, err := ReadVolumeBitmap(readerWriter) volumeBitmap, err := ReadVolumeBitmap(readerWriter)
if err != nil { if err != nil {
return err return err
@ -249,6 +173,12 @@ func DeleteFile(readerWriter ReaderWriterAt, path string) error {
return nil 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 // GetDirectoryAndFileNameFromPath gets the directory and filename from a path
func GetDirectoryAndFileNameFromPath(path string) (string, string) { func GetDirectoryAndFileNameFromPath(path string) (string, string) {
path = strings.ToUpper(path) path = strings.ToUpper(path)
@ -280,6 +210,10 @@ func updateVolumeBitmap(readerWriter ReaderWriterAt, blockList []int) error {
return writeVolumeBitmap(readerWriter, volumeBitmap) 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) { func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
// write index block with pointers to data blocks // write index block with pointers to data blocks
indexBuffer := make([]byte, 512) indexBuffer := make([]byte, 512)
@ -287,6 +221,9 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
if i < len(blockList)-1 { if i < len(blockList)-1 {
indexBuffer[i] = byte(blockList[i+1] & 0x00FF) indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
indexBuffer[i+256] = byte(blockList[i+1] >> 8) indexBuffer[i+256] = byte(blockList[i+1] >> 8)
} else {
indexBuffer[i] = 0
indexBuffer[i+256] = 0
} }
} }
WriteBlock(writer, blockList[0], indexBuffer) WriteBlock(writer, blockList[0], indexBuffer)
@ -313,11 +250,69 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
} }
func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) { func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) {
// write master index block with pointers to index blocks
indexBuffer := make([]byte, 512)
numberOfIndexBlocks := len(blockList)/256 + 1
if len(blockList)%256 == 0 {
numberOfIndexBlocks--
}
for i := 0; i < 256; i++ {
if i < numberOfIndexBlocks {
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
} else {
indexBuffer[i] = 0
indexBuffer[i+256] = 0
}
}
WriteBlock(writer, blockList[0], indexBuffer)
numberOfIndexBlocks++
// write index blocks
for i := 0; i < len(blockList)/256+1; i++ {
for j := 0; j < 256; j++ {
if i*256+j < len(blockList)-numberOfIndexBlocks {
indexBuffer[j] = byte(blockList[i*256+numberOfIndexBlocks+j] & 0x00FF)
indexBuffer[j+256] = byte(blockList[i*256+j+2] >> 8)
} else {
indexBuffer[j] = 0
indexBuffer[j+256] = 0
}
}
WriteBlock(writer, blockList[i+1], indexBuffer)
}
// write all data blocks
blockBuffer := make([]byte, 512)
blockPointer := 0
blockIndexNumber := numberOfIndexBlocks
for i := 0; i < len(buffer); i++ {
blockBuffer[blockPointer] = buffer[i]
if blockPointer == 511 {
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
blockPointer = 0
blockIndexNumber++
} else if i == len(buffer)-1 {
for j := blockPointer; j < 512; j++ {
blockBuffer[j] = 0
}
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
} else {
blockPointer++
}
}
}
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
return getBlocklist(reader, fileEntry, true)
}
func getAllBlockList(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
return getBlocklist(reader, fileEntry, false)
} }
// Returns all blocks, including index blocks // Returns all blocks, including index blocks
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) { func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int, error) {
blocks := make([]int, fileEntry.BlocksUsed) blocks := make([]int, fileEntry.BlocksUsed)
switch fileEntry.StorageType { switch fileEntry.StorageType {
@ -329,58 +324,56 @@ func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
blockOffset := 1
blocks[0] = fileEntry.KeyPointer blocks[0] = fileEntry.KeyPointer
for i := 0; i < fileEntry.BlocksUsed-1; i++ { 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 return blocks, nil
case StorageTree: case StorageTree:
// this is actually too large
dataBlocks := make([]int, fileEntry.BlocksUsed)
// this is also actually too large
numberOfIndexBlocks := fileEntry.BlocksUsed/256 + 2
indexBlocks := make([]int, numberOfIndexBlocks)
masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer) masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
blocks[0] = fileEntry.KeyPointer numberOfDataBlocks := 0
indexBlocks[0] = fileEntry.KeyPointer
indexBlockCount := 1
for i := 0; i < 128; i++ { for i := 0; i < 128; i++ {
index, err := ReadBlock(reader, int(masterIndex[i])+int(masterIndex[i+256])*256) indexBlock := int(masterIndex[i]) + int(masterIndex[i+256])*256
if indexBlock == 0 {
break
}
indexBlocks[indexBlockCount] = indexBlock
indexBlockCount++
index, err := ReadBlock(reader, indexBlock)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for j := 0; j < 256 && i*256+j < fileEntry.BlocksUsed; j++ { for j := 0; j < 256 && i*256+j < fileEntry.BlocksUsed; j++ {
if (int(index[j]) + int(index[j+256])*256) == 0 { 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") if dataOnly {
} return dataBlocks, nil
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
switch fileEntry.StorageType {
case StorageSeedling:
blocks := make([]int, 1)
blocks[0] = fileEntry.KeyPointer
return blocks, nil
case StorageSapling:
blocks := make([]int, fileEntry.BlocksUsed-1)
index, err := ReadBlock(reader, fileEntry.KeyPointer)
if err != nil {
return nil, err
} }
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
blocks[i] = int(index[i]) + int(index[i+256])*256
}
return blocks, nil
// case StorageTree:
// blocks := make([]int, fileEntry.BlocksUsed-fileEntry.BlocksUsed/256-1)
// masterIndex := ReadBlock(reader, fileEntry.KeyPointer)
// for i := 0; i < 128; i++ {
// blockNumber := 0
// blocks[j] = int(index[i]) + int(index[i+256])*256
// } 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")
@ -388,20 +381,16 @@ func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) { func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
numberOfBlocks := fileSize / 512 numberOfBlocks := fileSize / 512
//fmt.Printf("Number of blocks %d\n", numberOfBlocks)
if fileSize%512 > 0 { if fileSize%512 > 0 {
//fmt.Printf("Adding block for partial usage\n")
numberOfBlocks++ numberOfBlocks++
} }
if fileSize > 0x200 && fileSize <= 0x20000 { if fileSize > 0x200 && fileSize <= 0x20000 {
//fmt.Printf("Adding index block for sapling file\n")
numberOfBlocks++ // add index block numberOfBlocks++ // add index block
} }
if fileSize > 0x20000 && fileSize <= 0x1000000 { if fileSize > 0x20000 && fileSize <= 0x1000000 {
//fmt.Printf("Tree file\n")
// add index blocks for each 256 blocks // add index blocks for each 256 blocks
numberOfBlocks += numberOfBlocks / 256 numberOfBlocks += numberOfBlocks / 256
// add index block for any remaining blocks // add index block for any remaining blocks
@ -420,12 +409,12 @@ func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
return nil, err return nil, err
} }
//fmt.Printf("findFreeBlocks %d\n", numberOfBlocks)
blockList := findFreeBlocks(volumeBitmap, numberOfBlocks) blockList := findFreeBlocks(volumeBitmap, numberOfBlocks)
return blockList, nil return blockList[0:numberOfBlocks], nil
} }
// GetFileEntry returns a file entry for the given path
func GetFileEntry(reader io.ReaderAt, path string) (FileEntry, error) { func GetFileEntry(reader io.ReaderAt, path string) (FileEntry, error) {
directory, fileName := GetDirectoryAndFileNameFromPath(path) directory, fileName := GetDirectoryAndFileNameFromPath(path)
_, _, fileEntries, err := ReadDirectory(reader, directory) _, _, fileEntries, err := ReadDirectory(reader, directory)

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -9,7 +9,7 @@ import (
"testing" "testing"
) )
func TestCreatBlocklist(t *testing.T) { func TestCreateBlocklist(t *testing.T) {
var tests = []struct { var tests = []struct {
fileSize int fileSize int
wantBlocks int wantBlocks int

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

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 access to format a ProDOS drive image
package prodos package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2022 // Copyright Terence J. Boldt (c)2022-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,8 +7,13 @@
package prodos package prodos
import ( import (
"encoding/binary"
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time"
) )
// AddFilesFromHostDirectory fills the root volume with files // AddFilesFromHostDirectory fills the root volume with files
@ -16,9 +21,15 @@ import (
func AddFilesFromHostDirectory( func AddFilesFromHostDirectory(
readerWriter ReaderWriterAt, readerWriter ReaderWriterAt,
directory string, directory string,
volumeName string, path string,
numberOfBlocks int) error { recursive bool,
CreateVolume(readerWriter, volumeName, numberOfBlocks) ) error {
path, err := makeFullPath(path, readerWriter)
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
files, err := os.ReadDir(directory) files, err := os.ReadDir(directory)
if err != nil { if err != nil {
@ -31,8 +42,26 @@ func AddFilesFromHostDirectory(
return err return err
} }
if !file.IsDir() && info.Size() > 0 && info.Size() <= 0x20000 { if file.Name()[0] != '.' && !file.IsDir() && info.Size() > 0 && info.Size() <= 0x1000000 {
err = WriteFileFromFile(readerWriter, "", 0, 0, filepath.Join(directory, file.Name())) 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 { if err != nil {
return err return err
} }
@ -41,3 +70,134 @@ func AddFilesFromHostDirectory(
return nil 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
}

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -12,6 +12,7 @@ type MemoryFile struct {
size int size int
} }
// ReaderWriterAt is an interface for both ReaderAt and WriterAt combined
type ReaderWriterAt interface { type ReaderWriterAt interface {
ReadAt(data []byte, offset int64) (int, error) ReadAt(data []byte, offset int64) (int, error)
WriteAt(data []byte, offset int64) (int, error) WriteAt(data []byte, offset int64) (int, error)

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -99,6 +99,9 @@ func DumpFileEntry(fileEntry FileEntry) {
fmt.Printf("File type: %02X\n", fileEntry.FileType) fmt.Printf("File type: %02X\n", fileEntry.FileType)
fmt.Printf("Storage type: %02X\n", fileEntry.StorageType) fmt.Printf("Storage type: %02X\n", fileEntry.StorageType)
fmt.Printf("Header pointer: %04X\n", fileEntry.HeaderPointer) 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") fmt.Printf("\n")
} }
@ -118,11 +121,22 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
// DumpDirectoryHeader dumps the directory header as text // DumpDirectoryHeader dumps the directory header as text
func DumpDirectoryHeader(directoryHeader DirectoryHeader) { 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("Starting block: %04X\n", directoryHeader.StartingBlock)
fmt.Printf("Previous block: %04X\n", directoryHeader.PreviousBlock) fmt.Printf("Previous block: %04X\n", directoryHeader.PreviousBlock)
fmt.Printf("Next block: %04X\n", directoryHeader.NextBlock) 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 // DumpBlock dumps the block as hexadecimal and text

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -11,19 +11,20 @@ import (
) )
// DateTimeToProDOS converts Time to ProDOS date time // 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 // 49041 ($BF91) 49040 ($BF90)
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
// DATE: | year | month | day |
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
// //
// 49043 ($BF93) 49042 ($BF92) // 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
// DATE: | year | month | day |
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
// //
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 // 49043 ($BF93) 49042 ($BF92)
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ //
// TIME: | hour | | minute | // 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 { func DateTimeToProDOS(dateTime time.Time) []byte {
year := dateTime.Year() % 100 year := dateTime.Year() % 100
month := dateTime.Month() month := dateTime.Month()

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 package prodos
import ( import (

View File

@ -1,4 +1,4 @@
// Copyright Terence J. Boldt (c)2021-2022 // Copyright Terence J. Boldt (c)2021-2023
// Use of this source code is governed by an MIT // Use of this source code is governed by an MIT
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -14,7 +14,7 @@ func printReadme() {
fmt.Println(` fmt.Println(`
MIT License MIT License
Copyright (c)2021-2022 Terence Boldt Copyright (c)2021-2023 Terence Boldt
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal