mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-06-17 17:30:56 +00:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0aa838ffe1 | ||
|
231197bd8f | ||
|
2477b4a8a3 | ||
|
30f0b03054 | ||
|
ef2b505500 | ||
|
7f8679cdb4 | ||
|
98ab2d4f0a | ||
|
9e6c2fcc22 | ||
|
b80a835c72 | ||
|
10f896399d | ||
|
36c24e60ca | ||
|
286964d3ee | ||
|
1722e7b23a | ||
|
876564d261 | ||
|
ee3d187fb3 | ||
|
ab3b397139 | ||
|
6cc7db13e1 | ||
|
0beb2f41fa | ||
|
cb6b3a25ba | ||
|
8b54fa3235 | ||
|
0ac5bd39d0 |
|
@ -15,21 +15,28 @@ name: Codacy Security Scan
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '36 19 * * 4'
|
||||
- cron: '38 20 * * 5'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codacy-security-scan:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
name: Codacy Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout the repository to the GitHub Actions runner
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||
- name: Run Codacy Analysis CLI
|
||||
|
@ -49,6 +56,6 @@ jobs:
|
|||
|
||||
# Upload the SARIF file generated in the previous step
|
||||
- name: Upload SARIF results file
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
|
@ -13,12 +13,12 @@ name: "CodeQL"
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '30 10 * * 5'
|
||||
- cron: '44 11 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
@ -34,37 +34,43 @@ jobs:
|
|||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
|
@ -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)
|
||||
|
||||
## Current TODO list
|
||||
1. Allow > 128 KB file support
|
||||
2. Create/Delete directories
|
||||
3. Add file/directory 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
|
||||
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,5 @@
|
|||
module github.com/tjboldt/ProDOS-Utilities
|
||||
|
||||
go 1.16
|
||||
|
||||
require golang.org/x/image v0.15.0
|
||||
|
|
33
go.sum
Normal file
33
go.sum
Normal file
|
@ -0,0 +1,33 @@
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
333
main.go
333
main.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/tjboldt/ProDOS-Utilities/prodos"
|
||||
)
|
||||
|
||||
const version = "0.4.1"
|
||||
const version = "0.4.9"
|
||||
|
||||
func main() {
|
||||
var fileName string
|
||||
|
@ -32,7 +32,7 @@ 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, 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(&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)")
|
||||
|
@ -50,136 +50,225 @@ func main() {
|
|||
|
||||
switch command {
|
||||
case "ls":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
pathName = strings.ToUpper(pathName)
|
||||
volumeHeader, _, fileEntries, err := prodos.ReadDirectory(file, pathName)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
}
|
||||
if len(pathName) == 0 {
|
||||
pathName = "/" + volumeHeader.VolumeName
|
||||
}
|
||||
volumeBitmap, err := prodos.ReadVolumeBitmap(file)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
freeBlocks := prodos.GetFreeBlockCount(volumeBitmap, volumeHeader.TotalBlocks)
|
||||
prodos.DumpDirectory(freeBlocks, volumeHeader.TotalBlocks, pathName, fileEntries)
|
||||
ls(fileName, pathName)
|
||||
case "get":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
if len(pathName) == 0 {
|
||||
fmt.Println("Missing pathname")
|
||||
os.Exit(1)
|
||||
}
|
||||
getFile, err := prodos.LoadFile(file, pathName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read file %s: %s\n", pathName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(outFileName) == 0 {
|
||||
_, outFileName = prodos.GetDirectoryAndFileNameFromPath(pathName)
|
||||
}
|
||||
outFile, err := os.Create(outFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create output file %s: %s\n", outFileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(outFileName), ".bas") {
|
||||
fmt.Fprintf(outFile, prodos.ConvertBasicToText(getFile))
|
||||
} else {
|
||||
outFile.Write(getFile)
|
||||
}
|
||||
get(fileName, pathName, outFileName)
|
||||
case "put":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = prodos.WriteFileFromFile(file, pathName, fileType, auxType, inFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to write file %s", err)
|
||||
}
|
||||
put(fileName, pathName, fileType, auxType, inFileName)
|
||||
case "readblock":
|
||||
fmt.Printf("Reading block 0x%04X (%d):\n\n", blockNumber, blockNumber)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
block, err := prodos.ReadBlock(file, blockNumber)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
prodos.DumpBlock(block)
|
||||
readBlock(blockNumber, fileName)
|
||||
case "writeblock":
|
||||
fmt.Printf("Writing block 0x%04X (%d):\n\n", blockNumber, blockNumber)
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open drive image %s:\n %s", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
inFile, err := os.ReadFile(inFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open input file %s: %s", inFileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
prodos.WriteBlock(file, blockNumber, inFile)
|
||||
writeBlock(blockNumber, fileName, inFileName)
|
||||
case "create":
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create file: %s\n", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
prodos.CreateVolume(file, volumeName, volumeSize)
|
||||
create(fileName, volumeName, volumeSize)
|
||||
case "putall":
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create file: %s\n", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
err = prodos.AddFilesFromHostDirectory(file, inFileName)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to add host files: %s\n", err)
|
||||
return
|
||||
}
|
||||
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":
|
||||
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()
|
||||
fileEntry, err := prodos.GetFileEntry(file, pathName)
|
||||
prodos.DumpFileEntry(fileEntry)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -190,6 +190,9 @@ func ConvertTextToBasic(text string) ([]byte, error) {
|
|||
|
||||
// skip initial whitespace and look for the start of a line number
|
||||
if starting {
|
||||
if c == '\n' { // skip blank lines
|
||||
continue
|
||||
}
|
||||
if c == ' ' {
|
||||
continue
|
||||
}
|
||||
|
@ -230,6 +233,7 @@ func ConvertTextToBasic(text string) ([]byte, error) {
|
|||
parsingRem = false
|
||||
parsingString = false
|
||||
foundToken = false
|
||||
skipChars = 0
|
||||
currentByte += basicLine.Len()
|
||||
currentByte += 3
|
||||
// write address of next line
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -131,7 +131,6 @@ func createVolumeBitmap(numberOfBlocks int) []byte {
|
|||
markBlockInVolumeBitmap(volumeBitmap, i)
|
||||
}
|
||||
}
|
||||
//DumpBlock(volumeBitmap)
|
||||
|
||||
return volumeBitmap
|
||||
}
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides tests for access to volum bitmap on
|
||||
// a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -8,6 +8,8 @@
|
|||
package prodos
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
|
@ -16,6 +18,10 @@ func ReadBlock(reader io.ReaderAt, block int) ([]byte, error) {
|
|||
buffer := make([]byte, 512)
|
||||
|
||||
_, err := reader.ReadAt(buffer, int64(block)*512)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to read block %04X: %s", block, err.Error())
|
||||
err = errors.New(errString)
|
||||
}
|
||||
|
||||
return buffer, err
|
||||
}
|
||||
|
@ -23,5 +29,10 @@ func ReadBlock(reader io.ReaderAt, block int) ([]byte, error) {
|
|||
// WriteBlock writes a block to a ProDOS volume from a byte array
|
||||
func WriteBlock(writer io.WriterAt, block int, buffer []byte) error {
|
||||
_, err := writer.WriteAt(buffer, int64(block)*512)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to write block %04X: %s", block, err.Error())
|
||||
err = errors.New(errString)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -31,11 +31,21 @@ type VolumeHeader struct {
|
|||
|
||||
// DirectoryHeader from ProDOS
|
||||
type DirectoryHeader struct {
|
||||
Name string
|
||||
ActiveFileCount int
|
||||
StartingBlock int
|
||||
PreviousBlock int
|
||||
NextBlock int
|
||||
PreviousBlock int
|
||||
NextBlock int
|
||||
IsSubDirectory bool
|
||||
Name string
|
||||
CreationTime time.Time
|
||||
Version int
|
||||
MinVersion int
|
||||
Access int
|
||||
EntryLength int
|
||||
EntriesPerBlock int
|
||||
ActiveFileCount int
|
||||
StartingBlock int
|
||||
ParentBlock int
|
||||
ParentEntry int
|
||||
ParentEntryLength int
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -55,19 +65,20 @@ const (
|
|||
|
||||
// FileEntry from ProDOS
|
||||
type FileEntry struct {
|
||||
StorageType int
|
||||
FileName string
|
||||
FileType int
|
||||
CreationTime time.Time
|
||||
KeyPointer int
|
||||
Version int
|
||||
MinVersion int
|
||||
BlocksUsed int
|
||||
EndOfFile int
|
||||
Access int
|
||||
AuxType int
|
||||
ModifiedTime time.Time
|
||||
HeaderPointer int
|
||||
StorageType int
|
||||
FileName string
|
||||
FileType int
|
||||
CreationTime time.Time
|
||||
KeyPointer int
|
||||
Version int
|
||||
MinVersion int
|
||||
BlocksUsed int
|
||||
EndOfFile int
|
||||
Access int
|
||||
AuxType int
|
||||
ModifiedTime time.Time
|
||||
HeaderPointer int
|
||||
|
||||
DirectoryBlock int
|
||||
DirectoryOffset int
|
||||
}
|
||||
|
@ -86,6 +97,11 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
|
|||
path = fmt.Sprintf("/%s", volumeHeader.VolumeName)
|
||||
}
|
||||
|
||||
// add volume name if not full path
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
|
||||
}
|
||||
|
||||
path = strings.ToUpper(path)
|
||||
paths := strings.Split(path, "/")
|
||||
|
||||
|
@ -97,14 +113,109 @@ func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHead
|
|||
return volumeHeader, directoryHeader, fileEntries, nil
|
||||
}
|
||||
|
||||
func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntry, error) {
|
||||
_, directoryHeader, _, err := ReadDirectory(reader, directory)
|
||||
// CreateDirectory creates a directory information of a specified path
|
||||
// on a ProDOS image
|
||||
func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
|
||||
if len(path) == 0 {
|
||||
return errors.New("cannot create directory with path")
|
||||
}
|
||||
|
||||
// add volume name if not full path
|
||||
path, err := makeFullPath(path, readerWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parentPath, newDirectory := GetDirectoryAndFileNameFromPath(path)
|
||||
|
||||
existingFileEntry, _ := GetFileEntry(readerWriter, path)
|
||||
if existingFileEntry.StorageType != StorageDeleted {
|
||||
return errors.New("directory already exists")
|
||||
}
|
||||
|
||||
fileEntry, err := getFreeFileEntryInDirectory(readerWriter, parentPath)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
// get list of blocks to write file to
|
||||
blockList, err := createBlockList(readerWriter, 512)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
updateVolumeBitmap(readerWriter, blockList)
|
||||
|
||||
fileEntry.FileName = newDirectory
|
||||
fileEntry.BlocksUsed = 1
|
||||
fileEntry.CreationTime = time.Now()
|
||||
fileEntry.ModifiedTime = time.Now()
|
||||
fileEntry.AuxType = 0
|
||||
fileEntry.EndOfFile = 0x200
|
||||
fileEntry.FileType = 0x0F
|
||||
fileEntry.KeyPointer = blockList[0]
|
||||
fileEntry.Access = 0b11100011
|
||||
fileEntry.StorageType = StorageDirectory
|
||||
fileEntry.Version = 0x24
|
||||
fileEntry.MinVersion = 0x00
|
||||
|
||||
writeFileEntry(readerWriter, fileEntry)
|
||||
|
||||
err = incrementFileCount(readerWriter, fileEntry)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
directoryEntry := DirectoryHeader{
|
||||
PreviousBlock: 0,
|
||||
NextBlock: 0,
|
||||
IsSubDirectory: true,
|
||||
Name: newDirectory,
|
||||
CreationTime: time.Now(),
|
||||
Version: 0x24,
|
||||
MinVersion: 0,
|
||||
Access: 0xE3,
|
||||
EntryLength: 0x27,
|
||||
EntriesPerBlock: 0x0D,
|
||||
ActiveFileCount: 0,
|
||||
StartingBlock: blockList[0],
|
||||
ParentBlock: fileEntry.DirectoryBlock,
|
||||
ParentEntry: (fileEntry.DirectoryOffset - 0x04) / 0x27,
|
||||
ParentEntryLength: 0x27,
|
||||
}
|
||||
|
||||
err = writeDirectoryHeader(readerWriter, directoryEntry)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to create directory: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeFullPath(path string, reader io.ReaderAt) (string, error) {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
buffer, err := ReadBlock(reader, 0x0002)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
volumeHeader := parseVolumeHeader(buffer)
|
||||
path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func getFreeFileEntryInDirectory(readerWriter ReaderWriterAt, directory string) (FileEntry, error) {
|
||||
_, directoryHeader, _, err := ReadDirectory(readerWriter, directory)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
//DumpDirectoryHeader(directoryHeader)
|
||||
blockNumber := directoryHeader.StartingBlock
|
||||
buffer, err := ReadBlock(reader, blockNumber)
|
||||
buffer, err := ReadBlock(readerWriter, blockNumber)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
|
@ -113,23 +224,31 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
|
|||
|
||||
for {
|
||||
if entryNumber > 13 {
|
||||
blockNumber = int(buffer[2]) + int(buffer[3])*256
|
||||
// if we ran out of blocks in the directory, return empty
|
||||
// TODO: expand the directory to add more entries
|
||||
if blockNumber == 0 {
|
||||
return FileEntry{}, errors.New("no free file entries found")
|
||||
nextBlockNumber := int(buffer[2]) + int(buffer[3])*256
|
||||
// if we ran out of blocks in the directory, expand directory or fail
|
||||
if nextBlockNumber == 0 {
|
||||
if !directoryHeader.IsSubDirectory {
|
||||
return FileEntry{}, errors.New("no free file entries found")
|
||||
}
|
||||
nextBlockNumber, err = expandDirectory(readerWriter, nextBlockNumber, buffer, blockNumber, directoryHeader)
|
||||
if err != nil {
|
||||
return FileEntry{}, err
|
||||
}
|
||||
}
|
||||
blockNumber = nextBlockNumber
|
||||
// else read the next block in the directory
|
||||
buffer, err = ReadBlock(reader, blockNumber)
|
||||
buffer, err = ReadBlock(readerWriter, blockNumber)
|
||||
if err != nil {
|
||||
return FileEntry{}, nil
|
||||
}
|
||||
|
||||
entryOffset = 4
|
||||
entryNumber = 1
|
||||
}
|
||||
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
|
||||
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+0x28], blockNumber, entryOffset)
|
||||
|
||||
if fileEntry.StorageType == StorageDeleted {
|
||||
fileEntry = FileEntry{}
|
||||
fileEntry.DirectoryBlock = blockNumber
|
||||
fileEntry.DirectoryOffset = entryOffset
|
||||
fileEntry.HeaderPointer = directoryHeader.StartingBlock
|
||||
|
@ -141,6 +260,51 @@ func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntr
|
|||
}
|
||||
}
|
||||
|
||||
func expandDirectory(readerWriter ReaderWriterAt, nextBlockNumber int, buffer []byte, blockNumber int, directoryHeader DirectoryHeader) (int, error) {
|
||||
volumeBitMap, err := ReadVolumeBitmap(readerWriter)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to get volume bitmap to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
blockList := findFreeBlocks(volumeBitMap, 1)
|
||||
if len(blockList) != 1 {
|
||||
return 0, errors.New("failed to get free block to expand directory")
|
||||
}
|
||||
|
||||
nextBlockNumber = blockList[0]
|
||||
buffer[0x02] = byte(nextBlockNumber & 0x00FF)
|
||||
buffer[0x03] = byte(nextBlockNumber >> 8)
|
||||
WriteBlock(readerWriter, blockNumber, buffer)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to write block to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
|
||||
buffer = make([]byte, 0x200)
|
||||
buffer[0x00] = byte(blockNumber & 0x00FF)
|
||||
buffer[0x01] = byte(blockNumber >> 8)
|
||||
err = WriteBlock(readerWriter, nextBlockNumber, buffer)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to write new block to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
|
||||
updateVolumeBitmap(readerWriter, blockList)
|
||||
|
||||
buffer, err = ReadBlock(readerWriter, directoryHeader.ParentBlock)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to read parent block to expand directory: %s", err)
|
||||
return 0, errors.New(errString)
|
||||
}
|
||||
directoryEntryOffset := directoryHeader.ParentEntry*directoryHeader.EntryLength + 0x04
|
||||
directoryFileEntry := parseFileEntry(buffer[directoryEntryOffset:directoryEntryOffset+0x28], directoryHeader.ParentBlock, directoryHeader.ParentEntry*directoryHeader.EntryLength+0x04)
|
||||
directoryFileEntry.BlocksUsed++
|
||||
directoryFileEntry.EndOfFile += 0x200
|
||||
writeFileEntry(readerWriter, directoryFileEntry)
|
||||
|
||||
return nextBlockNumber, nil
|
||||
}
|
||||
|
||||
func getFileEntriesInDirectory(reader io.ReaderAt, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry, error) {
|
||||
buffer, err := ReadBlock(reader, blockNumber)
|
||||
if err != nil {
|
||||
|
@ -255,13 +419,14 @@ func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) {
|
|||
buffer[0x1E] = byte(fileEntry.Access)
|
||||
buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF)
|
||||
buffer[0x20] = byte(fileEntry.AuxType >> 8)
|
||||
modifiedTime := DateTimeToProDOS(fileEntry.CreationTime)
|
||||
modifiedTime := DateTimeToProDOS(fileEntry.ModifiedTime)
|
||||
for i := 0; i < 4; i++ {
|
||||
buffer[0x21+i] = modifiedTime[i]
|
||||
}
|
||||
buffer[0x25] = byte(fileEntry.HeaderPointer & 0x00FF)
|
||||
buffer[0x26] = byte(fileEntry.HeaderPointer >> 8)
|
||||
|
||||
//fmt.Printf("Writing file entry at block: %04X offset: %04X\n", fileEntry.DirectoryBlock, fileEntry.DirectoryOffset)
|
||||
_, err := writer.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset))
|
||||
if err != nil {
|
||||
|
||||
|
@ -281,7 +446,7 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
|
|||
bitmapBlock := int(buffer[39]) + int(buffer[40])*256
|
||||
totalBlocks := int(buffer[41]) + int(buffer[42])*256
|
||||
|
||||
if version > 0 || minVersion > 0 {
|
||||
if minVersion > 0 {
|
||||
panic("Unsupported ProDOS version")
|
||||
}
|
||||
|
||||
|
@ -303,22 +468,43 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
|
|||
func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
|
||||
previousBlock := int(buffer[0x00]) + int(buffer[0x01])*256
|
||||
nextBlock := int(buffer[0x02]) + int(buffer[0x03])*256
|
||||
filenameLength := buffer[0x04] & 15
|
||||
isSubDirectory := (buffer[0x04] & 0xF0) == 0xE0
|
||||
filenameLength := buffer[0x04] & 0x0F
|
||||
name := string(buffer[0x05 : filenameLength+0x05])
|
||||
creationTime := DateTimeFromProDOS(buffer[0x1C:0x20])
|
||||
version := int(buffer[0x20])
|
||||
minVersion := int(buffer[0x21])
|
||||
access := int(buffer[0x22])
|
||||
entryLength := int(buffer[0x23])
|
||||
entriesPerBlock := int(buffer[0x24])
|
||||
fileCount := int(buffer[0x25]) + int(buffer[0x26])*256
|
||||
parentBlock := int(buffer[0x27]) + int(buffer[0x28])*256
|
||||
parentEntry := int(buffer[0x29])
|
||||
parentEntryLength := int(buffer[0x2A])
|
||||
|
||||
directoryEntry := DirectoryHeader{
|
||||
PreviousBlock: previousBlock,
|
||||
NextBlock: nextBlock,
|
||||
StartingBlock: blockNumber,
|
||||
Name: name,
|
||||
ActiveFileCount: fileCount,
|
||||
PreviousBlock: previousBlock,
|
||||
NextBlock: nextBlock,
|
||||
StartingBlock: blockNumber,
|
||||
IsSubDirectory: isSubDirectory,
|
||||
Name: name,
|
||||
CreationTime: creationTime,
|
||||
Version: version,
|
||||
MinVersion: minVersion,
|
||||
Access: access,
|
||||
EntryLength: entryLength,
|
||||
EntriesPerBlock: entriesPerBlock,
|
||||
ActiveFileCount: fileCount,
|
||||
ParentBlock: parentBlock,
|
||||
ParentEntry: parentEntry,
|
||||
ParentEntryLength: parentEntryLength,
|
||||
}
|
||||
|
||||
return directoryEntry
|
||||
}
|
||||
|
||||
func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader DirectoryHeader) error {
|
||||
// Reading back the block preserves values including reserved fields
|
||||
buffer, err := ReadBlock(readerWriter, directoryHeader.StartingBlock)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -327,12 +513,40 @@ func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader Directory
|
|||
buffer[0x01] = byte(directoryHeader.PreviousBlock >> 8)
|
||||
buffer[0x02] = byte(directoryHeader.NextBlock & 0x00FF)
|
||||
buffer[0x03] = byte(directoryHeader.NextBlock >> 8)
|
||||
if directoryHeader.IsSubDirectory {
|
||||
buffer[0x04] = 0xE0
|
||||
} else {
|
||||
buffer[0x04] = 0xF0
|
||||
}
|
||||
buffer[0x04] = buffer[0x04] | byte(len(directoryHeader.Name))
|
||||
for i := 0; i < len(directoryHeader.Name); i++ {
|
||||
buffer[0x05+i] = directoryHeader.Name[i]
|
||||
}
|
||||
creationTime := DateTimeToProDOS(directoryHeader.CreationTime)
|
||||
for i := 0; i < 4; i++ {
|
||||
buffer[0x1C+i] = creationTime[i]
|
||||
}
|
||||
// Without these reserved bytes, reading the directory causes I/O ERROR
|
||||
buffer[0x14] = 0x75
|
||||
buffer[0x15] = byte(directoryHeader.Version)
|
||||
buffer[0x16] = byte(directoryHeader.MinVersion)
|
||||
buffer[0x17] = 0xC3
|
||||
buffer[0x18] = 0x0D
|
||||
buffer[0x19] = 0x27
|
||||
buffer[0x1A] = 0x00
|
||||
buffer[0x1B] = 0x00
|
||||
|
||||
buffer[0x20] = byte(directoryHeader.Version)
|
||||
buffer[0x21] = byte(directoryHeader.MinVersion)
|
||||
buffer[0x22] = byte(directoryHeader.Access)
|
||||
buffer[0x23] = byte(directoryHeader.EntryLength)
|
||||
buffer[0x24] = byte(directoryHeader.EntriesPerBlock)
|
||||
buffer[0x25] = byte(directoryHeader.ActiveFileCount & 0x00FF)
|
||||
buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8)
|
||||
buffer[0x27] = byte(directoryHeader.ParentBlock & 0x00FF)
|
||||
buffer[0x28] = byte(directoryHeader.ParentBlock >> 8)
|
||||
buffer[0x29] = byte(directoryHeader.ParentEntry)
|
||||
buffer[0x2A] = byte(directoryHeader.ParentEntryLength)
|
||||
WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer)
|
||||
|
||||
return nil
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
|
260
prodos/file.go
260
prodos/file.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -8,12 +8,9 @@
|
|||
package prodos
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
@ -45,100 +42,17 @@ func LoadFile(reader io.ReaderAt, path string) ([]byte, error) {
|
|||
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
|
||||
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)
|
||||
|
||||
if len(fileName) > 15 {
|
||||
return errors.New("filename too long")
|
||||
}
|
||||
|
||||
existingFileEntry, _ := GetFileEntry(readerWriter, path)
|
||||
if existingFileEntry.StorageType != StorageDeleted {
|
||||
DeleteFile(readerWriter, path)
|
||||
return errors.New(("file already exists"))
|
||||
}
|
||||
|
||||
// get list of blocks to write file to
|
||||
|
@ -149,7 +63,7 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
|
||||
// seedling file
|
||||
if len(buffer) <= 0x200 {
|
||||
WriteBlock(readerWriter, blockList[0], buffer)
|
||||
writeSeedlingFile(readerWriter, buffer, blockList)
|
||||
}
|
||||
|
||||
// sapling file needs index block
|
||||
|
@ -175,12 +89,14 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
}
|
||||
fileEntry.FileName = fileName
|
||||
fileEntry.BlocksUsed = len(blockList)
|
||||
fileEntry.CreationTime = time.Now()
|
||||
fileEntry.ModifiedTime = time.Now()
|
||||
fileEntry.CreationTime = createdTime
|
||||
fileEntry.ModifiedTime = modifiedTime
|
||||
fileEntry.AuxType = auxType
|
||||
fileEntry.EndOfFile = len(buffer)
|
||||
fileEntry.FileType = fileType
|
||||
fileEntry.KeyPointer = blockList[0]
|
||||
fileEntry.Version = 0x24
|
||||
fileEntry.MinVersion = 0x00
|
||||
fileEntry.Access = 0b11100011
|
||||
if len(blockList) == 1 {
|
||||
fileEntry.StorageType = StorageSeedling
|
||||
|
@ -192,7 +108,10 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
|
||||
writeFileEntry(readerWriter, fileEntry)
|
||||
|
||||
// increment file count
|
||||
return incrementFileCount(readerWriter, fileEntry)
|
||||
}
|
||||
|
||||
func incrementFileCount(readerWriter ReaderWriterAt, fileEntry FileEntry) error {
|
||||
directoryHeaderBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -207,6 +126,10 @@ func WriteFile(readerWriter ReaderWriterAt, path string, fileType int, auxType i
|
|||
// DeleteFile deletes a file from a ProDOS volume
|
||||
func DeleteFile(readerWriter ReaderWriterAt, path string) error {
|
||||
fileEntry, err := GetFileEntry(readerWriter, path)
|
||||
// DumpFileEntry(fileEntry)
|
||||
// oldDirectoryBlock, _ := ReadBlock(readerWriter, fileEntry.DirectoryBlock)
|
||||
// DumpBlock(oldDirectoryBlock)
|
||||
|
||||
if err != nil {
|
||||
return errors.New("file not found")
|
||||
}
|
||||
|
@ -218,10 +141,11 @@ func DeleteFile(readerWriter ReaderWriterAt, path string) error {
|
|||
}
|
||||
|
||||
// free the blocks
|
||||
blocks, err := getBlocklist(readerWriter, fileEntry)
|
||||
blocks, err := getAllBlockList(readerWriter, fileEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumeBitmap, err := ReadVolumeBitmap(readerWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -249,6 +173,12 @@ func DeleteFile(readerWriter ReaderWriterAt, path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// FileExists return true if the file exists
|
||||
func FileExists(reader io.ReaderAt, path string) (bool, error) {
|
||||
fileEntry, _ := GetFileEntry(reader, path)
|
||||
return fileEntry.StorageType != StorageDeleted, nil
|
||||
}
|
||||
|
||||
// GetDirectoryAndFileNameFromPath gets the directory and filename from a path
|
||||
func GetDirectoryAndFileNameFromPath(path string) (string, string) {
|
||||
path = strings.ToUpper(path)
|
||||
|
@ -280,6 +210,10 @@ func updateVolumeBitmap(readerWriter ReaderWriterAt, blockList []int) error {
|
|||
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)
|
||||
|
@ -287,6 +221,9 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
|||
if i < len(blockList)-1 {
|
||||
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
|
||||
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
|
||||
} else {
|
||||
indexBuffer[i] = 0
|
||||
indexBuffer[i+256] = 0
|
||||
}
|
||||
}
|
||||
WriteBlock(writer, blockList[0], indexBuffer)
|
||||
|
@ -313,11 +250,69 @@ func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
|||
}
|
||||
|
||||
func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
||||
// write master index block with pointers to index blocks
|
||||
indexBuffer := make([]byte, 512)
|
||||
numberOfIndexBlocks := len(blockList)/256 + 1
|
||||
if len(blockList)%256 == 0 {
|
||||
numberOfIndexBlocks--
|
||||
}
|
||||
for i := 0; i < 256; i++ {
|
||||
if i < numberOfIndexBlocks {
|
||||
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
|
||||
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
|
||||
} else {
|
||||
indexBuffer[i] = 0
|
||||
indexBuffer[i+256] = 0
|
||||
}
|
||||
}
|
||||
WriteBlock(writer, blockList[0], indexBuffer)
|
||||
numberOfIndexBlocks++
|
||||
|
||||
// write index blocks
|
||||
for i := 0; i < len(blockList)/256+1; i++ {
|
||||
for j := 0; j < 256; j++ {
|
||||
if i*256+j < len(blockList)-numberOfIndexBlocks {
|
||||
indexBuffer[j] = byte(blockList[i*256+numberOfIndexBlocks+j] & 0x00FF)
|
||||
indexBuffer[j+256] = byte(blockList[i*256+j+2] >> 8)
|
||||
} else {
|
||||
indexBuffer[j] = 0
|
||||
indexBuffer[j+256] = 0
|
||||
}
|
||||
}
|
||||
WriteBlock(writer, blockList[i+1], indexBuffer)
|
||||
}
|
||||
|
||||
// write all data blocks
|
||||
blockBuffer := make([]byte, 512)
|
||||
blockPointer := 0
|
||||
blockIndexNumber := numberOfIndexBlocks
|
||||
for i := 0; i < len(buffer); i++ {
|
||||
blockBuffer[blockPointer] = buffer[i]
|
||||
if blockPointer == 511 {
|
||||
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
|
||||
blockPointer = 0
|
||||
blockIndexNumber++
|
||||
} else if i == len(buffer)-1 {
|
||||
for j := blockPointer; j < 512; j++ {
|
||||
blockBuffer[j] = 0
|
||||
}
|
||||
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
|
||||
} else {
|
||||
blockPointer++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
return getBlocklist(reader, fileEntry, true)
|
||||
}
|
||||
|
||||
func getAllBlockList(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
return getBlocklist(reader, fileEntry, false)
|
||||
}
|
||||
|
||||
// Returns all blocks, including index blocks
|
||||
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry, dataOnly bool) ([]int, error) {
|
||||
blocks := make([]int, fileEntry.BlocksUsed)
|
||||
|
||||
switch fileEntry.StorageType {
|
||||
|
@ -329,58 +324,56 @@ func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockOffset := 1
|
||||
blocks[0] = fileEntry.KeyPointer
|
||||
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
|
||||
blocks[i+1] = int(index[i]) + int(index[i+256])*256
|
||||
blocks[i+blockOffset] = int(index[i]) + int(index[i+256])*256
|
||||
}
|
||||
if dataOnly {
|
||||
return blocks[1:], nil
|
||||
}
|
||||
return blocks, nil
|
||||
case StorageTree:
|
||||
// this is actually too large
|
||||
dataBlocks := make([]int, fileEntry.BlocksUsed)
|
||||
// this is also actually too large
|
||||
numberOfIndexBlocks := fileEntry.BlocksUsed/256 + 2
|
||||
indexBlocks := make([]int, numberOfIndexBlocks)
|
||||
masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks[0] = fileEntry.KeyPointer
|
||||
numberOfDataBlocks := 0
|
||||
|
||||
indexBlocks[0] = fileEntry.KeyPointer
|
||||
indexBlockCount := 1
|
||||
|
||||
for i := 0; i < 128; i++ {
|
||||
index, err := ReadBlock(reader, int(masterIndex[i])+int(masterIndex[i+256])*256)
|
||||
indexBlock := int(masterIndex[i]) + int(masterIndex[i+256])*256
|
||||
if indexBlock == 0 {
|
||||
break
|
||||
}
|
||||
indexBlocks[indexBlockCount] = indexBlock
|
||||
indexBlockCount++
|
||||
index, err := ReadBlock(reader, indexBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for j := 0; j < 256 && i*256+j < fileEntry.BlocksUsed; j++ {
|
||||
if (int(index[j]) + int(index[j+256])*256) == 0 {
|
||||
return blocks, nil
|
||||
break
|
||||
}
|
||||
blocks[i*256+j] = int(index[j]) + int(index[j+256])*256
|
||||
numberOfDataBlocks++
|
||||
dataBlocks[i*256+j] = int(index[j]) + int(index[j+256])*256
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported file storage type")
|
||||
}
|
||||
|
||||
func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
||||
switch fileEntry.StorageType {
|
||||
case StorageSeedling:
|
||||
blocks := make([]int, 1)
|
||||
blocks[0] = fileEntry.KeyPointer
|
||||
return blocks, nil
|
||||
case StorageSapling:
|
||||
blocks := make([]int, fileEntry.BlocksUsed-1)
|
||||
index, err := ReadBlock(reader, fileEntry.KeyPointer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if dataOnly {
|
||||
return dataBlocks, nil
|
||||
}
|
||||
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")
|
||||
|
@ -388,20 +381,16 @@ func getDataBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
|||
|
||||
func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
|
||||
numberOfBlocks := fileSize / 512
|
||||
//fmt.Printf("Number of blocks %d\n", numberOfBlocks)
|
||||
|
||||
if fileSize%512 > 0 {
|
||||
//fmt.Printf("Adding block for partial usage\n")
|
||||
numberOfBlocks++
|
||||
}
|
||||
|
||||
if fileSize > 0x200 && fileSize <= 0x20000 {
|
||||
//fmt.Printf("Adding index block for sapling file\n")
|
||||
numberOfBlocks++ // add index block
|
||||
}
|
||||
|
||||
if fileSize > 0x20000 && fileSize <= 0x1000000 {
|
||||
//fmt.Printf("Tree file\n")
|
||||
// add index blocks for each 256 blocks
|
||||
numberOfBlocks += numberOfBlocks / 256
|
||||
// add index block for any remaining blocks
|
||||
|
@ -420,10 +409,9 @@ func createBlockList(reader io.ReaderAt, fileSize int) ([]int, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
//fmt.Printf("findFreeBlocks %d\n", numberOfBlocks)
|
||||
blockList := findFreeBlocks(volumeBitmap, numberOfBlocks)
|
||||
|
||||
return blockList, nil
|
||||
return blockList[0:numberOfBlocks], nil
|
||||
}
|
||||
|
||||
// GetFileEntry returns a file entry for the given path
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -9,7 +9,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestCreatBlocklist(t *testing.T) {
|
||||
func TestCreateBlocklist(t *testing.T) {
|
||||
var tests = []struct {
|
||||
fileSize int
|
||||
wantBlocks int
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides tests for access to format a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
|
|
171
prodos/host.go
171
prodos/host.go
|
@ -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
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -7,15 +7,29 @@
|
|||
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) error {
|
||||
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 {
|
||||
|
@ -28,8 +42,26 @@ func AddFilesFromHostDirectory(
|
|||
return err
|
||||
}
|
||||
|
||||
if !file.IsDir() && info.Size() > 0 && info.Size() <= 0x20000 {
|
||||
err = WriteFileFromFile(readerWriter, "", 0, 0, filepath.Join(directory, file.Name()))
|
||||
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
|
||||
}
|
||||
|
@ -38,3 +70,134 @@ func AddFilesFromHostDirectory(
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFileFromFile writes a file to a ProDOS volume from a host file
|
||||
func WriteFileFromFile(
|
||||
readerWriter ReaderWriterAt,
|
||||
pathName string,
|
||||
fileType int,
|
||||
auxType int,
|
||||
modifiedTime time.Time,
|
||||
inFileName string,
|
||||
ignoreDuplicates bool,
|
||||
) error {
|
||||
|
||||
inFile, err := os.ReadFile(inFileName)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("write from file failed: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
if auxType == 0 && fileType == 0 {
|
||||
auxType, fileType, inFile, err = convertFileByType(inFileName, inFile)
|
||||
if err != nil {
|
||||
errString := fmt.Sprintf("failed to convert file: %s", err)
|
||||
return errors.New(errString)
|
||||
}
|
||||
}
|
||||
|
||||
trimExtensions := false
|
||||
if len(pathName) == 0 {
|
||||
_, pathName = filepath.Split(inFileName)
|
||||
pathName = strings.ToUpper(pathName)
|
||||
trimExtensions = true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(pathName, "/") {
|
||||
trimExtensions = true
|
||||
_, fileName := filepath.Split(inFileName)
|
||||
pathName = strings.ToUpper(pathName + fileName)
|
||||
}
|
||||
|
||||
if trimExtensions {
|
||||
ext := filepath.Ext(pathName)
|
||||
|
||||
if len(ext) > 0 {
|
||||
switch ext {
|
||||
case ".SYS", ".TXT", ".BAS", ".BIN":
|
||||
pathName = strings.TrimSuffix(pathName, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths := strings.SplitAfter(pathName, "/")
|
||||
if len(paths[len(paths)-1]) > 15 {
|
||||
paths[len(paths)-1] = paths[len(paths)-1][0:15]
|
||||
pathName = strings.Join(paths, "")
|
||||
}
|
||||
|
||||
// skip if file already exists and ignoring duplicates
|
||||
if ignoreDuplicates {
|
||||
exists, err := FileExists(readerWriter, pathName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return WriteFile(readerWriter, pathName, fileType, auxType, time.Now(), modifiedTime, inFile)
|
||||
}
|
||||
|
||||
func convertFileByType(inFileName string, inFile []byte) (int, int, []byte, error) {
|
||||
fileType := 0x06 // default to BIN
|
||||
auxType := 0x2000 // default to $2000
|
||||
|
||||
var err error
|
||||
|
||||
// Check for an AppleSingle file as produced by cc65
|
||||
if // Magic number
|
||||
binary.BigEndian.Uint32(inFile[0x00:]) == 0x00051600 &&
|
||||
// Version number
|
||||
binary.BigEndian.Uint32(inFile[0x04:]) == 0x00020000 &&
|
||||
// Number of entries
|
||||
binary.BigEndian.Uint16(inFile[0x18:]) == 0x0002 &&
|
||||
// Data Fork ID
|
||||
binary.BigEndian.Uint32(inFile[0x1A:]) == 0x00000001 &&
|
||||
// Offset
|
||||
binary.BigEndian.Uint32(inFile[0x1E:]) == 0x0000003A &&
|
||||
// Length
|
||||
binary.BigEndian.Uint32(inFile[0x22:]) == uint32(len(inFile))-0x3A &&
|
||||
// ProDOS File Info ID
|
||||
binary.BigEndian.Uint32(inFile[0x26:]) == 0x0000000B &&
|
||||
// Offset
|
||||
binary.BigEndian.Uint32(inFile[0x2A:]) == 0x00000032 &&
|
||||
// Length
|
||||
binary.BigEndian.Uint32(inFile[0x2E:]) == 0x00000008 {
|
||||
|
||||
fileType = int(binary.BigEndian.Uint16(inFile[0x34:]))
|
||||
auxType = int(binary.BigEndian.Uint32(inFile[0x36:]))
|
||||
inFile = inFile[0x3A:]
|
||||
} else {
|
||||
// use extension to determine file type
|
||||
ext := strings.ToUpper(filepath.Ext(inFileName))
|
||||
|
||||
switch ext {
|
||||
case ".BAS":
|
||||
inFile, err = ConvertTextToBasic(string(inFile))
|
||||
fileType = 0xFC
|
||||
auxType = 0x0801
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
case ".SYS":
|
||||
fileType = 0xFF
|
||||
auxType = 0x2000
|
||||
case ".BIN":
|
||||
fileType = 0x06
|
||||
auxType = 0x2000
|
||||
case ".TXT":
|
||||
inFile = []byte(strings.ReplaceAll(strings.ReplaceAll(string(inFile), "\r\n", "r"), "\n", "\r"))
|
||||
fileType = 0x04
|
||||
auxType = 0x0000
|
||||
case ".JPG", ".PNG":
|
||||
inFile = ConvertImageToHiResMonochrome(inFile)
|
||||
fileType = 0x06
|
||||
auxType = 0x2000
|
||||
}
|
||||
}
|
||||
|
||||
return auxType, fileType, inFile, err
|
||||
}
|
||||
|
|
224
prodos/image.go
Normal file
224
prodos/image.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides access to read, write and delete
|
||||
// files on a ProDOS drive image
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
// force import jpeg support by init only
|
||||
_ "image/jpeg"
|
||||
// force import png support by init only
|
||||
_ "image/png"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// 10 PRINT CHR$ (4)"open offsets"
|
||||
// 20 PRINT CHR$ (4)"write offsets"
|
||||
// 30 PRINT "offsets := [192]int{";
|
||||
// 40 FOR Y = 0 TO 191
|
||||
// 50 HPLOT 0,Y
|
||||
// 60 PRINT ( PEEK (39) * 256 + PEEK (38)) - 8192;
|
||||
// 70 IF Y < 191 THEN PRINT ", ";
|
||||
// 80 NEXT
|
||||
// 90 PRINT "}"
|
||||
// 100 PRINT CHR$ (4)"close offsets"
|
||||
var offsets = []int{0, 1024, 2048, 3072, 4096, 5120, 6144, 7168, 128, 1152, 2176, 3200, 4224, 5248, 6272, 7296, 256, 1280, 2304, 3328, 4352, 5376, 6400, 7424, 384, 1408, 2432, 3456, 4480, 5504, 6528, 7552, 512, 1536, 2560, 3584, 4608, 5632, 6656, 7680, 640, 1664, 2688, 3712, 4736, 5760, 6784, 7808, 768, 1792, 2816, 3840, 4864, 5888, 6912, 7936, 896, 1920, 2944, 3968, 4992, 6016, 7040, 8064, 40, 1064, 2088, 3112, 4136, 5160, 6184, 7208, 168, 1192, 2216, 3240, 4264, 5288, 6312, 7336, 296, 1320, 2344, 3368, 4392, 5416, 6440, 7464, 424, 1448, 2472, 3496, 4520, 5544, 6568, 7592, 552, 1576, 2600, 3624, 4648, 5672, 6696, 7720, 680, 1704, 2728, 3752, 4776, 5800, 6824, 7848, 808, 1832, 2856, 3880, 4904, 5928, 6952, 7976, 936, 1960, 2984, 4008, 5032, 6056, 7080, 8104, 80, 1104, 2128, 3152, 4176, 5200, 6224, 7248, 208, 1232, 2256, 3280, 4304, 5328, 6352, 7376, 336, 1360, 2384, 3408, 4432, 5456, 6480, 7504, 464, 1488, 2512, 3536, 4560, 5584, 6608, 7632, 592, 1616, 2640, 3664, 4688, 5712, 6736, 7760, 720, 1744, 2768, 3792, 4816, 5840, 6864, 7888, 848, 1872, 2896, 3920, 4944, 5968, 6992, 8016, 976, 2000, 3024, 4048, 5072, 6096, 7120, 8144}
|
||||
var pixel = []byte{1, 2, 4, 8, 16, 32, 64}
|
||||
|
||||
// ConvertImageToHiResMonochrome converts jpeg and png images to Apple II hi-res monochrome
|
||||
func ConvertImageToHiResMonochrome(imageBytes []byte) []byte {
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
a2imgSize := image.Rect(0, 0, 280, 192)
|
||||
|
||||
a2img := image.NewPaletted(a2imgSize, []color.Color{
|
||||
color.Black,
|
||||
color.White,
|
||||
})
|
||||
|
||||
a2monoImgSize := image.Rect(0, 0, 280, 192)
|
||||
|
||||
scaledImg := image.NewRGBA(a2monoImgSize)
|
||||
draw.BiLinear.Scale(scaledImg, a2monoImgSize, img, img.Bounds(), draw.Over, nil)
|
||||
draw.FloydSteinberg.Draw(a2img, a2monoImgSize, scaledImg, image.Point{})
|
||||
|
||||
hires := make([]byte, 8192)
|
||||
|
||||
for y := a2img.Bounds().Min.Y; y < a2img.Bounds().Max.Y; y++ {
|
||||
for x := a2img.Bounds().Min.X; x < a2img.Bounds().Max.X; x++ {
|
||||
if a2img.At(x, y) == color.White {
|
||||
hires[offsets[y]+x/7] |= pixel[x%7] | 128
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hires
|
||||
}
|
||||
|
||||
// ConvertImageToHiResColour converts jpeg and png images to Apple II hi-res colour
|
||||
func ConvertImageToHiResColour(imageBytes []byte) []byte {
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
a2colourImgSize := image.Rect(0, 0, 140, 192)
|
||||
|
||||
black := color.NRGBA{0, 0, 0, 255}
|
||||
green := color.NRGBA{20, 245, 60, 255}
|
||||
purple := color.NRGBA{255, 68, 253, 255}
|
||||
white := color.NRGBA{255, 255, 255, 255}
|
||||
orange := color.NRGBA{255, 106, 60, 255}
|
||||
blue := color.NRGBA{20, 207, 253, 255}
|
||||
|
||||
a2img := image.NewPaletted(a2colourImgSize, []color.Color{
|
||||
black,
|
||||
green,
|
||||
purple,
|
||||
white,
|
||||
orange,
|
||||
blue,
|
||||
})
|
||||
|
||||
scaledImg := image.NewRGBA(a2colourImgSize)
|
||||
draw.BiLinear.Scale(scaledImg, a2colourImgSize, img, img.Bounds(), draw.Over, nil)
|
||||
draw.FloydSteinberg.Draw(a2img, a2colourImgSize, scaledImg, image.Point{})
|
||||
|
||||
hires := make([]byte, 8192)
|
||||
|
||||
for y := a2img.Bounds().Min.Y; y < a2img.Bounds().Max.Y; y++ {
|
||||
for x7 := a2img.Bounds().Min.X; x7 < a2img.Bounds().Max.X; x7 += 7 {
|
||||
switch a2img.At(x7, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7] = 2
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] = 1
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7] = 2
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] = 1
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] = 3
|
||||
}
|
||||
switch a2img.At(x7+1, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7] |= 8
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] |= 4
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7] |= 8
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] |= 4
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] |= 12
|
||||
}
|
||||
switch a2img.At(x7+2, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7] |= 32
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] |= 16
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7] |= 32
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] |= 16
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] |= 48
|
||||
}
|
||||
switch a2img.At(x7+3, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 1
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7] |= 64
|
||||
hires[offsets[y]+x7*2/7] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 1
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7] |= 64
|
||||
hires[offsets[y]+x7*2/7] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7] |= 64
|
||||
hires[offsets[y]+x7*2/7+1] |= 1
|
||||
}
|
||||
switch a2img.At(x7+4, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 4
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7+1] |= 2
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 4
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7+1] |= 2
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7+1] |= 6
|
||||
}
|
||||
switch a2img.At(x7+5, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 16
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7+1] |= 8
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 16
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7+1] |= 8
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7+1] |= 24
|
||||
}
|
||||
switch a2img.At(x7+6, y) {
|
||||
case green:
|
||||
hires[offsets[y]+x7*2/7+1] |= 64
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case purple:
|
||||
hires[offsets[y]+x7*2/7+1] |= 32
|
||||
hires[offsets[y]+x7*2/7+1] &= 0x7F
|
||||
case orange:
|
||||
hires[offsets[y]+x7*2/7+1] |= 64
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case blue:
|
||||
hires[offsets[y]+x7*2/7+1] |= 32
|
||||
hires[offsets[y]+x7*2/7+1] |= 0x80
|
||||
case white:
|
||||
hires[offsets[y]+x7*2/7+1] |= 96
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hires
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -99,6 +99,9 @@ func DumpFileEntry(fileEntry FileEntry) {
|
|||
fmt.Printf("File type: %02X\n", fileEntry.FileType)
|
||||
fmt.Printf("Storage type: %02X\n", fileEntry.StorageType)
|
||||
fmt.Printf("Header pointer: %04X\n", fileEntry.HeaderPointer)
|
||||
fmt.Printf("Access: %04X\n", fileEntry.Access)
|
||||
fmt.Printf("Directory block: %04X\n", fileEntry.DirectoryBlock)
|
||||
fmt.Printf("Directory offset: %04X\n", fileEntry.DirectoryOffset)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
|
@ -118,11 +121,22 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
|
|||
|
||||
// DumpDirectoryHeader dumps the directory header as text
|
||||
func DumpDirectoryHeader(directoryHeader DirectoryHeader) {
|
||||
fmt.Printf("Name: %s\n", directoryHeader.Name)
|
||||
fmt.Printf("File count: %d\n", directoryHeader.ActiveFileCount)
|
||||
fmt.Printf("Starting block: %04X\n", directoryHeader.StartingBlock)
|
||||
fmt.Printf("Previous block: %04X\n", directoryHeader.PreviousBlock)
|
||||
fmt.Printf("Next block: %04X\n", directoryHeader.NextBlock)
|
||||
fmt.Printf("Is subdirectory: %t\n", directoryHeader.IsSubDirectory)
|
||||
fmt.Printf("Name: %s\n", directoryHeader.Name)
|
||||
fmt.Printf("Creation time: %s\n", TimeToString(directoryHeader.CreationTime))
|
||||
fmt.Printf("Version: %02X\n", directoryHeader.Version)
|
||||
fmt.Printf("MinVersion: %02X\n", directoryHeader.MinVersion)
|
||||
fmt.Printf("Access: %02X\n", directoryHeader.Access)
|
||||
fmt.Printf("Entry length: %02X\n", directoryHeader.EntryLength)
|
||||
fmt.Printf("Entries per block: %02X\n", directoryHeader.EntriesPerBlock)
|
||||
fmt.Printf("File count: %d\n", directoryHeader.ActiveFileCount)
|
||||
fmt.Printf("Active file count: %04X\n", directoryHeader.ActiveFileCount)
|
||||
fmt.Printf("Parent block: %04X\n", directoryHeader.ParentBlock)
|
||||
fmt.Printf("Parent entry: %02X\n", directoryHeader.ParentEntry)
|
||||
fmt.Printf("Parent entry length: %02X\n", directoryHeader.ParentEntryLength)
|
||||
}
|
||||
|
||||
// DumpBlock dumps the block as hexadecimal and text
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -11,19 +11,20 @@ import (
|
|||
)
|
||||
|
||||
// DateTimeToProDOS converts Time to ProDOS date time
|
||||
// 49041 ($BF91) 49040 ($BF90)
|
||||
//
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// DATE: | year | month | day |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// 49041 ($BF91) 49040 ($BF90)
|
||||
//
|
||||
// 49043 ($BF93) 49042 ($BF92)
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// DATE: | year | month | day |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
//
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// TIME: | hour | | minute |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// 49043 ($BF93) 49042 ($BF92)
|
||||
//
|
||||
// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
// TIME: | hour | | minute |
|
||||
// +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
func DateTimeToProDOS(dateTime time.Time) []byte {
|
||||
year := dateTime.Year() % 100
|
||||
month := dateTime.Month()
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file provides tests for conversion to and from ProDOS time format
|
||||
|
||||
package prodos
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Terence J. Boldt (c)2021-2022
|
||||
// Copyright Terence J. Boldt (c)2021-2023
|
||||
// Use of this source code is governed by an MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
@ -14,7 +14,7 @@ func printReadme() {
|
|||
fmt.Println(`
|
||||
MIT License
|
||||
|
||||
Copyright (c)2021-2022 Terence Boldt
|
||||
Copyright (c)2021-2023 Terence Boldt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
Loading…
Reference in New Issue
Block a user