mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-11-25 07:32:19 +00:00
455 lines
12 KiB
Go
455 lines
12 KiB
Go
// Copyright Terence J. Boldt (c)2021-2022
|
|
// 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 (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LoadFile loads in a file from a ProDOS volume into a byte array
|
|
func LoadFile(reader io.ReaderAt, path string) ([]byte, error) {
|
|
fileEntry, err := GetFileEntry(reader, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blockList, err := getDataBlocklist(reader, fileEntry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buffer := make([]byte, fileEntry.EndOfFile)
|
|
|
|
for i := 0; i < len(blockList); i++ {
|
|
block, err := ReadBlock(reader, blockList[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for j := 0; j < 512 && i*512+j < fileEntry.EndOfFile; j++ {
|
|
buffer[i*512+j] = block[j]
|
|
}
|
|
}
|
|
|
|
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 {
|
|
directory, fileName := GetDirectoryAndFileNameFromPath(path)
|
|
|
|
existingFileEntry, _ := GetFileEntry(readerWriter, path)
|
|
if existingFileEntry.StorageType != StorageDeleted {
|
|
DeleteFile(readerWriter, path)
|
|
}
|
|
|
|
// get list of blocks to write file to
|
|
blockList, err := createBlockList(readerWriter, len(buffer))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// seedling file
|
|
if len(buffer) <= 0x200 {
|
|
WriteBlock(readerWriter, blockList[0], buffer)
|
|
}
|
|
|
|
// sapling file needs index block
|
|
if len(buffer) > 0x200 && len(buffer) <= 0x20000 {
|
|
writeSaplingFile(readerWriter, buffer, blockList)
|
|
}
|
|
|
|
// tree file needs master index and index blocks
|
|
if len(buffer) > 0x20000 && len(buffer) <= 0x1000000 {
|
|
writeTreeFile(readerWriter, buffer, blockList)
|
|
}
|
|
|
|
if len(buffer) > 0x1000000 {
|
|
return errors.New("files > 16MB not supported by ProDOS")
|
|
}
|
|
|
|
updateVolumeBitmap(readerWriter, blockList)
|
|
|
|
// add file entry to directory
|
|
fileEntry, err := getFreeFileEntryInDirectory(readerWriter, directory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileEntry.FileName = fileName
|
|
fileEntry.BlocksUsed = len(blockList)
|
|
fileEntry.CreationTime = time.Now()
|
|
fileEntry.ModifiedTime = time.Now()
|
|
fileEntry.AuxType = auxType
|
|
fileEntry.EndOfFile = len(buffer)
|
|
fileEntry.FileType = fileType
|
|
fileEntry.KeyPointer = blockList[0]
|
|
fileEntry.Access = 0b11100011
|
|
if len(blockList) == 1 {
|
|
fileEntry.StorageType = StorageSeedling
|
|
} else if len(blockList) <= 257 {
|
|
fileEntry.StorageType = StorageSapling
|
|
} else {
|
|
fileEntry.StorageType = StorageTree
|
|
}
|
|
|
|
writeFileEntry(readerWriter, fileEntry)
|
|
|
|
// increment file count
|
|
directoryHeaderBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
directoryHeader := parseDirectoryHeader(directoryHeaderBlock, fileEntry.HeaderPointer)
|
|
directoryHeader.ActiveFileCount++
|
|
writeDirectoryHeader(readerWriter, directoryHeader)
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteFile deletes a file from a ProDOS volume
|
|
func DeleteFile(readerWriter ReaderWriterAt, path string) error {
|
|
fileEntry, err := GetFileEntry(readerWriter, path)
|
|
if err != nil {
|
|
return errors.New("file not found")
|
|
}
|
|
if fileEntry.StorageType == StorageDeleted {
|
|
return errors.New("file already deleted")
|
|
}
|
|
if fileEntry.StorageType == StorageDirectory {
|
|
return errors.New("directory deletion not supported")
|
|
}
|
|
|
|
// free the blocks
|
|
blocks, err := getBlocklist(readerWriter, fileEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
volumeBitmap, err := ReadVolumeBitmap(readerWriter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := 0; i < len(blocks); i++ {
|
|
freeBlockInVolumeBitmap(volumeBitmap, blocks[i])
|
|
}
|
|
writeVolumeBitmap(readerWriter, volumeBitmap)
|
|
|
|
// decrement the directory entry count
|
|
directoryBlock, err := ReadBlock(readerWriter, fileEntry.HeaderPointer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
directoryHeader := parseDirectoryHeader(directoryBlock, fileEntry.HeaderPointer)
|
|
|
|
directoryHeader.ActiveFileCount--
|
|
writeDirectoryHeader(readerWriter, directoryHeader)
|
|
|
|
// zero out directory entry
|
|
fileEntry.StorageType = 0
|
|
fileEntry.FileName = ""
|
|
writeFileEntry(readerWriter, fileEntry)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetDirectoryAndFileNameFromPath gets the directory and filename from a path
|
|
func GetDirectoryAndFileNameFromPath(path string) (string, string) {
|
|
path = strings.ToUpper(path)
|
|
paths := strings.Split(path, "/")
|
|
|
|
var directoryBuilder strings.Builder
|
|
|
|
for i := 1; i < len(paths)-1; i++ {
|
|
directoryBuilder.WriteString("/")
|
|
directoryBuilder.WriteString(paths[i])
|
|
}
|
|
|
|
directory := directoryBuilder.String()
|
|
fileName := paths[len(paths)-1]
|
|
|
|
return directory, fileName
|
|
}
|
|
|
|
func updateVolumeBitmap(readerWriter ReaderWriterAt, blockList []int) error {
|
|
|
|
volumeBitmap, err := ReadVolumeBitmap(readerWriter)
|
|
if err != nil {
|
|
fmt.Printf("%s", err)
|
|
return err
|
|
}
|
|
for i := 0; i < len(blockList); i++ {
|
|
markBlockInVolumeBitmap(volumeBitmap, blockList[i])
|
|
}
|
|
return writeVolumeBitmap(readerWriter, volumeBitmap)
|
|
}
|
|
|
|
func writeSaplingFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
|
// write index block with pointers to data blocks
|
|
indexBuffer := make([]byte, 512)
|
|
for i := 0; i < 256; i++ {
|
|
if i < len(blockList)-1 {
|
|
indexBuffer[i] = byte(blockList[i+1] & 0x00FF)
|
|
indexBuffer[i+256] = byte(blockList[i+1] >> 8)
|
|
}
|
|
}
|
|
WriteBlock(writer, blockList[0], indexBuffer)
|
|
|
|
// write all data blocks
|
|
blockBuffer := make([]byte, 512)
|
|
blockPointer := 0
|
|
blockIndexNumber := 1
|
|
for i := 0; i < len(buffer); i++ {
|
|
blockBuffer[blockPointer] = buffer[i]
|
|
if blockPointer == 511 {
|
|
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
|
|
blockPointer = 0
|
|
blockIndexNumber++
|
|
} else if i == len(buffer)-1 {
|
|
for j := blockPointer; j < 512; j++ {
|
|
blockBuffer[j] = 0
|
|
}
|
|
WriteBlock(writer, blockList[blockIndexNumber], blockBuffer)
|
|
} else {
|
|
blockPointer++
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeTreeFile(writer io.WriterAt, buffer []byte, blockList []int) {
|
|
|
|
}
|
|
|
|
// Returns all blocks, including index blocks
|
|
func getBlocklist(reader io.ReaderAt, fileEntry FileEntry) ([]int, error) {
|
|
blocks := make([]int, fileEntry.BlocksUsed)
|
|
|
|
switch fileEntry.StorageType {
|
|
case StorageSeedling:
|
|
blocks[0] = fileEntry.KeyPointer
|
|
return blocks, nil
|
|
case StorageSapling:
|
|
index, err := ReadBlock(reader, fileEntry.KeyPointer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
blocks[0] = fileEntry.KeyPointer
|
|
for i := 0; i < fileEntry.BlocksUsed-1; i++ {
|
|
blocks[i+1] = int(index[i]) + int(index[i+256])*256
|
|
}
|
|
return blocks, nil
|
|
case StorageTree:
|
|
masterIndex, err := ReadBlock(reader, fileEntry.KeyPointer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
blocks[0] = fileEntry.KeyPointer
|
|
for i := 0; i < 128; i++ {
|
|
index, err := ReadBlock(reader, int(masterIndex[i])+int(masterIndex[i+256])*256)
|
|
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
|
|
}
|
|
blocks[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
|
|
}
|
|
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
|
|
|
|
// }
|
|
}
|
|
|
|
return nil, errors.New("unsupported file storage type")
|
|
}
|
|
|
|
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
|
|
if numberOfBlocks%256 > 0 {
|
|
numberOfBlocks++
|
|
}
|
|
// add master index block
|
|
numberOfBlocks++
|
|
}
|
|
if fileSize > 0x1000000 {
|
|
return nil, errors.New("file size too large")
|
|
}
|
|
|
|
volumeBitmap, err := ReadVolumeBitmap(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//fmt.Printf("findFreeBlocks %d\n", numberOfBlocks)
|
|
blockList := findFreeBlocks(volumeBitmap, numberOfBlocks)
|
|
|
|
return blockList, nil
|
|
}
|
|
|
|
// GetFileEntry returns a file entry for the given path
|
|
func GetFileEntry(reader io.ReaderAt, path string) (FileEntry, error) {
|
|
directory, fileName := GetDirectoryAndFileNameFromPath(path)
|
|
_, _, fileEntries, err := ReadDirectory(reader, directory)
|
|
if err != nil {
|
|
return FileEntry{}, err
|
|
}
|
|
|
|
if fileEntries == nil || len(fileEntries) == 0 {
|
|
return FileEntry{}, errors.New("file entry not found")
|
|
}
|
|
|
|
var fileEntry FileEntry
|
|
|
|
for i := 0; i < len(fileEntries); i++ {
|
|
if fileEntries[i].FileName == fileName {
|
|
fileEntry = fileEntries[i]
|
|
}
|
|
}
|
|
|
|
if fileEntry.StorageType == StorageDeleted {
|
|
return FileEntry{}, errors.New("file not found")
|
|
}
|
|
|
|
return fileEntry, nil
|
|
}
|