ProDOS-Utilities/prodos/directory.go

449 lines
13 KiB
Go
Raw Normal View History

// Copyright Terence J. Boldt (c)2021-2023
// 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 {
2023-01-19 05:30:17 +00:00
PreviousBlock int
NextBlock int
Name string
CreationTime time.Time
Version int
MinVersion int
Access int
EntryLength int
EntriesPerBlock int
ActiveFileCount int
StartingBlock int
ParentBlock int
ParentEntry int
ParentEntryLength int
2021-06-06 03:22:30 +00:00
}
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
}
2023-01-19 05:30:17 +00:00
func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
parentPath, newDirectory := GetDirectoryAndFileNameFromPath(path)
existingFileEntry, _ := GetFileEntry(readerWriter, path)
if existingFileEntry.StorageType != StorageDeleted {
//DeleteFile(readerWriter, path)
return errors.New("directory already exists")
}
fileEntry, err := getFreeFileEntryInDirectory(readerWriter, parentPath)
if err != nil {
errString := fmt.Sprintf("failed to create directory: %s", err)
return errors.New(errString)
}
// get list of blocks to write file to
blockList, err := createBlockList(readerWriter, 512)
if err != nil {
errString := fmt.Sprintf("failed to create directory: %s", err)
return errors.New(errString)
}
updateVolumeBitmap(readerWriter, blockList)
fileEntry.FileName = newDirectory
fileEntry.BlocksUsed = 1
fileEntry.CreationTime = time.Now()
fileEntry.ModifiedTime = time.Now()
fileEntry.AuxType = 0
fileEntry.EndOfFile = 0x200
fileEntry.FileType = 0x0F
fileEntry.KeyPointer = blockList[0]
fileEntry.Access = 0b11100011
fileEntry.StorageType = StorageDirectory
writeFileEntry(readerWriter, fileEntry)
err = incrementFileCount(readerWriter, fileEntry)
if err != nil {
errString := fmt.Sprintf("failed to create directory: %s", err)
return errors.New(errString)
}
directoryEntry := DirectoryHeader{
PreviousBlock: 0,
NextBlock: 0,
Name: newDirectory,
CreationTime: time.Now(),
Version: 0x24,
MinVersion: 0,
Access: 0xE3,
EntryLength: 0x27,
EntriesPerBlock: 0x0D,
ActiveFileCount: 0,
StartingBlock: blockList[0],
ParentBlock: fileEntry.DirectoryBlock,
ParentEntry: fileEntry.DirectoryOffset,
ParentEntryLength: 0x27,
}
err = writeDirectoryHeader(readerWriter, directoryEntry)
if err != nil {
errString := fmt.Sprintf("failed to create directory: %s", err)
return errors.New(errString)
}
return nil
}
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 minVersion > 0 {
2021-06-06 03:22:30 +00:00
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])
2023-01-19 05:30:17 +00:00
creationTime := DateTimeFromProDOS(buffer[0x1C:0x20])
version := int(buffer[0x20])
minVersion := int(buffer[0x21])
access := int(buffer[0x22])
entryLength := int(buffer[0x23])
entriesPerBlock := int(buffer[0x24])
2021-06-12 02:43:35 +00:00
fileCount := int(buffer[0x25]) + int(buffer[0x26])*256
2023-01-19 05:30:17 +00:00
parentBlock := int(buffer[0x27]) + int(buffer[0x28])*256
parentEntry := int(buffer[0x29])
parentEntryLength := int(buffer[0x2A])
2021-06-06 03:22:30 +00:00
directoryEntry := DirectoryHeader{
2023-01-19 05:30:17 +00:00
PreviousBlock: previousBlock,
NextBlock: nextBlock,
StartingBlock: blockNumber,
Name: name,
CreationTime: creationTime,
Version: version,
MinVersion: minVersion,
Access: access,
EntryLength: entryLength,
EntriesPerBlock: entriesPerBlock,
ActiveFileCount: fileCount,
ParentBlock: parentBlock,
ParentEntry: parentEntry,
ParentEntryLength: parentEntryLength,
2021-06-06 03:22:30 +00:00
}
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]
}
2023-01-19 05:30:17 +00:00
creationTime := DateTimeToProDOS(directoryHeader.CreationTime)
for i := 0; i < 4; i++ {
buffer[0x1C+i] = creationTime[i]
}
buffer[0x20] = byte(directoryHeader.Version)
buffer[0x21] = byte(directoryHeader.MinVersion)
buffer[0x22] = byte(directoryHeader.Access)
buffer[0x23] = byte(directoryHeader.EntryLength)
buffer[0x24] = byte(directoryHeader.EntriesPerBlock)
2021-06-12 02:43:35 +00:00
buffer[0x25] = byte(directoryHeader.ActiveFileCount & 0x00FF)
buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8)
2023-01-19 05:30:17 +00:00
buffer[0x27] = byte(directoryHeader.ParentBlock & 0x00FF)
buffer[0x28] = byte(directoryHeader.ParentBlock >> 8)
buffer[0x29] = byte(directoryHeader.ParentEntry)
buffer[0x2A] = byte(directoryHeader.ParentEntryLength)
WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer)
return nil
2021-06-12 02:43:35 +00:00
}