From 6fd22018e34c55553777f30ee4f7c9f491310e80 Mon Sep 17 00:00:00 2001 From: Terence Boldt Date: Sat, 5 Jun 2021 23:22:30 -0400 Subject: [PATCH] Initial commit with directory read --- README.md | 3 +- go.mod | 3 + main.go | 42 ++++++++++ prodos/directory.go | 195 +++++++++++++++++++++++++++++++++++++++++++ prodos/dumpBlock.go | 20 +++++ prodos/readblock.go | 15 ++++ prodos/text.go | 105 +++++++++++++++++++++++ prodos/time.go | 58 +++++++++++++ prodos/writeBlock.go | 15 ++++ 9 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 go.mod create mode 100644 main.go create mode 100644 prodos/directory.go create mode 100644 prodos/dumpBlock.go create mode 100644 prodos/readblock.go create mode 100644 prodos/text.go create mode 100644 prodos/time.go create mode 100644 prodos/writeBlock.go diff --git a/README.md b/README.md index 0beb3e2..b95690a 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# ProDOS-Utilities \ No newline at end of file +# ProDOS-Utilities +This project is just starting. It will eventually be a library in Golang to read and write files on a ProDOS hard drive image plus a command line interface. So far it is an experiment. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b0fc0b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/tjboldt/ProDOS-Utilities + +go 1.16 diff --git a/main.go b/main.go new file mode 100644 index 0000000..37aa4c9 --- /dev/null +++ b/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "os" + + "github.com/tjboldt/ProDOS-Utilities/prodos" +) + +func main() { + if len(os.Args) < 2 || len(os.Args) > 3 { + fmt.Printf("Usage:") + fmt.Printf(" ProDOS-Utilities DRIVE_IMAGE") + fmt.Printf(" ProDOS-Utilities DRIVE_IMAGE /FULL_PATH") + } + + fileName := os.Args[1] + pathName := "" + + if len(os.Args) == 3 { + pathName = os.Args[2] + } + + // empty path or volume name means read root directory + volumeHeader, fileEntries := prodos.ReadDirectory(fileName, pathName) + + fmt.Printf("VOLUME: %s\n\n", volumeHeader.VolumeName) + fmt.Printf("NAME TYPE BLOCKS MODIFIED CREATED ENDFILE SUBTYPE\n\n") + + for i := 0; i < len(fileEntries); i++ { + fmt.Printf("%-15s %s %7d %s %s %8d %8d\n", + fileEntries[i].FileName, + prodos.FileTypeToString(fileEntries[i].FileType), + fileEntries[i].BlocksUsed, + prodos.TimeToString(fileEntries[i].ModifiedTime), + prodos.TimeToString(fileEntries[i].CreationTime), + fileEntries[i].EndOfFile, + fileEntries[i].AuxType, + ) + } + fmt.Printf("\n") +} diff --git a/prodos/directory.go b/prodos/directory.go new file mode 100644 index 0000000..017698b --- /dev/null +++ b/prodos/directory.go @@ -0,0 +1,195 @@ +package prodos + +import ( + "fmt" + "os" + "strings" + "time" +) + +type VolumeHeader struct { + VolumeName string + CreationTime time.Time + ActiveFileCount int + BitmapStartBlock int + TotalBlocks int + NextBlock int + EntryLength int + EntriesPerBlock int +} + +type DirectoryHeader struct { + Name string + ActiveFileCount int + NextBlock int +} + +const ( + StorageDeleted = 0 + StorageSeedling = 1 + StorageSapling = 2 + StorageTree = 3 + StoragePascal = 4 + StorageDirectory = 13 +) + +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 +} + +func ReadDirectory(driveFileName string, path string) (VolumeHeader, []FileEntry) { + file, err := os.OpenFile(driveFileName, os.O_RDWR, 0755) + if err != nil { + return VolumeHeader{}, nil + } + + buffer := ReadBlock(file, 2) + + volumeHeader := ParseVolumeHeader(buffer) + //dumpVolumeHeader(volumeHeader) + + if len(path) == 0 { + path = fmt.Sprintf("/%s", volumeHeader.VolumeName) + } + + path = strings.ToUpper(path) + paths := strings.Split(path, "/") + + fileEntries := getFileEntriesInDirectory(file, 2, 1, paths) + + return volumeHeader, fileEntries +} + +func getFileEntriesInDirectory(file *os.File, blockNumber int, currentPath int, paths []string) []FileEntry { + //fmt.Printf("Parsing '%s'...\n", paths[currentPath]) + + buffer := ReadBlock(file, blockNumber) + + directoryHeader := ParseDirectoryHeader(buffer) + + fileEntries := make([]FileEntry, directoryHeader.ActiveFileCount) + entryOffset := 43 // start at offset after header + activeEntries := 0 + entryNumber := 2 // header is essentially the first entry so start at 2 + + nextBlock := directoryHeader.NextBlock + + matchedDirectory := (currentPath == len(paths)-1) && (paths[currentPath] == directoryHeader.Name) + + if !matchedDirectory && (currentPath == len(paths)-1) { + // path not matched by last path part + return nil + } + + for { + if entryNumber > 13 { + entryOffset = 4 + entryNumber = 1 + if blockNumber == 0 { + return nil + } + buffer = ReadBlock(file, nextBlock) + nextBlock = int(buffer[2]) + int(buffer[3])*256 + } + fileEntry := parseFileEntry(buffer[entryOffset : entryOffset+40]) + //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) + } + activeEntries++ + if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount { + return fileEntries[0:activeEntries] + } + } + + entryNumber++ + entryOffset += 39 + } +} + +func parseFileEntry(buffer []byte) FileEntry { + storageType := int(buffer[0] >> 4) + fileNameLength := int(buffer[0] & 15) + fileName := string(buffer[1 : fileNameLength+1]) + fileType := int(buffer[16]) + startingBlock := int(buffer[17]) + int(buffer[18])*256 + 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]) + access := int(buffer[30]) + auxType := int(buffer[31]) + int(buffer[32])*256 + modifiedTime := DateTimeFromProDOS((buffer[33:37])) + + fileEntry := FileEntry{ + StorageType: storageType, + FileName: fileName, + FileType: fileType, + CreationTime: creationTime, + StartingBlock: startingBlock, + BlocksUsed: blocksUsed, + EndOfFile: endOfFile, + Access: access, + AuxType: auxType, + ModifiedTime: modifiedTime, + } + + return fileEntry +} + +func ParseVolumeHeader(buffer []byte) VolumeHeader { + nextBlock := int(buffer[2]) + int(buffer[3])*256 + filenameLength := buffer[4] & 15 + volumeName := string(buffer[5 : filenameLength+5]) + creationTime := DateTimeFromProDOS(buffer[28:32]) + version := int(buffer[32]) + minVersion := int(buffer[33]) + entryLength := int(buffer[35]) + entriesPerBlock := int(buffer[36]) + fileCount := int(buffer[37]) + int(buffer[38])*256 + bitmapBlock := int(buffer[39]) + int(buffer[40])*256 + totalBlocks := int(buffer[41]) + int(buffer[42])*256 + + if version > 0 || minVersion > 0 { + panic("Unsupported ProDOS version") + } + + volumeHeader := VolumeHeader{ + VolumeName: volumeName, + CreationTime: creationTime, + ActiveFileCount: fileCount, + BitmapStartBlock: bitmapBlock, + TotalBlocks: totalBlocks, + NextBlock: nextBlock, + EntriesPerBlock: entriesPerBlock, + EntryLength: entryLength, + } + 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 + + directoryEntry := DirectoryHeader{ + NextBlock: nextBlock, + Name: name, + ActiveFileCount: fileCount, + } + + return directoryEntry +} diff --git a/prodos/dumpBlock.go b/prodos/dumpBlock.go new file mode 100644 index 0000000..ea9ed1a --- /dev/null +++ b/prodos/dumpBlock.go @@ -0,0 +1,20 @@ +package prodos + +import "fmt" + +func DumpBlock(buffer []byte) { + for i := 0; i < len(buffer); i += 16 { + for j := i; j < i+16; j++ { + fmt.Printf("%02X ", buffer[j]) + } + for j := i; j < i+16; j++ { + c := buffer[j] & 127 + if c >= 32 { + fmt.Printf("%c", c) + } else { + fmt.Printf(".") + } + } + fmt.Printf("\n") + } +} diff --git a/prodos/readblock.go b/prodos/readblock.go new file mode 100644 index 0000000..3397c28 --- /dev/null +++ b/prodos/readblock.go @@ -0,0 +1,15 @@ +package prodos + +import ( + "os" +) + +func ReadBlock(file *os.File, block int) []byte { + buffer := make([]byte, 512) + + //fmt.Printf("Read block %d\n", block) + + file.ReadAt(buffer, int64(block)*512) + + return buffer +} diff --git a/prodos/text.go b/prodos/text.go new file mode 100644 index 0000000..6ffc818 --- /dev/null +++ b/prodos/text.go @@ -0,0 +1,105 @@ +package prodos + +import ( + "fmt" + "strings" + "time" +) + +func TimeToString(printTime time.Time) string { + return fmt.Sprintf("%04d-%s-%02d %02d:%02d", + printTime.Year(), + strings.ToUpper(printTime.Month().String()[0:3]), + printTime.Day(), + printTime.Hour(), + printTime.Minute(), + ) +} + +func FileTypeToString(fileType int) string { + switch fileType { + case 1: + return "BAD" + case 4: + return "TXT" + case 6: + return "BIN" + case 7: + return "FNT" + case 15: + return "DIR" + case 252: + return "BAS" + case 253: + return "VAR" + case 255: + return "SYS" + default: + return fmt.Sprintf("$%02X", fileType) + } + /* + File Type Preferred Use + $00 Typeless file (SOS and ProDOS) + $01 Bad block file + $02 * Pascal code file + $03 * Pascal text file + $04 ASCII text file (SOS and ProDOS) + $05 * Pascal data file + $06 General binary file (SOS and ProDOS) + $07 * Font file + $08 Graphics screen file + $09 * Business BASIC program file + $0A * Business BASIC data file + $0B * Word Processor file + $0C * SOS system file + $0D,$0E * SOS reserved + $0F Directory file (SOS and ProDOS) + $10 * RPS data file + $11 * RPS index file + $12 * AppleFile discard file + $13 * AppleFile model file + $14 * AppleFile report format file + $15 * Screen Library file + $16-$18 * SOS reserved + $19 AppleWorks Data Base file + $1A AppleWorks Word Processor file + $1B AppleWorks Spreadsheet file + $1C-$EE Reserved + $EF Pascal area + $F0 ProDOS CI added command file + $F1-$F8 ProDOS user defined files 1-8 + $F9 ProDOS reserved + $FA Integer BASIC program file + $FB Integer BASIC variable file + $FC Applesoft program file + $FD Applesoft variables file + $FE Relocatable code file (EDASM) + $FF ProDOS system file + */ +} + +func DumpFileEntry(fileEntry FileEntry) { + fmt.Printf("FileName: %s\n", fileEntry.FileName) + fmt.Printf("Creation time: %d-%s-%d %02d:%02d\n", fileEntry.CreationTime.Year(), fileEntry.CreationTime.Month(), fileEntry.CreationTime.Day(), fileEntry.CreationTime.Hour(), fileEntry.CreationTime.Minute()) + fmt.Printf("Modified time: %d-%s-%d %02d:%02d\n", fileEntry.ModifiedTime.Year(), fileEntry.ModifiedTime.Month(), fileEntry.ModifiedTime.Day(), fileEntry.ModifiedTime.Hour(), fileEntry.ModifiedTime.Minute()) + 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("File type: %02X\n", fileEntry.FileType) + fmt.Printf("Storage type: %02X\n", fileEntry.StorageType) + fmt.Printf("\n") +} + +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("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) + fmt.Printf("Bitmap starting block: %d\n", volumeHeader.BitmapStartBlock) + fmt.Printf("Total blocks: %d\n", volumeHeader.TotalBlocks) +} diff --git a/prodos/time.go b/prodos/time.go new file mode 100644 index 0000000..199fce8 --- /dev/null +++ b/prodos/time.go @@ -0,0 +1,58 @@ +package prodos + +import ( + "fmt" + "time" +) + +/* 49041 ($BF91) 49040 ($BF90) + + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +DATE: | year | month | day | + +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +TIME: | hour | | minute | + +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + + 49043 ($BF93) 49042 ($BF92) +*/ + +func DateTimeToProDOS(dateTime time.Time) []byte { + fmt.Printf("Sending date/time...\n") + + year := dateTime.Year() % 100 + month := dateTime.Month() + day := dateTime.Day() + hour := dateTime.Hour() + minute := dateTime.Minute() + + buffer := make([]byte, 4) + buffer[0] = ((byte(month) & 15) << 5) + byte(day) + buffer[1] = (byte(year) << 1) + (byte(month) >> 3) + buffer[2] = byte(minute) + buffer[3] = byte(hour) + + return buffer +} + +func DateTimeFromProDOS(buffer []byte) time.Time { + twoDigitYear := buffer[1] >> 1 + var year int + if twoDigitYear < 76 { + year = 2000 + int(twoDigitYear) + } else { + year = 1900 + int(twoDigitYear) + } + + month := int(buffer[0]>>5 + buffer[1]&1) + day := int(buffer[0] & 23) + hour := int(buffer[3]) + minute := int(buffer[2]) + + parsedTime := time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.Local) + + return parsedTime +} diff --git a/prodos/writeBlock.go b/prodos/writeBlock.go new file mode 100644 index 0000000..cb187ec --- /dev/null +++ b/prodos/writeBlock.go @@ -0,0 +1,15 @@ +package prodos + +import ( + "fmt" + "os" +) + +func WriteBlock(file *os.File, block int, buffer []byte) { + + fmt.Printf("Write block %d\n", block) + + file.WriteAt(buffer, int64(block)*512) + file.Sync() + fmt.Printf("Write block completed\n") +}