Add file deletion

This commit is contained in:
Terence Boldt 2021-06-11 22:43:35 -04:00
parent 7e623c530f
commit a04603f1e2
7 changed files with 228 additions and 68 deletions

8
.gitignore vendored
View File

@ -13,3 +13,11 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# Project specific
*.hdv
*.bin
*.xxd
*.swp
.DS_Store
ProDOS-Utilities

View File

@ -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). 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 ## Current command line functionality
1. Export files up to 128KB (only seedling and sapling files are supported) 1. Export files
2. List any directory 2. List any directory
3. Display volume bitmap 3. Display volume bitmap
4. Create new volume
5. Delete file
## Current library functionality ## Current library functionality
1. Read block 1. Read block
2. Write block 2. Write block
3. Read file (up to 128KB) 3. Read file
4. Read volume bitmap 4. Delete file
5. Write volume bitmap 5. Create new volume
6. Get list of file entries from any path 6. Read volume bitmap
7. Get volume header 7. Write volume bitmap
8. Get list of file entries from any path
9. Get volume header

11
main.go
View File

@ -18,7 +18,7 @@ func main() {
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, 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.StringVar(&outFileName, "outfile", "export.bin", "Name of file to write")
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")
@ -71,9 +71,14 @@ func main() {
os.Exit(1) os.Exit(1)
} }
outFile.Write(block) outFile.Write(block)
case "create": case "createvolume":
//fmt.Println("Create volume")
prodos.CreateVolume(fileName, volumeName, volumeSize) 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: default:
fmt.Printf("Command %s not handle\n", command) fmt.Printf("Command %s not handle\n", command)
os.Exit(1) os.Exit(1)

View File

@ -82,27 +82,55 @@ func MarkBlockInVolumeBitmap(volumeBitmap []byte, blockNumber int) {
bitToChange := blockNumber % 8 bitToChange := blockNumber % 8
byteToChange := blockNumber / 8 byteToChange := blockNumber / 8
byteToWrite := 0b11111111 byteToAnd := 0b11111111
switch bitToChange { switch bitToChange {
case 0: case 0:
byteToWrite = 0b01111111 byteToAnd = 0b01111111
case 1: case 1:
byteToWrite = 0b10111111 byteToAnd = 0b10111111
case 2: case 2:
byteToWrite = 0b11011111 byteToAnd = 0b11011111
case 3: case 3:
byteToWrite = 0b11101111 byteToAnd = 0b11101111
case 4: case 4:
byteToWrite = 0b11110111 byteToAnd = 0b11110111
case 5: case 5:
byteToWrite = 0b11111011 byteToAnd = 0b11111011
case 6: case 6:
byteToWrite = 0b11111101 byteToAnd = 0b11111101
case 7: case 7:
byteToWrite = 0b11111110 byteToAnd = 0b11111110
} }
//fmt.Printf("blockNumber: $%04X byteToWrite: 0b%08b volumeBitmap: $%02X byteToChange: $%04X\n", blockNumber, byteToWrite, volumeBitmap[byteToChange], byteToChange) //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)
} }

View File

