mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
f025957467
Before, `getBinary` did not work for 13 sector disks because it assumed that all tracks had 16 sectors. On top of that, `readSector` could not decode sectors with 5 and 3 encoding. And finally, `findSector` couldn't even find sectors with DOS 3.2 address fields. All of these have been fixed and some tests added to make sure that they keep working.
756 lines
24 KiB
TypeScript
756 lines
24 KiB
TypeScript
import { byte, word } from 'js/types';
|
|
import { debug, toHex } from 'js/util';
|
|
import ApplesoftDump from 'js/applesoft/decompiler';
|
|
import IntegerBASICDump from 'js/intbasic/decompiler';
|
|
import { MassStorageData, NibbleDisk } from '../types';
|
|
import { readSector, writeSector, _DO } from '../format_utils';
|
|
|
|
/** Usual track for VTOC */
|
|
export const DEFAULT_VTOC_TRACK = 0x11;
|
|
|
|
/** Usual sector for VTOC */
|
|
export const DEFAULT_VTOC_SECTOR = 0x00;
|
|
|
|
/** Usual track, sector for VTOC */
|
|
export const DEFAULT_VTOC_TRACK_SECTOR = {
|
|
track: DEFAULT_VTOC_TRACK,
|
|
sector: DEFAULT_VTOC_SECTOR,
|
|
} as const;
|
|
|
|
/**
|
|
* VTOC sector offsets
|
|
*/
|
|
export const VTOC_OFFSETS = {
|
|
CATALOG_TRACK: 0x01,
|
|
CATALOG_SECTOR: 0x02,
|
|
VERSION: 0x03,
|
|
VOLUME: 0x06,
|
|
TRACK_SECTOR_LIST_SIZE: 0x27,
|
|
LAST_ALLOCATION_TRACK: 0x30,
|
|
ALLOCATION_DIRECTION: 0x31,
|
|
TRACK_COUNT: 0x34,
|
|
SECTOR_COUNT: 0x35,
|
|
SECTOR_BYTE_COUNT_LOW: 0x36,
|
|
SECTOR_BYTE_COUNT_HIGH: 0x37,
|
|
FREE_SECTOR_MAP: 0x38,
|
|
} as const;
|
|
|
|
/**
|
|
* Catalog sector offsets
|
|
*/
|
|
export const CATALOG_OFFSETS = {
|
|
NEXT_CATALOG_TRACK: 0x01,
|
|
NEXT_CATALOG_SECTOR: 0x02,
|
|
ENTRY1: 0x0B,
|
|
ENTRY2: 0x2E,
|
|
ENTRY3: 0x51,
|
|
ENTRY4: 0x74,
|
|
ENTRY5: 0x97,
|
|
ENTRY6: 0xBA,
|
|
ENTRY7: 0xDD,
|
|
} as const;
|
|
|
|
/**
|
|
* Catalog entry offsets
|
|
*/
|
|
export const CATALOG_ENTRY_OFFSETS = {
|
|
SECTOR_LIST_TRACK: 0x00,
|
|
SECTOR_LIST_SECTOR: 0x01,
|
|
FILE_TYPE: 0x02,
|
|
FILE_NAME: 0x03,
|
|
DELETED_FILE_TRACK: 0x20,
|
|
FILE_LENGTH_LOW: 0x21,
|
|
FILE_LENGTH_HIGH: 0x22,
|
|
} as const;
|
|
|
|
export const CATALOG_ENTRY_LENGTH = 0x23;
|
|
|
|
/**
|
|
* VTOC Data
|
|
*/
|
|
export interface VTOC {
|
|
catalog: {
|
|
track: byte;
|
|
sector: byte;
|
|
};
|
|
version: byte;
|
|
volume: byte;
|
|
trackSectorListSize: byte;
|
|
lastAllocationTrack: byte;
|
|
allocationDirection: byte;
|
|
trackCount: byte;
|
|
sectorCount: byte;
|
|
sectorByteCount: byte;
|
|
trackSectorMap: boolean[][];
|
|
}
|
|
|
|
/**
|
|
* Track and sector data
|
|
*/
|
|
export interface TrackSector {
|
|
track: byte;
|
|
sector: byte;
|
|
}
|
|
|
|
/**
|
|
* File entry data
|
|
*/
|
|
export interface FileEntry {
|
|
locked: boolean;
|
|
deleted: boolean;
|
|
type: string;
|
|
size: number;
|
|
name: string;
|
|
trackSectorList: TrackSector;
|
|
}
|
|
|
|
/**
|
|
* File data
|
|
*/
|
|
export interface FileData {
|
|
address: word;
|
|
data: Uint8Array;
|
|
}
|
|
|
|
function isNibbleDisk(disk: NibbleDisk | MassStorageData): disk is NibbleDisk {
|
|
return !!((disk as NibbleDisk).encoding);
|
|
}
|
|
|
|
/**
|
|
* DOS 3.3 Volume object.
|
|
*/
|
|
export class DOS33 {
|
|
private vtoc: VTOC;
|
|
private files: FileEntry[];
|
|
|
|
/**
|
|
* Constructor can take either a nibblized disk, or a raw data
|
|
* object returned by MassStorage.getBinary()
|
|
*
|
|
* @param disk Nibble disk or MassStorageData object
|
|
*/
|
|
constructor(private disk: NibbleDisk | MassStorageData) {
|
|
this.vtoc = this.readVolumeTOC();
|
|
}
|
|
|
|
/**
|
|
* Method to read or write a sector, could be overloaded to support other
|
|
* data types. This uses the DOS logical to physical sector mapping.
|
|
*
|
|
* @param track Track to read/write
|
|
* @param sector Sector to read/write
|
|
* @param data If present, sector data to write
|
|
*
|
|
* @returns data read or written
|
|
*/
|
|
rwts(track: byte, sector: byte, data?: Uint8Array): Uint8Array {
|
|
if (data) {
|
|
if (isNibbleDisk(this.disk)) {
|
|
writeSector(this.disk, track, _DO[sector], data);
|
|
} else {
|
|
const offset = track * 0x1000 + sector * 0x100;
|
|
new Uint8Array(this.disk.data).set(data, offset);
|
|
}
|
|
} else {
|
|
if (isNibbleDisk(this.disk)) {
|
|
data = readSector(this.disk, track, _DO[sector]);
|
|
} else {
|
|
const offset = track * 0x1000 + sector * 0x100;
|
|
// Slice new array so modifications to apply to original track
|
|
data = new Uint8Array(this.disk.data.slice(offset, offset + 0x100));
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Creates a classic hex and ascii dump of a sector
|
|
*
|
|
* @param track Track to dump
|
|
* @param sector Sector to dump
|
|
* @returns String representation of sector
|
|
*/
|
|
dumpSector(track: byte, sector: byte) {
|
|
let result = '';
|
|
const data = this.rwts(track, sector);
|
|
let b;
|
|
for (let idx = 0; idx < 16; idx++) {
|
|
result += toHex(idx << 4) + ': ';
|
|
for (let jdx = 0; jdx < 16; jdx++) {
|
|
b = data[idx * 16 + jdx];
|
|
result += toHex(b) + ' ';
|
|
}
|
|
result += ' ';
|
|
for (let jdx = 0; jdx < 16; jdx++) {
|
|
b = data[idx * 16 + jdx] & 0x7f;
|
|
if (b >= 0x20 && b < 0x7f) {
|
|
result += String.fromCharCode(b);
|
|
} else {
|
|
result += '.';
|
|
}
|
|
}
|
|
result += '\n';
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Returns all the track sector pairs for a file.
|
|
*
|
|
* @param file File to read
|
|
* @param full Also return track sector map entries
|
|
* @returns Array of file track and sectors
|
|
*/
|
|
readFileTrackSectorList(file: FileEntry, full?: boolean) {
|
|
const fileTrackSectorList = [];
|
|
let { track, sector } = file.trackSectorList;
|
|
while (track || sector) {
|
|
if (full) {
|
|
fileTrackSectorList.push({ track, sector });
|
|
}
|
|
let jdx = 0; // offset in sector
|
|
const data = this.rwts(track, sector);
|
|
track = data[0x01];
|
|
sector = data[0x02];
|
|
let offset = 0x0C; // offset in data
|
|
while ((data[offset] || data[offset + 1]) && jdx < 121) {
|
|
fileTrackSectorList.push({
|
|
track: data[offset],
|
|
sector: data[offset + 1]
|
|
});
|
|
offset += 2;
|
|
jdx++;
|
|
}
|
|
}
|
|
return fileTrackSectorList;
|
|
}
|
|
|
|
/**
|
|
* Read a file from disk
|
|
*
|
|
* @param file File entry to read
|
|
* @returns Data for file, and load address if binary
|
|
*/
|
|
readFile(file: FileEntry): FileData {
|
|
let data: byte[] = [];
|
|
let idx;
|
|
const fileTrackSectorList = this.readFileTrackSectorList(file);
|
|
for (idx = 0; idx < fileTrackSectorList.length; idx++) {
|
|
const { track, sector } = fileTrackSectorList[idx];
|
|
data = data.concat([...this.rwts(track, sector)]);
|
|
}
|
|
let offset = 0;
|
|
let length = 0;
|
|
let address = 0;
|
|
|
|
switch (file.type) {
|
|
case 'I':
|
|
case 'A':
|
|
offset = 2;
|
|
length = data[0] | data[1] << 8;
|
|
break;
|
|
case 'T':
|
|
length = 0;
|
|
while (data[length]) { length++; }
|
|
break;
|
|
case 'B':
|
|
offset = 4;
|
|
address = data[0] | data[1] << 8;
|
|
length = data[2] | data[3] << 8;
|
|
break;
|
|
}
|
|
|
|
data = data.slice(offset, offset + length);
|
|
|
|
return { data: new Uint8Array(data), address };
|
|
}
|
|
|
|
/**
|
|
* Allocate a new sector for a file in the allocation list
|
|
*
|
|
* @returns track and sector pair
|
|
*/
|
|
allocateSector(): TrackSector {
|
|
const { vtoc } = this;
|
|
const findSector = (track: byte) => {
|
|
const sectorMap = vtoc.trackSectorMap[track];
|
|
return sectorMap.findIndex((sector: boolean) => sector);
|
|
};
|
|
|
|
const lastTrack = vtoc.lastAllocationTrack;
|
|
let track = lastTrack;
|
|
let sector = findSector(track);
|
|
while (sector === -1) {
|
|
if (vtoc.allocationDirection === 0x01) {
|
|
track = track - 1;
|
|
if (track < 0) {
|
|
track = vtoc.catalog.track;
|
|
vtoc.allocationDirection = 0xff;
|
|
}
|
|
} else {
|
|
track = track + 1;
|
|
if (track >= vtoc.trackCount) {
|
|
throw new Error('Insufficient free space');
|
|
}
|
|
}
|
|
sector = findSector(track);
|
|
}
|
|
|
|
vtoc.lastAllocationTrack = track;
|
|
vtoc.trackSectorMap[track][sector] = false;
|
|
|
|
return { track, sector };
|
|
}
|
|
|
|
/**
|
|
* Compute free sector count.
|
|
*
|
|
* @returns count of free sectors
|
|
*/
|
|
freeSectorCount() {
|
|
return this.vtoc.trackSectorMap.reduce((count, flags) => (
|
|
count + flags.reduce((count, flag) => (
|
|
count + (flag ? 1 : 0)
|
|
), 0)
|
|
), 0);
|
|
}
|
|
|
|
/**
|
|
* Compute used sector count
|
|
*
|
|
* @returns used sector count
|
|
*/
|
|
usedSectorCount() {
|
|
return this.vtoc.trackSectorMap.reduce((count, flags) => (
|
|
count + flags.reduce((count, flag) => (
|
|
count + (flag ? 0 : 1)
|
|
), 0)
|
|
), 0);
|
|
}
|
|
|
|
/**
|
|
* Writes a file to disk. If new file the file entry must also be
|
|
* added to the catalog.
|
|
*
|
|
* @param file File to write
|
|
* @param fileData File data to write, including address if binary
|
|
*/
|
|
writeFile(file: FileEntry, fileData: FileData) {
|
|
let prefix: byte[] = [];
|
|
let { data } = fileData;
|
|
switch (file.type) {
|
|
case 'A':
|
|
case 'I':
|
|
prefix = [
|
|
data.length % 0x100,
|
|
data.length >> 8
|
|
];
|
|
break;
|
|
case 'B':
|
|
prefix = [
|
|
fileData.address % 0x100,
|
|
fileData.address >> 8,
|
|
data.length % 0x100,
|
|
data.length >> 8
|
|
];
|
|
break;
|
|
}
|
|
data = new Uint8Array(prefix.length + data.length);
|
|
data.set(prefix);
|
|
data.set(data, prefix.length);
|
|
|
|
const { sectorByteCount, trackSectorListSize } = this.vtoc;
|
|
const dataRequiredSectors = Math.ceil(data.length / sectorByteCount);
|
|
const fileSectorListRequiredSectors = Math.ceil(dataRequiredSectors / trackSectorListSize);
|
|
const requiredSectors = dataRequiredSectors + fileSectorListRequiredSectors;
|
|
let idx;
|
|
let sectors: TrackSector[] = [];
|
|
|
|
if (file.trackSectorList) {
|
|
sectors = this.readFileTrackSectorList(file, true);
|
|
}
|
|
if (sectors.length > requiredSectors) {
|
|
for (idx = requiredSectors; idx < sectors.length; idx++) {
|
|
const { track, sector } = sectors[idx];
|
|
this.vtoc.trackSectorMap[track][sector] = true;
|
|
}
|
|
sectors = sectors.slice(0, requiredSectors);
|
|
}
|
|
if (sectors.length < requiredSectors) {
|
|
for (idx = sectors.length; idx < requiredSectors; idx++) {
|
|
sectors.push(this.allocateSector());
|
|
}
|
|
}
|
|
file.trackSectorList = { ...sectors[0] };
|
|
file.size = requiredSectors;
|
|
|
|
let jdx = 0;
|
|
let lastTrackSectorList = null;
|
|
|
|
for (idx = 0; idx < dataRequiredSectors; idx++) {
|
|
let sector: TrackSector;
|
|
let sectorData;
|
|
|
|
const { trackSectorListSize } = this.vtoc;
|
|
if (idx % trackSectorListSize === 0) {
|
|
sector = sectors.shift() as TrackSector;
|
|
sectorData = new Uint8Array();
|
|
if (lastTrackSectorList) {
|
|
lastTrackSectorList[0x01] = sector.track;
|
|
lastTrackSectorList[0x02] = sector.sector;
|
|
}
|
|
sectorData[0x05] = idx & 0xff;
|
|
sectorData[0x06] = idx >> 8;
|
|
for (jdx = 0; jdx < trackSectorListSize && jdx < sectors.length; jdx++) {
|
|
const offset = 0xC + jdx * 2;
|
|
sectorData[offset] = sectors[jdx].track;
|
|
sectorData[offset + 1] = sectors[jdx].sector;
|
|
}
|
|
lastTrackSectorList = sectorData;
|
|
this.rwts(sector.track, sector.sector, new Uint8Array(sectorData));
|
|
}
|
|
|
|
sector = sectors.shift() as TrackSector;
|
|
sectorData = new Uint8Array(0x100);
|
|
sectorData.set(data.slice(0, 0x100));
|
|
data = data.slice(0x100);
|
|
this.rwts(sector.track, sector.sector, sectorData);
|
|
}
|
|
this.writeVolumeTOC();
|
|
this.writeCatalog();
|
|
}
|
|
|
|
/**
|
|
* Convert a file into a string that can be displayed.
|
|
*
|
|
* @param file File to convert
|
|
* @returns A string representing the file
|
|
*/
|
|
dumpFile(file: FileEntry) {
|
|
let result = null;
|
|
const fileData = this.readFile(file);
|
|
switch (file.type) {
|
|
case 'A':
|
|
result = new ApplesoftDump(fileData.data, 0).decompile();
|
|
break;
|
|
case 'I':
|
|
result = new IntegerBASICDump(fileData.data).toString();
|
|
break;
|
|
case 'T':
|
|
result = '';
|
|
for (let idx = 0; idx < fileData.data.length; idx++) {
|
|
const char = fileData.data[idx] & 0x7f;
|
|
if (char < 0x20) {
|
|
if (char === 0xd) { // CR
|
|
result += '\n';
|
|
} else {
|
|
result += `$${toHex(char)}`;
|
|
}
|
|
} else {
|
|
result += String.fromCharCode(char);
|
|
}
|
|
}
|
|
break;
|
|
case 'B':
|
|
default: {
|
|
result = '';
|
|
let hex = '';
|
|
let ascii = '';
|
|
for (let idx = 0; idx < fileData.data.length; idx++) {
|
|
const val = fileData.data[idx];
|
|
if (idx % 16 === 0) {
|
|
if (idx !== 0) {
|
|
result += `${hex} ${ascii}\n`;
|
|
}
|
|
hex = '';
|
|
ascii = '';
|
|
result += `${toHex(fileData.address + idx, 4)}:`;
|
|
}
|
|
hex += ` ${toHex(val)}`;
|
|
ascii += (val & 0x7f) >= 0x20 ? String.fromCharCode(val & 0x7f) : '.';
|
|
}
|
|
result += '\n';
|
|
} break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Read VTOC data from disk
|
|
*
|
|
* @param trackSector Track and sector to read from, or default
|
|
* @returns VTOC
|
|
*/
|
|
readVolumeTOC(trackSector: TrackSector = DEFAULT_VTOC_TRACK_SECTOR) {
|
|
const data = this.rwts(trackSector.track, trackSector.sector);
|
|
this.vtoc = {
|
|
catalog: {
|
|
track: data[VTOC_OFFSETS.CATALOG_TRACK],
|
|
sector: data[VTOC_OFFSETS.CATALOG_SECTOR]
|
|
},
|
|
version: data[VTOC_OFFSETS.VERSION],
|
|
volume: data[VTOC_OFFSETS.VOLUME],
|
|
trackSectorListSize: data[VTOC_OFFSETS.TRACK_SECTOR_LIST_SIZE],
|
|
lastAllocationTrack: data[VTOC_OFFSETS.LAST_ALLOCATION_TRACK],
|
|
allocationDirection: data[VTOC_OFFSETS.ALLOCATION_DIRECTION],
|
|
trackCount: data[VTOC_OFFSETS.TRACK_COUNT],
|
|
sectorCount: data[VTOC_OFFSETS.SECTOR_COUNT],
|
|
sectorByteCount: data[VTOC_OFFSETS.SECTOR_BYTE_COUNT_LOW] |
|
|
(data[VTOC_OFFSETS.SECTOR_BYTE_COUNT_HIGH] << 8),
|
|
trackSectorMap: []
|
|
};
|
|
|
|
for (let idx = 0; idx < this.vtoc.trackCount; idx++) {
|
|
const sectorMap = [];
|
|
const offset = 0x38 + idx * 4;
|
|
let bitmap =
|
|
(data[offset] << 24) |
|
|
(data[offset + 1] << 16) |
|
|
(data[offset + 2] << 8) |
|
|
data[offset + 3];
|
|
|
|
for (let jdx = 0; jdx < this.vtoc.sectorCount; jdx++) {
|
|
sectorMap.unshift(!!(bitmap & 0x80000000));
|
|
bitmap <<= 1;
|
|
}
|
|
this.vtoc.trackSectorMap.push(sectorMap);
|
|
}
|
|
|
|
debug(`DISK VOLUME ${this.vtoc.volume}`);
|
|
|
|
return this.vtoc;
|
|
}
|
|
|
|
/**
|
|
* Write VTOC data back to disk.
|
|
*
|
|
* @param trackSector Track and sector to read from, or default
|
|
*/
|
|
writeVolumeTOC(trackSector: TrackSector = DEFAULT_VTOC_TRACK_SECTOR) {
|
|
const { vtoc } = this;
|
|
const data = new Uint8Array(0x100).fill(0);
|
|
data[VTOC_OFFSETS.CATALOG_TRACK] = vtoc.catalog.track;
|
|
data[VTOC_OFFSETS.CATALOG_SECTOR] = vtoc.catalog.sector;
|
|
data[VTOC_OFFSETS.VERSION] = vtoc.version || 3;
|
|
data[VTOC_OFFSETS.VOLUME] = vtoc.volume || 0xFE;
|
|
data[VTOC_OFFSETS.TRACK_SECTOR_LIST_SIZE] = vtoc.trackSectorListSize || 0x7a;
|
|
data[VTOC_OFFSETS.LAST_ALLOCATION_TRACK] = vtoc.lastAllocationTrack;
|
|
data[VTOC_OFFSETS.ALLOCATION_DIRECTION] = vtoc.allocationDirection;
|
|
data[VTOC_OFFSETS.TRACK_COUNT] = vtoc.trackCount;
|
|
data[VTOC_OFFSETS.SECTOR_COUNT] = vtoc.sectorCount;
|
|
data[VTOC_OFFSETS.SECTOR_BYTE_COUNT_LOW] = vtoc.sectorByteCount & 0xff;
|
|
data[VTOC_OFFSETS.SECTOR_BYTE_COUNT_HIGH] = vtoc.sectorByteCount >> 8;
|
|
|
|
for (let idx = 0; idx < vtoc.trackSectorMap.length; idx++) {
|
|
const offset = 0x38 + idx * 4;
|
|
const sectorMap = vtoc.trackSectorMap[idx];
|
|
|
|
let mask = 0;
|
|
for (let jdx = 0; jdx < sectorMap.length; jdx++) {
|
|
mask >>= 1;
|
|
if (sectorMap[jdx]) {
|
|
mask |= 0x80000000;
|
|
}
|
|
}
|
|
|
|
data[offset] = (mask >> 24) & 0xff;
|
|
data[offset + 1] = (mask >> 16) & 0xff;
|
|
data[offset + 2] = (mask >> 8) & 0xff;
|
|
data[offset + 3] = mask & 0xff;
|
|
}
|
|
this.rwts(trackSector.track, trackSector.sector, data);
|
|
}
|
|
|
|
/**
|
|
* Reads catalog from disk.
|
|
*
|
|
* @returns Catalog entries
|
|
*/
|
|
readCatalog(): FileEntry[] {
|
|
const { catalog } = this.vtoc;
|
|
this.files = [];
|
|
|
|
let catTrack = catalog.track;
|
|
let catSector = catalog.sector;
|
|
while (catSector || catTrack) {
|
|
const data = this.rwts(catTrack, catSector);
|
|
|
|
catTrack = data[CATALOG_OFFSETS.NEXT_CATALOG_TRACK];
|
|
catSector = data[CATALOG_OFFSETS.NEXT_CATALOG_SECTOR];
|
|
|
|
for (let idx = CATALOG_OFFSETS.ENTRY1; idx < 0x100; idx += CATALOG_ENTRY_LENGTH) {
|
|
const file: FileEntry = {
|
|
locked: false,
|
|
deleted: false,
|
|
type: 'A',
|
|
size: 0,
|
|
name: '',
|
|
trackSectorList: { track: 0, sector: 0 },
|
|
};
|
|
let str = '';
|
|
const entry = data.slice(idx, idx + CATALOG_ENTRY_LENGTH);
|
|
|
|
if (!entry[CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK]) {
|
|
continue;
|
|
}
|
|
|
|
file.trackSectorList = {
|
|
track: entry[CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK],
|
|
sector: entry[CATALOG_ENTRY_OFFSETS.SECTOR_LIST_SECTOR]
|
|
};
|
|
|
|
if (file.trackSectorList.track === 0xff) {
|
|
file.deleted = true;
|
|
file.trackSectorList.track = entry[CATALOG_ENTRY_OFFSETS.DELETED_FILE_TRACK];
|
|
}
|
|
|
|
// Locked
|
|
if (entry[CATALOG_ENTRY_OFFSETS.FILE_TYPE] & 0x80) {
|
|
file.locked = true;
|
|
}
|
|
|
|
str += file.locked ? '*' : ' ';
|
|
|
|
// File type
|
|
switch (entry[CATALOG_ENTRY_OFFSETS.FILE_TYPE] & 0x7f) {
|
|
case 0x00:
|
|
file.type = 'T';
|
|
break;
|
|
case 0x01:
|
|
file.type = 'I';
|
|
break;
|
|
case 0x02:
|
|
file.type = 'A';
|
|
break;
|
|
case 0x04:
|
|
file.type = 'B';
|
|
break;
|
|
case 0x08:
|
|
file.type = 'S';
|
|
break;
|
|
case 0x10:
|
|
file.type = 'R';
|
|
break;
|
|
case 0x20:
|
|
file.type = 'A';
|
|
break;
|
|
case 0x40:
|
|
file.type = 'B';
|
|
break;
|
|
}
|
|
str += file.type;
|
|
str += ' ';
|
|
|
|
// Size
|
|
file.size = entry[CATALOG_ENTRY_OFFSETS.FILE_LENGTH_LOW] |
|
|
entry[CATALOG_ENTRY_OFFSETS.FILE_LENGTH_HIGH] << 8;
|
|
str += Math.floor(file.size / 100);
|
|
str += Math.floor(file.size / 10) % 10;
|
|
str += file.size % 10;
|
|
str += ' ';
|
|
|
|
// Filename
|
|
for (let jdx = CATALOG_ENTRY_OFFSETS.FILE_NAME; jdx < 0x21; jdx++) {
|
|
file.name += String.fromCharCode(entry[jdx] & 0x7f);
|
|
}
|
|
str += file.name;
|
|
debug(str);
|
|
this.files.push(file);
|
|
}
|
|
}
|
|
return this.files;
|
|
}
|
|
|
|
/**
|
|
* Writes catalog back to disk
|
|
*/
|
|
writeCatalog() {
|
|
const { catalog } = this.vtoc;
|
|
|
|
let catTrack = catalog.track;
|
|
let catSector = catalog.sector;
|
|
while (catSector || catTrack) {
|
|
const data = this.rwts(catTrack, catSector);
|
|
|
|
for (let idx = CATALOG_OFFSETS.ENTRY1; idx < 0x100; idx += CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK) {
|
|
const file = this.files.shift();
|
|
|
|
if (!file?.trackSectorList) {
|
|
continue;
|
|
}
|
|
|
|
data[idx + CATALOG_ENTRY_OFFSETS.SECTOR_LIST_TRACK] = file.trackSectorList.track;
|
|
data[idx + CATALOG_ENTRY_OFFSETS.SECTOR_LIST_SECTOR] = file.trackSectorList.sector;
|
|
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] = file.locked ? 0x80 : 0x00;
|
|
|
|
// File type
|
|
switch (file.type) {
|
|
case 'T':
|
|
break;
|
|
case 'I':
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] |= 0x01;
|
|
break;
|
|
case 'A':
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] |= 0x02;
|
|
break;
|
|
case 'B':
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] |= 0x04;
|
|
break;
|
|
case 'S':
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] |= 0x08;
|
|
break;
|
|
case 'R':
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_TYPE] |= 0x10;
|
|
break;
|
|
}
|
|
|
|
// Size
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_LENGTH_LOW] = file.size & 0xff;
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_LENGTH_HIGH] = file.size >> 8;
|
|
|
|
// Filename
|
|
for (let jdx = 0; jdx < 0x1E; jdx++) {
|
|
data[idx + CATALOG_ENTRY_OFFSETS.FILE_NAME + jdx] = file.name.charCodeAt(jdx) | 0x80;
|
|
}
|
|
}
|
|
this.rwts(catTrack, catSector, data);
|
|
|
|
catTrack = data[CATALOG_OFFSETS.NEXT_CATALOG_TRACK];
|
|
catSector = data[CATALOG_OFFSETS.NEXT_CATALOG_SECTOR];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the volume number from the VTOC
|
|
*
|
|
* @returns Volume number
|
|
*/
|
|
getVolumeNumber() {
|
|
return this.vtoc.volume;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Very lose check for DOS disks, currently simply checks for the
|
|
* version byte in the probable VTOC.
|
|
*
|
|
* @param disk Image to check for DOS
|
|
* @returns true if VTOC version byte is 3
|
|
*/
|
|
export function isMaybeDOS33(disk: NibbleDisk | MassStorageData) {
|
|
let data;
|
|
if (isNibbleDisk(disk)) {
|
|
data = readSector(disk, DEFAULT_VTOC_TRACK, _DO[DEFAULT_VTOC_SECTOR]);
|
|
} else if (disk.data.byteLength > 0) {
|
|
data = new Uint8Array(
|
|
disk.data,
|
|
DEFAULT_VTOC_TRACK * 4096 + DEFAULT_VTOC_SECTOR * 0x100,
|
|
0x100
|
|
);
|
|
} else {
|
|
return false;
|
|
}
|
|
return data[VTOC_OFFSETS.VERSION] === 3;
|
|
}
|