From a04603f1e23069fa3402761ec4dccf4f3d4d97b4 Mon Sep 17 00:00:00 2001 From: Terence Boldt Date: Fri, 11 Jun 2021 22:43:35 -0400 Subject: [PATCH] Add file deletion --- .gitignore | 8 +++ README.md | 16 +++--- main.go | 11 ++-- prodos/bitmap.go | 48 ++++++++++++++---- prodos/directory.go | 120 ++++++++++++++++++++++++++++++++++---------- prodos/file.go | 86 ++++++++++++++++++++++++------- prodos/text.go | 7 +-- 7 files changed, 228 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index 66fd13c..b78bd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,11 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# Project specific +*.hdv +*.bin +*.xxd +*.swp +.DS_Store +ProDOS-Utilities diff --git a/README.md b/README.md index 2f1142e..cac242f 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,19 @@ This project is just starting but is intended to be both a command line tool and library to provide access to ProDOS based hard drive images. It is written in Go to be cross platform (Linux, Windows, macOS etc.). Functionality, naming and parameters are subject to change without notice. This project was started so I would be able to automate writing the firmware file into the drive image for one of my other projects [Apple2-IO-RPi](https://github.com/tjboldt/Apple2-IO-RPi). ## Current command line functionality -1. Export files up to 128KB (only seedling and sapling files are supported) +1. Export files 2. List any directory 3. Display volume bitmap +4. Create new volume +5. Delete file ## Current library functionality 1. Read block 2. Write block -3. Read file (up to 128KB) -4. Read volume bitmap -5. Write volume bitmap -6. Get list of file entries from any path -7. Get volume header +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 diff --git a/main.go b/main.go index 1eb32d0..8d8e329 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ func main() { 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, create") + 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.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") @@ -71,9 +71,14 @@ func main() { os.Exit(1) } outFile.Write(block) - case "create": - //fmt.Println("Create volume") + case "createvolume": prodos.CreateVolume(fileName, volumeName, volumeSize) + case "delete": + file, err := os.OpenFile(fileName, os.O_RDWR, 0755) + if err != nil { + os.Exit(1) + } + prodos.DeleteFile(file, pathName) default: fmt.Printf("Command %s not handle\n", command) os.Exit(1) diff --git a/prodos/bitmap.go b/prodos/bitmap.go index c8c21fb..a342468 100644 --- a/prodos/bitmap.go +++ b/prodos/bitmap.go @@ -82,27 +82,55 @@ func MarkBlockInVolumeBitmap(volumeBitmap []byte, blockNumber int) { bitToChange := blockNumber % 8 byteToChange := blockNumber / 8 - byteToWrite := 0b11111111 + byteToAnd := 0b11111111 switch bitToChange { case 0: - byteToWrite = 0b01111111 + byteToAnd = 0b01111111 case 1: - byteToWrite = 0b10111111 + byteToAnd = 0b10111111 case 2: - byteToWrite = 0b11011111 + byteToAnd = 0b11011111 case 3: - byteToWrite = 0b11101111 + byteToAnd = 0b11101111 case 4: - byteToWrite = 0b11110111 + byteToAnd = 0b11110111 case 5: - byteToWrite = 0b11111011 + byteToAnd = 0b11111011 case 6: - byteToWrite = 0b11111101 + byteToAnd = 0b11111101 case 7: - byteToWrite = 0b11111110 + byteToAnd = 0b11111110 } //fmt.Printf("blockNumber: $%04X byteToWrite: 0b%08b volumeBitmap: $%02X byteToChange: $%04X\n", blockNumber, byteToWrite, volumeBitmap[byteToChange], byteToChange) - volumeBitmap[byteToChange] &= byte(byteToWrite) + volumeBitmap[byteToChange] &= byte(byteToAnd) +} + +func FreeBlockInVolumeBitmap(volumeBitmap []byte, blockNumber int) { + bitToChange := blockNumber % 8 + byteToChange := blockNumber / 8 + + byteToOr := 0b00000000 + + switch bitToChange { + case 0: + byteToOr = 0b10000000 + case 1: + byteToOr = 0b01000000 + case 2: + byteToOr = 0b00100000 + case 3: + byteToOr = 0b00010000 + case 4: + byteToOr = 0b00001000 + case 5: + byteToOr = 0b00000100 + case 6: + byteToOr = 0b00000010 + case 7: + byteToOr = 0b00000001 + } + + volumeBitmap[byteToChange] |= byte(byteToOr) } diff --git a/prodos/directory.go b/prodos/directory.go index 2b4f6a0..cc759d2 100644 --- a/prodos/directory.go +++ b/prodos/directory.go @@ -16,6 +16,8 @@ type VolumeHeader struct { NextBlock int EntryLength int EntriesPerBlock int + MinVersion int + Version int } type DirectoryHeader struct { @@ -34,16 +36,21 @@ const ( ) type FileEntry struct { - StorageType int - FileName string - FileType int - CreationTime time.Time - StartingBlock int - BlocksUsed int - EndOfFile int - Access int - AuxType int - ModifiedTime time.Time + StorageType int + FileName string + FileType int + CreationTime time.Time + KeyPointer int + Version int + MinVersion int + BlocksUsed int + EndOfFile int + Access int + AuxType int + ModifiedTime time.Time + HeaderPointer int + DirectoryBlock int + DirectoryOffset int } func ReadDirectory(file *os.File, path string) (VolumeHeader, []FileEntry) { @@ -95,14 +102,14 @@ func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int, buffer = ReadBlock(file, nextBlock) nextBlock = int(buffer[2]) + int(buffer[3])*256 } - fileEntry := parseFileEntry(buffer[entryOffset : entryOffset+40]) + fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset) //DumpFileEntry(fileEntry) if fileEntry.StorageType != StorageDeleted { if matchedDirectory { fileEntries[activeEntries] = fileEntry } else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName { - return getFileEntriesInDirectory(file, fileEntry.StartingBlock, currentPath+1, paths) + return getFileEntriesInDirectory(file, fileEntry.KeyPointer, currentPath+1, paths) } activeEntries++ if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount { @@ -115,7 +122,7 @@ func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int, } } -func parseFileEntry(buffer []byte) FileEntry { +func parseFileEntry(buffer []byte, blockNumber int, entryOffset int) FileEntry { storageType := int(buffer[0] >> 4) fileNameLength := int(buffer[0] & 15) fileName := string(buffer[1 : fileNameLength+1]) @@ -124,26 +131,70 @@ func parseFileEntry(buffer []byte) FileEntry { blocksUsed := int(buffer[19]) + int(buffer[20])*256 endOfFile := int(buffer[21]) + int(buffer[22])*256 + int(buffer[23])*65536 creationTime := DateTimeFromProDOS(buffer[24:28]) + version := int(buffer[28]) + minVersion := int(buffer[29]) access := int(buffer[30]) auxType := int(buffer[31]) + int(buffer[32])*256 modifiedTime := DateTimeFromProDOS((buffer[33:37])) + headerPointer := int(buffer[0x25]) + int(buffer[0x26])*256 fileEntry := FileEntry{ - StorageType: storageType, - FileName: fileName, - FileType: fileType, - CreationTime: creationTime, - StartingBlock: startingBlock, - BlocksUsed: blocksUsed, - EndOfFile: endOfFile, - Access: access, - AuxType: auxType, - ModifiedTime: modifiedTime, + StorageType: storageType, + FileName: fileName, + FileType: fileType, + CreationTime: creationTime, + Version: version, + MinVersion: minVersion, + KeyPointer: startingBlock, + BlocksUsed: blocksUsed, + EndOfFile: endOfFile, + Access: access, + AuxType: auxType, + ModifiedTime: modifiedTime, + HeaderPointer: headerPointer, + DirectoryBlock: blockNumber, + DirectoryOffset: entryOffset, } return fileEntry } +func writeFileEntry(file *os.File, fileEntry FileEntry) { + buffer := make([]byte, 39) + buffer[0] = byte(fileEntry.StorageType)<<4 + byte(len(fileEntry.FileName)) + for i := 0; i < len(fileEntry.FileName); i++ { + buffer[i+1] = fileEntry.FileName[i] + } + buffer[0x10] = byte(fileEntry.FileType) + buffer[0x11] = byte(fileEntry.KeyPointer & 0xFF) + buffer[0x12] = byte(fileEntry.KeyPointer >> 8) + buffer[0x13] = byte(fileEntry.BlocksUsed & 0xFF) + buffer[0x14] = byte(fileEntry.BlocksUsed >> 8) + buffer[0x15] = byte(fileEntry.EndOfFile & 0x0000FF) + buffer[0x16] = byte(fileEntry.EndOfFile & 0x00FF00 >> 8) + buffer[0x17] = byte(fileEntry.EndOfFile & 0xFF0000 >> 16) + creationTime := DateTimeToProDOS(fileEntry.CreationTime) + for i := 0; i < 4; i++ { + buffer[0x18+i] = creationTime[i] + } + buffer[0x1C] = byte(fileEntry.Version) + buffer[0x1D] = byte(fileEntry.MinVersion) + buffer[0x1E] = byte(fileEntry.Access) + buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF) + buffer[0x20] = byte(fileEntry.AuxType >> 8) + modifiedTime := DateTimeToProDOS(fileEntry.CreationTime) + for i := 0; i < 4; i++ { + buffer[0x21+i] = modifiedTime[i] + } + buffer[0x25] = byte(fileEntry.HeaderPointer & 0x00FF) + buffer[0x26] = byte(fileEntry.HeaderPointer >> 8) + + _, err := file.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset)) + if err != nil { + + } +} + func parseVolumeHeader(buffer []byte) VolumeHeader { nextBlock := int(buffer[2]) + int(buffer[3])*256 filenameLength := buffer[4] & 15 @@ -170,15 +221,17 @@ func parseVolumeHeader(buffer []byte) VolumeHeader { NextBlock: nextBlock, EntriesPerBlock: entriesPerBlock, EntryLength: entryLength, + MinVersion: minVersion, + Version: version, } return volumeHeader } func parseDirectoryHeader(buffer []byte) DirectoryHeader { - nextBlock := int(buffer[2]) + int(buffer[3])*256 - filenameLength := buffer[4] & 15 - name := string(buffer[5 : filenameLength+5]) - fileCount := int(buffer[37]) + int(buffer[38])*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{ NextBlock: nextBlock, @@ -188,3 +241,16 @@ func parseDirectoryHeader(buffer []byte) DirectoryHeader { return directoryEntry } + +func writeDirectoryHeader(file *os.File, directoryHeader DirectoryHeader, blockNumber int) { + buffer := ReadBlock(file, blockNumber) + buffer[0x02] = byte(directoryHeader.NextBlock & 0x00FF) + buffer[0x03] = byte(directoryHeader.NextBlock >> 8) + buffer[0x04] = buffer[0x04] | byte(len(directoryHeader.Name)) + for i := 0; i < len(directoryHeader.Name); i++ { + buffer[0x05+i] = directoryHeader.Name[i] + } + buffer[0x25] = byte(directoryHeader.ActiveFileCount & 0x00FF) + buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8) + file.WriteAt(buffer, int64(blockNumber*512)) +} diff --git a/prodos/file.go b/prodos/file.go index 6764939..77e63c4 100644 --- a/prodos/file.go +++ b/prodos/file.go @@ -6,6 +6,47 @@ import ( ) func LoadFile(file *os.File, path string) []byte { + fileEntry := GetFileEntry(file, path) + + blockList := GetBlocklist(file, fileEntry) + + buffer := make([]byte, fileEntry.EndOfFile) + + for i := 0; i < len(blockList); i++ { + block := ReadBlock(file, blockList[i]) + for j := 0; j < 512 && i*512+j < fileEntry.EndOfFile; j++ { + buffer[i*512+j] = block[j] + } + } + + return buffer +} + +func GetBlocklist(file *os.File, fileEntry FileEntry) []int { + blocks := make([]int, fileEntry.BlocksUsed) + + switch fileEntry.StorageType { + case StorageSeedling: + blocks[0] = fileEntry.KeyPointer + case StorageSapling: + index := ReadBlock(file, fileEntry.KeyPointer) + for i := 0; i < fileEntry.BlocksUsed-1; i++ { + blocks[i] = int(index[i]) + int(index[i+256])*256 + } + case StorageTree: + masterIndex := ReadBlock(file, fileEntry.KeyPointer) + for i := 0; i < 128; i++ { + index := ReadBlock(file, int(masterIndex[i])+int(masterIndex[i+256])*256) + for j := 0; j < 256 && i*256+j < fileEntry.BlocksUsed; j++ { + blocks[i*256+j] = int(index[j]) + int(index[j+256])*256 + } + } + } + + return blocks +} + +func GetFileEntry(file *os.File, path string) FileEntry { path = strings.ToUpper(path) paths := strings.Split(path, "/") @@ -22,7 +63,7 @@ func LoadFile(file *os.File, path string) []byte { _, fileEntries := ReadDirectory(file, directory) if fileEntries == nil { - return nil + return FileEntry{} } var fileEntry FileEntry @@ -33,22 +74,29 @@ func LoadFile(file *os.File, path string) []byte { } } - switch fileEntry.StorageType { - case StorageSeedling: - return ReadBlock(file, fileEntry.StartingBlock)[0:fileEntry.EndOfFile] - case StorageSapling: - index := ReadBlock(file, fileEntry.StartingBlock) - buffer := make([]byte, fileEntry.EndOfFile) - for i := 0; i < 512 && index[i] > 0; i++ { - chunk := ReadBlock(file, int(index[i])+int(index[i+256])*256) - for j := i * 512; j < fileEntry.EndOfFile && j < i*512+512; j++ { - buffer[j] = chunk[j-i*512] - } - } - return buffer - case StorageTree: - // add tree file support later - return nil - } - return nil + return fileEntry +} + +func DeleteFile(file *os.File, path string) { + fileEntry := GetFileEntry(file, path) + + // free the blocks + blocks := GetBlocklist(file, fileEntry) + volumeBitmap := ReadVolumeBitmap(file) + for i := 0; i < len(blocks); i++ { + FreeBlockInVolumeBitmap(volumeBitmap, blocks[i]) + } + WriteVolumeBitmap(file, volumeBitmap) + + // zero out directory entry + fileEntry.StorageType = 0 + fileEntry.FileName = "" + writeFileEntry(file, fileEntry) + + // decrement the directory entry count + directoryBlock := ReadBlock(file, fileEntry.HeaderPointer) + directoryHeader := parseDirectoryHeader(directoryBlock) + + directoryHeader.ActiveFileCount-- + writeDirectoryHeader(file, directoryHeader, fileEntry.HeaderPointer) } diff --git a/prodos/text.go b/prodos/text.go index 6b042a9..f4a6aa7 100644 --- a/prodos/text.go +++ b/prodos/text.go @@ -85,9 +85,10 @@ func DumpFileEntry(fileEntry FileEntry) { fmt.Printf("AuxType: %04X\n", fileEntry.AuxType) fmt.Printf("EOF: %06X\n", fileEntry.EndOfFile) fmt.Printf("Blocks used: %04X\n", fileEntry.BlocksUsed) - fmt.Printf("Starting block: %04X\n", fileEntry.StartingBlock) + fmt.Printf("Starting block: %04X\n", fileEntry.KeyPointer) fmt.Printf("File type: %02X\n", fileEntry.FileType) fmt.Printf("Storage type: %02X\n", fileEntry.StorageType) + fmt.Printf("Header pointer: %04X\n", fileEntry.HeaderPointer) fmt.Printf("\n") } @@ -95,8 +96,8 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) { fmt.Printf("Next block: %d\n", volumeHeader.NextBlock) fmt.Printf("Volume name: %s\n", volumeHeader.VolumeName) fmt.Printf("Creation time: %d-%s-%d %02d:%02d\n", volumeHeader.CreationTime.Year(), volumeHeader.CreationTime.Month(), volumeHeader.CreationTime.Day(), volumeHeader.CreationTime.Hour(), volumeHeader.CreationTime.Minute()) - // fmt.Printf("ProDOS version (should be 0): %d\n", volumeHeader.Version) - // fmt.Printf("ProDOS mininum version (should be 0): %d\n", minVersion) + fmt.Printf("ProDOS version (should be 0): %d\n", volumeHeader.Version) + fmt.Printf("ProDOS mininum version (should be 0): %d\n", volumeHeader.MinVersion) fmt.Printf("Entry length (should be 39): %d\n", volumeHeader.EntryLength) fmt.Printf("Entries per block (should be 13): %d\n", volumeHeader.EntriesPerBlock) fmt.Printf("File count: %d\n", volumeHeader.ActiveFileCount)