diff --git a/README.md b/README.md index cac242f..7e07580 100644 --- a/README.md +++ b/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 diff --git a/main.go b/main.go index 8d8e329..7e2074b 100644 --- a/main.go +++ b/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 { diff --git a/prodos/bitmap.go b/prodos/bitmap.go index a342468..375234b 100644 --- a/prodos/bitmap.go +++ b/prodos/bitmap.go @@ -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 +} diff --git a/prodos/bitmap_test.go b/prodos/bitmap_test.go index 32f6d94..f9494d6 100644 --- a/prodos/bitmap_test.go +++ b/prodos/bitmap_test.go @@ -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) + } + }) + } +} diff --git a/prodos/directory.go b/prodos/directory.go index cc759d2..561ef3b 100644 --- a/prodos/directory.go +++ b/prodos/directory.go @@ -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, } diff --git a/prodos/file.go b/prodos/file.go index 77e63c4..52e7424 100644 --- a/prodos/file.go +++ b/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) diff --git a/prodos/text.go b/prodos/text.go index f4a6aa7..201e7ef 100644 --- a/prodos/text.go +++ b/prodos/text.go @@ -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++ {