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 ## Current command line functionality
1. Export files 1. Export files
2. List any directory 2. Write files (currently only works with < 128K files)
3. Display volume bitmap 3. List any directory
4. Create new volume 4. Display volume bitmap
5. Delete file 5. Create new volume
6. Delete file
## Current library functionality ## Current library functionality
1. Read block 1. Read block
2. Write block 2. Write block
3. Read file 3. Read file
4. Delete file 4. Write file
5. Create new volume 5. Delete file
6. Read volume bitmap 6. Create new volume
7. Write volume bitmap 7. Read volume bitmap
8. Get list of file entries from any path 8. Write volume bitmap
9. Get volume header 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 pathName string
var command string var command string
var outFileName string var outFileName string
var inFileName string
var blockNumber int var blockNumber int
var volumeSize int var volumeSize int
var volumeName string var volumeName string
flag.StringVar(&fileName, "driveimage", "", "A ProDOS format drive image") flag.StringVar(&fileName, "driveimage", "", "A ProDOS format drive image")
flag.StringVar(&pathName, "path", "", "Path name in ProDOS 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(&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.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.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") flag.IntVar(&blockNumber, "block", 0, "A block number to read/write from 0 to 65535")
@ -36,7 +38,7 @@ func main() {
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
volumeHeader, fileEntries := prodos.ReadDirectory(file, pathName) volumeHeader, _, fileEntries := prodos.ReadDirectory(file, pathName)
prodos.DumpDirectory(volumeHeader, fileEntries) prodos.DumpDirectory(volumeHeader, fileEntries)
case "volumebitmap": case "volumebitmap":
file, err := os.OpenFile(fileName, os.O_RDWR, 0755) file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
@ -55,11 +57,28 @@ func main() {
os.Exit(1) os.Exit(1)
} }
getFile := prodos.LoadFile(file, pathName) getFile := prodos.LoadFile(file, pathName)
if len(outFileName) == 0 {
_, outFileName = prodos.GetDirectoryAndFileNameFromPath(pathName)
}
outFile, err := os.Create(outFileName) outFile, err := os.Create(outFileName)
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
outFile.Write(getFile) 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": case "readblock":
file, err := os.OpenFile(fileName, os.O_RDWR, 0755) file, err := os.OpenFile(fileName, os.O_RDWR, 0755)
if err != nil { if err != nil {

View File

@ -74,7 +74,21 @@ func CreateVolumeBitmap(numberOfBlocks int) []byte {
return volumeBitmap 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 return nil
} }
@ -134,3 +148,31 @@ func FreeBlockInVolumeBitmap(volumeBitmap []byte, blockNumber int) {
volumeBitmap[byteToChange] |= byte(byteToOr) 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 { type DirectoryHeader struct {
Name string Name string
ActiveFileCount int ActiveFileCount int
StartingBlock int
PreviousBlock int
NextBlock int NextBlock int
} }
@ -53,11 +55,10 @@ type FileEntry struct {
DirectoryOffset int 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) buffer := ReadBlock(file, 2)
volumeHeader := parseVolumeHeader(buffer) volumeHeader := parseVolumeHeader(buffer)
//dumpVolumeHeader(volumeHeader)
if len(path) == 0 { if len(path) == 0 {
path = fmt.Sprintf("/%s", volumeHeader.VolumeName) path = fmt.Sprintf("/%s", volumeHeader.VolumeName)
@ -66,17 +67,52 @@ func ReadDirectory(file *os.File, path string) (VolumeHeader, []FileEntry) {
path = strings.ToUpper(path) path = strings.ToUpper(path)
paths := strings.Split(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 { func GetFreeFileEntryInDirectory(file *os.File, directory string) FileEntry {
//fmt.Printf("Parsing '%s'...\n", paths[currentPath]) _, 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) buffer := ReadBlock(file, blockNumber)
directoryHeader := parseDirectoryHeader(buffer) directoryHeader := parseDirectoryHeader(buffer, blockNumber)
fileEntries := make([]FileEntry, directoryHeader.ActiveFileCount) fileEntries := make([]FileEntry, directoryHeader.ActiveFileCount)
entryOffset := 43 // start at offset after header 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) { if !matchedDirectory && (currentPath == len(paths)-1) {
// path not matched by last path part // path not matched by last path part
return nil return DirectoryHeader{}, nil
} }
for { for {
@ -97,24 +133,23 @@ func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int,
entryOffset = 4 entryOffset = 4
entryNumber = 1 entryNumber = 1
if blockNumber == 0 { if blockNumber == 0 {
return nil return DirectoryHeader{}, nil
} }
buffer = ReadBlock(file, nextBlock) buffer = ReadBlock(file, nextBlock)
nextBlock = int(buffer[2]) + int(buffer[3])*256 nextBlock = int(buffer[2]) + int(buffer[3])*256
} }
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset) fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
//DumpFileEntry(fileEntry)
if fileEntry.StorageType != StorageDeleted { if fileEntry.StorageType != StorageDeleted {
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
return directoryHeader, fileEntries[0:activeEntries]
}
if matchedDirectory { if matchedDirectory {
fileEntries[activeEntries] = fileEntry fileEntries[activeEntries] = fileEntry
} else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName { } else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName {
return getFileEntriesInDirectory(file, fileEntry.KeyPointer, currentPath+1, paths) return getFileEntriesInDirectory(file, fileEntry.KeyPointer, currentPath+1, paths)
} }
activeEntries++ activeEntries++
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
return fileEntries[0:activeEntries]
}
} }
entryNumber++ entryNumber++
@ -227,14 +262,17 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
return 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 nextBlock := int(buffer[0x02]) + int(buffer[0x03])*256
filenameLength := buffer[0x04] & 15 filenameLength := buffer[0x04] & 15
name := string(buffer[0x05 : filenameLength+0x05]) name := string(buffer[0x05 : filenameLength+0x05])
fileCount := int(buffer[0x25]) + int(buffer[0x26])*256 fileCount := int(buffer[0x25]) + int(buffer[0x26])*256
directoryEntry := DirectoryHeader{ directoryEntry := DirectoryHeader{
PreviousBlock: previousBlock,
NextBlock: nextBlock, NextBlock: nextBlock,
StartingBlock: blockNumber,
Name: name, Name: name,
ActiveFileCount: fileCount, ActiveFileCount: fileCount,
} }

View File

@ -3,6 +3,7 @@ package prodos
import ( import (
"os" "os"
"strings" "strings"
"time"
) )
func LoadFile(file *os.File, path string) []byte { func LoadFile(file *os.File, path string) []byte {
@ -22,6 +23,71 @@ func LoadFile(file *os.File, path string) []byte {
return buffer 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 { func GetBlocklist(file *os.File, fileEntry FileEntry) []int {
blocks := make([]int, fileEntry.BlocksUsed) blocks := make([]int, fileEntry.BlocksUsed)
@ -46,21 +112,34 @@ func GetBlocklist(file *os.File, fileEntry FileEntry) []int {
return blocks return blocks
} }
func GetFileEntry(file *os.File, path string) FileEntry { func CreateBlockList(file *os.File, fileSize int) []int {
path = strings.ToUpper(path) numberOfBlocks := fileSize / 512
paths := strings.Split(path, "/") if fileSize%512 > 0 {
numberOfBlocks++
var directoryBuilder strings.Builder
for i := 1; i < len(paths)-1; i++ {
directoryBuilder.WriteString("/")
directoryBuilder.WriteString(paths[i])
} }
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() return blockList
fileName := paths[len(paths)-1] }
_, fileEntries := ReadDirectory(file, directory) func GetFileEntry(file *os.File, path string) FileEntry {
directory, fileName := GetDirectoryAndFileNameFromPath(path)
_, _, fileEntries := ReadDirectory(file, directory)
if fileEntries == nil { if fileEntries == nil {
return FileEntry{} return FileEntry{}
@ -77,8 +156,28 @@ func GetFileEntry(file *os.File, path string) FileEntry {
return 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) { func DeleteFile(file *os.File, path string) {
fileEntry := GetFileEntry(file, path) fileEntry := GetFileEntry(file, path)
if fileEntry.StorageType == StorageDeleted {
return
}
// free the blocks // free the blocks
blocks := GetBlocklist(file, fileEntry) blocks := GetBlocklist(file, fileEntry)
@ -95,7 +194,7 @@ func DeleteFile(file *os.File, path string) {
// decrement the directory entry count // decrement the directory entry count
directoryBlock := ReadBlock(file, fileEntry.HeaderPointer) directoryBlock := ReadBlock(file, fileEntry.HeaderPointer)
directoryHeader := parseDirectoryHeader(directoryBlock) directoryHeader := parseDirectoryHeader(directoryBlock, fileEntry.HeaderPointer)
directoryHeader.ActiveFileCount-- directoryHeader.ActiveFileCount--
writeDirectoryHeader(file, directoryHeader, fileEntry.HeaderPointer) writeDirectoryHeader(file, directoryHeader, fileEntry.HeaderPointer)

View File

@ -105,6 +105,14 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
fmt.Printf("Total blocks: %d\n", volumeHeader.TotalBlocks) 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) { func DumpBlock(buffer []byte) {
for i := 0; i < len(buffer); i += 16 { for i := 0; i < len(buffer); i += 16 {
for j := i; j < i+16; j++ { for j := i; j < i+16; j++ {