Debugger disk info groundwork (#145)
This commit is contained in:
parent
9dcc741305
commit
e414f8d105
|
@ -76,10 +76,29 @@ export default class ApplesoftDecompiler {
|
|||
* fail.
|
||||
*
|
||||
* @param program The program bytes.
|
||||
* @param base
|
||||
* @param base Address of start of program, or 0 to compute start
|
||||
*/
|
||||
constructor(private readonly program: ReadonlyUint8Array,
|
||||
private readonly base: word = 0x801) {
|
||||
constructor(
|
||||
private readonly program: ReadonlyUint8Array,
|
||||
private readonly base: word = 0x801
|
||||
) {
|
||||
if (this.base === 0) {
|
||||
// Signals that we're loading a file from disk, and
|
||||
// addresses are arbitrarily absolute, so we compute
|
||||
// base by taking the next line address and adjusting it to
|
||||
// the actual beginning of the next line.
|
||||
const nextLine = this.wordAt(0);
|
||||
// Start at beginning of first line
|
||||
let nextLineIndex = 4;
|
||||
// Find 0 at end of line
|
||||
while (program[nextLineIndex]) {
|
||||
nextLineIndex++;
|
||||
}
|
||||
// Move to beginning of next line
|
||||
nextLineIndex++;
|
||||
// Adjust base
|
||||
this.base = nextLine - nextLineIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the 2-byte word at the given offset. */
|
||||
|
@ -99,8 +118,7 @@ export default class ApplesoftDecompiler {
|
|||
*/
|
||||
private forEachLine(
|
||||
from: number, to: number,
|
||||
callback: (offset: word) => void): void
|
||||
{
|
||||
callback: (offset: word) => void): void {
|
||||
let count = 0;
|
||||
let offset = 0;
|
||||
let nextLineAddr = this.wordAt(offset);
|
||||
|
|
|
@ -4,7 +4,6 @@ import { rom as readOnlyRom } from '../roms/cards/cffa';
|
|||
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
|
||||
import { ProDOSVolume } from '../formats/prodos';
|
||||
import createBlockDisk from '../formats/block';
|
||||
import { dump } from '../formats/prodos/utils';
|
||||
import {
|
||||
BlockDisk,
|
||||
BlockFormat,
|
||||
|
@ -441,7 +440,6 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
|||
this._identity[drive][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
|
||||
|
||||
const prodos = new ProDOSVolume(disk);
|
||||
dump(prodos);
|
||||
|
||||
this._name[drive] = disk.name;
|
||||
this._partitions[drive] = prodos;
|
||||
|
@ -485,7 +483,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
|||
if (!blockDisk) {
|
||||
return null;
|
||||
}
|
||||
const { name, blocks } = blockDisk;
|
||||
const { name, blocks, readOnly } = blockDisk;
|
||||
let ext;
|
||||
let data: ArrayBuffer;
|
||||
if (this._metadata[drive]) {
|
||||
|
@ -503,6 +501,7 @@ export default class CFFA implements Card, MassStorage<BlockFormat>, Restorable<
|
|||
name,
|
||||
ext,
|
||||
data,
|
||||
readOnly,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
PROCESS_JSON_DISK,
|
||||
PROCESS_JSON,
|
||||
ENCODING_BITSTREAM,
|
||||
MassStorage,
|
||||
MassStorageData,
|
||||
} from '../formats/types';
|
||||
|
||||
|
@ -309,7 +310,7 @@ function setDriveState(state: DriveState) {
|
|||
/**
|
||||
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
||||
*/
|
||||
export default class DiskII implements Card<State> {
|
||||
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
|
||||
private drives: Drive[] = [
|
||||
{ // Drive 1
|
||||
|
@ -415,15 +416,15 @@ export default class DiskII implements Card<State> {
|
|||
|
||||
/**
|
||||
* Spin the disk under the read/write head for WOZ images.
|
||||
*
|
||||
*
|
||||
* This implementation emulates every clock cycle of the 2 MHz
|
||||
* sequencer since the last time it was called in order to
|
||||
* determine the current state. Because this is called on
|
||||
* every access to the softswitches, the data in the latch
|
||||
* will be correct on every read.
|
||||
*
|
||||
*
|
||||
* The emulation of the disk makes a few simplifying assumptions:
|
||||
*
|
||||
*
|
||||
* * The motor turns on instantly.
|
||||
* * The head moves tracks instantly.
|
||||
* * The length (in bits) of each track of the WOZ image
|
||||
|
@ -962,24 +963,27 @@ export default class DiskII implements Card<State> {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ disks
|
||||
getBinary(drive: DriveNumber): MassStorageData | null {
|
||||
// TODO(flan): Does not work with WOZ or D13 disks
|
||||
getBinary(drive: DriveNumber, ext?: NibbleFormat): MassStorageData | null {
|
||||
const cur = this.drives[drive - 1];
|
||||
if (!isNibbleDrive(cur)) {
|
||||
return null;
|
||||
}
|
||||
// TODO(flan): Assumes 16-sectors
|
||||
const len = (16 * cur.tracks.length * 256);
|
||||
const { format, name, readOnly, tracks, volume } = cur;
|
||||
const len = format === 'nib' ?
|
||||
tracks.reduce((acc, track) => acc + track.length, 0) :
|
||||
this.sectors * tracks.length * 256;
|
||||
const data = new Uint8Array(len);
|
||||
|
||||
ext = ext ?? format;
|
||||
let idx = 0;
|
||||
for (let t = 0; t < cur.tracks.length; t++) {
|
||||
if (cur.format === 'nib') {
|
||||
data.set(cur.tracks[t], idx);
|
||||
idx += cur.tracks[t].length;
|
||||
for (let t = 0; t < tracks.length; t++) {
|
||||
if (ext === 'nib') {
|
||||
data.set(tracks[t], idx);
|
||||
idx += tracks[t].length;
|
||||
} else {
|
||||
for (let s = 0; s < 0x10; s++) {
|
||||
const sector = readSector(cur, t, s);
|
||||
const sector = readSector({ ...cur, format: ext }, t, s);
|
||||
data.set(sector, idx);
|
||||
idx += sector.length;
|
||||
}
|
||||
|
@ -987,13 +991,15 @@ export default class DiskII implements Card<State> {
|
|||
}
|
||||
|
||||
return {
|
||||
ext: 'dsk',
|
||||
name: cur.name,
|
||||
data: data.buffer
|
||||
ext,
|
||||
name,
|
||||
data: data.buffer,
|
||||
readOnly,
|
||||
volume,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ disks
|
||||
// TODO(flan): Does not work with WOZ or D13 disks
|
||||
getBase64(drive: DriveNumber) {
|
||||
const cur = this.drives[drive - 1];
|
||||
if (!isNibbleDrive(cur)) {
|
||||
|
|
|
@ -5,8 +5,6 @@ import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData }
|
|||
import CPU6502, { CpuState, flags } from '../cpu6502';
|
||||
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
|
||||
import createBlockDisk from '../formats/block';
|
||||
import { ProDOSVolume } from '../formats/prodos';
|
||||
import { dump } from '../formats/prodos/utils';
|
||||
import { DriveNumber } from '../formats/types';
|
||||
|
||||
const ID = 'SMARTPORT.J.S';
|
||||
|
@ -132,7 +130,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
private busy: boolean[] = [];
|
||||
private busyTimeout: ReturnType<typeof setTimeout>[] = [];
|
||||
private ext: string[] = [];
|
||||
private metadata: Array<HeaderData|null> = [];
|
||||
private metadata: Array<HeaderData | null> = [];
|
||||
|
||||
constructor(
|
||||
private cpu: CPU6502,
|
||||
|
@ -550,12 +548,14 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
}
|
||||
|
||||
setBinary(drive: DriveNumber, name: string, fmt: string, rawData: ArrayBuffer) {
|
||||
const volume = 254;
|
||||
const readOnly = false;
|
||||
let volume = 254;
|
||||
let readOnly = false;
|
||||
if (fmt === '2mg') {
|
||||
const header = read2MGHeader(rawData);
|
||||
this.metadata[drive] = header;
|
||||
const { bytes, offset } = header;
|
||||
volume = header.volume;
|
||||
readOnly = header.readOnly;
|
||||
rawData = rawData.slice(offset, offset + bytes);
|
||||
} else {
|
||||
this.metadata[drive] = null;
|
||||
|
@ -571,9 +571,6 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
this.disks[drive] = createBlockDisk(options);
|
||||
this.callbacks?.label(drive, name);
|
||||
|
||||
const prodos = new ProDOSVolume(this.disks[drive]);
|
||||
dump(prodos);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -583,7 +580,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
}
|
||||
const disk = this.disks[drive];
|
||||
const ext = this.ext[drive];
|
||||
const { name } = disk;
|
||||
const { name, readOnly } = disk;
|
||||
let data: ArrayBuffer;
|
||||
if (ext === '2mg') {
|
||||
data = create2MGFromBlockDisk(this.metadata[drive], disk);
|
||||
|
@ -599,6 +596,7 @@ export default class SmartPort implements Card, MassStorage<BlockFormat>, Restor
|
|||
name,
|
||||
ext,
|
||||
data,
|
||||
readOnly,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export const Apple2 = (props: Apple2Props) => {
|
|||
const [apple2, setApple2] = useState<Apple2Impl>();
|
||||
const [error, setError] = useState<unknown>();
|
||||
const [ready, setReady] = useState(false);
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
const [showDebug, setShowDebug] = useState(true);
|
||||
const drivesReady = useMemo(() => new Ready(setError), []);
|
||||
|
||||
const io = apple2?.getIO();
|
||||
|
|
|
@ -20,4 +20,5 @@
|
|||
flex-direction: row;
|
||||
border-bottom: 2px groove;
|
||||
margin-bottom: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Inset } from '../Inset';
|
||||
import { Tab, Tabs } from '../Tabs';
|
||||
import { Apple2 } from 'js/apple2';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { CPU } from './CPU';
|
||||
|
||||
import styles from './css/Debugger.module.css';
|
||||
import { Applesoft } from './Applesoft';
|
||||
import { CPU } from './CPU';
|
||||
import { Disks } from './Disks';
|
||||
import { Memory } from './Memory';
|
||||
import { VideoModes } from './VideoModes';
|
||||
|
||||
import styles from './css/Debugger.module.css';
|
||||
|
||||
interface DebuggerProps {
|
||||
apple2: Apple2 | undefined;
|
||||
}
|
||||
|
@ -27,13 +29,15 @@ export const Debugger = ({ apple2 }: DebuggerProps) => {
|
|||
<Tab>CPU</Tab>
|
||||
<Tab>Video</Tab>
|
||||
<Tab>Memory</Tab>
|
||||
<Tab>Disks</Tab>
|
||||
<Tab>Applesoft</Tab>
|
||||
</Tabs>
|
||||
<div className={styles.debugger}>
|
||||
{selected === 0 ? <CPU apple2={apple2} /> : null}
|
||||
{selected === 1 ? <VideoModes apple2={apple2} /> : null}
|
||||
{selected === 2 ? <Memory apple2={apple2} /> : null}
|
||||
{selected === 3 ? <Applesoft apple2={apple2} /> : null}
|
||||
{selected === 3 ? <Disks apple2={apple2} /> : null}
|
||||
{selected === 4 ? <Applesoft apple2={apple2} /> : null}
|
||||
</div>
|
||||
</Inset>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,421 @@
|
|||
import { h, Fragment } from 'preact';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import cs from 'classnames';
|
||||
import { Apple2 as Apple2Impl } from 'js/apple2';
|
||||
import { BlockDisk, DiskFormat, DriveNumber, MassStorage, NibbleDisk } from 'js/formats/types';
|
||||
import { slot } from 'js/apple2io';
|
||||
import DiskII from 'js/cards/disk2';
|
||||
import SmartPort from 'js/cards/smartport';
|
||||
import createDiskFrom2MG from 'js/formats/2mg';
|
||||
import createBlockDisk from 'js/formats/block';
|
||||
import { ProDOSVolume } from 'js/formats/prodos';
|
||||
import { FILE_TYPES, STORAGE_TYPES } from 'js/formats/prodos/constants';
|
||||
import { Directory } from 'js/formats/prodos/directory';
|
||||
import { FileEntry } from 'js/formats/prodos/file_entry';
|
||||
import { VDH } from 'js/formats/prodos/vdh';
|
||||
import { toHex } from 'js/util';
|
||||
|
||||
import styles from './css/Disks.module.css';
|
||||
import debuggerStyles from './css/Debugger.module.css';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import { DOS33, FileEntry as DOSEntry, isMaybeDOS33 } from 'js/formats/dos/dos33';
|
||||
import createDiskFromDOS from 'js/formats/do';
|
||||
import { FileData, FileViewer } from './FileViewer';
|
||||
|
||||
/**
|
||||
* Formats a short date string
|
||||
*
|
||||
* @param date Data object
|
||||
* @returns Short string date
|
||||
*/
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard for determining whether a disk is a nibble or block based disk
|
||||
*
|
||||
* @param disk NibbleDisk or BlockDisk
|
||||
* @returns true if is BlockDisk
|
||||
*/
|
||||
function isBlockDisk(disk: NibbleDisk | BlockDisk): disk is BlockDisk {
|
||||
return !!((disk as BlockDisk).blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for FileListing component
|
||||
*/
|
||||
interface FileListingProps {
|
||||
volume: ProDOSVolume;
|
||||
fileEntry: FileEntry;
|
||||
depth: number;
|
||||
setFileData: (fileData: FileData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a ProDOS file entry.
|
||||
*
|
||||
* @param depth Depth of listing from root
|
||||
* @param fileEntry ProDOS file entry to display
|
||||
* @returns FileListing component
|
||||
*/
|
||||
const FileListing = ({ depth, fileEntry, setFileData }: FileListingProps) => {
|
||||
const deleted = fileEntry.storageType === STORAGE_TYPES.DELETED;
|
||||
const doSetFileData = useCallback(() => {
|
||||
const binary = fileEntry.getFileData();
|
||||
const text = fileEntry.getFileText();
|
||||
if (binary && text) {
|
||||
setFileData({
|
||||
binary,
|
||||
text,
|
||||
fileName: fileEntry.name,
|
||||
});
|
||||
}
|
||||
}, [fileEntry, setFileData]);
|
||||
return (
|
||||
<tr>
|
||||
<td
|
||||
className={cs(styles.filename, { [styles.deleted]: deleted })}
|
||||
title={fileEntry.name}
|
||||
onClick={doSetFileData}
|
||||
>
|
||||
{'| '.repeat(depth)}
|
||||
{deleted ?
|
||||
<i className="fas fa-file-circle-xmark" /> :
|
||||
<i className="fas fa-file" />
|
||||
}
|
||||
{' '}
|
||||
{fileEntry.name}
|
||||
</td>
|
||||
<td>{FILE_TYPES[fileEntry.fileType] ?? `$${toHex(fileEntry.fileType)}`}</td>
|
||||
<td>{`$${toHex(fileEntry.auxType, 4)}`}</td>
|
||||
<td>{fileEntry.blocksUsed}</td>
|
||||
<td>{formatDate(fileEntry.creation)}</td>
|
||||
<td>{formatDate(fileEntry.lastMod)}</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for DirectoryListing Component.
|
||||
*/
|
||||
interface DirectoryListingProps {
|
||||
volume: ProDOSVolume;
|
||||
dirEntry: VDH | Directory;
|
||||
depth: number;
|
||||
setFileData: (fileData: FileData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays information about a ProDOS directory, recursing through child
|
||||
* directories.
|
||||
*
|
||||
* @param volume ProDOS volume
|
||||
* @param depth Current directory depth
|
||||
* @param dirEntry Current directory entry to display
|
||||
* @returns DirectoryListing component
|
||||
*/
|
||||
const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryListingProps) => {
|
||||
const [open, setOpen] = useState(depth === 0);
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td
|
||||
className={styles.filename}
|
||||
onClick={() => setOpen((open) => !open)}
|
||||
title={dirEntry.name}
|
||||
>
|
||||
{'| '.repeat(depth)}
|
||||
<i className={cs('fas', { 'fa-folder-open': open, 'fa-folder-closed': !open })} />
|
||||
{' '}
|
||||
{dirEntry.name}
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{formatDate(dirEntry.creation)}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{open && dirEntry.entries.map((fileEntry, idx) => {
|
||||
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
|
||||
const dirEntry = new Directory(volume, fileEntry);
|
||||
return <DirectoryListing
|
||||
key={idx}
|
||||
depth={depth + 1}
|
||||
volume={volume}
|
||||
dirEntry={dirEntry}
|
||||
setFileData={setFileData}
|
||||
/>;
|
||||
} else {
|
||||
return <FileListing
|
||||
key={idx}
|
||||
depth={depth + 1}
|
||||
volume={volume}
|
||||
fileEntry={fileEntry}
|
||||
setFileData={setFileData}
|
||||
/>;
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for CatalogEntry component
|
||||
*/
|
||||
interface CatalogEntryProps {
|
||||
dos: DOS33;
|
||||
fileEntry: DOSEntry;
|
||||
setFileData: (fileData: FileData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for a single DOS 3.x catalog entry
|
||||
*
|
||||
* @param entry Catalog entry to display
|
||||
* @returns CatalogEntry component
|
||||
*/
|
||||
const CatalogEntry = ({ dos, fileEntry, setFileData }: CatalogEntryProps) => {
|
||||
const doSetFileData = useCallback(() => {
|
||||
const { data } = dos.readFile(fileEntry);
|
||||
setFileData({
|
||||
binary: data,
|
||||
text: dos.dumpFile(fileEntry),
|
||||
fileName: fileEntry.name,
|
||||
});
|
||||
}, [dos, fileEntry, setFileData]);
|
||||
|
||||
return (
|
||||
<tr onClick={doSetFileData}>
|
||||
<td className={cs(styles.filename, { [styles.deleted]: fileEntry.deleted })}>
|
||||
{fileEntry.locked && <i className="fas fa-lock" />}
|
||||
{' '}
|
||||
{fileEntry.name}
|
||||
</td>
|
||||
<td>{fileEntry.type}</td>
|
||||
<td>{fileEntry.size}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Catalog component props
|
||||
*/
|
||||
interface CatalogProps {
|
||||
dos: DOS33;
|
||||
setFileData: (fileData: FileData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOS 3.3 disk catalog component
|
||||
*
|
||||
* @param dos DOS 3.3 disk object
|
||||
* @returns Catalog component
|
||||
*/
|
||||
|
||||
const Catalog = ({ dos, setFileData }: CatalogProps) => {
|
||||
const catalog = useMemo(() => dos.readCatalog(), [dos]);
|
||||
return (
|
||||
<>
|
||||
{catalog.map((fileEntry, idx) => (
|
||||
<CatalogEntry
|
||||
key={idx}
|
||||
dos={dos}
|
||||
fileEntry={fileEntry}
|
||||
setFileData={setFileData}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for DiskInfo component
|
||||
*/
|
||||
interface DiskInfoProps {
|
||||
massStorage: MassStorage<DiskFormat>;
|
||||
drive: DriveNumber;
|
||||
setFileData: (fileData: FileData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top level disk info component, handles determining what sort of disk
|
||||
* is present and using the appropriate sub-component depending on whether
|
||||
* it's a ProDOS block disk or a DOS 3.3 disk.
|
||||
*
|
||||
* TODO(whscullin): Does not handle woz or 13 sector.
|
||||
*
|
||||
* @param massStorage The storage device
|
||||
* @param drive The drive number
|
||||
* @returns DiskInfo component
|
||||
*/
|
||||
const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => {
|
||||
const disk = useMemo(() => {
|
||||
const massStorageData = massStorage.getBinary(drive, 'po');
|
||||
if (massStorageData) {
|
||||
const { name, data, readOnly, ext } = massStorageData;
|
||||
let disk: BlockDisk | NibbleDisk | null = null;
|
||||
if (ext === '2mg') {
|
||||
disk = createDiskFrom2MG({
|
||||
name,
|
||||
rawData: data,
|
||||
readOnly,
|
||||
volume: 254,
|
||||
});
|
||||
} else if (data.byteLength < 800 * 1024) {
|
||||
const doData = massStorage.getBinary(drive, 'do');
|
||||
if (doData) {
|
||||
if (isMaybeDOS33(doData)) {
|
||||
disk = createDiskFromDOS({
|
||||
name,
|
||||
rawData: doData.data,
|
||||
readOnly,
|
||||
volume: 254,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!disk) {
|
||||
disk = createBlockDisk({
|
||||
name,
|
||||
rawData: data,
|
||||
readOnly,
|
||||
volume: 254,
|
||||
});
|
||||
}
|
||||
return disk;
|
||||
}
|
||||
return null;
|
||||
}, [massStorage, drive]);
|
||||
|
||||
if (disk) {
|
||||
try {
|
||||
if (isBlockDisk(disk)) {
|
||||
if (disk.blocks.length) {
|
||||
const prodos = new ProDOSVolume(disk);
|
||||
const { totalBlocks } = prodos.vdh();
|
||||
const freeCount = prodos.bitMap().freeBlocks().length;
|
||||
const usedCount = totalBlocks - freeCount;
|
||||
return (
|
||||
<div className={styles.volume}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.filename}>Filename</th>
|
||||
<th className={styles.type}>Type</th>
|
||||
<th className={styles.aux}>Aux</th>
|
||||
<th className={styles.blocks}>Blocks</th>
|
||||
<th className={styles.created}>Created</th>
|
||||
<th className={styles.modified}>Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<DirectoryListing
|
||||
depth={0}
|
||||
volume={prodos}
|
||||
dirEntry={prodos.vdh()}
|
||||
setFileData={setFileData}
|
||||
/>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={1}>Blocks Free: {freeCount}</td>
|
||||
<td colSpan={3}>Used: {usedCount}</td>
|
||||
<td colSpan={2}>Total: {totalBlocks}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const dos = new DOS33(disk);
|
||||
return (
|
||||
<div className={styles.volume}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.filename}>Filename</th>
|
||||
<th className={styles.type}>Type</th>
|
||||
<th className={styles.sectors}>Sectors</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Catalog dos={dos} setFileData={setFileData} />
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Volume Number:</td>
|
||||
<td colSpan={3}>{dos.getVolumeNumber()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Used Sectors:</td>
|
||||
<td colSpan={3}>{dos.usedSectorCount()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Free Sectors:</td>
|
||||
<td colSpan={3}>{dos.freeSectorCount()}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return <pre>Unknown volume</pre>;
|
||||
}
|
||||
}
|
||||
return <pre>No disk</pre>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Disks component props
|
||||
*/
|
||||
export interface DisksProps {
|
||||
apple2: Apple2Impl;
|
||||
}
|
||||
|
||||
/**
|
||||
* A debugger panel that displays information about currently mounted
|
||||
* disks.
|
||||
*
|
||||
* @param apple2 The apple2 object
|
||||
* @returns Disks component
|
||||
*/
|
||||
export const Disks = ({ apple2 }: DisksProps) => {
|
||||
const [fileData, setFileData] = useState<FileData | null>(null);
|
||||
const io = apple2.getIO();
|
||||
const cards: MassStorage<DiskFormat>[] = [];
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setFileData(null);
|
||||
}, []);
|
||||
|
||||
for (let idx = 0; idx <= 7; idx++) {
|
||||
const card = io.getSlot(idx as slot);
|
||||
if (card instanceof DiskII || card instanceof SmartPort) {
|
||||
cards.push(card);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{cards.map((card, idx) => (
|
||||
<div key={idx}>
|
||||
<div className={debuggerStyles.subHeading}>
|
||||
{card.constructor.name} - 1
|
||||
</div>
|
||||
<DiskInfo massStorage={card} drive={1} setFileData={setFileData} />
|
||||
<div className={debuggerStyles.subHeading}>
|
||||
{card.constructor.name} - 2
|
||||
</div>
|
||||
<DiskInfo massStorage={card} drive={2} setFileData={setFileData} />
|
||||
</div>
|
||||
))}
|
||||
<FileViewer fileData={fileData} onClose={onClose} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
import { h, Fragment } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { Modal, ModalContent, ModalFooter } from '../Modal';
|
||||
|
||||
import styles from './css/FileViewer.module.css';
|
||||
|
||||
export interface FileData {
|
||||
fileName: string;
|
||||
binary: Uint8Array;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface FileViewerProps {
|
||||
fileData: FileData | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const FileViewer = ({ fileData, onClose }: FileViewerProps) => {
|
||||
const [binaryHref, setBinaryHref] = useState('');
|
||||
const [textHref, setTextHref] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (fileData) {
|
||||
const { binary, text } = fileData;
|
||||
const binaryBlob = new Blob(
|
||||
[binary],
|
||||
{ type: 'application/octet-stream' }
|
||||
);
|
||||
const binaryHref = window.URL.createObjectURL(binaryBlob);
|
||||
setBinaryHref(binaryHref);
|
||||
const textBlob = new Blob(
|
||||
[text],
|
||||
{ type: 'application/octet-stream' }
|
||||
);
|
||||
const textHref = window.URL.createObjectURL(textBlob);
|
||||
setTextHref(textHref);
|
||||
}
|
||||
}, [fileData]);
|
||||
|
||||
if (!fileData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { fileName, text } = fileData;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={true} onClose={onClose} title={fileName}>
|
||||
<ModalContent>
|
||||
<pre className={styles.fileViewer} tabIndex={-1} >
|
||||
{text}
|
||||
</pre>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<a
|
||||
download={`${fileName}.bin`}
|
||||
href={binaryHref}
|
||||
role="button"
|
||||
>
|
||||
Download Raw
|
||||
</a>
|
||||
<a
|
||||
download={`${fileName}.txt`}
|
||||
href={textHref}
|
||||
role="button"
|
||||
>
|
||||
Download Text
|
||||
</a>
|
||||
<button onClick={onClose}>Close</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
.volume {
|
||||
font-family: monospace;
|
||||
width: 100%;
|
||||
border: 1px inset;
|
||||
background-color: white;
|
||||
overflow-y: auto;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.volume table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.volume tfoot td {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
.volume thead th {
|
||||
border-bottom: 1px solid black;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.volume tbody {
|
||||
line-height: 0.8em;
|
||||
}
|
||||
|
||||
.filename {
|
||||
width: 16em;
|
||||
max-width: 16em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.type {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.sectors {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.aux {
|
||||
width: 3em;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.fileViewer {
|
||||
background: white;
|
||||
font: monospace;
|
||||
width: 60em;
|
||||
padding: 1em;
|
||||
height: 60vh;
|
||||
overflow-y: auto;
|
||||
border: 2px inset #f0edd0;
|
||||
white-space: pre-wrap;
|
||||
}
|
|
@ -0,0 +1,754 @@
|
|||
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 } 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,
|
||||
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.
|
||||
*
|
||||
* @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, 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, 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.FILE_NAME + 0x20];
|
||||
}
|
||||
|
||||
// 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, 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;
|
||||
}
|
|
@ -319,8 +319,14 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: m
|
|||
return buf;
|
||||
}
|
||||
|
||||
export interface TrackNibble {
|
||||
track: byte;
|
||||
sector: byte;
|
||||
nibble: byte;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a sector of data from a nibblized disk
|
||||
* Finds a sector of data from a nibblized disk
|
||||
*
|
||||
* TODO(flan): Does not work on WOZ disks
|
||||
*
|
||||
|
@ -329,7 +335,7 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: m
|
|||
* @param sector sector number to read
|
||||
* @returns An array of sector data bytes.
|
||||
*/
|
||||
export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory {
|
||||
export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNibble | null {
|
||||
const _sector = disk.format === 'po' ? _PO[sector] : _DO[sector];
|
||||
let val, state = 0;
|
||||
let idx = 0;
|
||||
|
@ -352,7 +358,6 @@ export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory
|
|||
}
|
||||
}
|
||||
let t = 0, s = 0, v = 0, checkSum;
|
||||
const data = new Uint8Array(256);
|
||||
while (retry < 4) {
|
||||
switch (state) {
|
||||
case 0:
|
||||
|
@ -380,38 +385,7 @@ export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory
|
|||
break;
|
||||
case 4: // Data
|
||||
if (s === _sector && t === track) {
|
||||
const data2 = [];
|
||||
let last = 0;
|
||||
for (let jdx = 0x55; jdx >= 0; jdx--) {
|
||||
val = detrans62[_readNext() - 0x80] ^ last;
|
||||
data2[jdx] = val;
|
||||
last = val;
|
||||
}
|
||||
for (let jdx = 0; jdx < 0x100; jdx++) {
|
||||
val = detrans62[_readNext() - 0x80] ^ last;
|
||||
data[jdx] = val;
|
||||
last = val;
|
||||
}
|
||||
checkSum = detrans62[_readNext() - 0x80] ^ last;
|
||||
if (checkSum) {
|
||||
debug('Invalid data checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
|
||||
}
|
||||
for (let kdx = 0, jdx = 0x55; kdx < 0x100; kdx++) {
|
||||
data[kdx] <<= 1;
|
||||
if ((data2[jdx] & 0x01) !== 0) {
|
||||
data[kdx] |= 0x01;
|
||||
}
|
||||
data2[jdx] >>= 1;
|
||||
|
||||
data[kdx] <<= 1;
|
||||
if ((data2[jdx] & 0x01) !== 0) {
|
||||
data[kdx] |= 0x01;
|
||||
}
|
||||
data2[jdx] >>= 1;
|
||||
|
||||
if (--jdx < 0) jdx = 0x55;
|
||||
}
|
||||
return data;
|
||||
return { track, sector, nibble: idx };
|
||||
}
|
||||
else
|
||||
_skipBytes(0x159); // Skip data, checksum and footer
|
||||
|
@ -421,7 +395,92 @@ export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory
|
|||
break;
|
||||
}
|
||||
}
|
||||
return new Uint8Array();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a sector of data from a nibblized disk
|
||||
*
|
||||
* TODO(flan): Does not work on WOZ disks
|
||||
*
|
||||
* @param disk Nibble disk
|
||||
* @param track track number to read
|
||||
* @param sector sector number to read
|
||||
* @returns An array of sector data bytes.
|
||||
*/
|
||||
export function readSector(disk: NibbleDisk, track: byte, sector: byte): Uint8Array {
|
||||
const trackNibble = findSector(disk, track, sector);
|
||||
if (!trackNibble) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
const { nibble } = trackNibble;
|
||||
const cur = disk.tracks[track];
|
||||
|
||||
let idx = nibble;
|
||||
function _readNext() {
|
||||
const result = cur[idx++];
|
||||
if (nibble >= cur.length) {
|
||||
idx = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const data = new Uint8Array(256);
|
||||
const data2 = [];
|
||||
let last = 0;
|
||||
let val;
|
||||
|
||||
for (let jdx = 0x55; jdx >= 0; jdx--) {
|
||||
val = detrans62[_readNext() - 0x80] ^ last;
|
||||
data2[jdx] = val;
|
||||
last = val;
|
||||
}
|
||||
for (let jdx = 0; jdx < 0x100; jdx++) {
|
||||
val = detrans62[_readNext() - 0x80] ^ last;
|
||||
data[jdx] = val;
|
||||
last = val;
|
||||
}
|
||||
const checkSum = detrans62[_readNext() - 0x80] ^ last;
|
||||
if (checkSum) {
|
||||
debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum));
|
||||
}
|
||||
for (let kdx = 0, jdx = 0x55; kdx < 0x100; kdx++) {
|
||||
data[kdx] <<= 1;
|
||||
if ((data2[jdx] & 0x01) !== 0) {
|
||||
data[kdx] |= 0x01;
|
||||
}
|
||||
data2[jdx] >>= 1;
|
||||
|
||||
data[kdx] <<= 1;
|
||||
if ((data2[jdx] & 0x01) !== 0) {
|
||||
data[kdx] |= 0x01;
|
||||
}
|
||||
data2[jdx] >>= 1;
|
||||
|
||||
if (--jdx < 0) jdx = 0x55;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a sector of data from a nibblized disk
|
||||
*
|
||||
* TODO(flan): Does not work on WOZ disks
|
||||
*
|
||||
* @param disk Nibble disk
|
||||
* @param track track number to read
|
||||
* @param sector sector number to read
|
||||
* @returns An array of sector data bytes.
|
||||
*/
|
||||
export function writeSector(disk: NibbleDisk, track: byte, sector: byte, _data: Uint8Array): boolean {
|
||||
const trackNibble = findSector(disk, track, sector);
|
||||
if (!trackNibble) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Todo
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -497,7 +556,7 @@ export function jsonDecode(data: string): NibbleDisk {
|
|||
*/
|
||||
|
||||
export function analyseDisk(disk: NibbleDisk) {
|
||||
for (let track = 0; track < 35; track++) {
|
||||
for (let track = 0; track < disk.tracks.length; track++) {
|
||||
let outStr = `${toHex(track)}: `;
|
||||
let val, state = 0;
|
||||
let idx = 0;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
|
|||
export default function createDiskFromProDOS(options: DiskOptions) {
|
||||
const { data, name, side, rawData, volume, readOnly } = options;
|
||||
const disk: NibbleDisk = {
|
||||
format: 'nib',
|
||||
format: 'po',
|
||||
encoding: ENCODING_NIBBLE,
|
||||
name,
|
||||
side,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { word } from 'js/types';
|
||||
import { ProDOSVolume } from '.';
|
||||
|
||||
export interface ProDOSFileData {
|
||||
data: Uint8Array;
|
||||
address: word;
|
||||
}
|
||||
|
||||
export abstract class ProDOSFile {
|
||||
constructor(public volume: ProDOSVolume) { }
|
||||
|
||||
abstract read(): Uint8Array;
|
||||
abstract write(data: Uint8Array): void;
|
||||
}
|
|
@ -13,11 +13,36 @@ export class BitMap {
|
|||
this.blocks = volume.blocks();
|
||||
}
|
||||
|
||||
|
||||
allocBlock () {
|
||||
freeBlocks() {
|
||||
const free: word[] = [];
|
||||
let blockOffset = 0;
|
||||
let byteOffset = 0;
|
||||
let bitOffset = 0;
|
||||
let bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset];
|
||||
for (let idx = 0; idx < this.vdh.totalBlocks; idx++) {
|
||||
const blockOffset = this.vdh.bitMapPointer + Math.floor(idx / BLOCK_ENTRIES);
|
||||
const bitMapBlock = this.blocks[blockOffset];
|
||||
const currentByte = bitMapBlock[byteOffset];
|
||||
const mask = 1 << bitOffset;
|
||||
if (currentByte & mask) {
|
||||
free.push(idx);
|
||||
}
|
||||
bitOffset += 1;
|
||||
if (bitOffset > 7) {
|
||||
bitOffset = 0;
|
||||
byteOffset += 1;
|
||||
if (byteOffset > (BLOCK_ENTRIES >> 3)) {
|
||||
byteOffset = 0;
|
||||
blockOffset += 1;
|
||||
bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset];
|
||||
}
|
||||
}
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
allocBlock() {
|
||||
for (let idx = 0; idx < this.vdh.totalBlocks; idx++) {
|
||||
const blockOffset = Math.floor(idx / BLOCK_ENTRIES);
|
||||
const bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset];
|
||||
const byteOffset = (idx - blockOffset * BLOCK_ENTRIES) >> 8;
|
||||
const bits = bitMapBlock[byteOffset];
|
||||
if (bits !== 0xff) {
|
||||
|
@ -38,11 +63,11 @@ export class BitMap {
|
|||
if (block >= this.vdh.totalBlocks) {
|
||||
throw new Error('Block out of range');
|
||||
}
|
||||
const blockOffset = this.vdh.bitMapPointer + Math.floor(block / BLOCK_ENTRIES);
|
||||
const blockOffset = Math.floor(block / BLOCK_ENTRIES);
|
||||
const byteOffset = (block - blockOffset * BLOCK_ENTRIES) >> 8;
|
||||
const bitOffset = block & 0x7;
|
||||
|
||||
const bitMapBlock = this.blocks[blockOffset];
|
||||
const bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset];
|
||||
|
||||
bitMapBlock[byteOffset] &= 0xff ^ (0x01 << bitOffset);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { byte } from 'js/types';
|
||||
|
||||
export const BLOCK_SIZE = 512;
|
||||
|
||||
export const STORAGE_TYPES = {
|
||||
|
@ -5,6 +7,8 @@ export const STORAGE_TYPES = {
|
|||
SEEDLING: 0x1,
|
||||
SAPLING: 0x2,
|
||||
TREE: 0x3,
|
||||
PASCAL: 0x4,
|
||||
EXTENDED: 0x5,
|
||||
DIRECTORY: 0xD,
|
||||
SUBDIRECTORY_HEADER: 0xE,
|
||||
VDH_HEADER: 0xF
|
||||
|
@ -19,7 +23,7 @@ export const ACCESS_TYPES = {
|
|||
ALL: 0xE3
|
||||
} as const;
|
||||
|
||||
export const FILE_TYPES = {
|
||||
export const FILE_TYPES: Record<byte, string> = {
|
||||
0x00: 'UNK', // Typeless file (SOS and ProDOS)
|
||||
0x01: 'BAD', // Bad block file
|
||||
0x02: 'PDC', // Pascal code file
|
||||
|
|
|
@ -47,6 +47,7 @@ export class Directory {
|
|||
constructor(private volume: ProDOSVolume, private fileEntry: FileEntry) {
|
||||
this.blocks = this.volume.blocks();
|
||||
this.vdh = this.volume.vdh();
|
||||
this.read();
|
||||
}
|
||||
|
||||
read(fileEntry?: FileEntry) {
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import { dateToUint32, readFileName, writeFileName, uint32ToDate } from './utils';
|
||||
import { STORAGE_TYPES, ACCESS_TYPES } from './constants';
|
||||
import type { byte, word } from 'js/types';
|
||||
import { toHex } from 'js/util';
|
||||
import { ProDOSVolume } from '.';
|
||||
import { VDH } from './vdh';
|
||||
import { Directory } from './directory';
|
||||
import { ProDOSFile } from './base_file';
|
||||
import { SaplingFile } from './sapling_file';
|
||||
import { SeedlingFile } from './seedling_file';
|
||||
import { TreeFile } from './tree_file';
|
||||
import ApplesoftDump from 'js/applesoft/decompiler';
|
||||
|
||||
const ENTRY_OFFSETS = {
|
||||
STORAGE_TYPE: 0x00,
|
||||
|
@ -39,6 +45,8 @@ export class FileEntry {
|
|||
keyPointer: word = 0;
|
||||
headerPointer: word = 0;
|
||||
|
||||
constructor(public volume: ProDOSVolume) { }
|
||||
|
||||
read(block: DataView, offset: word) {
|
||||
this.block = block;
|
||||
this.offset = offset;
|
||||
|
@ -81,6 +89,60 @@ export class FileEntry {
|
|||
this.block.setUint32(this.offset + ENTRY_OFFSETS.LAST_MOD, dateToUint32(this.lastMod), true);
|
||||
this.block.setUint16(this.offset + ENTRY_OFFSETS.HEADER_POINTER, this.headerPointer, true);
|
||||
}
|
||||
|
||||
getFileData() {
|
||||
let file: ProDOSFile | null = null;
|
||||
|
||||
switch (this.storageType) {
|
||||
case STORAGE_TYPES.SEEDLING:
|
||||
file = new SeedlingFile(this.volume, this);
|
||||
break;
|
||||
case STORAGE_TYPES.SAPLING:
|
||||
file = new SaplingFile(this.volume, this);
|
||||
break;
|
||||
case STORAGE_TYPES.TREE:
|
||||
file = new TreeFile(this.volume, this);
|
||||
break;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
return file.read();
|
||||
}
|
||||
}
|
||||
|
||||
getFileText() {
|
||||
const data = this.getFileData();
|
||||
let result: string | null = null;
|
||||
let address = 0;
|
||||
|
||||
if (data) {
|
||||
if (this.fileType === 0xFC) { // BAS
|
||||
result = new ApplesoftDump(data, 0).decompile();
|
||||
} else {
|
||||
if (this.fileType === 0x06) { // BIN
|
||||
address = this.auxType;
|
||||
}
|
||||
result = '';
|
||||
let hex = '';
|
||||
let ascii = '';
|
||||
for (let idx = 0; idx < data.length; idx++) {
|
||||
const val = data[idx];
|
||||
if (idx % 16 === 0) {
|
||||
if (idx !== 0) {
|
||||
result += `${hex} ${ascii}\n`;
|
||||
}
|
||||
hex = '';
|
||||
ascii = '';
|
||||
result += `${toHex(address + idx, 4)}:`;
|
||||
}
|
||||
hex += ` ${toHex(val)}`;
|
||||
ascii += (val & 0x7f) >= 0x20 ? String.fromCharCode(val & 0x7f) : '.';
|
||||
}
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function readEntries(volume: ProDOSVolume, block: DataView, header: VDH | Directory) {
|
||||
|
@ -90,17 +152,20 @@ export function readEntries(volume: ProDOSVolume, block: DataView, header: VDH |
|
|||
let count = 2;
|
||||
let next = header.next;
|
||||
|
||||
for (let idx = 0; idx < header.fileCount; idx++) {
|
||||
const fileEntry = new FileEntry();
|
||||
for (let idx = 0; idx < header.fileCount;) {
|
||||
const fileEntry = new FileEntry(volume);
|
||||
fileEntry.read(block, offset);
|
||||
entries.push(fileEntry);
|
||||
if (fileEntry.storageType !== STORAGE_TYPES.DELETED) {
|
||||
idx++;
|
||||
}
|
||||
offset += header.entryLength;
|
||||
count++;
|
||||
if (count >= header.entriesPerBlock) {
|
||||
if (count > header.entriesPerBlock) {
|
||||
block = new DataView(blocks[next].buffer);
|
||||
next = block.getUint16(0x02, true);
|
||||
offset = 0x4;
|
||||
count = 0;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,15 +2,16 @@ import { ProDOSVolume } from '.';
|
|||
import type { BitMap } from './bit_map';
|
||||
import { BLOCK_SIZE, STORAGE_TYPES } from './constants';
|
||||
import { FileEntry } from './file_entry';
|
||||
import { ProDOSFile } from './base_file';
|
||||
|
||||
export class SaplingFile {
|
||||
export class SaplingFile extends ProDOSFile {
|
||||
blocks: Uint8Array[];
|
||||
bitMap: BitMap;
|
||||
|
||||
constructor(private volume: ProDOSVolume, private fileEntry: FileEntry) {
|
||||
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) {
|
||||
super(volume);
|
||||
this.blocks = this.volume.blocks();
|
||||
this.bitMap = this.volume.bitMap();
|
||||
|
||||
}
|
||||
|
||||
getBlockPointers() {
|
||||
|
@ -19,7 +20,9 @@ export class SaplingFile {
|
|||
|
||||
const pointers = [this.fileEntry.keyPointer];
|
||||
for (let idx = 0; idx < 256; idx++) {
|
||||
const seedlingPointer = seedlingPointers.getUint16(idx * 2);
|
||||
const seedlingPointer =
|
||||
seedlingPointers.getUint8(idx) |
|
||||
(seedlingPointers.getUint8(0x100 + idx) << 8);
|
||||
if (seedlingPointer) {
|
||||
pointers.push(seedlingPointer);
|
||||
}
|
||||
|
@ -27,16 +30,19 @@ export class SaplingFile {
|
|||
return pointers;
|
||||
}
|
||||
|
||||
// TODO(whscullin): Why did I not use getBlockPointers for these...
|
||||
read() {
|
||||
const saplingBlock = this.blocks[this.fileEntry.keyPointer];
|
||||
const seedlingPointers = new DataView(saplingBlock);
|
||||
const seedlingPointers = new DataView(saplingBlock.buffer);
|
||||
|
||||
let remainingLength = this.fileEntry.eof;
|
||||
const data = new Uint8Array(remainingLength);
|
||||
let offset = 0;
|
||||
let idx = 0;
|
||||
while (remainingLength > 0) {
|
||||
const seedlingPointer = seedlingPointers.getUint16(idx * 2);
|
||||
const seedlingPointer =
|
||||
seedlingPointers.getUint8(idx) |
|
||||
(seedlingPointers.getUint8(0x100 + idx) << 8);
|
||||
if (seedlingPointer) {
|
||||
const seedlingBlock = this.blocks[seedlingPointer];
|
||||
const bytes = seedlingBlock.slice(0, Math.min(BLOCK_SIZE, remainingLength));
|
||||
|
@ -63,7 +69,8 @@ export class SaplingFile {
|
|||
|
||||
while (remainingLength > 0) {
|
||||
const seedlingPointer = this.bitMap.allocBlock();
|
||||
seedlingPointers.setUint16(idx * 2, seedlingPointer, true);
|
||||
seedlingPointers.setUint8(idx, seedlingPointer & 0xff);
|
||||
seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8);
|
||||
const seedlingBlock = this.blocks[seedlingPointer];
|
||||
seedlingBlock.set(data.slice(offset, Math.min(BLOCK_SIZE, remainingLength)));
|
||||
idx++;
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import type { ProDOSVolume } from '.';
|
||||
import { ProDOSFile } from './base_file';
|
||||
import { BitMap } from './bit_map';
|
||||
import { STORAGE_TYPES } from './constants';
|
||||
import { FileEntry } from './file_entry';
|
||||
|
||||
export class SeedlingFile {
|
||||
export class SeedlingFile extends ProDOSFile {
|
||||
blocks: Uint8Array[];
|
||||
bitMap: BitMap;
|
||||
|
||||
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) {
|
||||
super(volume);
|
||||
this.blocks = volume.blocks();
|
||||
this.bitMap = volume.bitMap();
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import type { ProDOSVolume } from '.';
|
||||
import { ProDOSFile } from './base_file';
|
||||
import { BitMap } from './bit_map';
|
||||
import { BLOCK_SIZE, STORAGE_TYPES } from './constants';
|
||||
import type { FileEntry } from './file_entry';
|
||||
|
||||
export class TreeFile {
|
||||
export class TreeFile extends ProDOSFile {
|
||||
private bitMap: BitMap;
|
||||
private blocks: Uint8Array[];
|
||||
|
||||
constructor (volume: ProDOSVolume, private fileEntry: FileEntry) {
|
||||
constructor(volume: ProDOSVolume, private fileEntry: FileEntry) {
|
||||
super(volume);
|
||||
this.blocks = volume.blocks();
|
||||
this.bitMap = volume.bitMap();
|
||||
}
|
||||
|
@ -17,12 +19,16 @@ export class TreeFile {
|
|||
const saplingPointers = new DataView(treeBlock);
|
||||
const pointers = [];
|
||||
for (let idx = 0; idx < 256; idx++) {
|
||||
const saplingPointer = saplingPointers.getUint16(idx * 2);
|
||||
const saplingPointer =
|
||||
saplingPointers.getUint8(idx) |
|
||||
(saplingPointers.getUint8(0x100 + idx) << 8);
|
||||
if (saplingPointer) {
|
||||
pointers.push(saplingPointer);
|
||||
const seedlingPointers = new DataView(this.blocks[saplingPointer]);
|
||||
for (let jdx = 0; jdx < 256; jdx++) {
|
||||
const seedlingPointer = seedlingPointers.getUint16(idx * 2);
|
||||
const seedlingPointer =
|
||||
seedlingPointers.getUint8(idx) |
|
||||
(seedlingPointers.getUint8(0x100 + idx) << 8);
|
||||
if (seedlingPointer) {
|
||||
pointers.push(seedlingPointer);
|
||||
}
|
||||
|
@ -32,6 +38,7 @@ export class TreeFile {
|
|||
return pointers;
|
||||
}
|
||||
|
||||
// TODO(whscullin): Why did I not use getBlockPointers for these...
|
||||
read() {
|
||||
const treeBlock = this.blocks[this.fileEntry.keyPointer];
|
||||
const saplingPointers = new DataView(treeBlock);
|
||||
|
@ -41,14 +48,18 @@ export class TreeFile {
|
|||
let idx = 0;
|
||||
|
||||
while (remainingLength > 0) {
|
||||
const saplingPointer = saplingPointers.getUint16(idx * 2, true);
|
||||
const saplingPointer =
|
||||
saplingPointers.getUint8(idx) |
|
||||
(saplingPointers.getUint8(0x100 + idx) << 8);
|
||||
let jdx = 0;
|
||||
if (saplingPointer) {
|
||||
const saplingBlock = this.blocks[saplingPointer];
|
||||
const seedlingPointers = new DataView(saplingBlock);
|
||||
|
||||
while (jdx < 256 && remainingLength > 0) {
|
||||
const seedlingPointer = seedlingPointers.getUint16(idx * 2, true);
|
||||
const seedlingPointer =
|
||||
seedlingPointers.getUint8(idx) |
|
||||
(seedlingPointers.getUint8(0x100 + idx) << 8);
|
||||
if (seedlingPointer) {
|
||||
const seedlingBlock = this.blocks[seedlingPointer];
|
||||
const bytes = seedlingBlock.slice(Math.min(BLOCK_SIZE, remainingLength));
|
||||
|
@ -83,14 +94,16 @@ export class TreeFile {
|
|||
while (remainingLength > 0) {
|
||||
const saplingPointer = this.bitMap.allocBlock();
|
||||
const saplingBlock = this.blocks[saplingPointer];
|
||||
saplingPointers.setUint16(idx * 2, saplingPointer, true);
|
||||
saplingPointers.setUint8(idx, saplingPointer & 0xff);
|
||||
saplingPointers.setUint8(0x100 + idx, saplingPointer >> 8);
|
||||
const seedlingPointers = new DataView(saplingBlock);
|
||||
|
||||
let jdx = 0;
|
||||
|
||||
while (jdx < 256 && remainingLength > 0) {
|
||||
const seedlingPointer = this.bitMap.allocBlock();
|
||||
seedlingPointers.setUint16(idx * 2, seedlingPointer, true);
|
||||
seedlingPointers.setUint8(idx, seedlingPointer & 0xff);
|
||||
seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8);
|
||||
const seedlingBlock = this.blocks[seedlingPointer];
|
||||
seedlingBlock.set(data.slice(offset, Math.min(BLOCK_SIZE, remainingLength)));
|
||||
jdx++;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { debug } from '../../util';
|
||||
import { STORAGE_TYPES } from './constants';
|
||||
import { Directory } from './directory';
|
||||
import type { byte, word } from 'js/types';
|
||||
|
@ -19,7 +18,7 @@ export function uint32ToDate(val: word) {
|
|||
const hour = hourMinute >> 8;
|
||||
const min = hourMinute & 0xff;
|
||||
|
||||
return new Date(1900 + year, month - 1, day, hour, min);
|
||||
return new Date(year < 70 ? 2000 + year : 1900 + year, month - 1, day, hour, min);
|
||||
}
|
||||
return new Date(0);
|
||||
}
|
||||
|
@ -50,9 +49,13 @@ export function readFileName(block: DataView, offset: word, nameLength: byte, ca
|
|||
if (!(caseBits & 0x8000)) {
|
||||
caseBits = 0;
|
||||
}
|
||||
// File is deleted, brute force old name
|
||||
if (nameLength === 0) {
|
||||
nameLength = 15;
|
||||
}
|
||||
for (let idx = 0; idx < nameLength; idx++) {
|
||||
caseBits <<= 1;
|
||||
const char = String.fromCharCode(block.getUint8(offset + idx));
|
||||
const char = String.fromCharCode(block.getUint8(offset + idx) & 0x7f);
|
||||
name += caseBits & 0x8000 ? char.toLowerCase() : char;
|
||||
}
|
||||
return name;
|
||||
|
@ -74,27 +77,29 @@ export function writeFileName(block: DataView, offset: word, name: string) {
|
|||
|
||||
export function dumpDirectory(volume: ProDOSVolume, dirEntry: FileEntry, depth: string) {
|
||||
const dir = new Directory(volume, dirEntry);
|
||||
dir.read();
|
||||
let str = '';
|
||||
|
||||
for (let idx = 0; idx < dir.entries.length; idx++) {
|
||||
const fileEntry = dir.entries[idx];
|
||||
if (fileEntry.storageType !== STORAGE_TYPES.DELETED) {
|
||||
debug(depth, fileEntry.name);
|
||||
str += depth + fileEntry.name + '\n';
|
||||
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
|
||||
dumpDirectory(volume, fileEntry, depth + ' ');
|
||||
str += dumpDirectory(volume, fileEntry, depth + ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function dump(volume: ProDOSVolume) {
|
||||
const vdh = volume.vdh();
|
||||
debug(vdh.name);
|
||||
let str = vdh.name;
|
||||
for (let idx = 0; idx < vdh.entries.length; idx++) {
|
||||
const fileEntry = vdh.entries[idx];
|
||||
debug(fileEntry.name);
|
||||
str += fileEntry.name + '\n';
|
||||
if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) {
|
||||
dumpDirectory(volume, fileEntry, ' ');
|
||||
str += dumpDirectory(volume, fileEntry, ' ');
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ export const BLOCK_FORMATS = [
|
|||
'po',
|
||||
] as const;
|
||||
|
||||
export const DISK_FORMATS = [...NIBBLE_FORMATS, ...BLOCK_FORMATS ] as const;
|
||||
export const DISK_FORMATS = [...NIBBLE_FORMATS, ...BLOCK_FORMATS] as const;
|
||||
|
||||
export type NibbleFormat = MemberOf<typeof NIBBLE_FORMATS>;
|
||||
export type BlockFormat = MemberOf<typeof BLOCK_FORMATS>;
|
||||
|
@ -217,6 +217,8 @@ export type FormatWorkerResponse =
|
|||
export interface MassStorageData {
|
||||
name: string;
|
||||
ext: string;
|
||||
readOnly: boolean;
|
||||
volume?: byte;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
|
@ -225,5 +227,5 @@ export interface MassStorageData {
|
|||
*/
|
||||
export interface MassStorage<T> {
|
||||
setBinary(drive: number, name: string, ext: T, data: ArrayBuffer): boolean;
|
||||
getBinary(drive: number): MassStorageData | null;
|
||||
getBinary(drive: number, ext?: T): MassStorageData | null;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { byte, Memory, word } from 'js/types';
|
||||
import { byte, word } from 'js/types';
|
||||
|
||||
const LETTERS =
|
||||
' ' +
|
||||
' !"#$%&\'()*+,-./0123456789:;<=>?' +
|
||||
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
|
||||
'`abcdefghijklmnopqrstuvwxyz{|}~ ';
|
||||
' ' +
|
||||
' !"#$%&\'()*+,-./0123456789:;<=>?' +
|
||||
'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_' +
|
||||
'`abcdefghijklmnopqrstuvwxyz{|}~ ';
|
||||
|
||||
const TOKENS: Record<byte, string> = {
|
||||
0x00: 'HIMEM:',
|
||||
0x01: '$01',
|
||||
0x02: '_',
|
||||
0x03: ':',
|
||||
0x04: 'LOAD',
|
||||
|
@ -45,8 +47,8 @@ const TOKENS: Record<byte, string> = {
|
|||
0x25: 'THEN',
|
||||
0x26: ',',
|
||||
0x27: ',',
|
||||
0x28: '\'',
|
||||
0x29: '\'',
|
||||
0x28: '"',
|
||||
0x29: '"',
|
||||
0x2A: '(',
|
||||
0x2B: '!',
|
||||
0x2C: '!',
|
||||
|
@ -114,6 +116,7 @@ const TOKENS: Record<byte, string> = {
|
|||
0x6A: ',',
|
||||
0x6B: 'AT',
|
||||
0x6C: 'VLIN',
|
||||
0x6D: ',',
|
||||
0x6E: 'AT',
|
||||
0x6F: 'VTAB',
|
||||
0x70: '=',
|
||||
|
@ -121,6 +124,7 @@ const TOKENS: Record<byte, string> = {
|
|||
0x72: ')',
|
||||
0x73: ')',
|
||||
0x74: 'LIST',
|
||||
0x75: ',',
|
||||
0x76: 'LIST',
|
||||
0x77: 'POP',
|
||||
0x78: 'NODSP',
|
||||
|
@ -133,15 +137,11 @@ const TOKENS: Record<byte, string> = {
|
|||
0x7F: 'IN#'
|
||||
};
|
||||
|
||||
export default class IntBasicDump
|
||||
{
|
||||
constructor(private mem: Memory) {}
|
||||
export default class IntBasicDump {
|
||||
constructor(private data: Uint8Array) { }
|
||||
|
||||
private readByte(addr: word) {
|
||||
const page = addr >> 8,
|
||||
off = addr & 0xff;
|
||||
|
||||
return this.mem.read(page, off);
|
||||
return this.data[addr];
|
||||
}
|
||||
|
||||
private readWord(addr: word) {
|
||||
|
@ -151,12 +151,14 @@ export default class IntBasicDump
|
|||
return (msb << 8) | lsb;
|
||||
}
|
||||
|
||||
toString () {
|
||||
toString() {
|
||||
let str = '';
|
||||
let addr = this.readWord(0xca); // Start
|
||||
const himem = this.readWord(0x4c);
|
||||
let addr = 0;
|
||||
const himem = this.data.length;
|
||||
do {
|
||||
/*var len = */this.readByte(addr++);
|
||||
let inRem = false;
|
||||
let inQuote = false;
|
||||
/* const length = */ this.readByte(addr++);
|
||||
const lineno = this.readWord(addr);
|
||||
addr += 2;
|
||||
|
||||
|
@ -165,20 +167,18 @@ export default class IntBasicDump
|
|||
let val = 0;
|
||||
do {
|
||||
val = this.readByte(addr++);
|
||||
if (val >= 0xB0 && val <= 0xB9) {
|
||||
if (!inRem && !inQuote && val >= 0xB0 && val <= 0xB9) {
|
||||
str += this.readWord(addr);
|
||||
addr += 2;
|
||||
}
|
||||
else if (val < 0x80 && val > 0x01) {
|
||||
} else if (val < 0x80 && val > 0x01) {
|
||||
const t = TOKENS[val];
|
||||
if (t.length > 1)
|
||||
str += ' ';
|
||||
if (t.length > 1) { str += ' '; }
|
||||
str += t;
|
||||
if (t.length > 1)
|
||||
str += ' ';
|
||||
}
|
||||
else if (val > 0x80)
|
||||
str += LETTERS[val - 0x80];
|
||||
if (t.length > 1) { str += ' '; }
|
||||
if (val === 0x28) { inQuote = true; }
|
||||
if (val === 0x29) { inQuote = false; }
|
||||
if (val === 0x5d) { inRem = true; }
|
||||
} else if (val > 0x80) { str += LETTERS[val - 0x80]; }
|
||||
} while (val !== 0x01);
|
||||
str += '\n';
|
||||
} while (addr < himem);
|
||||
|
|
|
@ -34,6 +34,15 @@ describe('ApplesoftDecompiler', () => {
|
|||
expect(program).toEqual(' 10 PRINT "Hello, World!"\n');
|
||||
});
|
||||
|
||||
it('correctly computes the base address when 0 is passed in', () => {
|
||||
const compiler = new ApplesoftCompiler();
|
||||
compiler.compile('10 PRINT "Hello, World!"\n20 GOTO 10');
|
||||
|
||||
const decompiler = new ApplesoftDecompiler(compiler.program(), 0);
|
||||
const program = decompiler.list();
|
||||
expect(program).toEqual(' 10 PRINT "Hello, World!"\n 20 GOTO 10\n');
|
||||
});
|
||||
|
||||
it('lists a program with a long line', () => {
|
||||
const compiler = new ApplesoftCompiler();
|
||||
compiler.compile('10 PRINT "Hello, World!"\n'
|
||||
|
@ -219,4 +228,4 @@ describe('ApplesoftDecompiler', () => {
|
|||
const program = decompiler.decompile({ style: 'pretty' });
|
||||
expect(program).toEqual('10 HPLOT X, Y : GOTO 10');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue