Added file write support

This commit is contained in:
Terence Boldt 2021-06-25 21:15:20 -04:00
parent a04603f1e2
commit aa6fb993ee
7 changed files with 275 additions and 40 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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