mirror of
https://github.com/tjboldt/ProDOS-Utilities.git
synced 2024-12-27 05:29:49 +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