2017-03-12 21:47:18 +00:00
|
|
|
// Copyright © 2017 Zellyn Hunter <zellyn@gmail.com>
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
// Package prodos contains routines for working with the on-device
|
2017-03-12 21:47:18 +00:00
|
|
|
// structures of Apple ProDOS.
|
|
|
|
package prodos
|
|
|
|
|
|
|
|
import (
|
2017-03-14 02:20:29 +00:00
|
|
|
"encoding/binary"
|
2017-03-12 21:47:18 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
|
2017-03-18 02:26:15 +00:00
|
|
|
"github.com/zellyn/diskii/lib/disk"
|
|
|
|
)
|
2017-03-12 21:47:18 +00:00
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
// Storage types.
|
|
|
|
const (
|
|
|
|
TypeDeleted = 0
|
|
|
|
TypeSeedling = 0x1
|
|
|
|
TypeSapling = 0x2
|
|
|
|
TypeTree = 0x3
|
|
|
|
TypeSubdirectory = 0xD
|
|
|
|
TypeSubdirectoryHeader = 0xE
|
|
|
|
TypeVolumeDirectoryHeader = 0xF
|
|
|
|
)
|
|
|
|
|
2017-03-25 01:52:57 +00:00
|
|
|
// blockBase represents a 512-byte block of data.
|
|
|
|
type blockBase struct {
|
|
|
|
block uint16 // Block index this data was loaded from.
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetBlock gets the block index from a blockBase.
|
|
|
|
func (bb blockBase) GetBlock() uint16 {
|
|
|
|
return bb.block
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetBlock sets the block index of a blockBase.
|
|
|
|
func (bb *blockBase) SetBlock(block uint16) {
|
|
|
|
bb.block = block
|
|
|
|
}
|
|
|
|
|
|
|
|
// A bitmapPart is a single block of a volumeBitMap.
|
|
|
|
type bitmapPart struct {
|
|
|
|
blockBase
|
|
|
|
data disk.Block
|
|
|
|
}
|
2017-03-12 21:47:18 +00:00
|
|
|
|
2017-03-30 02:24:55 +00:00
|
|
|
// Ensure that bitmapPart is valid BlockSource and BlockSink.
|
|
|
|
var _ disk.BlockSource = (*bitmapPart)(nil)
|
|
|
|
var _ disk.BlockSink = (*bitmapPart)(nil)
|
|
|
|
|
2017-03-25 01:52:57 +00:00
|
|
|
// FromBlock unmarshals a bitmapPart from a Block.
|
|
|
|
func (bp *bitmapPart) FromBlock(block disk.Block) error {
|
|
|
|
bp.data = block
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToBlock marshals a bitmapPart struct to a block.
|
|
|
|
func (bp bitmapPart) ToBlock() (disk.Block, error) {
|
|
|
|
return bp.data, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type VolumeBitMap []bitmapPart
|
|
|
|
|
|
|
|
func NewVolumeBitMap(startBlock uint16, blocks uint16) VolumeBitMap {
|
|
|
|
vbm := VolumeBitMap(make([]bitmapPart, (blocks+(512*8)-1)/(512*8)))
|
|
|
|
for i := range vbm {
|
|
|
|
vbm[i].SetBlock(startBlock + uint16(i))
|
|
|
|
}
|
2017-03-12 21:47:18 +00:00
|
|
|
for b := 0; b < int(blocks); b++ {
|
|
|
|
vbm.MarkUnused(uint16(b))
|
|
|
|
}
|
|
|
|
return vbm
|
|
|
|
}
|
|
|
|
|
|
|
|
func (vbm VolumeBitMap) MarkUsed(block uint16) {
|
|
|
|
vbm.mark(block, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (vbm VolumeBitMap) MarkUnused(block uint16) {
|
|
|
|
vbm.mark(block, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (vbm VolumeBitMap) mark(block uint16, set bool) {
|
|
|
|
byteIndex := block >> 3
|
|
|
|
blockIndex := byteIndex / 512
|
|
|
|
blockByteIndex := byteIndex % 512
|
|
|
|
bit := byte(1 << (7 - (block & 7)))
|
|
|
|
if set {
|
2017-03-25 01:52:57 +00:00
|
|
|
vbm[blockIndex].data[blockByteIndex] |= bit
|
2017-03-12 21:47:18 +00:00
|
|
|
} else {
|
2017-03-25 01:52:57 +00:00
|
|
|
vbm[blockIndex].data[blockByteIndex] &^= bit
|
2017-03-12 21:47:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// IsFree returns true if the given block on the device is free,
|
|
|
|
// according to the VolumeBitMap.
|
|
|
|
func (vbm VolumeBitMap) IsFree(block uint16) bool {
|
2017-03-12 21:47:18 +00:00
|
|
|
byteIndex := block >> 3
|
|
|
|
blockIndex := byteIndex / 512
|
|
|
|
blockByteIndex := byteIndex % 512
|
|
|
|
bit := byte(1 << (7 - (block & 7)))
|
2017-03-25 01:52:57 +00:00
|
|
|
return vbm[blockIndex].data[blockByteIndex]&bit > 0
|
2017-03-12 21:47:18 +00:00
|
|
|
}
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
// readVolumeBitMap reads the entire volume bitmap from a block
|
2017-03-30 02:24:55 +00:00
|
|
|
// device.
|
2017-04-13 03:22:45 +00:00
|
|
|
func readVolumeBitMap(bd disk.BlockDevice, startBlock uint16) (VolumeBitMap, error) {
|
2017-03-12 21:47:18 +00:00
|
|
|
blocks := bd.Blocks() / 4096
|
2017-03-25 01:52:57 +00:00
|
|
|
vbm := NewVolumeBitMap(startBlock, blocks)
|
|
|
|
for i := 0; i < len(vbm); i++ {
|
|
|
|
if err := disk.UnmarshalBlock(bd, &vbm[i], vbm[i].GetBlock()); err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot read block %d (device block %d) of Volume Bit Map: %v", i, vbm[i].GetBlock(), err)
|
2017-03-12 21:47:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return VolumeBitMap(vbm), nil
|
|
|
|
}
|
|
|
|
|
2017-03-25 01:52:57 +00:00
|
|
|
// Write writes the Volume Bit Map to a block device.
|
|
|
|
func (vbm VolumeBitMap) Write(bd disk.BlockDevice) error {
|
|
|
|
for i, bp := range vbm {
|
|
|
|
if err := disk.MarshalBlock(bd, bp); err != nil {
|
|
|
|
return fmt.Errorf("cannot write block %d (device block %d) of Volume Bit Map: %v", i, bp.GetBlock(), err)
|
2017-03-12 21:47:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// DateTime represents the 4-byte ProDOS y/m/d h/m timestamp.
|
2017-03-12 21:47:18 +00:00
|
|
|
type DateTime struct {
|
|
|
|
YMD [2]byte
|
|
|
|
HM [2]byte
|
|
|
|
}
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// toBytes returns a four-byte slice representing a DateTime.
|
|
|
|
func (dt DateTime) toBytes() []byte {
|
|
|
|
return []byte{dt.YMD[0], dt.YMD[1], dt.HM[0], dt.HM[1]}
|
|
|
|
}
|
|
|
|
|
|
|
|
// fromBytes turns a slice of four bytes back into a DateTime.
|
2017-03-15 01:27:02 +00:00
|
|
|
func (dt *DateTime) fromBytes(b []byte) {
|
2017-03-14 02:20:29 +00:00
|
|
|
if len(b) != 4 {
|
2017-03-15 01:27:02 +00:00
|
|
|
panic(fmt.Sprintf("DateTime expects 4 bytes; got %d", len(b)))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
dt.YMD[0] = b[0]
|
|
|
|
dt.YMD[1] = b[1]
|
|
|
|
dt.HM[0] = b[2]
|
|
|
|
dt.HM[1] = b[3]
|
2017-03-15 01:27:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate checks a DateTime for problems, returning a slice of errors
|
|
|
|
func (dt DateTime) Validate(fieldDescription string) (errors []error) {
|
|
|
|
if dt.HM[0] >= 24 {
|
|
|
|
errors = append(errors, fmt.Errorf("%s expects hour<24; got %d", fieldDescription, dt.HM[0]))
|
|
|
|
}
|
|
|
|
if dt.HM[1] >= 60 {
|
|
|
|
errors = append(errors, fmt.Errorf("%s expects minute<60; got %x", fieldDescription, dt.HM[1]))
|
|
|
|
}
|
|
|
|
return errors
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-03-12 21:47:18 +00:00
|
|
|
// VolumeDirectoryKeyBlock is the struct used to hold the ProDOS Volume Directory Key
|
|
|
|
// Block structure. See page 4-4 of Beneath Apple ProDOS.
|
|
|
|
type VolumeDirectoryKeyBlock struct {
|
2017-03-30 02:24:55 +00:00
|
|
|
blockBase
|
2017-03-12 21:47:18 +00:00
|
|
|
Prev uint16 // Pointer to previous block (always zero: the KeyBlock is the first Volume Directory block
|
|
|
|
Next uint16 // Pointer to next block in the Volume Directory
|
|
|
|
Header VolumeDirectoryHeader
|
|
|
|
Descriptors [12]FileDescriptor
|
2017-03-15 01:27:02 +00:00
|
|
|
Extra byte // Trailing byte (so we don't lose it)
|
2017-03-12 21:47:18 +00:00
|
|
|
}
|
|
|
|
|
2017-03-30 02:24:55 +00:00
|
|
|
// Ensure that VolumeDirectoryKeyBlock is valid BlockSource and BlockSink.
|
|
|
|
var _ disk.BlockSource = (*VolumeDirectoryKeyBlock)(nil)
|
|
|
|
var _ disk.BlockSink = (*VolumeDirectoryKeyBlock)(nil)
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// ToBlock marshals the VolumeDirectoryKeyBlock to a Block of bytes.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (vdkb VolumeDirectoryKeyBlock) ToBlock() (disk.Block, error) {
|
2017-03-18 02:26:15 +00:00
|
|
|
var block disk.Block
|
2017-03-14 02:20:29 +00:00
|
|
|
binary.LittleEndian.PutUint16(block[0x0:0x2], vdkb.Prev)
|
|
|
|
binary.LittleEndian.PutUint16(block[0x2:0x4], vdkb.Next)
|
|
|
|
copyBytes(block[0x04:0x02b], vdkb.Header.toBytes())
|
|
|
|
for i, desc := range vdkb.Descriptors {
|
|
|
|
copyBytes(block[0x2b+i*0x27:0x2b+(i+1)*0x27], desc.toBytes())
|
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
block[511] = vdkb.Extra
|
2017-03-30 02:24:55 +00:00
|
|
|
return block, nil
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FromBlock unmarshals a Block of bytes into a VolumeDirectoryKeyBlock.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (vdkb *VolumeDirectoryKeyBlock) FromBlock(block disk.Block) error {
|
2017-03-14 02:20:29 +00:00
|
|
|
vdkb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
|
2017-03-15 01:27:02 +00:00
|
|
|
vdkb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
|
|
|
|
vdkb.Header.fromBytes(block[0x04:0x2b])
|
|
|
|
for i := range vdkb.Descriptors {
|
|
|
|
vdkb.Descriptors[i].fromBytes(block[0x2b+i*0x27 : 0x2b+(i+1)*0x27])
|
|
|
|
}
|
|
|
|
vdkb.Extra = block[511]
|
2017-03-30 02:24:55 +00:00
|
|
|
return nil
|
2017-03-15 01:27:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates a VolumeDirectoryKeyBlock for valid values.
|
|
|
|
func (vdkb VolumeDirectoryKeyBlock) Validate() (errors []error) {
|
2017-03-14 02:20:29 +00:00
|
|
|
if vdkb.Prev != 0 {
|
2017-03-15 01:27:02 +00:00
|
|
|
errors = append(errors, fmt.Errorf("Volume Directory Key Block should have a `Previous` block of 0, got $%04x", vdkb.Prev))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
errors = append(errors, vdkb.Header.Validate()...)
|
|
|
|
for _, desc := range vdkb.Descriptors {
|
|
|
|
errors = append(errors, desc.Validate()...)
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
if vdkb.Extra != 0 {
|
|
|
|
errors = append(errors, fmt.Errorf("Expected last byte of Volume Directory Key Block == 0x0; got 0x%02x", vdkb.Extra))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
return errors
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-03-12 21:47:18 +00:00
|
|
|
// VolumeDirectoryBlock is a normal (non-key) segment in the Volume Directory Header.
|
|
|
|
type VolumeDirectoryBlock struct {
|
2017-03-30 02:24:55 +00:00
|
|
|
blockBase
|
2017-03-12 21:47:18 +00:00
|
|
|
Prev uint16 // Pointer to previous block in the Volume Directory.
|
|
|
|
Next uint16 // Pointer to next block in the Volume Directory.
|
|
|
|
Descriptors [13]FileDescriptor
|
2017-03-15 01:27:02 +00:00
|
|
|
Extra byte // Trailing byte (so we don't lose it)
|
2017-03-12 21:47:18 +00:00
|
|
|
}
|
|
|
|
|
2017-03-30 02:24:55 +00:00
|
|
|
// Ensure that VolumeDirectoryBlock is valid BlockSource and BlockSink.
|
|
|
|
var _ disk.BlockSource = (*VolumeDirectoryBlock)(nil)
|
|
|
|
var _ disk.BlockSink = (*VolumeDirectoryBlock)(nil)
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// ToBlock marshals a VolumeDirectoryBlock to a Block of bytes.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (vdb VolumeDirectoryBlock) ToBlock() (disk.Block, error) {
|
2017-03-18 02:26:15 +00:00
|
|
|
var block disk.Block
|
2017-03-14 02:20:29 +00:00
|
|
|
binary.LittleEndian.PutUint16(block[0x0:0x2], vdb.Prev)
|
|
|
|
binary.LittleEndian.PutUint16(block[0x2:0x4], vdb.Next)
|
|
|
|
for i, desc := range vdb.Descriptors {
|
|
|
|
copyBytes(block[0x04+i*0x27:0x04+(i+1)*0x27], desc.toBytes())
|
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
block[511] = vdb.Extra
|
2017-03-30 02:24:55 +00:00
|
|
|
return block, nil
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FromBlock unmarshals a Block of bytes into a VolumeDirectoryBlock.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (vdb *VolumeDirectoryBlock) FromBlock(block disk.Block) error {
|
2017-03-14 02:20:29 +00:00
|
|
|
vdb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
|
|
|
|
vdb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
|
|
|
|
for i := range vdb.Descriptors {
|
2017-03-15 01:27:02 +00:00
|
|
|
vdb.Descriptors[i].fromBytes(block[0x4+i*0x27 : 0x4+(i+1)*0x27])
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
vdb.Extra = block[511]
|
2017-03-30 02:24:55 +00:00
|
|
|
return nil
|
2017-03-15 01:27:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates a VolumeDirectoryBlock for valid values.
|
|
|
|
func (vdb VolumeDirectoryBlock) Validate() (errors []error) {
|
|
|
|
for _, desc := range vdb.Descriptors {
|
|
|
|
errors = append(errors, desc.Validate()...)
|
|
|
|
}
|
|
|
|
if vdb.Extra != 0 {
|
|
|
|
errors = append(errors, fmt.Errorf("Expected last byte of Volume Directory Block == 0x0; got 0x%02x", vdb.Extra))
|
|
|
|
}
|
|
|
|
return errors
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-03-12 21:47:18 +00:00
|
|
|
type VolumeDirectoryHeader struct {
|
|
|
|
TypeAndNameLength byte // Storage type (top four bits) and volume name length (lower four).
|
|
|
|
VolumeName [15]byte // Volume name (actual length defined in TypeAndNameLength)
|
|
|
|
Unused1 [8]byte
|
|
|
|
Creation DateTime // Date and time volume was formatted
|
|
|
|
Version byte
|
|
|
|
MinVersion byte
|
2017-03-14 02:20:29 +00:00
|
|
|
Access Access
|
2017-03-12 21:47:18 +00:00
|
|
|
EntryLength byte // Length of each entry in the Volume Directory: usually $27
|
|
|
|
EntriesPerBlock byte // Usually $0D
|
|
|
|
FileCount uint16 // Number of active entries in the Volume Directory, not counting the Volume Directory Header
|
|
|
|
BitMapPointer uint16 // Block number of start of VolumeBitMap. Usually 6
|
|
|
|
TotalBlocks uint16 // Total number of blocks on the device. $118 (280) for a 35-track diskette.
|
|
|
|
}
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// toBytes converts a VolumeDirectoryHeader to a slice of bytes.
|
|
|
|
func (vdh VolumeDirectoryHeader) toBytes() []byte {
|
|
|
|
buf := make([]byte, 0x27)
|
|
|
|
buf[0] = vdh.TypeAndNameLength
|
|
|
|
copyBytes(buf[1:0x10], vdh.VolumeName[:])
|
|
|
|
copyBytes(buf[0x10:0x18], vdh.Unused1[:])
|
|
|
|
copyBytes(buf[0x18:0x1c], vdh.Creation.toBytes())
|
|
|
|
buf[0x1c] = vdh.Version
|
|
|
|
buf[0x1d] = vdh.MinVersion
|
|
|
|
buf[0x1e] = byte(vdh.Access)
|
|
|
|
buf[0x1f] = vdh.EntryLength
|
|
|
|
buf[0x20] = vdh.EntriesPerBlock
|
2017-03-15 01:27:02 +00:00
|
|
|
binary.LittleEndian.PutUint16(buf[0x21:0x23], vdh.FileCount)
|
|
|
|
binary.LittleEndian.PutUint16(buf[0x23:0x25], vdh.BitMapPointer)
|
|
|
|
binary.LittleEndian.PutUint16(buf[0x25:0x27], vdh.TotalBlocks)
|
2017-03-14 02:20:29 +00:00
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
|
|
|
// fromBytes unmarshals a slice of bytes into a VolumeDirectoryHeader.
|
2017-03-15 01:27:02 +00:00
|
|
|
func (vdh *VolumeDirectoryHeader) fromBytes(buf []byte) {
|
2017-03-14 02:20:29 +00:00
|
|
|
if len(buf) != 0x27 {
|
2017-03-15 01:27:02 +00:00
|
|
|
panic(fmt.Sprintf("VolumeDirectoryHeader should be 0x27 bytes long; got 0x%02x", len(buf)))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
vdh.TypeAndNameLength = buf[0]
|
|
|
|
copyBytes(vdh.VolumeName[:], buf[1:0x10])
|
|
|
|
copyBytes(vdh.Unused1[:], buf[0x10:0x18])
|
2017-03-15 01:27:02 +00:00
|
|
|
vdh.Creation.fromBytes(buf[0x18:0x1c])
|
2017-03-14 02:20:29 +00:00
|
|
|
vdh.Version = buf[0x1c]
|
|
|
|
vdh.MinVersion = buf[0x1d]
|
|
|
|
vdh.Access = Access(buf[0x1e])
|
|
|
|
vdh.EntryLength = buf[0x1f]
|
|
|
|
vdh.EntriesPerBlock = buf[0x20]
|
2017-03-15 01:27:02 +00:00
|
|
|
vdh.FileCount = binary.LittleEndian.Uint16(buf[0x21:0x23])
|
|
|
|
vdh.BitMapPointer = binary.LittleEndian.Uint16(buf[0x23:0x25])
|
|
|
|
vdh.TotalBlocks = binary.LittleEndian.Uint16(buf[0x25:0x27])
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates a VolumeDirectoryHeader for valid values.
|
|
|
|
func (vdh VolumeDirectoryHeader) Validate() (errors []error) {
|
|
|
|
errors = append(errors, vdh.Creation.Validate("creation date/time of VolumeDirectoryHeader")...)
|
|
|
|
return errors
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-03-12 21:47:18 +00:00
|
|
|
type Access byte
|
|
|
|
|
|
|
|
const (
|
|
|
|
AccessReadable Access = 0x01
|
|
|
|
AccessWritable Access = 0x02
|
|
|
|
AccessChangedSinceBackup Access = 0x20
|
|
|
|
AccessRenamable Access = 0x40
|
|
|
|
AccessDestroyable Access = 0x80
|
|
|
|
)
|
|
|
|
|
|
|
|
// FileDescriptor is the entry in the volume directory for a file or
|
|
|
|
// subdirectory.
|
|
|
|
type FileDescriptor struct {
|
|
|
|
TypeAndNameLength byte // Storage type (top four bits) and volume name length (lower four)
|
|
|
|
FileName [15]byte // Filename (actual length defined in TypeAndNameLength)
|
|
|
|
FileType byte // ProDOS / SOS filetype
|
|
|
|
KeyPointer uint16 // block number of key block for file
|
|
|
|
BlocksUsed uint16 // Total number of blocks used including index blocks and data blocks. For a subdirectory, the number of directory blocks
|
|
|
|
Eof [3]byte // 3-byte offset of EOF from first byte. For sequential files, just the length
|
|
|
|
Creation DateTime // Date and time of of file creation
|
|
|
|
Version byte
|
|
|
|
MinVersion byte
|
|
|
|
Access Access
|
|
|
|
// For TXT files, random access record length (L from OPEN)
|
|
|
|
// For BIN files, load address for binary image (A from BSAVE)
|
|
|
|
// For BAS files, load address for program image (when SAVEd)
|
|
|
|
// For VAR files, address of compressed variables image (when STOREd)
|
|
|
|
// For SYS files, load address for system program (usually $2000)
|
|
|
|
AuxType uint16
|
|
|
|
LastMod DateTime
|
|
|
|
HeaderPointer uint16 // Block number of the key block for the directory which describes this file.
|
|
|
|
}
|
|
|
|
|
2017-04-12 02:36:06 +00:00
|
|
|
// descriptor returns a disk.Descriptor for a FileDescriptor.
|
|
|
|
func (fd FileDescriptor) descriptor() disk.Descriptor {
|
|
|
|
desc := disk.Descriptor{
|
|
|
|
Name: fd.Name(),
|
|
|
|
Blocks: int(fd.BlocksUsed),
|
|
|
|
Length: int(fd.Eof[0]) + int(fd.Eof[1])<<8 + int(fd.Eof[2])<<16,
|
|
|
|
Locked: false, // TODO(zellyn): use prodos-style access in disk.Descriptor
|
|
|
|
Type: disk.Filetype(fd.FileType),
|
|
|
|
}
|
|
|
|
return desc
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:27:02 +00:00
|
|
|
// Name returns the string filename of a file descriptor.
|
|
|
|
func (fd FileDescriptor) Name() string {
|
2017-03-14 02:20:29 +00:00
|
|
|
return string(fd.FileName[0 : fd.TypeAndNameLength&0xf])
|
|
|
|
}
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
// Type returns the type of a file descriptor.
|
|
|
|
func (fd FileDescriptor) Type() byte {
|
|
|
|
return fd.TypeAndNameLength >> 4
|
|
|
|
}
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// toBytes converts a FileDescriptor to a slice of bytes.
|
|
|
|
func (fd FileDescriptor) toBytes() []byte {
|
|
|
|
buf := make([]byte, 0x27)
|
|
|
|
buf[0] = fd.TypeAndNameLength
|
|
|
|
copyBytes(buf[1:0x10], fd.FileName[:])
|
|
|
|
buf[0x10] = fd.FileType
|
|
|
|
binary.LittleEndian.PutUint16(buf[0x11:0x13], fd.KeyPointer)
|
|
|
|
binary.LittleEndian.PutUint16(buf[0x13:0x15], fd.BlocksUsed)
|
|
|
|
copyBytes(buf[0x15:0x18], fd.Eof[:])
|
|
|
|
copyBytes(buf[0x18:0x1c], fd.Creation.toBytes())
|
|
|
|
buf[0x1c] = fd.Version
|
|
|
|
buf[0x1d] = fd.MinVersion
|
|
|
|
buf[0x1e] = byte(fd.Access)
|
|
|
|
binary.LittleEndian.PutUint16(buf[0x1f:0x21], fd.AuxType)
|
|
|
|
copyBytes(buf[0x21:0x25], fd.LastMod.toBytes())
|
|
|
|
binary.LittleEndian.PutUint16(buf[0x25:0x27], fd.HeaderPointer)
|
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
|
|
|
// fromBytes unmarshals a slice of bytes into a FileDescriptor.
|
2017-03-15 01:27:02 +00:00
|
|
|
func (fd *FileDescriptor) fromBytes(buf []byte) {
|
2017-03-14 02:20:29 +00:00
|
|
|
if len(buf) != 0x27 {
|
2017-03-15 01:27:02 +00:00
|
|
|
panic(fmt.Sprintf("FileDescriptor should be 0x27 bytes long; got 0x%02x", len(buf)))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
fd.TypeAndNameLength = buf[0]
|
|
|
|
copyBytes(fd.FileName[:], buf[1:0x10])
|
|
|
|
fd.FileType = buf[0x10]
|
|
|
|
fd.KeyPointer = binary.LittleEndian.Uint16(buf[0x11:0x13])
|
|
|
|
fd.BlocksUsed = binary.LittleEndian.Uint16(buf[0x13:0x15])
|
|
|
|
copyBytes(fd.Eof[:], buf[0x15:0x18])
|
2017-03-15 01:27:02 +00:00
|
|
|
fd.Creation.fromBytes(buf[0x18:0x1c])
|
2017-03-14 02:20:29 +00:00
|
|
|
fd.Version = buf[0x1c]
|
|
|
|
fd.MinVersion = buf[0x1d]
|
|
|
|
fd.Access = Access(buf[0x1e])
|
|
|
|
fd.AuxType = binary.LittleEndian.Uint16(buf[0x1f:0x21])
|
2017-03-15 01:27:02 +00:00
|
|
|
fd.LastMod.fromBytes(buf[0x21:0x25])
|
2017-03-14 02:20:29 +00:00
|
|
|
fd.HeaderPointer = binary.LittleEndian.Uint16(buf[0x25:0x27])
|
2017-03-15 01:27:02 +00:00
|
|
|
}
|
2017-03-14 02:20:29 +00:00
|
|
|
|
2017-03-15 01:27:02 +00:00
|
|
|
// Validate validates a FileDescriptor for valid values.
|
|
|
|
func (fd FileDescriptor) Validate() (errors []error) {
|
|
|
|
errors = append(errors, fd.Creation.Validate(fmt.Sprintf("creation date/time of FileDescriptor %q", fd.Name()))...)
|
|
|
|
errors = append(errors, fd.LastMod.Validate(fmt.Sprintf("last modification date/time of FileDescriptor %q", fd.Name()))...)
|
|
|
|
return errors
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-03-12 21:47:18 +00:00
|
|
|
// An index block contains 256 16-bit block numbers, pointing to other
|
|
|
|
// blocks. The LSBs are stored in the first half, MSBs in the second.
|
2017-03-18 02:26:15 +00:00
|
|
|
type IndexBlock disk.Block
|
2017-03-12 21:47:18 +00:00
|
|
|
|
|
|
|
// Get the blockNum'th block number from an index block.
|
|
|
|
func (i IndexBlock) Get(blockNum byte) uint16 {
|
|
|
|
return uint16(i[blockNum]) + uint16(i[256+int(blockNum)])<<8
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the blockNum'th block number in an index block.
|
|
|
|
func (i IndexBlock) Set(blockNum byte, block uint16) {
|
|
|
|
i[blockNum] = byte(block)
|
|
|
|
i[256+int(blockNum)] = byte(block >> 8)
|
|
|
|
}
|
2017-03-14 02:20:29 +00:00
|
|
|
|
|
|
|
// SubdirectoryKeyBlock is the struct used to hold the first entry in
|
|
|
|
// a subdirectory structure.
|
|
|
|
type SubdirectoryKeyBlock struct {
|
2017-03-30 02:24:55 +00:00
|
|
|
blockBase
|
2017-03-14 02:20:29 +00:00
|
|
|
Prev uint16 // Pointer to previous block (always zero: the KeyBlock is the first Volume Directory block
|
|
|
|
Next uint16 // Pointer to next block in the Volume Directory
|
|
|
|
Header SubdirectoryHeader
|
|
|
|
Descriptors [12]FileDescriptor
|
2017-03-15 01:27:02 +00:00
|
|
|
Extra byte // Trailing byte (so we don't lose it)
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-03-30 02:24:55 +00:00
|
|
|
// Ensure that SubdirectoryKeyBlock is valid BlockSource and BlockSink.
|
|
|
|
var _ disk.BlockSource = (*SubdirectoryKeyBlock)(nil)
|
|
|
|
var _ disk.BlockSink = (*SubdirectoryKeyBlock)(nil)
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// ToBlock marshals the SubdirectoryKeyBlock to a Block of bytes.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (skb SubdirectoryKeyBlock) ToBlock() (disk.Block, error) {
|
2017-03-18 02:26:15 +00:00
|
|
|
var block disk.Block
|
2017-03-14 02:20:29 +00:00
|
|
|
binary.LittleEndian.PutUint16(block[0x0:0x2], skb.Prev)
|
|
|
|
binary.LittleEndian.PutUint16(block[0x2:0x4], skb.Next)
|
|
|
|
copyBytes(block[0x04:0x02b], skb.Header.toBytes())
|
|
|
|
for i, desc := range skb.Descriptors {
|
|
|
|
copyBytes(block[0x2b+i*0x27:0x2b+(i+1)*0x27], desc.toBytes())
|
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
block[511] = skb.Extra
|
2017-03-30 02:24:55 +00:00
|
|
|
return block, nil
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FromBlock unmarshals a Block of bytes into a SubdirectoryKeyBlock.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (skb *SubdirectoryKeyBlock) FromBlock(block disk.Block) error {
|
2017-03-14 02:20:29 +00:00
|
|
|
skb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
|
2017-03-15 01:27:02 +00:00
|
|
|
skb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
|
|
|
|
skb.Header.fromBytes(block[0x04:0x2b])
|
|
|
|
for i := range skb.Descriptors {
|
|
|
|
skb.Descriptors[i].fromBytes(block[0x2b+i*0x27 : 0x2b+(i+1)*0x27])
|
|
|
|
}
|
|
|
|
skb.Extra = block[511]
|
2017-03-30 02:24:55 +00:00
|
|
|
return nil
|
2017-03-15 01:27:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates a SubdirectoryKeyBlock for valid values.
|
|
|
|
func (skb SubdirectoryKeyBlock) Validate() (errors []error) {
|
2017-03-14 02:20:29 +00:00
|
|
|
if skb.Prev != 0 {
|
2017-03-15 01:27:02 +00:00
|
|
|
errors = append(errors, fmt.Errorf("Subdirectory Key Block should have a `Previous` block of 0, got $%04x", skb.Prev))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
errors = append(errors, skb.Header.Validate()...)
|
|
|
|
for _, desc := range skb.Descriptors {
|
|
|
|
errors = append(errors, desc.Validate()...)
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
if skb.Extra != 0 {
|
|
|
|
errors = append(errors, fmt.Errorf("Expected last byte of Subdirectory Key Block == 0x0; got 0x%02x", skb.Extra))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
return errors
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SubdirectoryBlock is a normal (non-key) segment in a Subdirectory.
|
|
|
|
type SubdirectoryBlock struct {
|
2017-03-30 02:24:55 +00:00
|
|
|
blockBase
|
2017-03-14 02:20:29 +00:00
|
|
|
Prev uint16 // Pointer to previous block in the Volume Directory.
|
|
|
|
Next uint16 // Pointer to next block in the Volume Directory.
|
|
|
|
Descriptors [13]FileDescriptor
|
2017-03-15 01:27:02 +00:00
|
|
|
Extra byte // Trailing byte (so we don't lose it)
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-03-30 02:24:55 +00:00
|
|
|
// Ensure that SubdirectoryBlock is valid BlockSource and BlockSink.
|
|
|
|
var _ disk.BlockSource = (*SubdirectoryBlock)(nil)
|
|
|
|
var _ disk.BlockSink = (*SubdirectoryBlock)(nil)
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// ToBlock marshals a SubdirectoryBlock to a Block of bytes.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (sb SubdirectoryBlock) ToBlock() (disk.Block, error) {
|
2017-03-18 02:26:15 +00:00
|
|
|
var block disk.Block
|
2017-03-14 02:20:29 +00:00
|
|
|
binary.LittleEndian.PutUint16(block[0x0:0x2], sb.Prev)
|
|
|
|
binary.LittleEndian.PutUint16(block[0x2:0x4], sb.Next)
|
|
|
|
for i, desc := range sb.Descriptors {
|
|
|
|
copyBytes(block[0x04+i*0x27:0x04+(i+1)*0x27], desc.toBytes())
|
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
block[511] = sb.Extra
|
2017-03-30 02:24:55 +00:00
|
|
|
return block, nil
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FromBlock unmarshals a Block of bytes into a SubdirectoryBlock.
|
2017-03-30 02:24:55 +00:00
|
|
|
func (sb *SubdirectoryBlock) FromBlock(block disk.Block) error {
|
2017-03-14 02:20:29 +00:00
|
|
|
sb.Prev = binary.LittleEndian.Uint16(block[0x0:0x2])
|
|
|
|
sb.Next = binary.LittleEndian.Uint16(block[0x2:0x4])
|
|
|
|
for i := range sb.Descriptors {
|
2017-03-15 01:27:02 +00:00
|
|
|
sb.Descriptors[i].fromBytes(block[0x4+i*0x27 : 0x4+(i+1)*0x27])
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
2017-03-15 01:27:02 +00:00
|
|
|
sb.Extra = block[511]
|
2017-03-30 02:24:55 +00:00
|
|
|
return nil
|
2017-03-15 01:27:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates a SubdirectoryBlock for valid values.
|
|
|
|
func (sb SubdirectoryBlock) Validate() (errors []error) {
|
|
|
|
for _, desc := range sb.Descriptors {
|
|
|
|
errors = append(errors, desc.Validate()...)
|
|
|
|
}
|
|
|
|
if sb.Extra != 0 {
|
|
|
|
errors = append(errors, fmt.Errorf("Expected last byte of Subdirectory Block == 0x0; got 0x%02x", sb.Extra))
|
|
|
|
}
|
|
|
|
return errors
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type SubdirectoryHeader struct {
|
|
|
|
TypeAndNameLength byte // Storage type (top four bits) and subdirectory name length (lower four).
|
|
|
|
SubdirectoryName [15]byte // Subdirectory name (actual length defined in TypeAndNameLength)
|
|
|
|
SeventyFive byte // Must contain $75 (!?)
|
|
|
|
Unused1 [7]byte
|
|
|
|
Creation DateTime // Date and time volume was formatted
|
|
|
|
Version byte
|
|
|
|
MinVersion byte
|
|
|
|
Access Access
|
|
|
|
EntryLength byte // Length of each entry in the Subdirectory: usually $27
|
|
|
|
EntriesPerBlock byte // Usually $0D
|
|
|
|
FileCount uint16 // Number of active entries in the Subdirectory, not counting the Subdirectory Header
|
|
|
|
ParentPointer uint16 // The block number of the key (first) block of the directory that contains the entry that describes this subdirectory
|
|
|
|
ParentEntry byte // Index in the parent directory for this subdirectory's entry (counting from parent header = 0)
|
|
|
|
ParentEntryLength byte // Usually $27
|
|
|
|
}
|
|
|
|
|
|
|
|
// toBytes converts a SubdirectoryHeader to a slice of bytes.
|
|
|
|
func (sh SubdirectoryHeader) toBytes() []byte {
|
|
|
|
buf := make([]byte, 0x27)
|
|
|
|
buf[0] = sh.TypeAndNameLength
|
|
|
|
copyBytes(buf[1:0x10], sh.SubdirectoryName[:])
|
|
|
|
buf[0x10] = sh.SeventyFive
|
|
|
|
copyBytes(buf[0x11:0x18], sh.Unused1[:])
|
|
|
|
copyBytes(buf[0x18:0x1c], sh.Creation.toBytes())
|
|
|
|
buf[0x1c] = sh.Version
|
|
|
|
buf[0x1d] = sh.MinVersion
|
|
|
|
buf[0x1e] = byte(sh.Access)
|
|
|
|
buf[0x1f] = sh.EntryLength
|
|
|
|
buf[0x20] = sh.EntriesPerBlock
|
2017-03-15 01:27:02 +00:00
|
|
|
binary.LittleEndian.PutUint16(buf[0x21:0x23], sh.FileCount)
|
|
|
|
binary.LittleEndian.PutUint16(buf[0x23:0x25], sh.ParentPointer)
|
|
|
|
buf[0x25] = sh.ParentEntry
|
|
|
|
buf[0x26] = sh.ParentEntryLength
|
2017-03-14 02:20:29 +00:00
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
|
|
|
// fromBytes unmarshals a slice of bytes into a SubdirectoryHeader.
|
2017-03-15 01:27:02 +00:00
|
|
|
func (sh *SubdirectoryHeader) fromBytes(buf []byte) {
|
2017-03-14 02:20:29 +00:00
|
|
|
if len(buf) != 0x27 {
|
2017-03-15 01:27:02 +00:00
|
|
|
panic(fmt.Sprintf("VolumeDirectoryHeader should be 0x27 bytes long; got 0x%02x", len(buf)))
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
sh.TypeAndNameLength = buf[0]
|
|
|
|
copyBytes(sh.SubdirectoryName[:], buf[1:0x10])
|
|
|
|
sh.SeventyFive = buf[0x10]
|
|
|
|
copyBytes(sh.Unused1[:], buf[0x11:0x18])
|
2017-03-15 01:27:02 +00:00
|
|
|
sh.Creation.fromBytes(buf[0x18:0x1c])
|
2017-03-14 02:20:29 +00:00
|
|
|
sh.Version = buf[0x1c]
|
|
|
|
sh.MinVersion = buf[0x1d]
|
|
|
|
sh.Access = Access(buf[0x1e])
|
|
|
|
sh.EntryLength = buf[0x1f]
|
|
|
|
sh.EntriesPerBlock = buf[0x20]
|
2017-03-15 01:27:02 +00:00
|
|
|
sh.FileCount = binary.LittleEndian.Uint16(buf[0x21:0x23])
|
|
|
|
sh.ParentPointer = binary.LittleEndian.Uint16(buf[0x23:0x25])
|
|
|
|
sh.ParentEntry = buf[0x25]
|
|
|
|
sh.ParentEntryLength = buf[0x26]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate validates a SubdirectoryHeader for valid values.
|
|
|
|
func (sh SubdirectoryHeader) Validate() (errors []error) {
|
|
|
|
if sh.SeventyFive != 0x75 {
|
|
|
|
errors = append(errors, fmt.Errorf("Byte after subdirectory name %q should be 0x75; got 0x%02x", sh.Name(), sh.SeventyFive))
|
|
|
|
}
|
|
|
|
errors = append(errors, sh.Creation.Validate(fmt.Sprintf("subdirectory %q header creation date/time", sh.Name()))...)
|
|
|
|
return errors
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the string filename of a subdirectory header.
|
|
|
|
func (sh SubdirectoryHeader) Name() string {
|
|
|
|
return string(sh.SubdirectoryName[0 : sh.TypeAndNameLength&0xf])
|
2017-03-14 02:20:29 +00:00
|
|
|
}
|
|
|
|
|
2017-04-12 02:36:06 +00:00
|
|
|
// Volume is the in-memory representation of a device's volume
|
|
|
|
// information.
|
|
|
|
type Volume struct {
|
2017-04-13 03:22:45 +00:00
|
|
|
keyBlock *VolumeDirectoryKeyBlock
|
|
|
|
blocks []*VolumeDirectoryBlock
|
|
|
|
bitmap *VolumeBitMap
|
|
|
|
subdirsByBlock map[uint16]*Subdirectory
|
|
|
|
subdirsByName map[string]*Subdirectory
|
2017-04-12 02:36:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Subdirectory is the in-memory representation of a single
|
|
|
|
// subdirectory's information.
|
|
|
|
type Subdirectory struct {
|
|
|
|
keyBlock *SubdirectoryKeyBlock
|
|
|
|
blocks []*SubdirectoryBlock
|
|
|
|
}
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
// descriptors returns a slice of all top-level file descriptors in a
|
|
|
|
// volume, deleted or not.
|
|
|
|
func (v Volume) descriptors() []FileDescriptor {
|
|
|
|
var descs []FileDescriptor
|
|
|
|
|
|
|
|
descs = append(descs, v.keyBlock.Descriptors[:]...)
|
|
|
|
for _, block := range v.blocks {
|
|
|
|
descs = append(descs, block.Descriptors[:]...)
|
|
|
|
}
|
|
|
|
return descs
|
|
|
|
}
|
|
|
|
|
|
|
|
// subdirDescriptors returns a slice of all top-level file descriptors
|
|
|
|
// in a volume that are subdirectories.
|
|
|
|
func (v Volume) subdirDescriptors() []FileDescriptor {
|
|
|
|
var descs []FileDescriptor
|
|
|
|
|
|
|
|
for _, desc := range v.descriptors() {
|
|
|
|
if desc.Type() == TypeSubdirectory {
|
|
|
|
descs = append(descs, desc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return descs
|
|
|
|
}
|
|
|
|
|
|
|
|
// readVolume reads the entire volume and subdirectories from a device
|
2017-04-12 02:36:06 +00:00
|
|
|
// into memory.
|
2017-04-13 03:22:45 +00:00
|
|
|
func readVolume(bd disk.BlockDevice, keyBlock uint16) (Volume, error) {
|
2017-04-12 02:36:06 +00:00
|
|
|
v := Volume{
|
2017-04-13 03:22:45 +00:00
|
|
|
keyBlock: &VolumeDirectoryKeyBlock{},
|
|
|
|
subdirsByBlock: make(map[uint16]*Subdirectory),
|
|
|
|
subdirsByName: make(map[string]*Subdirectory),
|
2017-04-12 02:36:06 +00:00
|
|
|
}
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
if err := disk.UnmarshalBlock(bd, v.keyBlock, keyBlock); err != nil {
|
|
|
|
return v, fmt.Errorf("cannot read first block of volume directory (block %d): %v", keyBlock, err)
|
2017-04-12 02:36:06 +00:00
|
|
|
}
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
if vbm, err := readVolumeBitMap(bd, v.keyBlock.Header.BitMapPointer); err != nil {
|
2017-04-12 02:36:06 +00:00
|
|
|
return v, err
|
|
|
|
} else {
|
|
|
|
v.bitmap = &vbm
|
|
|
|
}
|
|
|
|
|
|
|
|
for block := v.keyBlock.Next; block != 0; block = v.blocks[len(v.blocks)-1].Next {
|
|
|
|
vdb := VolumeDirectoryBlock{}
|
|
|
|
if err := disk.UnmarshalBlock(bd, &vdb, block); err != nil {
|
|
|
|
return v, err
|
|
|
|
}
|
|
|
|
v.blocks = append(v.blocks, &vdb)
|
|
|
|
}
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
sdds := v.subdirDescriptors()
|
|
|
|
|
|
|
|
for i := 0; i < len(sdds); i++ {
|
|
|
|
sdd := sdds[i]
|
|
|
|
sub, err := readSubdirectory(bd, sdd)
|
|
|
|
if err != nil {
|
|
|
|
return v, err
|
|
|
|
}
|
|
|
|
v.subdirsByBlock[sdd.KeyPointer] = &sub
|
|
|
|
sdds = append(sdds, sub.subdirDescriptors()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, sd := range v.subdirsByBlock {
|
|
|
|
name := sd.keyBlock.Header.Name()
|
|
|
|
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, v.subdirsByBlock)
|
|
|
|
if err != nil {
|
|
|
|
return v, err
|
|
|
|
}
|
|
|
|
if parentName != "" {
|
|
|
|
name = parentName + "/" + name
|
|
|
|
}
|
|
|
|
|
|
|
|
v.subdirsByName[name] = sd
|
|
|
|
}
|
2017-04-12 02:36:06 +00:00
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
2017-04-13 03:22:45 +00:00
|
|
|
// descriptors returns a slice of all top-level file descriptors in a
|
|
|
|
// subdirectory, deleted or not.
|
|
|
|
func (s Subdirectory) descriptors() []FileDescriptor {
|
|
|
|
var descs []FileDescriptor
|
|
|
|
|
|
|
|
descs = append(descs, s.keyBlock.Descriptors[:]...)
|
|
|
|
for _, block := range s.blocks {
|
|
|
|
descs = append(descs, block.Descriptors[:]...)
|
|
|
|
}
|
|
|
|
return descs
|
|
|
|
}
|
|
|
|
|
|
|
|
// subdirDescriptors returns a slice of all top-level file descriptors
|
|
|
|
// in a subdirectory that are subdirectories.
|
|
|
|
func (s Subdirectory) subdirDescriptors() []FileDescriptor {
|
|
|
|
var descs []FileDescriptor
|
|
|
|
|
|
|
|
for _, desc := range s.descriptors() {
|
|
|
|
if desc.Type() == TypeSubdirectory {
|
|
|
|
descs = append(descs, desc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return descs
|
|
|
|
}
|
|
|
|
|
|
|
|
// fullDirName returns the full recursive directory name of the given parent directory.
|
|
|
|
func parentDirName(parentDirectoryBlock uint16, keyBlock uint16, subdirMap map[uint16]*Subdirectory) (string, error) {
|
|
|
|
if parentDirectoryBlock == keyBlock {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
sd := subdirMap[parentDirectoryBlock]
|
|
|
|
if sd == nil {
|
|
|
|
return "", fmt.Errorf("Unable to find subdirectory for block %d", parentDirectoryBlock)
|
|
|
|
}
|
|
|
|
parentName, err := parentDirName(sd.keyBlock.Header.ParentPointer, keyBlock, subdirMap)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if parentName == "" {
|
|
|
|
return sd.keyBlock.Header.Name(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return parentName + "/" + sd.keyBlock.Header.Name(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// readSubdirectory reads a single subdirectory from a device into
|
|
|
|
// memory.
|
|
|
|
func readSubdirectory(bd disk.BlockDevice, fd FileDescriptor) (Subdirectory, error) {
|
|
|
|
s := Subdirectory{
|
|
|
|
keyBlock: &SubdirectoryKeyBlock{},
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := disk.UnmarshalBlock(bd, s.keyBlock, fd.KeyPointer); err != nil {
|
|
|
|
return s, fmt.Errorf("cannot read first block of subdirectory %q (block %d): %v", fd.Name(), fd.KeyPointer, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for block := s.keyBlock.Next; block != 0; block = s.blocks[len(s.blocks)-1].Next {
|
|
|
|
sdb := SubdirectoryBlock{}
|
|
|
|
if err := disk.UnmarshalBlock(bd, &sdb, block); err != nil {
|
|
|
|
return s, err
|
|
|
|
}
|
|
|
|
s.blocks = append(s.blocks, &sdb)
|
|
|
|
}
|
|
|
|
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
2017-03-14 02:20:29 +00:00
|
|
|
// copyBytes is just like the builtin copy, but just for byte slices,
|
|
|
|
// and it checks that dst and src have the same length.
|
|
|
|
func copyBytes(dst, src []byte) int {
|
|
|
|
if len(dst) != len(src) {
|
|
|
|
panic(fmt.Sprintf("copyBytes called with differing lengths %d and %d", len(dst), len(src)))
|
|
|
|
}
|
|
|
|
return copy(dst, src)
|
|
|
|
}
|
2017-03-18 02:26:15 +00:00
|
|
|
|
|
|
|
// operator is a disk.Operator - an interface for performing
|
|
|
|
// high-level operations on files and directories.
|
|
|
|
type operator struct {
|
|
|
|
dev disk.BlockDevice
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ disk.Operator = operator{}
|
|
|
|
|
|
|
|
// operatorName is the keyword name for the operator that undestands
|
|
|
|
// prodos disks/devices.
|
|
|
|
const operatorName = "prodos"
|
|
|
|
|
|
|
|
// Name returns the name of the operator.
|
|
|
|
func (o operator) Name() string {
|
|
|
|
return operatorName
|
|
|
|
}
|
|
|
|
|
2018-06-07 02:27:15 +00:00
|
|
|
// Order returns the sector or block order of the underlying storage.
|
|
|
|
func (o operator) Order() string {
|
|
|
|
return o.dev.Order()
|
|
|
|
}
|
|
|
|
|
2017-03-18 02:26:15 +00:00
|
|
|
// HasSubdirs returns true if the underlying operating system on the
|
|
|
|
// disk allows subdirectories.
|
|
|
|
func (o operator) HasSubdirs() bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Catalog returns a catalog of disk entries. subdir should be empty
|
|
|
|
// for operating systems that do not support subdirectories.
|
|
|
|
func (o operator) Catalog(subdir string) ([]disk.Descriptor, error) {
|
2017-04-13 03:22:45 +00:00
|
|
|
|
|
|
|
vol, err := readVolume(o.dev, 2)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var result []disk.Descriptor
|
|
|
|
|
|
|
|
if subdir == "" {
|
|
|
|
for _, desc := range vol.descriptors() {
|
|
|
|
if desc.Type() != TypeDeleted {
|
|
|
|
result = append(result, desc.descriptor())
|
|
|
|
}
|
2017-03-18 02:26:15 +00:00
|
|
|
}
|
2017-04-13 03:22:45 +00:00
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
sd, ok := vol.subdirsByName[subdir]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("subdirectory %q not found", subdir)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, desc := range sd.descriptors() {
|
|
|
|
if desc.Type() != TypeDeleted {
|
|
|
|
result = append(result, desc.descriptor())
|
2017-03-18 02:26:15 +00:00
|
|
|
}
|
2017-04-13 03:22:45 +00:00
|
|
|
}
|
|
|
|
return result, nil
|
2017-03-18 02:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetFile retrieves a file by name.
|
|
|
|
func (o operator) GetFile(filename string) (disk.FileInfo, error) {
|
|
|
|
return disk.FileInfo{}, fmt.Errorf("%s doesn't implement GetFile yet", operatorName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete deletes a file by name. It returns true if the file was
|
|
|
|
// deleted, false if it didn't exist.
|
|
|
|
func (o operator) Delete(filename string) (bool, error) {
|
|
|
|
return false, fmt.Errorf("%s doesn't implement Delete yet", operatorName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PutFile writes a file by name. If the file exists and overwrite
|
|
|
|
// is false, it returns with an error. Otherwise it returns true if
|
|
|
|
// an existing file was overwritten.
|
|
|
|
func (o operator) PutFile(fileInfo disk.FileInfo, overwrite bool) (existed bool, err error) {
|
|
|
|
return false, fmt.Errorf("%s doesn't implement PutFile yet", operatorName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write writes the underlying device blocks to the given writer.
|
|
|
|
func (o operator) Write(w io.Writer) (int, error) {
|
|
|
|
return o.dev.Write(w)
|
|
|
|
}
|
2017-03-23 02:27:27 +00:00
|
|
|
|
|
|
|
// deviceOperatorFactory is the factory that returns prodos operators
|
|
|
|
// given device images.
|
|
|
|
func deviceOperatorFactory(bd disk.BlockDevice) (disk.Operator, error) {
|
|
|
|
op := operator{dev: bd}
|
|
|
|
_, err := op.Catalog("")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Cannot read catalog. Underlying error: %v", err)
|
|
|
|
}
|
|
|
|
return op, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// diskOperatorFactory is the factory that returns dos3 operators
|
|
|
|
// given disk images.
|
|
|
|
func diskOperatorFactory(sd disk.SectorDisk) (disk.Operator, error) {
|
|
|
|
bd, err := disk.BlockDeviceFromSectorDisk(sd)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return deviceOperatorFactory(bd)
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
disk.RegisterDeviceOperatorFactory(operatorName, deviceOperatorFactory)
|
|
|
|
disk.RegisterDiskOperatorFactory(operatorName, diskOperatorFactory)
|
|
|
|
}
|