mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-12-28 11:32:05 +00:00
Added file write support
This commit is contained in:
parent
a04603f1e2
commit
aa6fb993ee
22
README.md
22
README.md
@ -3,18 +3,20 @@ This project is just starting but is intended to be both a command line tool and
|
||||
|
||||
## Current command line functionality
|
||||
1. Export files
|
||||
2. List any directory
|
||||
3. Display volume bitmap
|
||||
4. Create new volume
|
||||
5. Delete file
|
||||
2. Write files (currently only works with < 128K files)
|
||||
3. List any directory
|
||||
4. Display volume bitmap
|
||||
5. Create new volume
|
||||
6. Delete file
|
||||
|
||||
## Current library functionality
|
||||
1. Read block
|
||||
2. Write block
|
||||
3. Read file
|
||||
4. Delete file
|
||||
5. Create new volume
|
||||
6. Read volume bitmap
|
||||
7. Write volume bitmap
|
||||
8. Get list of file entries from any path
|
||||
9. Get volume header
|
||||
4. Write file
|
||||
5. Delete file
|
||||
6. Create new volume
|
||||
7. Read volume bitmap
|
||||
8. Write volume bitmap
|
||||
9. Get list of file entries from any path
|
||||
10. Get volume header
|
||||
|
23
main.go
23
main.go
@ -13,13 +13,15 @@ func main() {
|
||||
var pathName string
|
||||
var command string
|
||||
var outFileName string
|
||||
var inFileName string
|
||||
var blockNumber int
|
||||
var volumeSize int
|
||||
var volumeName string
|
||||
flag.StringVar(&fileName, "driveimage", "", "A ProDOS format drive image")
|
||||
flag.StringVar(&pathName, "path", "", "Path name in ProDOS drive image")
|
||||
flag.StringVar(&command, "command", "ls", "Command to execute: ls, get, put, volumebitmap, readblock, writeblock, createvolume, delete")
|
||||
flag.StringVar(&outFileName, "outfile", "export.bin", "Name of file to write")
|
||||
flag.StringVar(&outFileName, "outfile", "", "Name of file to write")
|
||||
flag.StringVar(&inFileName, "infile", "", "Name of file to read")
|
||||
flag.IntVar(&volumeSize, "volumesize", 65535, "Number of blocks to create the volume with")
|
||||
flag.StringVar(&volumeName, "volumename", "NO.NAME", "Specifiy a name for the volume from 1 to 15 characters")
|
||||
flag.IntVar(&blockNumber, "block", 0, "A block number to read/write from 0 to 65535")
|
||||
@ -36,7 +38,7 @@ func main() {
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
volumeHeader, fileEntries := prodos.ReadDirectory(file, pathName)
|
||||
volumeHeader, _, fileEntries := prodos.ReadDirectory(file, pathName)
|
||||
prodos.DumpDirectory(volumeHeader, fileEntries)
|
||||
case "volumebitmap":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
@ -55,11 +57,28 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
getFile := prodos.LoadFile(file, pathName)
|
||||
if len(outFileName) == 0 {
|
||||
_, outFileName = prodos.GetDirectoryAndFileNameFromPath(pathName)
|
||||
}
|
||||
outFile, err := os.Create(outFileName)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
outFile.Write(getFile)
|
||||
case "put":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(pathName) == 0 {
|
||||
fmt.Println("Missing pathname")
|
||||
os.Exit(1)
|
||||
}
|
||||
inFile, err := os.ReadFile(inFileName)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
prodos.WriteFile(file, pathName, inFile)
|
||||
case "readblock":
|
||||
file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
|
@ -74,7 +74,21 @@ func CreateVolumeBitmap(numberOfBlocks int) []byte {
|
||||
return volumeBitmap
|
||||
}
|
||||
|
||||
func FindFreeBlocks(numberOfBlocks int) []int {
|
||||
func FindFreeBlocks(volumeBitmap []byte, numberOfBlocks int) []int {
|
||||
blocks := make([]int, numberOfBlocks)
|
||||
|
||||
blocksFound := 0
|
||||
|
||||
for i := 0; i < len(volumeBitmap)*8; i++ {
|
||||
if CheckFreeBlockInVolumeBitmap(volumeBitmap, i) {
|
||||
blocks[blocksFound] = i
|
||||
blocksFound++
|
||||
if blocksFound == numberOfBlocks {
|
||||
return blocks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -134,3 +148,31 @@ func FreeBlockInVolumeBitmap(volumeBitmap []byte, blockNumber int) {
|
||||
|
||||
volumeBitmap[byteToChange] |= byte(byteToOr)
|
||||
}
|
||||
|
||||
func CheckFreeBlockInVolumeBitmap(volumeBitmap []byte, blockNumber int) bool {
|
||||
bitToCheck := blockNumber % 8
|
||||
byteToCheck := blockNumber / 8
|
||||
|
||||
byteToAnd := 0b00000000
|
||||
|
||||
switch bitToCheck {
|
||||
case 0:
|
||||
byteToAnd = 0b10000000
|
||||
case 1:
|
||||
byteToAnd = 0b01000000
|
||||
case 2:
|
||||
byteToAnd = 0b00100000
|
||||
case 3:
|
||||
byteToAnd = 0b00010000
|
||||
case 4:
|
||||
byteToAnd = 0b00001000
|
||||
case 5:
|
||||
byteToAnd = 0b00000100
|
||||
case 6:
|
||||
byteToAnd = 0b00000010
|
||||
case 7:
|
||||
byteToAnd = 0b00000001
|
||||
}
|
||||
|
||||
return (volumeBitmap[byteToCheck] & byte(byteToAnd)) > 0
|
||||
}
|
||||
|
@ -26,3 +26,30 @@ func TestCreateVolumeBitmap(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFreeBlockInVolumeBitmap(t *testing.T) {
|
||||
var tests = []struct {
|
||||
blocks int
|
||||
want bool
|
||||
}{
|
||||
{0, false}, // boot block
|
||||
{1, false}, // SOS boot block
|
||||
{2, false}, // volume root
|
||||
{21, false}, // end of volume bitmap
|
||||
{22, true}, // beginning of free space
|
||||
{8192, true}, // more free space
|
||||
{65534, true}, // last free block
|
||||
{65535, false}, // can't use last block because volume size is 0xFFFF, not 0x10000
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("%d", tt.blocks)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
volumeBitMap := CreateVolumeBitmap(65535)
|
||||
ans := CheckFreeBlockInVolumeBitmap(volumeBitMap, tt.blocks)
|
||||
if ans != tt.want {
|
||||
t.Errorf("got %t, want %t", ans, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ type VolumeHeader struct {
|
||||
type DirectoryHeader struct {
|
||||
Name string
|
||||
ActiveFileCount int
|
||||
StartingBlock int
|
||||
PreviousBlock int
|
||||
NextBlock int
|
||||
}
|
||||
|
||||
@ -53,11 +55,10 @@ type FileEntry struct {
|
||||
DirectoryOffset int
|
||||
}
|
||||
|
||||
func ReadDirectory(file *os.File, path string) (VolumeHeader, []FileEntry) {
|
||||
func ReadDirectory(file *os.File, path string) (VolumeHeader, DirectoryHeader, []FileEntry) {
|
||||
buffer := ReadBlock(file, 2)
|
||||
|
||||
volumeHeader := parseVolumeHeader(buffer)
|
||||
//dumpVolumeHeader(volumeHeader)
|
||||
|
||||
if len(path) == 0 {
|
||||
path = fmt.Sprintf("/%s", volumeHeader.VolumeName)
|
||||
@ -66,17 +67,52 @@ func ReadDirectory(file *os.File, path string) (VolumeHeader, []FileEntry) {
|
||||
path = strings.ToUpper(path)
|
||||
paths := strings.Split(path, "/")
|
||||
|
||||
fileEntries := getFileEntriesInDirectory(file, 2, 1, paths)
|
||||
directoryHeader, fileEntries := getFileEntriesInDirectory(file, 2, 1, paths)
|
||||
|
||||
return volumeHeader, fileEntries
|
||||
return volumeHeader, directoryHeader, fileEntries
|
||||
}
|
||||
|
||||
func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int, paths []string) []FileEntry {
|
||||
//fmt.Printf("Parsing '%s'...\n", paths[currentPath])
|
||||
func GetFreeFileEntryInDirectory(file *os.File, directory string) FileEntry {
|
||||
_, directoryHeader, _ := ReadDirectory(file, directory)
|
||||
DumpDirectoryHeader(directoryHeader)
|
||||
blockNumber := directoryHeader.StartingBlock
|
||||
buffer := ReadBlock(file, blockNumber)
|
||||
|
||||
entryOffset := 43 // start at offset after header
|
||||
entryNumber := 2 // header is essentially the first entry so start at 2
|
||||
|
||||
for {
|
||||
if entryNumber > 13 {
|
||||
blockNumber = int(buffer[2]) + int(buffer[3])*256
|
||||
// if we ran out of blocks in the directory, return empty
|
||||
// TODO: expand the directory to add more entries
|
||||
if blockNumber == 0 {
|
||||
return FileEntry{}
|
||||
}
|
||||
// else read the next block in the directory
|
||||
buffer = ReadBlock(file, blockNumber)
|
||||
entryOffset = 4
|
||||
entryNumber = 1
|
||||
}
|
||||
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
|
||||
|
||||
if fileEntry.StorageType == StorageDeleted {
|
||||
fileEntry.DirectoryBlock = blockNumber
|
||||
fileEntry.DirectoryOffset = entryOffset
|
||||
fileEntry.HeaderPointer = directoryHeader.StartingBlock
|
||||
return fileEntry
|
||||
}
|
||||
|
||||
entryNumber++
|
||||
entryOffset += 39
|
||||
}
|
||||
}
|
||||
|
||||
func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry) {
|
||||
|
||||
buffer := ReadBlock(file, blockNumber)
|
||||
|
||||
directoryHeader := parseDirectoryHeader(buffer)
|
||||
directoryHeader := parseDirectoryHeader(buffer, blockNumber)
|
||||
|
||||
fileEntries := make([]FileEntry, directoryHeader.ActiveFileCount)
|
||||
entryOffset := 43 // start at offset after header
|
||||
@ -89,7 +125,7 @@ func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int,
|
||||
|
||||
if !matchedDirectory && (currentPath == len(paths)-1) {
|
||||
// path not matched by last path part
|
||||
return nil
|
||||
return DirectoryHeader{}, nil
|
||||
}
|
||||
|
||||
for {
|
||||
@ -97,24 +133,23 @@ func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int,
|
||||
entryOffset = 4
|
||||
entryNumber = 1
|
||||
if blockNumber == 0 {
|
||||
return nil
|
||||
return DirectoryHeader{}, nil
|
||||
}
|
||||
buffer = ReadBlock(file, nextBlock)
|
||||
nextBlock = int(buffer[2]) + int(buffer[3])*256
|
||||
}
|
||||
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
|
||||
//DumpFileEntry(fileEntry)
|
||||
|
||||
if fileEntry.StorageType != StorageDeleted {
|
||||
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
|
||||
return directoryHeader, fileEntries[0:activeEntries]
|
||||
}
|
||||
if matchedDirectory {
|
||||
fileEntries[activeEntries] = fileEntry
|
||||
} else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName {
|
||||
return getFileEntriesInDirectory(file, fileEntry.KeyPointer, currentPath+1, paths)
|
||||
}
|
||||
activeEntries++
|
||||
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
|
||||
return fileEntries[0:activeEntries]
|
||||
}
|
||||
}
|
||||
|
||||
entryNumber++
|
||||
@ -227,14 +262,17 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
|
||||
return volumeHeader
|
||||
}
|
||||
|
||||
func parseDirectoryHeader(buffer []byte) DirectoryHeader {
|
||||
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
|
||||
name := string(buffer[0x05 : filenameLength+0x05])
|
||||
fileCount := int(buffer[0x25]) + int(buffer[0x26])*256
|
||||
|
||||
directoryEntry := DirectoryHeader{
|
||||
PreviousBlock: previousBlock,
|
||||
NextBlock: nextBlock,
|
||||
StartingBlock: blockNumber,
|
||||
Name: name,
|
||||
ActiveFileCount: fileCount,
|
||||
}
|
||||
|
125
prodos/file.go
125
prodos/file.go
@ -3,6 +3,7 @@ package prodos
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LoadFile(file *os.File, path string) []byte {
|
||||
@ -22,6 +23,71 @@ func LoadFile(file *os.File, path string) []byte {
|
||||
return buffer
|
||||
}
|
||||
|
||||
func WriteFile(file *os.File, path string, buffer []byte) {
|
||||
directory, fileName := GetDirectoryAndFileNameFromPath(path)
|
||||
|
||||
DeleteFile(file, path)
|
||||
|
||||
// get list of blocks to write file to
|
||||
blockList := CreateBlockList(file, len(buffer))
|
||||
|
||||
fileEntry := GetFreeFileEntryInDirectory(file, directory)
|
||||
|
||||
// seedling file
|
||||
if len(buffer) <= 0x200 {
|
||||
WriteBlock(file, blockList[0], buffer)
|
||||
fileEntry.StorageType = StorageSeedling
|
||||
}
|
||||
|
||||
// sapling file needs index block
|
||||
if len(buffer) > 0x200 && len(buffer) <= 0x20000 {
|
||||
fileEntry.StorageType = StorageSapling
|
||||
|
||||
// write index block with pointers to data blocks
|
||||
indexBuffer := make([]byte, 512)
|
||||
for i := 0; i < 256; i++ {
|
||||
if i < len(blockList) {
|
||||
indexBuffer[i] = byte(blockList[i] & 0x00FF)
|
||||
indexBuffer[i+256] = byte(blockList[i] >> 8)
|
||||
}
|
||||
}
|
||||
WriteBlock(file, blockList[0], indexBuffer)
|
||||
|
||||
// write all data blocks
|
||||
blockBuffer := make([]byte, 512)
|
||||
blockPointer := 0
|
||||
blockIndexNumber := 1
|
||||
for i := 0; i < len(buffer); i++ {
|
||||
blockBuffer[blockPointer] = buffer[i]
|
||||
if blockPointer == 512 {
|
||||
WriteBlock(file, blockList[blockIndexNumber], blockBuffer)
|
||||
blockPointer = 0
|
||||
blockIndexNumber++
|
||||
}
|
||||
if i == len(buffer)-1 {
|
||||
for j := blockPointer; j < 512; j++ {
|
||||
blockBuffer[j] = 0
|
||||
}
|
||||
WriteBlock(file, blockList[blockIndexNumber], blockBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add tree file
|
||||
|
||||
// add file entry to directory
|
||||
fileEntry.FileName = fileName
|
||||
fileEntry.BlocksUsed = len(blockList)
|
||||
fileEntry.CreationTime = time.Now()
|
||||
fileEntry.ModifiedTime = time.Now()
|
||||
fileEntry.AuxType = 0x2000
|
||||
fileEntry.EndOfFile = len(buffer)
|
||||
fileEntry.FileType = 0x06
|
||||
fileEntry.KeyPointer = blockList[0]
|
||||
|
||||
writeFileEntry(file, fileEntry)
|
||||
}
|
||||
|
||||
func GetBlocklist(file *os.File, fileEntry FileEntry) []int {
|
||||
blocks := make([]int, fileEntry.BlocksUsed)
|
||||
|
||||
@ -46,21 +112,34 @@ func GetBlocklist(file *os.File, fileEntry FileEntry) []int {
|
||||
return blocks
|
||||
}
|
||||
|
||||
func GetFileEntry(file *os.File, path string) FileEntry {
|
||||
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])
|
||||
func CreateBlockList(file *os.File, fileSize int) []int {
|
||||
numberOfBlocks := fileSize / 512
|
||||
if fileSize%512 > 0 {
|
||||
numberOfBlocks++
|
||||
}
|
||||
if fileSize > 0x200 && fileSize <= 0x20000 {
|
||||
numberOfBlocks++ // add index block
|
||||
}
|
||||
if fileSize > 0x20000 {
|
||||
// add master index block
|
||||
numberOfBlocks++
|
||||
// add index blocks for each 128 blocks
|
||||
numberOfBlocks += numberOfBlocks / 128
|
||||
// add index block for any remaining blocks
|
||||
if numberOfBlocks%128 > 0 {
|
||||
numberOfBlocks++
|
||||
}
|
||||
}
|
||||
volumeBitmap := ReadVolumeBitmap(file)
|
||||
blockList := FindFreeBlocks(volumeBitmap, numberOfBlocks)
|
||||
|
||||
directory := directoryBuilder.String()
|
||||
fileName := paths[len(paths)-1]
|
||||
return blockList
|
||||
}
|
||||
|
||||
_, fileEntries := ReadDirectory(file, directory)
|
||||
func GetFileEntry(file *os.File, path string) FileEntry {
|
||||
directory, fileName := GetDirectoryAndFileNameFromPath(path)
|
||||
|
||||
_, _, fileEntries := ReadDirectory(file, directory)
|
||||
|
||||
if fileEntries == nil {
|
||||
return FileEntry{}
|
||||
@ -77,8 +156,28 @@ func GetFileEntry(file *os.File, path string) FileEntry {
|
||||
return fileEntry
|
||||
}
|
||||
|
||||
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 DeleteFile(file *os.File, path string) {
|
||||
fileEntry := GetFileEntry(file, path)
|
||||
if fileEntry.StorageType == StorageDeleted {
|
||||
return
|
||||
}
|
||||
|
||||
// free the blocks
|
||||
blocks := GetBlocklist(file, fileEntry)
|
||||
@ -95,7 +194,7 @@ func DeleteFile(file *os.File, path string) {
|
||||
|
||||
// decrement the directory entry count
|
||||
directoryBlock := ReadBlock(file, fileEntry.HeaderPointer)
|
||||
directoryHeader := parseDirectoryHeader(directoryBlock)
|
||||
directoryHeader := parseDirectoryHeader(directoryBlock, fileEntry.HeaderPointer)
|
||||
|
||||
directoryHeader.ActiveFileCount--
|
||||
writeDirectoryHeader(file, directoryHeader, fileEntry.HeaderPointer)
|
||||
|
@ -105,6 +105,14 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
|
||||
fmt.Printf("Total blocks: %d\n", volumeHeader.TotalBlocks)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func DumpBlock(buffer []byte) {
|
||||
for i := 0; i < len(buffer); i += 16 {
|
||||
for j := i; j < i+16; j++ {
|
||||
|
Loading…
Reference in New Issue
Block a user