mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-12-28 11:32:05 +00:00
Initial commit with directory read
This commit is contained in:
parent
4348f912e4
commit
6fd22018e3
@ -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.
|
42
main.go
Normal file
42
main.go
Normal 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
195
prodos/directory.go
Normal 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
20
prodos/dumpBlock.go
Normal 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
15
prodos/readblock.go
Normal 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
105
prodos/text.go
Normal 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
58
prodos/time.go
Normal 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
15
prodos/writeBlock.go
Normal 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")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user