ProDOS-Utilities/prodos/directory.go

340 lines
10 KiB
Go
Raw Normal View History

// Copyright Terence J. Boldt (c)2021-2022
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// This file provides access to read, write, delete
// fand parse directories on a ProDOS drive image
2021-06-06 03:22:30 +00:00
package prodos
import (
2021-06-30 04:04:59 +00:00
"errors"
2021-06-06 03:22:30 +00:00
"fmt"
"io"
2021-06-06 03:22:30 +00:00
"strings"
"time"
)
2022-01-23 22:30:18 +00:00
// VolumeHeader from ProDOS
2021-06-06 03:22:30 +00:00
type VolumeHeader struct {
VolumeName string
CreationTime time.Time
ActiveFileCount int
BitmapStartBlock int
TotalBlocks int
NextBlock int
EntryLength int
EntriesPerBlock int
2021-06-12 02:43:35 +00:00
MinVersion int
Version int
2021-06-06 03:22:30 +00:00
}
2022-01-23 22:30:18 +00:00
// DirectoryHeader from ProDOS
2021-06-06 03:22:30 +00:00
type DirectoryHeader struct {
Name string
ActiveFileCount int
2021-06-26 01:15:20 +00:00
StartingBlock int
PreviousBlock int
2021-06-06 03:22:30 +00:00
NextBlock int
}
const (
2022-01-23 23:57:16 +00:00
// StorageDeleted signifies file is deleted
2022-01-23 23:51:44 +00:00
StorageDeleted = 0
2022-01-23 23:57:16 +00:00
// StorageSeedling signifies file is <= 512 bytes
2022-01-23 23:51:44 +00:00
StorageSeedling = 1
2022-01-23 23:57:16 +00:00
// StorageSapling signifies file is > 512 bytes and <= 128 KB
2022-01-23 23:51:44 +00:00
StorageSapling = 2
2022-01-23 23:57:16 +00:00
// StorageTree signifies file is > 128 KB and <= 16 MB
2022-01-23 23:51:44 +00:00
StorageTree = 3
2022-01-23 23:57:16 +00:00
// StoragePascal signifies pascal storage area
2022-01-23 23:51:44 +00:00
StoragePascal = 4
2022-01-23 23:57:16 +00:00
// StorageDirectory signifies directory
2021-06-06 03:22:30 +00:00
StorageDirectory = 13
)
2022-01-23 22:30:18 +00:00
// FileEntry from ProDOS
2021-06-06 03:22:30 +00:00
type FileEntry struct {
2021-06-12 02:43:35 +00:00
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
2021-06-06 03:22:30 +00:00
}
2022-01-23 22:30:18 +00:00
// ReadDirectory reads the directory information from a specified path
// on a ProDOS image
func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHeader, []FileEntry, error) {
buffer, err := ReadBlock(reader, 2)
if err != nil {
return VolumeHeader{}, DirectoryHeader{}, nil, err
}
2021-06-06 03:22:30 +00:00
2021-06-06 12:00:20 +00:00
volumeHeader := parseVolumeHeader(buffer)
2021-06-06 03:22:30 +00:00
if len(path) == 0 {
path = fmt.Sprintf("/%s", volumeHeader.VolumeName)
}
path = strings.ToUpper(path)
paths := strings.Split(path, "/")
directoryHeader, fileEntries, err := getFileEntriesInDirectory(reader, 2, 1, paths)
if err != nil {
return VolumeHeader{}, DirectoryHeader{}, nil, err
}
2021-06-06 03:22:30 +00:00
return volumeHeader, directoryHeader, fileEntries, nil
2021-06-06 03:22:30 +00:00
}
func getFreeFileEntryInDirectory(reader io.ReaderAt, directory string) (FileEntry, error) {
_, directoryHeader, _, err := ReadDirectory(reader, directory)
if err != nil {
return FileEntry{}, err
}
2021-06-30 04:04:59 +00:00
//DumpDirectoryHeader(directoryHeader)
2021-06-26 01:15:20 +00:00
blockNumber := directoryHeader.StartingBlock
buffer, err := ReadBlock(reader, blockNumber)
if err != nil {
return FileEntry{}, err
}
2021-06-26 01:15:20 +00:00
entryOffset := 43 // start at offset after header
entryNumber := 2 // header is essentially the first entry so start at 2
for {
if entryNumber > 13 {
blockNumber = int(buffer[2]) + int(buffer[3])*256
// if we ran out of blocks in the directory, return empty
// TODO: expand the directory to add more entries
if blockNumber == 0 {
return FileEntry{}, errors.New("no free file entries found")
2021-06-26 01:15:20 +00:00
}
// else read the next block in the directory
buffer, err = ReadBlock(reader, blockNumber)
if err != nil {
return FileEntry{}, nil
}
2021-06-26 01:15:20 +00:00
entryOffset = 4
entryNumber = 1
}
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
if fileEntry.StorageType == StorageDeleted {
fileEntry.DirectoryBlock = blockNumber
fileEntry.DirectoryOffset = entryOffset
fileEntry.HeaderPointer = directoryHeader.StartingBlock
2021-06-30 04:04:59 +00:00
return fileEntry, nil
2021-06-26 01:15:20 +00:00
}
entryNumber++
entryOffset += 39
}
}
func getFileEntriesInDirectory(reader io.ReaderAt, blockNumber int, currentPath int, paths []string) (DirectoryHeader, []FileEntry, error) {
buffer, err := ReadBlock(reader, blockNumber)
if err != nil {
return DirectoryHeader{}, nil, err
}
2021-06-06 03:22:30 +00:00
2021-06-26 01:15:20 +00:00
directoryHeader := parseDirectoryHeader(buffer, blockNumber)
2021-06-06 03:22:30 +00:00
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 DirectoryHeader{}, nil, errors.New("path not matched")
2021-06-06 03:22:30 +00:00
}
for {
if entryNumber > 13 {
entryOffset = 4
entryNumber = 1
if blockNumber == 0 {
return DirectoryHeader{}, nil, nil
}
buffer, err = ReadBlock(reader, nextBlock)
if err != nil {
return DirectoryHeader{}, nil, err
2021-06-06 03:22:30 +00:00
}
nextBlock = int(buffer[2]) + int(buffer[3])*256
}
2021-06-12 02:43:35 +00:00
fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)
2021-06-06 03:22:30 +00:00
if fileEntry.StorageType != StorageDeleted {
2021-06-26 01:15:20 +00:00
if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
return directoryHeader, fileEntries[0:activeEntries], nil
2021-06-26 01:15:20 +00:00
}
2021-06-06 03:22:30 +00:00
if matchedDirectory {
fileEntries[activeEntries] = fileEntry
} else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName {
return getFileEntriesInDirectory(reader, fileEntry.KeyPointer, currentPath+1, paths)
2021-06-06 03:22:30 +00:00
}
activeEntries++
}
entryNumber++
entryOffset += 39
}
}
2021-06-12 02:43:35 +00:00
func parseFileEntry(buffer []byte, blockNumber int, entryOffset int) FileEntry {
2021-06-06 03:22:30 +00:00
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])
2021-06-12 02:43:35 +00:00
version := int(buffer[28])
minVersion := int(buffer[29])
2021-06-06 03:22:30 +00:00
access := int(buffer[30])
auxType := int(buffer[31]) + int(buffer[32])*256
modifiedTime := DateTimeFromProDOS((buffer[33:37]))
2021-06-12 02:43:35 +00:00
headerPointer := int(buffer[0x25]) + int(buffer[0x26])*256
2021-06-06 03:22:30 +00:00
fileEntry := FileEntry{
2021-06-12 02:43:35 +00:00
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,
2021-06-06 03:22:30 +00:00
}
return fileEntry
}
func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) {
2021-06-12 02:43:35 +00:00
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 := writer.WriteAt(buffer, int64(fileEntry.DirectoryBlock*512+fileEntry.DirectoryOffset))
2021-06-12 02:43:35 +00:00
if err != nil {
}
}
2021-06-06 12:00:20 +00:00
func parseVolumeHeader(buffer []byte) VolumeHeader {
2021-06-06 03:22:30 +00:00
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,
2021-06-12 02:43:35 +00:00
MinVersion: minVersion,
Version: version,
2021-06-06 03:22:30 +00:00
}
return volumeHeader
}
2021-06-26 01:15:20 +00:00
func parseDirectoryHeader(buffer []byte, blockNumber int) DirectoryHeader {
previousBlock := int(buffer[0x00]) + int(buffer[0x01])*256
2021-06-12 02:43:35 +00:00
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
2021-06-06 03:22:30 +00:00
directoryEntry := DirectoryHeader{
2021-06-26 01:15:20 +00:00
PreviousBlock: previousBlock,
2021-06-06 03:22:30 +00:00
NextBlock: nextBlock,
2021-06-26 01:15:20 +00:00
StartingBlock: blockNumber,
2021-06-06 03:22:30 +00:00
Name: name,
ActiveFileCount: fileCount,
}
return directoryEntry
}
2021-06-12 02:43:35 +00:00
func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader DirectoryHeader) error {
buffer, err := ReadBlock(readerWriter, directoryHeader.StartingBlock)
if err != nil {
return err
}
2021-06-30 12:22:08 +00:00
buffer[0x00] = byte(directoryHeader.PreviousBlock & 0x00FF)
buffer[0x01] = byte(directoryHeader.PreviousBlock >> 8)
2021-06-12 02:43:35 +00:00
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)
WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer)
return nil
2021-06-12 02:43:35 +00:00
}