mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-12-27 05:29:49 +00:00
Add file deletion
This commit is contained in:
parent
7e623c530f
commit
a04603f1e2
8
.gitignore
vendored
8
.gitignore
vendored
@ -13,3 +13,11 @@
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Project specific
|
||||
*.hdv
|
||||
*.bin
|
||||
*.xxd
|
||||
*.swp
|
||||
.DS_Store
|
||||
ProDOS-Utilities
|
||||
|
16
README.md
16
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
|
||||
|
11
main.go
11
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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user