@ -16,6 +16,8 @@ type VolumeHeader struct {
NextBlock int NextBlock int
EntryLength int EntryLength int
EntriesPerBlock int EntriesPerBlock int
MinVersion int
Version int
} }
type DirectoryHeader struct { type DirectoryHeader struct {
@ -34,16 +36,21 @@ const (
) )
type FileEntry struct { type FileEntry struct {
StorageType int StorageType int
FileName string FileName string
FileType int FileType int
CreationTime time.Time CreationTime time.Time
StartingBlock int KeyPointer int
BlocksUsed int Version int
EndOfFile int MinVersion int
Access int BlocksUsed int
AuxType int EndOfFile int
ModifiedTime time.Time Access int
AuxType int
ModifiedTime time.Time
HeaderPointer int
DirectoryBlock int
DirectoryOffset int
} }
func ReadDirectory(file *os.File, path string) (VolumeHeader, []FileEntry) { 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) 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]) fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
//DumpFileEntry(fileEntry) //DumpFileEntry(fileEntry)
if fileEntry.StorageType != StorageDeleted { if fileEntry.StorageType != StorageDeleted {
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.StartingBlock, currentPath+1, paths) return getFileEntriesInDirectory(file, fileEntry.KeyPointer, currentPath+1, paths)
} }
activeEntries++ activeEntries++
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount { 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) storageType := int(buffer[0] >> 4)
fileNameLength := int(buffer[0] & 15) fileNameLength := int(buffer[0] & 15)
fileName := string(buffer[1 : fileNameLength+1]) fileName := string(buffer[1 : fileNameLength+1])
@ -124,26 +131,70 @@ func parseFileEntry(buffer []byte) FileEntry {
blocksUsed := int(buffer[19]) + int(buffer[20])*256 blocksUsed := int(buffer[19]) + int(buffer[20])*256
endOfFile := int(buffer[21]) + int(buffer[22])*256 + int(buffer[23])*65536 endOfFile := int(buffer[21]) + int(buffer[22])*256 + int(buffer[23])*65536
creationTime := DateTimeFromProDOS(buffer[24:28]) creationTime := DateTimeFromProDOS(buffer[24:28])
version := int(buffer[28])
minVersion := int(buffer[29])
access := int(buffer[30]) access := int(buffer[30])
auxType := int(buffer[31]) + int(buffer[32])*256 auxType := int(buffer[31]) + int(buffer[32])*256
modifiedTime := DateTimeFromProDOS((buffer[33:37])) modifiedTime := DateTimeFromProDOS((buffer[33:37]))
headerPointer := int(buffer[0x25]) + int(buffer[0x26])*256
fileEntry := FileEntry{ fileEntry := FileEntry{
StorageType: storageType, StorageType: storageType,
FileName: fileName, FileName: fileName,
FileType: fileType, FileType: fileType,
CreationTime: creationTime, CreationTime: creationTime,
StartingBlock: startingBlock, Version: version,
BlocksUsed: blocksUsed, MinVersion: minVersion,
EndOfFile: endOfFile, KeyPointer: startingBlock,
Access: access, BlocksUsed: blocksUsed,
AuxType: auxType, EndOfFile: endOfFile,
ModifiedTime: modifiedTime, Access: access,
AuxType: auxType,
ModifiedTime: modifiedTime,
HeaderPointer: headerPointer,
DirectoryBlock: blockNumber,
DirectoryOffset: entryOffset,
} }
return fileEntry 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 { func parseVolumeHeader(buffer []byte) VolumeHeader {
nextBlock := int(buffer[2]) + int(buffer[3])*256 nextBlock := int(buffer[2]) + int(buffer[3])*256
filenameLength := buffer[4] & 15 filenameLength := buffer[4] & 15
@ -170,15 +221,17 @@ func parseVolumeHeader(buffer []byte) VolumeHeader {
NextBlock: nextBlock, NextBlock: nextBlock,
EntriesPerBlock: entriesPerBlock, EntriesPerBlock: entriesPerBlock,
EntryLength: entryLength, EntryLength: entryLength,
MinVersion: minVersion,
Version: version,
} }
return volumeHeader return volumeHeader
} }
func parseDirectoryHeader(buffer []byte) DirectoryHeader { func parseDirectoryHeader(buffer []byte) DirectoryHeader {
nextBlock := int(buffer[2]) + int(buffer[3])*256 nextBlock := int(buffer[0x02]) + int(buffer[0x03])*256
filenameLength := buffer[4] & 15 filenameLength := buffer[0x04] & 15
name := string(buffer[5 : filenameLength+5]) name := string(buffer[0x05 : filenameLength+0x05])
fileCount := int(buffer[37]) + int(buffer[38])*256 fileCount := int(buffer[0x25]) + int(buffer[0x26])*256
directoryEntry := DirectoryHeader{ directoryEntry := DirectoryHeader{
NextBlock: nextBlock, NextBlock: nextBlock,
@ -188,3 +241,16 @@ func parseDirectoryHeader(buffer []byte) DirectoryHeader {
return directoryEntry 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))
}

View File

@ -6,6 +6,47 @@ import (
) )
func LoadFile(file *os.File, path string) []byte { 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) path = strings.ToUpper(path)
paths := strings.Split(path, "/") paths := strings.Split(path, "/")
@ -22,7 +63,7 @@ func LoadFile(file *os.File, path string) []byte {
_, fileEntries := ReadDirectory(file, directory) _, fileEntries := ReadDirectory(file, directory)
if fileEntries == nil { if fileEntries == nil {
return nil return FileEntry{}
} }
var fileEntry FileEntry var fileEntry FileEntry
@ -33,22 +74,29 @@ func LoadFile(file *os.File, path string) []byte {
} }
} }
switch fileEntry.StorageType { return fileEntry
case StorageSeedling: }
return ReadBlock(file, fileEntry.StartingBlock)[0:fileEntry.EndOfFile]
case StorageSapling: func DeleteFile(file *os.File, path string) {
index := ReadBlock(file, fileEntry.StartingBlock) fileEntry := GetFileEntry(file, path)
buffer := make([]byte, fileEntry.EndOfFile)
for i := 0; i < 512 && index[i] > 0; i++ { // free the blocks
chunk := ReadBlock(file, int(index[i])+int(index[i+256])*256) blocks := GetBlocklist(file, fileEntry)
for j := i * 512; j < fileEntry.EndOfFile && j < i*512+512; j++ { volumeBitmap := ReadVolumeBitmap(file)
buffer[j] = chunk[j-i*512] for i := 0; i < len(blocks); i++ {
} FreeBlockInVolumeBitmap(volumeBitmap, blocks[i])
} }
return buffer WriteVolumeBitmap(file, volumeBitmap)
case StorageTree:
// add tree file support later // zero out directory entry
return nil fileEntry.StorageType = 0
} fileEntry.FileName = ""
return nil writeFileEntry(file, fileEntry)
// decrement the directory entry count
directoryBlock := ReadBlock(file, fileEntry.HeaderPointer)
directoryHeader := parseDirectoryHeader(directoryBlock)
directoryHeader.ActiveFileCount--
writeDirectoryHeader(file, directoryHeader, fileEntry.HeaderPointer)
} }

