Initial commit with directory read

This commit is contained in:
Terence Boldt 2021-06-05 23:22:30 -04:00
parent 4348f912e4
commit 6fd22018e3
9 changed files with 455 additions and 1 deletions

View File

@ -1 +1,2 @@
# ProDOS-Utilities
# 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.

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/tjboldt/ProDOS-Utilities
go 1.16

42
main.go Normal file
View File

@ -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")
}

195
prodos/directory.go Normal file
View File

@ -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
}

20
prodos/dumpBlock.go Normal file
View File

@ -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")
}
}

15
prodos/readblock.go Normal file
View File

@ -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
}

105
prodos/text.go Normal file
View File

@ -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)
}

58
prodos/time.go Normal file
View File

@ -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
}

15
prodos/writeBlock.go Normal file
View File

@ -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")
}