diff --git a/lib/dos33/dos33.go b/lib/dos33/dos33.go new file mode 100644 index 0000000..6239bb6 --- /dev/null +++ b/lib/dos33/dos33.go @@ -0,0 +1,228 @@ +// Copyright © 2016 Zellyn Hunter + +// Package dos33 contains routines for working with the on-disk structures of DOS 3.3. +package dos33 + +import ( + "encoding/binary" + "fmt" +) + +type FreeSectorMap [4]byte // Bit map of free sectors in a track + +// VTOC is the struct used to hold the DOS 3.3 VTOC structure. +// See page 4-2 of Beneath Apple DOS. +type VTOC struct { + Unused1 byte // Not used + CatalogTrack byte // Track number of first catalog sector + CatalogSector byte // Sector number of first catalog sector + DOSRelease byte // Release number of DOS used to INIT this diskette + Unused2 [2]byte // Not used + Volume byte // Diskette volume number (1-254) + Unused3 [32]byte // Not used + // Maximum number of track/secotr pairs which will fit in one file + // track/sector list sector (122 for 256 byte sectors) + TrackSectorListMaxSize byte + Unused4 [8]byte // Not used + LastTrack byte // Last track where sectors were allocated + TrackDirection int8 // Direction of track allocation (+1 or -1) + Unused5 [2]byte + NumTracks byte // Number of tracks per diskette (normally 35) + NumSectors byte // Number of sectors per track (13 or 16) + BytesPerSector uint16 // Number of bytes per sector (LO/HI format) + FreeSectors [50]FreeSectorMap +} + +// MarshalBinary marshals the VTOC sector to bytes. Error is always nil. +func (v VTOC) MarshalBinary() (data []byte, err error) { + buf := make([]byte, 256) + buf[0x00] = v.Unused1 + buf[0x01] = v.CatalogTrack + buf[0x02] = v.CatalogSector + buf[0x03] = v.DOSRelease + copyBytes(buf[0x04:0x06], v.Unused2[:]) + buf[0x06] = v.Volume + copyBytes(buf[0x07:0x27], v.Unused3[:]) + buf[0x27] = v.TrackSectorListMaxSize + copyBytes(buf[0x28:0x30], v.Unused4[:]) + buf[0x30] = v.LastTrack + buf[0x31] = byte(v.TrackDirection) + copyBytes(buf[0x32:0x34], v.Unused5[:]) + buf[0x34] = v.NumTracks + buf[0x35] = v.NumSectors + binary.LittleEndian.PutUint16(buf[0x36:0x38], v.BytesPerSector) + for i, m := range v.FreeSectors { + copyBytes(buf[0x38+4*i:0x38+4*i+4], m[:]) + } + return buf, nil +} + +// 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) +} + +// UnmarshalBinary unmarshals the VTOC sector from bytes. Input is +// expected to be exactly 256 bytes. +func (v *VTOC) UnmarshalBinary(data []byte) error { + if len(data) != 256 { + return fmt.Errorf("VTOC.UnmarshalBinary expects exactly 256 bytes; got %d", len(data)) + } + + v.Unused1 = data[0x00] + v.CatalogTrack = data[0x01] + v.CatalogSector = data[0x02] + v.DOSRelease = data[0x03] + copyBytes(v.Unused2[:], data[0x04:0x06]) + v.Volume = data[0x06] + copyBytes(v.Unused3[:], data[0x07:0x27]) + v.TrackSectorListMaxSize = data[0x27] + copyBytes(v.Unused4[:], data[0x28:0x30]) + v.LastTrack = data[0x30] + v.TrackDirection = int8(data[0x31]) + copyBytes(v.Unused5[:], data[0x32:0x34]) + v.NumTracks = data[0x34] + v.NumSectors = data[0x35] + v.BytesPerSector = binary.LittleEndian.Uint16(data[0x36:0x38]) + for i := range v.FreeSectors { + copyBytes(v.FreeSectors[i][:], data[0x38+4*i:0x38+4*i+4]) + } + + return nil +} + +func DefaultVTOC() VTOC { + v := VTOC{ + CatalogTrack: 0x11, + CatalogSector: 0x0f, + DOSRelease: 0x03, + Volume: 0x01, + TrackSectorListMaxSize: 122, + LastTrack: 0x00, // TODO(zellyn): what should this be? + TrackDirection: 1, + NumTracks: 0x23, + NumSectors: 0x10, + BytesPerSector: 0x100, + } + for i := range v.FreeSectors { + v.FreeSectors[i] = FreeSectorMap{} + if i < 35 { + v.FreeSectors[i] = FreeSectorMap([4]byte{0xff, 0xff, 0x00, 0x00}) + } + } + return v +} + +// CatalogSector is the struct used to hold the DOS 3.3 Catalog +// sector. +type CatalogSector struct { + Unused1 byte // Not used + NextTrack byte // Track number of next catalog sector (usually 11 hex) + NextSector byte // Sector number of next catalog sector + Unused2 [8]byte // Not used + FileDescs [7]FileDesc // File descriptive entries +} + +// MarshalBinary marshals the CatalogSector to bytes. Error is always nil. +func (cs CatalogSector) MarshalBinary() (data []byte, err error) { + buf := make([]byte, 256) + buf[0x00] = cs.Unused1 + buf[0x01] = cs.NextTrack + buf[0x02] = cs.NextSector + copyBytes(buf[0x03:0x0b], cs.Unused2[:]) + for i, fd := range cs.FileDescs { + fdBytes, _ := fd.MarshalBinary() + copyBytes(buf[0x0b+35*i:0x0b+35*(i+1)], fdBytes) + } + return buf, nil +} + +// UnmarshalBinary unmarshals the CatalogSector from bytes. Input is +// expected to be exactly 256 bytes. +func (cs *CatalogSector) UnmarshalBinary(data []byte) error { + if len(data) != 256 { + return fmt.Errorf("CatalogSector.UnmarshalBinary expects exactly 256 bytes; got %d", len(data)) + } + + cs.Unused1 = data[0x00] + cs.NextTrack = data[0x01] + cs.NextSector = data[0x02] + copyBytes(cs.Unused2[:], data[0x03:0x0b]) + + for i := range cs.FileDescs { + if err := cs.FileDescs[i].UnmarshalBinary(data[0x0b+35*i : 0x0b+35*(i+1)]); err != nil { + return err + } + } + + return nil +} + +type Filetype byte + +const ( + // Hex 80+file type - file is locked + // Hex 00+file type - file is not locked + FiletypeLocked Filetype = 0x80 + + FileTypeText Filetype = 0x00 // Text file + FileTypeInteger Filetype = 0x01 // INTEGER BASIC file + FileTypeApplesoft Filetype = 0x02 // APPLESOFT BASIC file + FileTypeBinary Filetype = 0x04 // BINARY file + FileTypeS Filetype = 0x08 // S type file + FileTypeRelocatable Filetype = 0x10 // RELOCATABLE object module file + FileTypeA Filetype = 0x20 // A type file + FileTypeB Filetype = 0x40 // B type file +) + +// FileDesc is the struct used to represent the DOS 3.3 File +// Descriptive entry. +type FileDesc struct { + // Track of first track/sector list sector. If this is a deleted + // file, this byte contains a hex FF and the original track number + // is copied to the last byte of the file name field (BYTE 20). If + // this byte contains a hex 00, the entry is assumed to never have + // been used and is available for use. (This means track 0 can never + // be used for data even if the DOS image is "wiped" from the + // diskette.) + TrackSectorListTrack byte + TrackSectorListSector byte // Sector of first track/sector list sector + Filetype Filetype // File type and flags + Filename [30]byte // File name (30 characters) Length of file in + // sectors (LO/HI format). The CATALOG command will only format the + // LO byte of this length giving 1-255 but a full 65,535 may be + // stored here. + SectorCount uint16 +} + +// MarshalBinary marshals the FileDesc to bytes. Error is always nil. +func (fd FileDesc) MarshalBinary() (data []byte, err error) { + buf := make([]byte, 35) + buf[0x00] = fd.TrackSectorListTrack + buf[0x01] = fd.TrackSectorListSector + buf[0x02] = byte(fd.Filetype) + copyBytes(buf[0x03:0x21], fd.Filename[:]) + binary.LittleEndian.PutUint16(buf[0x21:0x23], fd.SectorCount) + + return buf, nil +} + +// UnmarshalBinary unmarshals the FileDesc from bytes. Input is +// expected to be exactly 35 bytes. +func (fd *FileDesc) UnmarshalBinary(data []byte) error { + if len(data) != 35 { + return fmt.Errorf("FileDesc.UnmarshalBinary expects exactly 35 bytes; got %d", len(data)) + } + + fd.TrackSectorListTrack = data[0x00] + fd.TrackSectorListSector = data[0x01] + fd.Filetype = Filetype(data[0x02]) + copyBytes(data[0x03:0x21], fd.Filename[:]) + fd.SectorCount = binary.LittleEndian.Uint16(data[0x21:0x23]) + + return nil +} diff --git a/lib/dos33/dos33_test.go b/lib/dos33/dos33_test.go new file mode 100644 index 0000000..6d6c2fa --- /dev/null +++ b/lib/dos33/dos33_test.go @@ -0,0 +1,49 @@ +package dos33 + +import ( + "crypto/rand" + "reflect" + "testing" +) + +// TestVTOCMarshalRoundtrip checks a simple roundtrip of VTOC data. +func TestVTOCMarshalRoundtrip(t *testing.T) { + buf := make([]byte, 256) + rand.Read(buf) + vtoc1 := &VTOC{} + if err := vtoc1.UnmarshalBinary(buf); err != nil { + t.Fatal(err) + } + buf2, _ := vtoc1.MarshalBinary() + if !reflect.DeepEqual(buf, buf2) { + t.Errorf("Buffers differ: %v != %v", buf, buf2) + } + vtoc2 := &VTOC{} + if err := vtoc2.UnmarshalBinary(buf2); err != nil { + t.Fatal(err) + } + if *vtoc1 != *vtoc2 { + t.Errorf("Structs differ: %v != %v", vtoc1, vtoc2) + } +} + +// TestCatalogSectorMarshalRoundtrip checks a simple roundtrip of CatalogSector data. +func TestCatalogSectorMarshalRoundtrip(t *testing.T) { + buf := make([]byte, 256) + rand.Read(buf) + cs1 := &CatalogSector{} + if err := cs1.UnmarshalBinary(buf); err != nil { + t.Fatal(err) + } + buf2, _ := cs1.MarshalBinary() + if !reflect.DeepEqual(buf, buf2) { + t.Errorf("Buffers differ: %v != %v", buf, buf2) + } + cs2 := &CatalogSector{} + if err := cs2.UnmarshalBinary(buf2); err != nil { + t.Fatal(err) + } + if *cs1 != *cs2 { + t.Errorf("Structs differ: %v != %v", cs1, cs2) + } +}