View File

@ -85,9 +85,10 @@ func DumpFileEntry(fileEntry FileEntry) {
fmt.Printf("AuxType: %04X\n", fileEntry.AuxType) fmt.Printf("AuxType: %04X\n", fileEntry.AuxType)
fmt.Printf("EOF: %06X\n", fileEntry.EndOfFile) fmt.Printf("EOF: %06X\n", fileEntry.EndOfFile)
fmt.Printf("Blocks used: %04X\n", fileEntry.BlocksUsed) 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("File type: %02X\n", fileEntry.FileType)
fmt.Printf("Storage type: %02X\n", fileEntry.StorageType) fmt.Printf("Storage type: %02X\n", fileEntry.StorageType)
fmt.Printf("Header pointer: %04X\n", fileEntry.HeaderPointer)
fmt.Printf("\n") fmt.Printf("\n")
} }
@ -95,8 +96,8 @@ func DumpVolumeHeader(volumeHeader VolumeHeader) {
fmt.Printf("Next block: %d\n", volumeHeader.NextBlock) fmt.Printf("Next block: %d\n", volumeHeader.NextBlock)
fmt.Printf("Volume name: %s\n", volumeHeader.VolumeName) 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("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 version (should be 0): %d\n", volumeHeader.Version)
// fmt.Printf("ProDOS mininum version (should be 0): %d\n", minVersion) 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("Entry length (should be 39): %d\n", volumeHeader.EntryLength)
fmt.Printf("Entries per block (should be 13): %d\n", volumeHeader.EntriesPerBlock) fmt.Printf("Entries per block (should be 13): %d\n", volumeHeader.EntriesPerBlock)
fmt.Printf("File count: %d\n", volumeHeader.ActiveFileCount) fmt.Printf("File count: %d\n", volumeHeader.ActiveFileCount)