mirror of
https://github.com/paleotronic/diskm8.git
synced 2024-09-07 21:54:28 +00:00
1934 lines
43 KiB
Go
1934 lines
43 KiB
Go
package disk
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
"fmt"
|
|
)
|
|
|
|
type VDH struct {
|
|
Data []byte
|
|
blockid int
|
|
blockoffset int
|
|
}
|
|
|
|
func (fd *VDH) CreateTime() time.Time {
|
|
|
|
b := fd.Data[0x1C:0x20]
|
|
|
|
return prodosStampBytesToTime(b)
|
|
|
|
}
|
|
|
|
func (fd *VDH) SetCreateTime(t time.Time) {
|
|
|
|
b := timeToProdosStampBytes(t)
|
|
for i, v := range b {
|
|
fd.Data[0x1C+i] = v
|
|
}
|
|
|
|
}
|
|
|
|
func (fd *VDH) SetData(data []byte, blockid, blockoffset int) {
|
|
|
|
fd.blockid = blockid
|
|
fd.blockoffset = blockoffset
|
|
|
|
if fd.Data == nil && len(data) == 39 {
|
|
fd.Data = data
|
|
//Println("VDH: ")
|
|
//Dump(data)
|
|
}
|
|
|
|
for i, v := range data {
|
|
fd.Data[i] = v
|
|
}
|
|
}
|
|
|
|
func (fd *VDH) SetName(name string) {
|
|
|
|
name = strings.ToUpper(name)
|
|
|
|
if len(name) > 15 {
|
|
name = name[:15]
|
|
}
|
|
|
|
l := len(name)
|
|
|
|
for i, v := range []byte(name) {
|
|
fd.Data[1+i] = v
|
|
}
|
|
|
|
fd.Data[0] = (fd.Data[0] & 0xf0) | byte(l)
|
|
|
|
}
|
|
|
|
func (fd *VDH) SetStorageType(t ProDOSStorageType) {
|
|
|
|
fd.Data[0] = (fd.Data[0] & 0x0f) | (byte(t) << 4)
|
|
|
|
}
|
|
|
|
func (fd *VDH) GetNameLength() int {
|
|
return int(fd.Data[0] & 0xf)
|
|
}
|
|
|
|
func (fd *VDH) SetNameLength(l int) {
|
|
fd.Data[0] = (fd.Data[0] & 0xf0) | byte(l&0x0f)
|
|
}
|
|
|
|
func (fd *VDH) GetStorageType() ProDOSStorageType {
|
|
return ProDOSStorageType((fd.Data[0]) >> 4)
|
|
}
|
|
|
|
func (fd *VDH) GetDirName() string {
|
|
return fd.GetVolumeName()
|
|
}
|
|
|
|
func (fd *VDH) SetVolumeName(s string) {
|
|
if len(s) > 15 {
|
|
s = s[:15]
|
|
}
|
|
l := len(s)
|
|
for i := 0; i < 15; i++ {
|
|
if i < l {
|
|
fd.Data[1+i] = byte(s[i])
|
|
} else {
|
|
fd.Data[1+i] = 0x00
|
|
}
|
|
}
|
|
fd.SetNameLength(l)
|
|
}
|
|
|
|
func (fd *VDH) GetVolumeName() string {
|
|
|
|
l := fd.GetNameLength()
|
|
|
|
b := fd.Data[1 : 1+l]
|
|
|
|
s := ""
|
|
for _, v := range b {
|
|
s += string(rune(PokeToAscii(uint(v), false)))
|
|
}
|
|
|
|
return strings.Trim(s, " ")
|
|
|
|
}
|
|
|
|
func (fd *VDH) GetVersion() int {
|
|
return int(fd.Data[28])
|
|
}
|
|
|
|
func (fd *VDH) SetVersion(b int) {
|
|
fd.Data[28] = byte(b)
|
|
}
|
|
|
|
func (fd *VDH) GetMinVersion() int {
|
|
return int(fd.Data[29])
|
|
}
|
|
|
|
func (fd *VDH) SetMinVersion(b int) {
|
|
fd.Data[29] = byte(b)
|
|
}
|
|
|
|
func (fd *VDH) GetAccess() ProDOSAccessMode {
|
|
return ProDOSAccessMode(fd.Data[30])
|
|
}
|
|
|
|
func (fd *VDH) SetAccess(m ProDOSAccessMode) {
|
|
fd.Data[30] = byte(m)
|
|
}
|
|
|
|
func (fd *VDH) GetEntryLength() int {
|
|
return int(fd.Data[31])
|
|
}
|
|
|
|
func (fd *VDH) SetEntryLength(b int) {
|
|
fd.Data[31] = byte(b & 0xff)
|
|
}
|
|
|
|
func (fd *VDH) GetEntriesPerBlock() int {
|
|
return int(fd.Data[32])
|
|
}
|
|
|
|
func (fd *VDH) SetEntriesPerBlock(b int) {
|
|
fd.Data[32] = byte(b & 0xff)
|
|
}
|
|
|
|
func (fd *VDH) GetFileCount() int {
|
|
return int(fd.Data[33]) + 256*int(fd.Data[34])
|
|
}
|
|
|
|
func (fd *VDH) SetFileCount(c int) {
|
|
fd.Data[33] = byte(c & 0xff)
|
|
fd.Data[34] = byte(c / 0x100)
|
|
}
|
|
|
|
func (fd *VDH) GetBitmapPointer() int {
|
|
return int(fd.Data[35]) + 256*int(fd.Data[36])
|
|
}
|
|
|
|
func (fd *VDH) GetTotalBlocks() int {
|
|
return int(fd.Data[37]) + 256*int(fd.Data[38])
|
|
}
|
|
|
|
func (fd *VDH) SetTotalBlocks(b int) {
|
|
fd.Data[37] = byte(b % 256)
|
|
fd.Data[38] = byte(b / 256)
|
|
}
|
|
|
|
func (fd *VDH) GetDirParentPointer() int {
|
|
return int(fd.Data[35]) + 256*int(fd.Data[35])
|
|
}
|
|
|
|
func (fd *VDH) SetDirParentPointer(b int) {
|
|
fd.Data[35] = byte(b & 0xff)
|
|
fd.Data[36] = byte(b / 0x100)
|
|
}
|
|
|
|
func (fd *VDH) GetDirParentEntry() int {
|
|
return int(fd.Data[37])
|
|
}
|
|
|
|
func (fd *VDH) SetDirParentEntry(b int) {
|
|
fd.Data[37] = byte(b & 0xff)
|
|
}
|
|
|
|
func (fd *VDH) GetDirParentEntryLength() int {
|
|
return int(fd.Data[38])
|
|
}
|
|
|
|
func (fd *VDH) SetDirParentEntryLength(b int) {
|
|
fd.Data[38] = byte(b & 0xff)
|
|
}
|
|
|
|
func (fd *VDH) Publish(dsk *DSKWrapper) error {
|
|
bd, err := dsk.PRODOSGetBlock(fd.blockid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, v := range fd.Data {
|
|
bd[fd.blockoffset+i] = v
|
|
}
|
|
//fmt.Printf("Writing dir header at block %d\n", fd.blockid)
|
|
|
|
//fmt.Printf("Data=%v\n", bd)
|
|
|
|
return dsk.PRODOSWrite(fd.blockid, bd)
|
|
}
|
|
|
|
func (dsk *DSKWrapper) IsProDOS() (bool, DiskFormat, SectorOrder) {
|
|
|
|
oldFormat := dsk.Format
|
|
oldLayout := dsk.Layout
|
|
|
|
defer func() {
|
|
dsk.Format = oldFormat
|
|
dsk.Layout = oldLayout
|
|
}()
|
|
|
|
if len(dsk.Data) == STD_DISK_BYTES {
|
|
|
|
layouts := []SectorOrder{SectorOrderDOS33, SectorOrderDOS33Alt, SectorOrderProDOS, SectorOrderProDOSLinear}
|
|
|
|
for _, l := range layouts {
|
|
|
|
dsk.Layout = l
|
|
vdh, err := dsk.PRODOSGetVDH(2)
|
|
if err != nil {
|
|
return false, oldFormat, oldLayout
|
|
}
|
|
|
|
if vdh.GetTotalBlocks() == 280 && vdh.GetStorageType() == 0xf {
|
|
return true, GetDiskFormat(DF_PRODOS), l
|
|
}
|
|
|
|
}
|
|
|
|
} else if len(dsk.Data) == PRODOS_800KB_DISK_BYTES {
|
|
|
|
layouts := []SectorOrder{SectorOrderDOS33, SectorOrderDOS33Alt, SectorOrderProDOS}
|
|
|
|
for _, l := range layouts {
|
|
|
|
dsk.Layout = l
|
|
vdh, err := dsk.PRODOSGetVDH(2)
|
|
if err != nil {
|
|
return false, oldFormat, oldLayout
|
|
}
|
|
|
|
if vdh.GetTotalBlocks() == 1600 && vdh.GetStorageType() == 0xf {
|
|
return true, GetDiskFormat(DF_PRODOS_800KB), l
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fmt.Println("Trying alternative format identification")
|
|
|
|
layouts := []SectorOrder{SectorOrderDOS33, SectorOrderDOS33Alt, SectorOrderProDOS, SectorOrderProDOSLinear}
|
|
|
|
for _, l := range layouts {
|
|
|
|
dsk.Layout = l
|
|
vdh, err := dsk.PRODOSGetVDH(2)
|
|
if err != nil {
|
|
return false, oldFormat, oldLayout
|
|
}
|
|
|
|
fmt.Printf("Blocks = %d, Size/512 = %d, Storage Type = %d\n", vdh.GetTotalBlocks(), len(dsk.Data)/512, vdh.GetStorageType())
|
|
|
|
if vdh.GetStorageType() == 0xf && vdh.GetTotalBlocks() == len(dsk.Data)/512 {
|
|
return true, GetDiskFormat(DF_PRODOS_CUSTOM), l
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, oldFormat, oldLayout
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOS800GetVolumeBitmap() (ProDOSVolumeBitmap, error) {
|
|
|
|
var vb ProDOSVolumeBitmap
|
|
|
|
vdh, err := dsk.PRODOS800GetVDH(2)
|
|
if err != nil {
|
|
return vb, err
|
|
}
|
|
|
|
b := vdh.GetBitmapPointer()
|
|
|
|
data, err := dsk.PRODOS800GetBlock(b)
|
|
if err != nil {
|
|
return vb, err
|
|
}
|
|
|
|
//copy(vb[:], data)
|
|
|
|
vb = ProDOSVolumeBitmap{
|
|
Data: data,
|
|
blockid: b,
|
|
blockoffset: 0,
|
|
}
|
|
|
|
return vb, nil
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSGetVolumeBitmap() (ProDOSVolumeBitmap, error) {
|
|
|
|
var vb ProDOSVolumeBitmap
|
|
|
|
vdh, err := dsk.PRODOSGetVDH(2)
|
|
if err != nil {
|
|
return vb, err
|
|
}
|
|
|
|
b := vdh.GetBitmapPointer()
|
|
|
|
data, err := dsk.PRODOSGetBlock(b)
|
|
if err != nil {
|
|
return vb, err
|
|
}
|
|
|
|
//copy(vb[:], data)
|
|
vb = ProDOSVolumeBitmap{
|
|
Data: data,
|
|
blockid: b,
|
|
blockoffset: 0,
|
|
}
|
|
|
|
return vb, nil
|
|
|
|
}
|
|
|
|
type ProDOSVolumeBitmap struct {
|
|
Data []byte
|
|
blockid int
|
|
blockoffset int
|
|
}
|
|
|
|
func (vb ProDOSVolumeBitmap) IsBlockFree(b int) bool {
|
|
|
|
bidx := b / 8
|
|
bit := 7 - (b % 8)
|
|
mask := byte(1 << uint(bit))
|
|
|
|
return (vb.Data[bidx] & mask) == mask
|
|
|
|
}
|
|
|
|
func (vb ProDOSVolumeBitmap) SetBlockFree(b int, free bool) {
|
|
|
|
//log.Printf("Freeing block %d", b)
|
|
|
|
bidx := b / 8
|
|
bit := 7 - (b % 8)
|
|
setmask := byte(1 << uint(bit))
|
|
clrmask := 0xff ^ setmask
|
|
|
|
if free {
|
|
vb.Data[bidx] = vb.Data[bidx] | setmask
|
|
} else {
|
|
vb.Data[bidx] = vb.Data[bidx] & clrmask
|
|
}
|
|
}
|
|
|
|
// -- Prodos file descriptor
|
|
type ProDOSFileDescriptor struct {
|
|
Data []byte
|
|
blockid int
|
|
blockoffset int
|
|
entryNum int
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetData(data []byte, blockid int, blockoffset int) {
|
|
|
|
fd.blockid = blockid
|
|
fd.blockoffset = blockoffset
|
|
|
|
if fd.Data == nil && len(data) == 39 {
|
|
fd.Data = data
|
|
return
|
|
}
|
|
|
|
for i, v := range data {
|
|
fd.Data[i] = v
|
|
}
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) GetNameLength() int {
|
|
return int(fd.Data[0] & 0xf)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) GetStorageType() ProDOSStorageType {
|
|
return ProDOSStorageType((fd.Data[0]) >> 4)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) Name() string {
|
|
|
|
l := fd.GetNameLength()
|
|
|
|
b := fd.Data[1 : 1+l]
|
|
|
|
s := ""
|
|
for _, v := range b {
|
|
s += string(rune(PokeToAscii(uint(v), false)))
|
|
}
|
|
|
|
s = strings.ToLower(strings.Trim(s, " "))
|
|
|
|
switch fd.Type() {
|
|
case FileType_PD_APP:
|
|
s += ".a"
|
|
case FileType_PD_INT:
|
|
s += ".i"
|
|
case FileType_PD_BIN:
|
|
s += ".s"
|
|
case FileType_PD_TXT:
|
|
s += ".t"
|
|
case FileType_PD_SYS:
|
|
s += ".s"
|
|
}
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) NameUnadorned() string {
|
|
|
|
l := fd.GetNameLength()
|
|
|
|
b := fd.Data[1 : 1+l]
|
|
|
|
s := ""
|
|
for _, v := range b {
|
|
s += string(rune(PokeToAscii(uint(v), false)))
|
|
}
|
|
|
|
s = strings.ToLower(strings.Trim(s, " "))
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetName(name string) {
|
|
|
|
name = strings.ToUpper(name)
|
|
|
|
if len(name) > 15 {
|
|
name = name[:15]
|
|
}
|
|
|
|
l := len(name)
|
|
|
|
for i, v := range []byte(name) {
|
|
fd.Data[1+i] = v
|
|
}
|
|
|
|
fd.Data[0] = (fd.Data[0] & 0xf0) | byte(l)
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetStorageType(t ProDOSStorageType) {
|
|
|
|
fd.Data[0] = (fd.Data[0] & 0x0f) | (byte(t) << 4)
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetAccessMode(t ProDOSAccessMode) {
|
|
|
|
fd.Data[0x1e] = byte(t)
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) AccessMode() ProDOSAccessMode {
|
|
return ProDOSAccessMode(fd.Data[0x1e])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetLocked(b bool) {
|
|
accessMode := fd.AccessMode()
|
|
if b {
|
|
accessMode = accessMode & (AccessType_Changed | AccessType_Readable)
|
|
} else {
|
|
accessMode = accessMode | AccessType_Destroy | AccessType_Writable | AccessType_Rename
|
|
}
|
|
fd.SetAccessMode(accessMode)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) IsLocked() bool {
|
|
a := fd.AccessMode()
|
|
return a&(AccessType_Destroy|AccessType_Rename|AccessType_Writable) == 0
|
|
}
|
|
|
|
func prodosStampBytesToTime(in []byte) time.Time {
|
|
dbits := (int(in[0x01]) << 8) | int(in[0x00])
|
|
day := dbits & 31
|
|
month := (dbits >> 5) & 15
|
|
year := (dbits >> 9) & 127
|
|
tbits := (int(in[0x03]) << 8) | int(in[0x02])
|
|
mins := tbits & 63
|
|
hours := (tbits >> 8)
|
|
|
|
if year < 70 {
|
|
year += 100
|
|
}
|
|
year += 1900
|
|
|
|
return time.Date(year, time.Month(month), day, hours, mins, 0, 0, time.Local)
|
|
}
|
|
|
|
func timeToProdosStampBytes(t time.Time) []byte {
|
|
year, month, day, hour, minute := t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute()
|
|
year = year - 1900
|
|
if year > 99 {
|
|
year -= 100
|
|
}
|
|
|
|
dbits := (year << 9) | (month << 5) | day
|
|
tbits := (hour << 8) | minute
|
|
|
|
return []byte{
|
|
byte(dbits & 0xff),
|
|
byte(dbits >> 8),
|
|
byte(tbits & 0xff),
|
|
byte(tbits >> 8),
|
|
}
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) CreateTime() time.Time {
|
|
|
|
b := fd.Data[0x18:0x1C]
|
|
|
|
return prodosStampBytesToTime(b)
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetCreateTime(t time.Time) {
|
|
|
|
b := timeToProdosStampBytes(t)
|
|
for i, v := range b {
|
|
fd.Data[0x18+i] = v
|
|
}
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) ModTime() time.Time {
|
|
|
|
b := fd.Data[0x21:0x25]
|
|
|
|
return prodosStampBytesToTime(b)
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetModTime(t time.Time) {
|
|
|
|
b := timeToProdosStampBytes(t)
|
|
for i, v := range b {
|
|
fd.Data[0x21+i] = v
|
|
}
|
|
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) GetVersion() int {
|
|
return int(fd.Data[0x1c])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetVersion(b int) {
|
|
fd.Data[0x1c] = byte(b)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) GetMinVersion() int {
|
|
return int(fd.Data[0x1d])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetMinVersion(b int) {
|
|
fd.Data[0x1d] = byte(b)
|
|
}
|
|
|
|
type ProDOSAccessMode byte
|
|
|
|
const (
|
|
AccessType_Destroy ProDOSAccessMode = 0x80
|
|
AccessType_Rename ProDOSAccessMode = 0x40
|
|
AccessType_Changed ProDOSAccessMode = 0x20
|
|
AccessType_Writable ProDOSAccessMode = 0x02
|
|
AccessType_Readable ProDOSAccessMode = 0x01
|
|
//
|
|
AccessType_Default ProDOSAccessMode = AccessType_Readable | AccessType_Writable | AccessType_Rename | AccessType_Destroy
|
|
)
|
|
|
|
type ProDOSStorageType byte
|
|
|
|
const (
|
|
StorageType_Inactive ProDOSStorageType = 0x0
|
|
StorageType_Seedling ProDOSStorageType = 0x1
|
|
StorageType_Sapling ProDOSStorageType = 0x2
|
|
StorageType_Tree ProDOSStorageType = 0x3
|
|
StorageType_SubDir_File ProDOSStorageType = 0xd
|
|
StorageType_SubDir_Header ProDOSStorageType = 0xe
|
|
StorageType_Volume_Header ProDOSStorageType = 0xf
|
|
)
|
|
|
|
type ProDOSFileType byte
|
|
|
|
const (
|
|
FileType_PD_None ProDOSFileType = 0x00
|
|
FileType_PD_TXT ProDOSFileType = 0x04
|
|
FileType_PD_Directory ProDOSFileType = 0x0f
|
|
FileType_PD_BIN ProDOSFileType = 0x06
|
|
FileType_PD_INT ProDOSFileType = 0xfa
|
|
FileType_PD_INT_Var ProDOSFileType = 0xfb
|
|
FileType_PD_APP ProDOSFileType = 0xfc
|
|
FileType_PD_APP_Var ProDOSFileType = 0xfd
|
|
FileType_PD_Reloc ProDOSFileType = 0xfe
|
|
FileType_PD_SYS ProDOSFileType = 0xff
|
|
)
|
|
|
|
var ProDOSTypeMap = map[ProDOSFileType][2]string{
|
|
0x00: [2]string{"UNK", "Unknown"},
|
|
0x01: [2]string{"BAD", "Bad Block"},
|
|
0x02: [2]string{"PCD", "Pascal Code"},
|
|
0x03: [2]string{"PTX", "Pascal Text"},
|
|
0x04: [2]string{"TXT", "ASCII Text"},
|
|
0x05: [2]string{"PDA", "Pascal Data"},
|
|
0x06: [2]string{"BIN", "Binary File"},
|
|
0x07: [2]string{"FNT", "Apple III Font"},
|
|
0x08: [2]string{"FOT", "HiRes/Double HiRes Graphics"},
|
|
0x09: [2]string{"BA3", "Apple III Basic Program"},
|
|
0x0A: [2]string{"DA3", "Apple III Basic Data"},
|
|
0x0B: [2]string{"WPF", "Generic Word Processing"},
|
|
0x0C: [2]string{"SOS", "SOS System File"},
|
|
0x0F: [2]string{"DIR", "ProDOS Directory"},
|
|
0x10: [2]string{"RPD", "RPS Data"},
|
|
0x11: [2]string{"RPI", "RPS Index"},
|
|
0x12: [2]string{"AFD", "AppleFile Discard"},
|
|
0x13: [2]string{"AFM", "AppleFile Model"},
|
|
0x14: [2]string{"AFR", "AppleFile Report"},
|
|
0x15: [2]string{"SCL", "Screen Library"},
|
|
0x16: [2]string{"PFS", "PFS Document"},
|
|
0x19: [2]string{"ADB", "AppleWorks Database"},
|
|
0x1A: [2]string{"AWP", "AppleWorks Word Processing"},
|
|
0x1B: [2]string{"ASP", "AppleWorks Spreadsheet"},
|
|
0x80: [2]string{"GES", "System File"},
|
|
0x81: [2]string{"GEA", "Desk Accessory"},
|
|
0x82: [2]string{"GEO", "Application"},
|
|
0x83: [2]string{"GED", "Document"},
|
|
0x84: [2]string{"GEF", "Font"},
|
|
0x85: [2]string{"GEP", "Printer Driver"},
|
|
0x86: [2]string{"GEI", "Input Driver"},
|
|
0x87: [2]string{"GEX", "Auxiliary Driver"},
|
|
0x89: [2]string{"GEV", "Swap File"},
|
|
0x8B: [2]string{"GEC", "Clock Driver"},
|
|
0x8C: [2]string{"GEK", "Interface Card Driver"},
|
|
0x8D: [2]string{"GEW", "Formatting Data"},
|
|
0xA0: [2]string{"WP", " WordPerfect"},
|
|
0xAB: [2]string{"GSB", "Apple IIgs BASIC Program"},
|
|
0xAC: [2]string{"TDF", "Apple IIgs BASIC TDF"},
|
|
0xAD: [2]string{"BDF", "Apple IIgs BASIC Data"},
|
|
0x60: [2]string{"PRE", "PC Pre-Boot"},
|
|
0x6B: [2]string{"BIO", "PC BIOS"},
|
|
0x66: [2]string{"NCF", "ProDOS File Navigator Command File"},
|
|
0x6D: [2]string{"DVR", "PC Driver"},
|
|
0x6E: [2]string{"PRE", "PC Pre-Boot"},
|
|
0x6F: [2]string{"HDV", "PC Hard Disk Image"},
|
|
0x50: [2]string{"GWP", "Apple IIgs Word Processing"},
|
|
0x51: [2]string{"GSS", "Apple IIgs Spreadsheet"},
|
|
0x52: [2]string{"GDB", "Apple IIgs Database"},
|
|
0x53: [2]string{"DRW", "Object Oriented Graphics"},
|
|
0x54: [2]string{"GDP", "Apple IIgs Desktop Publishing"},
|
|
0x55: [2]string{"HMD", "HyperMedia"},
|
|
0x56: [2]string{"EDU", "Educational Program Data"},
|
|
0x57: [2]string{"STN", "Stationery"},
|
|
0x58: [2]string{"HLP", "Help File"},
|
|
0x59: [2]string{"COM", "Communications"},
|
|
0x5A: [2]string{"CFG", "Configuration"},
|
|
0x5B: [2]string{"ANM", "Animation"},
|
|
0x5C: [2]string{"MUM", "Multimedia"},
|
|
0x5D: [2]string{"ENT", "Entertainment"},
|
|
0x5E: [2]string{"DVU", "Development Utility"},
|
|
0x41: [2]string{"OCR", "Optical Character Recognition"},
|
|
0x42: [2]string{"FTD", "File Type Definitions"},
|
|
0x20: [2]string{"TDM", "Desktop Manager File"},
|
|
0x21: [2]string{"IPS", "Instant Pascal Source"},
|
|
0x22: [2]string{"UPV", "UCSD Pascal Volume"},
|
|
0x29: [2]string{"3SD", "SOS Directory"},
|
|
0x2A: [2]string{"8SC", "Source Code"},
|
|
0x2B: [2]string{"8OB", "Object Code"},
|
|
0x2C: [2]string{"8IC", "Interpreted Code"},
|
|
0x2D: [2]string{"8LD", "Language Data"},
|
|
0x2E: [2]string{"P8C", "ProDOS 8 Code Module"},
|
|
0xB0: [2]string{"SRC", "Apple IIgs Source Code"},
|
|
0xB1: [2]string{"OBJ", "Apple IIgs Object Code"},
|
|
0xB2: [2]string{"LIB", "Apple IIgs Library"},
|
|
0xB3: [2]string{"S16", "Apple IIgs Application Program"},
|
|
0xB4: [2]string{"RTL", "Apple IIgs Runtime Library"},
|
|
0xB5: [2]string{"EXE", "Apple IIgs Shell Script"},
|
|
0xB6: [2]string{"PIF", "Apple IIgs Permanent INIT"},
|
|
0xB7: [2]string{"TIF", "Apple IIgs Temporary INIT"},
|
|
0xB8: [2]string{"NDA", "Apple IIgs New Desk Accessory"},
|
|
0xB9: [2]string{"CDA", "Apple IIgs Classic Desk Accessory"},
|
|
0xBA: [2]string{"TOL", "Apple IIgs Tool"},
|
|
0xBB: [2]string{"DRV", "Apple IIgs Device Driver"},
|
|
0xBC: [2]string{"LDF", "Apple IIgs Generic Load File"},
|
|
0xBD: [2]string{"FST", "Apple IIgs File System Translator"},
|
|
0xBF: [2]string{"DOC", "Apple IIgs Document "},
|
|
0xC0: [2]string{"PNT", "Apple IIgs Packed Super HiRes"},
|
|
0xC1: [2]string{"PIC", "Apple IIgs Super HiRes"},
|
|
0xC2: [2]string{"ANI", "PaintWorks Animation"},
|
|
0xC3: [2]string{"PAL", "PaintWorks Palette"},
|
|
0xC5: [2]string{"OOG", "Object-Oriented Graphics"},
|
|
0xC6: [2]string{"SCR", "Script"},
|
|
0xC7: [2]string{"CDV", "Apple IIgs Control Panel"},
|
|
0xC8: [2]string{"FON", "Apple IIgs Font"},
|
|
0xC9: [2]string{"FND", "Apple IIgs Finder Data"},
|
|
0xCA: [2]string{"ICN", "Apple IIgs Icon "},
|
|
0xD5: [2]string{"MUS", "Music"},
|
|
0xD6: [2]string{"INS", "Instrument"},
|
|
0xD7: [2]string{"MDI", "MIDI"},
|
|
0xD8: [2]string{"SND", "Apple IIgs Audio"},
|
|
0xDB: [2]string{"DBM", "DB Master Document"},
|
|
0xE0: [2]string{"LBR", "Archive"},
|
|
0xE2: [2]string{"ATK", "AppleTalk Data"},
|
|
0xEE: [2]string{"R16", "EDASM 816 Relocatable Code"},
|
|
0xEF: [2]string{"PAR", "Pascal Area"},
|
|
0xF0: [2]string{"CMD", "ProDOS Command File"},
|
|
0xF1: [2]string{"OVL", "User Defined 1"},
|
|
0xF2: [2]string{"UD2", "User Defined 2"},
|
|
0xF3: [2]string{"UD3", "User Defined 3"},
|
|
0xF4: [2]string{"UD4", "User Defined 4"},
|
|
0xF5: [2]string{"BAT", "User Defined 5"},
|
|
0xF6: [2]string{"UD6", "User Defined 6"},
|
|
0xF7: [2]string{"UD7", "User Defined 7"},
|
|
0xF8: [2]string{"PRG", "User Defined 8"},
|
|
0xF9: [2]string{"P16", "ProDOS-16 System File"},
|
|
0xFA: [2]string{"INT", "Integer BASIC Program"},
|
|
0xFB: [2]string{"IVR", "Integer BASIC Variables"},
|
|
0xFC: [2]string{"BAS", "Applesoft BASIC Program"},
|
|
0xFD: [2]string{"VAR", "Applesoft BASIC Variables"},
|
|
0xFE: [2]string{"REL", "EDASM Relocatable Code"},
|
|
0xFF: [2]string{"SYS", "ProDOS-8 System File"},
|
|
}
|
|
|
|
func (t ProDOSFileType) String() string {
|
|
|
|
info, ok := ProDOSTypeMap[t]
|
|
if ok {
|
|
return info[1]
|
|
}
|
|
|
|
return "Unknown"
|
|
}
|
|
|
|
func (ft ProDOSFileType) Ext() string {
|
|
|
|
info, ok := ProDOSTypeMap[ft]
|
|
if ok {
|
|
return info[0]
|
|
}
|
|
|
|
return "BIN"
|
|
}
|
|
|
|
func ProDOSFileTypeFromExt(ext string) ProDOSFileType {
|
|
for ft, info := range ProDOSTypeMap {
|
|
if strings.ToUpper(ext) == info[0] {
|
|
return ft
|
|
}
|
|
}
|
|
return 0x06
|
|
}
|
|
|
|
func (t ProDOSFileType) Valid() bool {
|
|
|
|
if t == FileType_PD_None {
|
|
return false
|
|
}
|
|
|
|
_, ok := ProDOSTypeMap[t]
|
|
|
|
return ok
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) Type() ProDOSFileType {
|
|
return ProDOSFileType(fd.Data[16])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetType(t ProDOSFileType) {
|
|
fd.Data[16] = byte(t)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) IndexBlock() int {
|
|
return int(fd.Data[17]) + 256*int(fd.Data[18])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetIndexBlock(b int) {
|
|
fd.Data[17] = byte(b & 0xff)
|
|
fd.Data[18] = byte(b / 256)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) TotalBlocks() int {
|
|
return int(fd.Data[19]) + 256*int(fd.Data[20])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetTotalBlocks(b int) {
|
|
fd.Data[19] = byte(b & 0xff)
|
|
fd.Data[20] = byte(b / 256)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) AuxType() int {
|
|
return int(fd.Data[31]) + 256*int(fd.Data[32])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetAuxType(b int) {
|
|
fd.Data[31] = byte(b & 0xff)
|
|
fd.Data[32] = byte(b / 256)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) TotalSectors() int {
|
|
return fd.TotalBlocks() / 2
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) Size() int {
|
|
return int(fd.Data[21]) + 256*int(fd.Data[22]) + 65536*int(fd.Data[23])
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetSize(v int) {
|
|
fd.Data[21] = byte(v & 0xff)
|
|
fd.Data[22] = byte((v >> 8) & 0xff)
|
|
fd.Data[23] = byte((v >> 16) & 0xff)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) SetHeaderPointer(v int) {
|
|
fd.Data[0x25] = byte(v & 0xff)
|
|
fd.Data[0x26] = byte((v >> 8) & 0xff)
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) HeaderPointer() int {
|
|
return int(fd.Data[0x25]) + 256*int(fd.Data[0x26])
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOS800GetBlock(block int) ([]byte, error) {
|
|
|
|
if len(d.Data) > 819200 {
|
|
maxblocks := len(d.Data) / 512
|
|
if block < maxblocks {
|
|
offset := 512 * block
|
|
return d.Data[offset : offset+512], nil
|
|
}
|
|
return nil, errors.New("Invalid block")
|
|
}
|
|
|
|
t, s1, s2 := d.PRODOS800GetBlockSectors(block)
|
|
|
|
e := d.Seek(t, s1)
|
|
if e != nil {
|
|
return []byte(nil), e
|
|
}
|
|
data := make([]byte, 0)
|
|
c1 := d.Read()
|
|
data = append(data, c1...)
|
|
|
|
e = d.Seek(t, s2)
|
|
if e != nil {
|
|
return []byte(nil), e
|
|
}
|
|
c2 := d.Read()
|
|
data = append(data, c2...)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSGetBlock(block int) ([]byte, error) {
|
|
|
|
t, s1, s2 := d.PRODOSGetBlockSectors(block)
|
|
|
|
e := d.Seek(t, s1)
|
|
if e != nil {
|
|
return []byte(nil), e
|
|
}
|
|
data := make([]byte, 0)
|
|
c1 := d.Read()
|
|
data = append(data, c1...)
|
|
|
|
e = d.Seek(t, s2)
|
|
if e != nil {
|
|
return []byte(nil), e
|
|
}
|
|
c2 := d.Read()
|
|
data = append(data, c2...)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOS800GetVDH(b int) (*VDH, error) {
|
|
|
|
data, e := d.PRODOS800GetBlock(b)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
|
|
vdh := &VDH{}
|
|
vdh.SetData(data[4:43], b, 4)
|
|
return vdh, nil
|
|
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSGetVDH(b int) (*VDH, error) {
|
|
|
|
data, e := d.PRODOSGetBlock(b)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
|
|
vdh := &VDH{}
|
|
vdh.SetData(data[4:43], b, 4)
|
|
return vdh, nil
|
|
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSSetVDH(b int, vdh *VDH) error {
|
|
|
|
data, e := d.PRODOSGetBlock(b)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
|
|
for i, v := range vdh.Data {
|
|
data[4+i] = v
|
|
}
|
|
|
|
return d.PRODOSWrite(b, data)
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSGetCatalogPathed(start int, path string, pattern string) (*VDH, []ProDOSFileDescriptor, error) {
|
|
|
|
path = strings.Trim(path, "/")
|
|
//fmt.Printf("PRODOSGetCatalogPathed(%d, \"%s\", \"%s\" )\n", start, path, pattern)
|
|
|
|
//start := 2 // where we start our descent
|
|
|
|
if path != "" {
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
|
|
subdir := parts[0]
|
|
parts = parts[1:]
|
|
|
|
// find subdirectories
|
|
vdh, files, e := d.PRODOSGetCatalog(start, subdir)
|
|
if e != nil {
|
|
return vdh, files, e
|
|
}
|
|
if len(files) != 1 {
|
|
return vdh, files, e
|
|
}
|
|
if files[0].Type() != FileType_PD_Directory {
|
|
return vdh, files, errors.New("Not a directory")
|
|
}
|
|
|
|
// ok, we found the directory...
|
|
if len(parts) > 0 {
|
|
// more subdirs
|
|
//fmt.Printf(">> Entering prodos subdir [%s]\n", files[0].Name())
|
|
newpathstr := strings.Join(parts, "/")
|
|
return d.PRODOSGetCatalogPathed(files[0].IndexBlock(), newpathstr, pattern)
|
|
} else {
|
|
// get directory based on this block
|
|
//fmt.Printf("-- Entering prodos subdir [%s]\n", files[0].Name())
|
|
return d.PRODOSGetCatalog(files[0].IndexBlock(), pattern)
|
|
}
|
|
|
|
} else {
|
|
return d.PRODOSGetCatalog(start, pattern)
|
|
}
|
|
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSGetCatalog(startblock int, pattern string) (*VDH, []ProDOSFileDescriptor, error) {
|
|
|
|
//fmt.Printf("GetCatalogProDOS(%d, %s)\n", startblock, pattern)
|
|
|
|
var err error
|
|
var re *regexp.Regexp
|
|
var patterntmp string
|
|
if pattern != "" {
|
|
patterntmp = strings.Replace(pattern, ".", "[.]", -1)
|
|
patterntmp = strings.Replace(patterntmp, "*", ".*", -1)
|
|
patterntmp = "(?i)^" + patterntmp + "$"
|
|
re = regexp.MustCompile(patterntmp)
|
|
}
|
|
|
|
var files []ProDOSFileDescriptor
|
|
var e error
|
|
var vtoc *VDH
|
|
|
|
fmt.Printf("FormatID = %s", d.Format.String())
|
|
|
|
if d.Format.ID == DF_PRODOS_800KB || d.Format.ID == DF_PRODOS_CUSTOM {
|
|
fmt.Println("HC VDH")
|
|
vtoc, e = d.PRODOS800GetVDH(startblock)
|
|
} else {
|
|
fmt.Println("LC VDH")
|
|
vtoc, e = d.PRODOSGetVDH(startblock)
|
|
}
|
|
if e != nil {
|
|
return vtoc, files, e
|
|
}
|
|
|
|
activeentries := 0
|
|
filecount := vtoc.GetFileCount()
|
|
blockentries := 2
|
|
entriesperblock := vtoc.GetEntriesPerBlock()
|
|
refnum := startblock
|
|
|
|
var data []byte
|
|
if d.Format.ID == DF_PRODOS_800KB || d.Format.ID == DF_PRODOS_CUSTOM {
|
|
data, _ = d.PRODOS800GetBlock(refnum)
|
|
} else {
|
|
data, _ = d.PRODOSGetBlock(refnum)
|
|
}
|
|
|
|
nextblock := int(data[2]) + 256*int(data[3])
|
|
|
|
fmt.Printf("ActiveCount = %d\n", filecount)
|
|
|
|
entrypointer := 4 + PRODOS_ENTRY_SIZE
|
|
|
|
for activeentries < filecount {
|
|
|
|
if data[entrypointer] != 0x00 {
|
|
// Valid entry
|
|
chunk := data[entrypointer : entrypointer+PRODOS_ENTRY_SIZE]
|
|
fd := ProDOSFileDescriptor{}
|
|
fd.SetData(chunk, refnum, entrypointer)
|
|
|
|
if fd.Type().Valid() {
|
|
|
|
var skipname bool = false
|
|
if re != nil {
|
|
fmt.Printf("Checking [%s] against regex /%s/\n", fd.Name(), patterntmp)
|
|
skipname = !re.MatchString(fd.Name())
|
|
}
|
|
|
|
if fd.GetStorageType() != StorageType_Inactive && !skipname {
|
|
files = append(files, fd)
|
|
}
|
|
|
|
}
|
|
|
|
activeentries++
|
|
}
|
|
|
|
if activeentries < filecount {
|
|
if blockentries == entriesperblock {
|
|
refnum = nextblock
|
|
if d.Format.ID == DF_PRODOS_800KB || d.Format.ID == DF_PRODOS_CUSTOM {
|
|
data, err = d.PRODOS800GetBlock(refnum)
|
|
} else {
|
|
data, err = d.PRODOSGetBlock(refnum)
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
nextblock = int(data[2]) + 256*int(data[3])
|
|
blockentries = 0x01
|
|
entrypointer = 0x04
|
|
} else {
|
|
entrypointer += PRODOS_ENTRY_SIZE
|
|
blockentries++
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return vtoc, files, nil
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSReadFileSectors(fd ProDOSFileDescriptor, maxblocks int) ([]byte, error) {
|
|
|
|
var data, index, chunk []byte
|
|
var e error
|
|
|
|
switch fd.GetStorageType() {
|
|
case StorageType_Seedling:
|
|
/* single block pointed to */
|
|
if d.Format.ID == DF_PRODOS_800KB || d.Format.ID == DF_PRODOS_CUSTOM {
|
|
data, _ = d.PRODOS800GetBlock(fd.IndexBlock())
|
|
} else {
|
|
data, _ = d.PRODOSGetBlock(fd.IndexBlock())
|
|
}
|
|
count := fd.Size()
|
|
if count > len(data) {
|
|
count = len(data)
|
|
}
|
|
return data[:count], e
|
|
case StorageType_Sapling:
|
|
if d.Format.ID == DF_PRODOS_800KB || d.Format.ID == DF_PRODOS_CUSTOM {
|
|
index, _ = d.PRODOS800GetBlock(fd.IndexBlock())
|
|
} else {
|
|
index, _ = d.PRODOSGetBlock(fd.IndexBlock())
|
|
}
|
|
data := make([]byte, 0)
|
|
bptr := 0
|
|
remaining := fd.Size() - len(data)
|
|
for len(data) < fd.Size() && bptr+256 < len(index) {
|
|
blocknum := int(index[bptr]) + 256*int(index[bptr+256])
|
|
|
|
//fmt.Printf("File block %d (%d %d)\n", blocknum, int(index[bptr]), int(index[bptr+1]))
|
|
|
|
if d.Format.ID == DF_PRODOS_800KB {
|
|
chunk, e = d.PRODOS800GetBlock(blocknum)
|
|
} else {
|
|
chunk, e = d.PRODOSGetBlock(blocknum)
|
|
}
|
|
if e != nil {
|
|
return data, e
|
|
}
|
|
|
|
count := 512
|
|
if remaining < count {
|
|
count = remaining
|
|
}
|
|
|
|
data = append(data, chunk[:count]...)
|
|
|
|
bptr += 1
|
|
remaining = fd.Size() - len(data)
|
|
}
|
|
return data, e
|
|
case StorageType_Tree:
|
|
return []byte(nil), errors.New("Unimplemented yet... soz :(")
|
|
}
|
|
|
|
return []byte(nil), nil
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSReadFile(fd ProDOSFileDescriptor) (int, int, []byte, error) {
|
|
|
|
data, e := d.PRODOSReadFileSectors(fd, -1)
|
|
|
|
if e != nil {
|
|
return 0, 0, data, e
|
|
}
|
|
|
|
switch fd.Type() {
|
|
case FileType_PD_INT:
|
|
return fd.Size(), fd.AuxType(), IntegerDetoks(data), nil
|
|
case FileType_PD_APP:
|
|
return fd.Size(), fd.AuxType(), ApplesoftDetoks(data), nil
|
|
case FileType_PD_TXT:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
case FileType_PD_BIN:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
default:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
}
|
|
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSReadFileRaw(fd ProDOSFileDescriptor) (int, int, []byte, error) {
|
|
|
|
data, e := d.PRODOSReadFileSectors(fd, -1)
|
|
|
|
if e != nil {
|
|
return 0, 0, data, e
|
|
}
|
|
|
|
switch fd.Type() {
|
|
case FileType_PD_INT:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
case FileType_PD_APP:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
case FileType_PD_TXT:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
case FileType_PD_BIN:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
default:
|
|
return fd.Size(), fd.AuxType(), data, nil
|
|
}
|
|
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSChecksumBlock(b int) string {
|
|
bl, _ := d.PRODOSGetBlock(b)
|
|
return Checksum(bl)
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOS800ChecksumBlock(b int) string {
|
|
bl, _ := d.PRODOS800GetBlock(b)
|
|
return Checksum(bl)
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOSGetBlockSectors(block int) (int, int, int) {
|
|
|
|
bpt := d.Format.USPT() / 2
|
|
|
|
track := block / bpt
|
|
|
|
bo := block % bpt
|
|
|
|
if d.Layout == SectorOrderProDOSLinear || len(d.Data) >= PRODOS_800KB_DISK_BYTES {
|
|
return track, bo * 2, bo*2 + 1
|
|
}
|
|
|
|
switch bo {
|
|
case 0:
|
|
return track, 0x0, 0xe
|
|
case 1:
|
|
return track, 0xd, 0xc
|
|
case 2:
|
|
return track, 0xb, 0xa
|
|
case 3:
|
|
return track, 0x9, 0x8
|
|
case 4:
|
|
return track, 0x7, 0x6
|
|
case 5:
|
|
return track, 0x5, 0x4
|
|
case 6:
|
|
return track, 0x3, 0x2
|
|
case 7:
|
|
return track, 0x1, 0xf
|
|
}
|
|
|
|
return track, 0x0, 0xe
|
|
|
|
}
|
|
|
|
func (d *DSKWrapper) PRODOS800GetBlockSectors(block int) (int, int, int) {
|
|
|
|
dblBlock := block * 2
|
|
|
|
spt := 40
|
|
|
|
track := dblBlock / spt
|
|
s1 := dblBlock % spt
|
|
s2 := (dblBlock + 1) % spt
|
|
|
|
return track, s1, s2
|
|
|
|
}
|
|
|
|
// PRODOSGetDirBlocks returns a list of blocks for the current directory level
|
|
func (dsk *DSKWrapper) PRODOSGetDirBlocks(start int) (*VDH, []int, [][]byte, error) {
|
|
blocks := make([]int, 0)
|
|
chunks := make([][]byte, 0)
|
|
|
|
vdh, err := dsk.PRODOSGetVDH(start)
|
|
if err != nil {
|
|
return vdh, blocks, chunks, err
|
|
}
|
|
|
|
// okay lets trail the directory
|
|
blocks = append(blocks, start)
|
|
data, _ := dsk.PRODOSGetBlock(start)
|
|
chunks = append(chunks, data)
|
|
nextBlock := int(data[0x02]) + 256*int(data[0x03])
|
|
for nextBlock != 0 {
|
|
blocks = append(blocks, nextBlock)
|
|
data, _ := dsk.PRODOSGetBlock(nextBlock)
|
|
chunks = append(chunks, data)
|
|
nextBlock = int(data[0x02]) + 256*int(data[0x03])
|
|
}
|
|
|
|
return vdh, blocks, chunks, nil
|
|
|
|
}
|
|
|
|
// PRODOSFindDirBlocks locates the blocks for a given directory (if it exists!)
|
|
func (dsk *DSKWrapper) PRODOSFindDirBlocks(start int, path string) (*VDH, []int, [][]byte, error) {
|
|
|
|
path = strings.Trim(path, "/")
|
|
|
|
if path == "" {
|
|
return dsk.PRODOSGetDirBlocks(start)
|
|
}
|
|
|
|
segments := strings.Split(path, "/")
|
|
target := segments[0]
|
|
segments = segments[1:]
|
|
|
|
vdh, files, err := dsk.PRODOSGetCatalog(start, "")
|
|
if err != nil {
|
|
return vdh, nil, nil, err
|
|
}
|
|
|
|
for _, f := range files {
|
|
|
|
if f.Type() != FileType_PD_Directory {
|
|
continue
|
|
}
|
|
|
|
if strings.ToLower(target) == strings.ToLower(f.NameUnadorned()) {
|
|
// matched path
|
|
newpath := strings.Join(segments, "/")
|
|
return dsk.PRODOSFindDirBlocks(f.IndexBlock(), newpath)
|
|
}
|
|
|
|
}
|
|
|
|
return vdh, nil, nil, errors.New("Path not found")
|
|
|
|
}
|
|
|
|
// PRODOSGetFirstFreeEntry for a given path, will find the first free file descriptor
|
|
// it will expand the directory if needed to create a free block
|
|
func (dsk *DSKWrapper) PRODOSGetFirstFreeEntry(path string, name string, grow bool) (*ProDOSFileDescriptor, error) {
|
|
|
|
vdh, blockList, blockData, err := dsk.PRODOSFindDirBlocks(2, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries := 0
|
|
|
|
// if we are here, we found the right directory...
|
|
for idx, data := range blockData {
|
|
count := 0
|
|
|
|
if idx == 0 {
|
|
count = 1
|
|
entries = 1
|
|
}
|
|
|
|
for count < vdh.GetEntriesPerBlock() {
|
|
offset := 4 + count*PRODOS_ENTRY_SIZE
|
|
chunk := data[offset : offset+PRODOS_ENTRY_SIZE]
|
|
fd := &ProDOSFileDescriptor{}
|
|
fd.SetData(chunk, blockList[idx], offset)
|
|
fd.entryNum = entries
|
|
|
|
//Printf("--> Check entry: %s, %v, %v\n", fd.NameUnadorned(), fd.CreateTime(), fd.ModTime())
|
|
|
|
if fd.GetStorageType() != 0x00 && strings.ToLower(fd.NameUnadorned()) == strings.ToLower(name) {
|
|
return fd, nil
|
|
} else if fd.GetStorageType() == 0x00 {
|
|
//fmt.Printf("found at entry %d in block %d\n", entries, blockList[idx])
|
|
return fd, nil
|
|
}
|
|
|
|
count += 1
|
|
entries++
|
|
}
|
|
}
|
|
|
|
if !grow {
|
|
return nil, errors.New("No free slot: told not to grow directory")
|
|
}
|
|
|
|
// If we got here, we need to create a new block
|
|
freeBlocks, err := dsk.PRODOSGetFreeBlocks(1, vdh.GetTotalBlocks())
|
|
if err != nil {
|
|
return nil, errors.New("Could not extend directory")
|
|
}
|
|
|
|
data, err := dsk.PRODOSGetBlock(freeBlocks[0])
|
|
prevBlock := blockList[len(blockList)-1]
|
|
data[0x00] = byte(prevBlock & 0xff)
|
|
data[0x01] = byte(prevBlock / 0x100)
|
|
|
|
offset := 4
|
|
chunk := data[offset : offset+PRODOS_ENTRY_SIZE]
|
|
fd := &ProDOSFileDescriptor{}
|
|
fd.entryNum = entries
|
|
fd.SetData(chunk, freeBlocks[0], offset)
|
|
|
|
dsk.PRODOSMarkBlocks(freeBlocks, false)
|
|
|
|
return fd, nil
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSMarkBlocks(list []int, free bool) error {
|
|
vbm, err := dsk.PRODOSGetVolumeBitmap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, b := range list {
|
|
vbm.SetBlockFree(b, free)
|
|
}
|
|
|
|
//fmt.Printf("Writing Volume bitmap to block %d\n", vbm.blockid)
|
|
|
|
return dsk.PRODOSWrite(vbm.blockid, vbm.Data)
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSGetFreeBlocks(count int, totalBlocks int) ([]int, error) {
|
|
|
|
vbm, err := dsk.PRODOSGetVolumeBitmap()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b := 0
|
|
|
|
blocks := make([]int, 0)
|
|
|
|
for b < totalBlocks && len(blocks) < count {
|
|
if vbm.IsBlockFree(b) {
|
|
blocks = append(blocks, b)
|
|
}
|
|
b++
|
|
}
|
|
|
|
if len(blocks) == count {
|
|
return blocks, nil
|
|
}
|
|
|
|
return blocks, errors.New("Not enough blocks")
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSDeleteFile(path string, name string) error {
|
|
|
|
fd, err := dsk.PRODOSGetNamedEntry(path, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if fd.GetStorageType() == 0x00 {
|
|
return errors.New("Not found")
|
|
}
|
|
|
|
// At this stage we have a match
|
|
access := fd.AccessMode()
|
|
if access&AccessType_Destroy == 0 {
|
|
return errors.New("Permission denied")
|
|
}
|
|
if access&AccessType_Writable == 0 {
|
|
return errors.New("Read-only file")
|
|
}
|
|
|
|
// Make sure its either a sapling or a seedling
|
|
st := fd.GetStorageType()
|
|
if st != StorageType_Sapling && st != StorageType_Seedling && st != StorageType_SubDir_File {
|
|
return errors.New("Special file deletion not implemented: yet.")
|
|
} else if st == StorageType_SubDir_File {
|
|
return dsk.PRODOSDeleteDirectory(path, name)
|
|
}
|
|
|
|
var removeBlocks []int
|
|
switch st {
|
|
case StorageType_Seedling:
|
|
removeBlocks = append(removeBlocks, fd.IndexBlock())
|
|
case StorageType_Sapling:
|
|
removeBlocks = append(removeBlocks, fd.IndexBlock())
|
|
ib, err := dsk.PRODOSGetBlock(removeBlocks[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
maxBlocks := 1600
|
|
if len(dsk.Data) == STD_DISK_BYTES {
|
|
maxBlocks = 280
|
|
}
|
|
|
|
i := 0
|
|
b := int(ib[i]) + 256*int(ib[256+i])
|
|
for i < 256 && b != 0 && b <= maxBlocks {
|
|
removeBlocks = append(removeBlocks, b)
|
|
b = int(ib[i]) + 256*int(ib[256+i])
|
|
i++
|
|
}
|
|
}
|
|
|
|
err = dsk.PRODOSMarkBlocks(removeBlocks, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// // get the VDH -- we need this later
|
|
vdh, _, _, err := dsk.PRODOSFindDirBlocks(2, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// now delete the fileentry
|
|
fd.SetStorageType(StorageType_Inactive)
|
|
err = fd.Publish(dsk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// update filecount
|
|
vdh.SetFileCount(vdh.GetFileCount() - 1)
|
|
|
|
return vdh.Publish(dsk)
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSWriteFile(path string, name string, kind ProDOSFileType, data []byte, auxtype int) error {
|
|
|
|
name = strings.ToUpper(name)
|
|
|
|
nst := StorageType_Seedling
|
|
blocksNeeded := len(data)/512 + 1
|
|
totalBlocks := blocksNeeded
|
|
if blocksNeeded > 1 {
|
|
nst = StorageType_Sapling
|
|
totalBlocks++ // extra block for blocklist
|
|
} else if blocksNeeded > 256 {
|
|
return errors.New("Not implemented: Tree write")
|
|
}
|
|
|
|
var origTime time.Time
|
|
var origAccess ProDOSAccessMode
|
|
|
|
fd, err := dsk.PRODOSGetNamedEntry(path, name)
|
|
if err == nil {
|
|
origTime = fd.CreateTime() // we need this later
|
|
origAccess = fd.AccessMode()
|
|
err = dsk.PRODOSDeleteFile(path, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
|
|
fd, err = dsk.PRODOSGetFirstFreeEntry(path, name, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
vdh, err := dsk.PRODOSGetVDH(2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
freeBlocks, err := dsk.PRODOSGetFreeBlocks(totalBlocks, vdh.GetTotalBlocks())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Okay got enough blocks
|
|
switch nst {
|
|
case StorageType_Sapling:
|
|
err = dsk.PRODOSWriteSaplingBlocks(freeBlocks[0], freeBlocks[1:], data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case StorageType_Seedling:
|
|
//fmt.Printf("Write Seedling %d bytes data to block %d\n", len(data), freeBlocks[0])
|
|
err = dsk.PRODOSWrite(freeBlocks[0], data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Get the current directories directory header
|
|
dvdh, blocks, _, err := dsk.PRODOSFindDirBlocks(2, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// common details - note we preserve access mode and time when overwriting the same file
|
|
fd.SetAuxType(auxtype)
|
|
fd.SetName(name)
|
|
fd.SetType(kind)
|
|
fd.SetTotalBlocks(totalBlocks)
|
|
fd.SetIndexBlock(freeBlocks[0])
|
|
fd.SetSize(len(data))
|
|
fd.SetStorageType(nst)
|
|
if origAccess == 0x00 {
|
|
fd.SetAccessMode(AccessType_Default)
|
|
} else {
|
|
fd.SetAccessMode(origAccess)
|
|
}
|
|
if origTime.IsZero() {
|
|
fd.SetCreateTime(time.Now())
|
|
} else {
|
|
fd.SetCreateTime(origTime)
|
|
}
|
|
fd.SetModTime(time.Now())
|
|
fd.SetHeaderPointer(blocks[0])
|
|
|
|
fd.Publish(dsk)
|
|
|
|
dvdh.SetFileCount(dvdh.GetFileCount() + 1)
|
|
dvdh.Publish(dsk)
|
|
|
|
err = dsk.PRODOSMarkBlocks(freeBlocks, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (fd *ProDOSFileDescriptor) Publish(dsk *DSKWrapper) error {
|
|
|
|
//fmt.Printf("Writing FD data back to block %d, offset %d\n", fd.blockid, fd.blockoffset)
|
|
|
|
bd, err := dsk.PRODOSGetBlock(fd.blockid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, v := range fd.Data {
|
|
bd[fd.blockoffset+i] = v
|
|
}
|
|
// for i, _ := range bd {
|
|
// bd[i] = byte(0xff ^ (i % 2))
|
|
// }
|
|
|
|
//Dump(bd)
|
|
|
|
return dsk.PRODOSWrite(fd.blockid, bd)
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSWrite(b int, data []byte) error {
|
|
|
|
//log.Printf("====> Request to write block %.4x", b)
|
|
|
|
for len(data) < 512 {
|
|
data = append(data, 0x00)
|
|
}
|
|
|
|
t, s1, s2 := dsk.PRODOSGetBlockSectors(b)
|
|
|
|
err := dsk.Seek(t, s1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dsk.Write(data[:256])
|
|
err = dsk.Seek(t, s2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dsk.Write(data[256:])
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSWriteSaplingBlocks(indexBlock int, dataBlocks []int, data []byte) error {
|
|
|
|
if len(dataBlocks) > 256 || len(data) > 0x20000 {
|
|
return errors.New("Too many data blocks")
|
|
}
|
|
|
|
ib := make([]byte, 512)
|
|
for i, blocknum := range dataBlocks {
|
|
// index the block
|
|
ib[0+i] = byte(blocknum & 0xff)
|
|
ib[256+i] = byte(blocknum / 0x100)
|
|
|
|
// data offset...
|
|
ptr := 512 * i
|
|
end := ptr + 512
|
|
if end > len(data) {
|
|
end = len(data)
|
|
}
|
|
chunk := data[ptr:end]
|
|
for len(chunk) < 512 {
|
|
chunk = append(chunk, 0x00)
|
|
}
|
|
err := dsk.PRODOSWrite(blocknum, chunk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return dsk.PRODOSWrite(indexBlock, ib)
|
|
}
|
|
|
|
// PRODOSCreateDirectory tries to create a subdirectory...
|
|
func (dsk *DSKWrapper) PRODOSCreateDirectory(path string, name string) error {
|
|
|
|
vdh, err := dsk.PRODOSGetVDH(2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fd, err := dsk.PRODOSGetNamedEntry(path, name)
|
|
if err == nil {
|
|
return errors.New("Item exists")
|
|
}
|
|
|
|
fd, err = dsk.PRODOSGetFirstFreeEntry(path, name, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// got file descriptor
|
|
if fd.GetStorageType() != 0 {
|
|
|
|
if fd.Type() == FileType_PD_Directory {
|
|
return errors.New("Directory already exists")
|
|
}
|
|
|
|
return errors.New("Type mismatch")
|
|
}
|
|
|
|
// Get the current directories directory header
|
|
dvdh, blocks, _, err := dsk.PRODOSFindDirBlocks(2, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// find a freeBlock for the directory
|
|
freeBlocks, err := dsk.PRODOSGetFreeBlocks(1, vdh.GetTotalBlocks())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// got a file descriptor
|
|
fd.SetStorageType(StorageType_SubDir_File)
|
|
fd.SetAccessMode(AccessType_Default | AccessType_Changed)
|
|
fd.SetCreateTime(time.Now())
|
|
fd.SetModTime(time.Now())
|
|
fd.SetName(name)
|
|
fd.SetType(FileType_PD_Directory)
|
|
fd.SetIndexBlock(freeBlocks[0]) // <------- block for the directory header
|
|
fd.SetTotalBlocks(1)
|
|
fd.SetSize(512)
|
|
fd.SetHeaderPointer(blocks[0])
|
|
fd.SetMinVersion(0x00)
|
|
fd.SetVersion(0x23)
|
|
|
|
err = dsk.PRODOSInitDirectoryBlock(freeBlocks[0], blocks[0], dvdh.GetEntriesPerBlock(), dvdh.GetEntryLength(), fd.entryNum, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = dsk.PRODOSMarkBlocks(freeBlocks, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dvdh.SetFileCount(vdh.GetFileCount() + 1)
|
|
dvdh.Publish(dsk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fd.Publish(dsk)
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSInitDirectoryBlock(targetBlock int, parentBlock int, entriesPerBlock int, entrySize int, entryNum int, name string) error {
|
|
|
|
block := make([]byte, 512)
|
|
dh := &VDH{
|
|
Data: block[4 : 4+PRODOS_ENTRY_SIZE],
|
|
blockid: targetBlock,
|
|
blockoffset: 4,
|
|
}
|
|
|
|
// Must be set (beneath Apple ProDOS)
|
|
dh.Data[0x10] = 0x75
|
|
|
|
dh.SetStorageType(StorageType_SubDir_Header)
|
|
dh.SetName(name)
|
|
dh.SetCreateTime(time.Now())
|
|
dh.SetAccess(AccessType_Default)
|
|
dh.SetEntriesPerBlock(entriesPerBlock)
|
|
dh.SetEntryLength(entrySize)
|
|
dh.SetFileCount(0)
|
|
dh.SetDirParentPointer(parentBlock)
|
|
dh.SetDirParentEntry(entryNum)
|
|
dh.SetDirParentEntryLength(entrySize)
|
|
dh.SetMinVersion(0x00)
|
|
dh.SetVersion(0x23)
|
|
|
|
return dsk.PRODOSWrite(targetBlock, block)
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSDeleteDirectory(path string, name string) error {
|
|
|
|
fd, err := dsk.PRODOSGetNamedEntry(path, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// got file descriptor, is it empty
|
|
if fd.GetStorageType() == 0x00 {
|
|
return errors.New("Path not found")
|
|
}
|
|
|
|
if fd.Type() != FileType_PD_Directory {
|
|
return errors.New("Not a directory")
|
|
}
|
|
|
|
// Get the current directories directory header
|
|
_, files, err := dsk.PRODOSGetCatalog(fd.IndexBlock(), "*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove any files in subdirectory
|
|
for _, subfile := range files {
|
|
if subfile.GetStorageType() == StorageType_SubDir_File {
|
|
// FOLDER, RECURSE
|
|
err = dsk.PRODOSDeleteDirectory(path+"/"+name, subfile.NameUnadorned())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if subfile.GetStorageType() == StorageType_Tree {
|
|
// TREE FILE -- UNSUPPORTED FOR NOW
|
|
return errors.New("Tree file handling not supported currently")
|
|
} else {
|
|
err = dsk.PRODOSDeleteFile(path+"/"+name, subfile.NameUnadorned())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check again and make sure it is empty
|
|
_, files, err = dsk.PRODOSGetCatalog(fd.IndexBlock(), "*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(files) > 0 {
|
|
return errors.New("Could not delete all subfiles")
|
|
}
|
|
|
|
// Get blocks that make up the directory
|
|
_, dirBlocks, _, err := dsk.PRODOSFindDirBlocks(2, path+"/"+name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Free all the things!
|
|
err = dsk.PRODOSMarkBlocks(dirBlocks, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// free file entry
|
|
fd.SetStorageType(StorageType_Inactive)
|
|
err = fd.Publish(dsk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// reduce count by 1 at top level
|
|
tvdh, _, _, err := dsk.PRODOSFindDirBlocks(2, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tvdh.SetFileCount(tvdh.GetFileCount() - 1)
|
|
return tvdh.Publish(dsk)
|
|
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSSetLocked(path, name string, lock bool) error {
|
|
|
|
// We cheat here a bit and use the get first free entry call with
|
|
// autogrow turned off.
|
|
fd, err := dsk.PRODOSGetNamedEntry(path, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fd.SetLocked(lock)
|
|
return fd.Publish(dsk)
|
|
|
|
}
|
|
|
|
// PRODOSGetNamedEntry for a given path, will find the file descriptor with name
|
|
func (dsk *DSKWrapper) PRODOSGetNamedEntry(path string, name string) (*ProDOSFileDescriptor, error) {
|
|
|
|
//fmt.Printf(">>> Path = %s\n", path)
|
|
|
|
vdh, blockList, blockData, err := dsk.PRODOSFindDirBlocks(2, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries := 0
|
|
|
|
// if we are here, we found the right directory...
|
|
for idx, data := range blockData {
|
|
count := 0
|
|
|
|
if idx == 0 {
|
|
count = 1
|
|
entries = 1
|
|
}
|
|
|
|
for count < vdh.GetEntriesPerBlock() {
|
|
offset := 4 + count*PRODOS_ENTRY_SIZE
|
|
chunk := data[offset : offset+PRODOS_ENTRY_SIZE]
|
|
fd := &ProDOSFileDescriptor{}
|
|
fd.SetData(chunk, blockList[idx], offset)
|
|
fd.entryNum = entries
|
|
|
|
//fmt.Printf(">>> Comparing %s to %s\n", strings.ToLower(fd.NameUnadorned()), strings.ToLower(name))
|
|
|
|
if fd.GetStorageType() != 0x00 && strings.ToLower(fd.NameUnadorned()) == strings.ToLower(name) {
|
|
return fd, nil
|
|
}
|
|
|
|
count += 1
|
|
entries++
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("Not found")
|
|
}
|
|
|
|
func (dsk *DSKWrapper) PRODOSRenameFile(path, name, newname string) error {
|
|
|
|
fd, err := dsk.PRODOSGetNamedEntry(path, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = dsk.PRODOSGetNamedEntry(path, newname)
|
|
if err == nil {
|
|
return errors.New("New name already exists")
|
|
}
|
|
|
|
// can rename here
|
|
fd.SetName(newname)
|
|
return fd.Publish(dsk)
|
|
|
|
}
|