diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index 57542c0..805e439 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -10,9 +10,9 @@ import createBlockDisk from '../formats/block'; import { BlockDisk, BlockFormat, + BlockStorage, Disk, DRIVE_NUMBERS, - MassStorage, MassStorageData, MemoryBlockDisk, } from 'js/formats/types'; @@ -96,9 +96,7 @@ export interface CFFAState { disks: Array; } -export default class CFFA - implements Card, MassStorage, Restorable -{ +export default class CFFA implements Card, BlockStorage, Restorable { // CFFA internal Flags private _disableSignalling = false; @@ -505,7 +503,7 @@ export default class CFFA diskState.disk.readOnly, diskState.blocks ); - await this.setBlockVolume(idx, disk); + await this.setBlockDisk(idx, disk); } else { this.resetBlockVolume(idx); } @@ -528,7 +526,7 @@ export default class CFFA } } - async setBlockVolume(drive: number, disk: BlockDisk): Promise { + async setBlockDisk(drive: number, disk: BlockDisk): Promise { drive = drive - 1; const partition = this._partitions[drive]; if (!partition) { @@ -549,6 +547,11 @@ export default class CFFA } } + async getBlockDisk(drive: number): Promise { + drive = drive - 1; + return this._partitions[drive]; + } + // Assign a raw disk image to a drive. Must be 2mg or raw PO image. setBinary( @@ -576,7 +579,7 @@ export default class CFFA }; const disk = createBlockDisk(format, options); - return this.setBlockVolume(drive, disk); + return this.setBlockDisk(drive, disk); } async getBinary(drive: number): Promise { diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index e08005c..2484a9b 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -2,7 +2,6 @@ import { debug, toHex } from '../util'; import { rom as smartPortRom } from '../roms/cards/smartport'; import { Card, Restorable, byte, word, rom } from '../types'; import { - MassStorage, BlockDisk, BlockFormat, MassStorageData, @@ -10,6 +9,7 @@ import { Disk, MemoryBlockDisk, DRIVE_NUMBERS, + BlockStorage, } from '../formats/types'; import { CPU6502, flags } from '@whscullin/cpu6502'; import { @@ -154,7 +154,7 @@ const DEVICE_TYPE_SCSI_HD = 0x07; // $0E: Clock // $0F: Modem export default class SmartPort - implements Card, MassStorage, Restorable + implements Card, BlockStorage, Restorable { private rom: rom; private disks: BlockDisk[] = []; @@ -662,12 +662,17 @@ export default class SmartPort async setBlockDisk(driveNo: DriveNumber, disk: BlockDisk) { this.disks[driveNo] = disk; + this.ext[driveNo] = disk.format; const volumeName = await this.getVolumeName(driveNo); const name = volumeName || disk.metadata.name; this.callbacks?.label(driveNo, name); } + async getBlockDisk(driveNo: DriveNumber): Promise { + return this.disks[driveNo]; + } + resetBlockDisk(driveNo: DriveNumber) { delete this.disks[driveNo]; } @@ -710,7 +715,7 @@ export default class SmartPort return null; } const disk = this.disks[drive]; - const ext = this.ext[drive]; + const ext = this.disks[drive].format; const { readOnly } = disk; const { name } = disk.metadata; let data: ArrayBuffer; diff --git a/js/components/debugger/Disks.tsx b/js/components/debugger/Disks.tsx index 9c0826f..f804858 100644 --- a/js/components/debugger/Disks.tsx +++ b/js/components/debugger/Disks.tsx @@ -1,4 +1,4 @@ -import { h, Fragment } from 'preact'; +import { h, Fragment, JSX } from 'preact'; import { useEffect, useMemo } from 'preact/hooks'; import cs from 'classnames'; import { Apple2 as Apple2Impl } from 'js/apple2'; @@ -8,6 +8,7 @@ import { DriveNumber, FloppyDisk, isBlockDiskFormat, + isBlockStorage, isNibbleDisk, MassStorage, } from 'js/formats/types'; @@ -144,6 +145,41 @@ const DirectoryListing = ({ setFileData, }: DirectoryListingProps) => { const [open, setOpen] = useState(depth === 0); + const [children, setChildren] = useState([]); + useEffect(() => { + const load = async () => { + const children: JSX.Element[] = []; + for (let idx = 0; idx < dirEntry.entries.length; idx++) { + const fileEntry = dirEntry.entries[idx]; + if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) { + const dirEntry = new Directory(volume, fileEntry); + await dirEntry.init(); + children.push( + + ); + } else { + children.push( + + ); + } + } + setChildren(children); + }; + void load(); + }, [depth, dirEntry, setFileData, volume]); + return ( <> @@ -167,31 +203,7 @@ const DirectoryListing = ({ {formatDate(dirEntry.creation)} - {open && - dirEntry.entries.map((fileEntry, idx) => { - if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) { - const dirEntry = new Directory(volume, fileEntry); - return ( - - ); - } else { - return ( - - ); - } - })} + {open && children} ); }; @@ -300,6 +312,21 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => { const [proDOSData, setProDOSData] = useState(); useEffect(() => { const load = async () => { + if (isBlockStorage(massStorage)) { + const disk = await massStorage.getBlockDisk(driveNo); + if (disk) { + const prodos = new ProDOSVolume(disk); + const vdh = await prodos.vdh(); + const bitMap = await prodos.bitMap(); + const freeBlocks = await bitMap.freeBlocks(); + const freeCount = freeBlocks.length; + setProDOSData({ freeCount, prodos, vdh }); + } else { + setProDOSData(undefined); + } + setDisk(disk); + return; + } const massStorageData = await massStorage.getBinary(driveNo, 'po'); if (massStorageData) { const { data, readOnly, ext } = massStorageData; @@ -343,7 +370,6 @@ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => { } setDisk(disk); } - return null; }; void load(); }, [massStorage, driveNo]); diff --git a/js/components/util/http_block_disk.ts b/js/components/util/http_block_disk.ts index 6628d5e..bd804fb 100644 --- a/js/components/util/http_block_disk.ts +++ b/js/components/util/http_block_disk.ts @@ -1,3 +1,4 @@ +import { HeaderData, read2MGHeader } from 'js/formats/2mg'; import { BlockDisk, BlockFormat, @@ -5,14 +6,28 @@ import { ENCODING_BLOCK, } from 'js/formats/types'; +class Deferred { + promise: Promise; + resolve: (value: T) => void; + reject: (error: Error) => void; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} + export class HttpBlockDisk implements BlockDisk { encoding: typeof ENCODING_BLOCK = ENCODING_BLOCK; format: BlockFormat = 'po'; metadata: DiskMetadata; readOnly: boolean = false; + headerData: HeaderData | null = null; blocks: Uint8Array[] = []; - fetchMap: Promise[] = []; + fetchMap: Deferred[] = []; constructor( name: string, @@ -22,40 +37,61 @@ export class HttpBlockDisk implements BlockDisk { this.metadata = { name }; } + private async getHeaderData(): Promise { + if (this.format === '2mg') { + if (!this.headerData) { + const header = await fetch(this.url, { + headers: { range: 'bytes=0-63' }, + }); + const headerBody = await header.arrayBuffer(); + this.headerData = read2MGHeader(headerBody); + } + return this.headerData; + } + return null; + } + async blockCount(): Promise { - return this.contentLength; + const headerData = await this.getHeaderData(); + if (headerData) { + return headerData.bytes >> 9; + } else { + return this.contentLength >> 9; + } } async read(blockNumber: number): Promise { - const blockCount = 5; + const blockShift = 5; if (!this.blocks[blockNumber]) { - const fetchBlock = blockNumber >> blockCount; - const fetchPromise = this.fetchMap[fetchBlock]; - if (fetchPromise !== undefined) { - const response = await fetchPromise; - if (!response.ok) { - throw new Error(`Error loading: ${response.statusText}`); - } - if (!response.body) { - throw new Error('Error loading: no body'); - } + const fetchBlock = blockNumber >> blockShift; + const deferred = this.fetchMap[fetchBlock]; + if (deferred !== undefined) { + await deferred.promise; } else { - const start = 512 * (fetchBlock << blockCount); - const end = start + (512 << blockCount); - this.fetchMap[fetchBlock] = fetch(this.url, { + const deferred = new Deferred(); + this.fetchMap[fetchBlock] = deferred; + const headerData = await this.getHeaderData(); + const headerSize = headerData?.offset ?? 0; + const start = 512 * (fetchBlock << blockShift) + headerSize; + const end = start + (512 << blockShift) - 1; + const response = await fetch(this.url, { headers: { range: `bytes=${start}-${end}` }, }); - const response = await this.fetchMap[fetchBlock]; if (!response.ok) { - throw new Error(`Error loading: ${response.statusText}`); + const error = new Error( + `Error loading: ${response.statusText}` + ); + deferred.reject(error); + throw error; } if (!response.body) { - throw new Error('Error loading: no body'); + const error = new Error('Error loading: no body'); + deferred.reject(error); + throw error; } - const blob = await response.blob(); - const buffer = await new Response(blob).arrayBuffer(); - const startBlock = fetchBlock << blockCount; - const endBlock = startBlock + (1 << blockCount); + const buffer = await response.arrayBuffer(); + const startBlock = fetchBlock << blockShift; + const endBlock = startBlock + (1 << blockShift); let startOffset = 0; for (let idx = startBlock; idx < endBlock; idx++) { const endOffset = startOffset + 512; @@ -64,6 +100,7 @@ export class HttpBlockDisk implements BlockDisk { ); startOffset += 512; } + deferred.resolve(true); } } return this.blocks[blockNumber]; diff --git a/js/formats/prodos/utils.ts b/js/formats/prodos/utils.ts index c0ef120..26107e8 100644 --- a/js/formats/prodos/utils.ts +++ b/js/formats/prodos/utils.ts @@ -86,12 +86,13 @@ export function writeFileName(block: DataView, offset: word, name: string) { return caseBits; } -export function dumpDirectory( +export async function dumpDirectory( volume: ProDOSVolume, dirEntry: FileEntry, depth: string ) { const dir = new Directory(volume, dirEntry); + await dir.init(); let str = ''; for (let idx = 0; idx < dir.entries.length; idx++) { @@ -99,7 +100,7 @@ export function dumpDirectory( if (fileEntry.storageType !== STORAGE_TYPES.DELETED) { str += depth + fileEntry.name + '\n'; if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) { - str += dumpDirectory(volume, fileEntry, depth + ' '); + str += await dumpDirectory(volume, fileEntry, depth + ' '); } } } @@ -113,7 +114,7 @@ export async function dump(volume: ProDOSVolume) { const fileEntry = vdh.entries[idx]; str += fileEntry.name + '\n'; if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) { - str += dumpDirectory(volume, fileEntry, ' '); + str += await dumpDirectory(volume, fileEntry, ' '); } } return str; diff --git a/js/formats/types.ts b/js/formats/types.ts index d52a2bd..416d266 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -313,3 +313,14 @@ export interface MassStorage { ): Promise; getBinary(drive: number, ext?: T): Promise; } + +export interface BlockStorage extends MassStorage { + setBlockDisk(drive: number, blockDisk: BlockDisk): Promise; + getBlockDisk(drive: number): Promise; +} + +export function isBlockStorage( + storage: MassStorage +): storage is BlockStorage { + return 'getBlockDisk' in storage; +} diff --git a/tsconfig.json b/tsconfig.json index e4cb9d3..035fbb0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,10 +7,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "target": "es6", - "lib": [ - "DOM", - "ES6" - ], + "lib": ["DOM", "ES6"], "noImplicitAny": true, "noImplicitThis": true, "noUnusedLocals": true, @@ -18,31 +15,18 @@ "exactOptionalPropertyTypes": true, "moduleResolution": "node", "resolveJsonModule": true, + "skipLibCheck": true, "sourceMap": true, "strictNullChecks": true, "outDir": "dist", "baseUrl": ".", "allowJs": true, "paths": { - "*": [ - "node_modules/*", - "types/*" - ], - "js/*": [ - "js/*" - ], - "json/*": [ - "json/*" - ], - "test/*": [ - "test/*" - ] + "*": ["node_modules/*", "types/*"], + "js/*": ["js/*"], + "json/*": ["json/*"], + "test/*": ["test/*"] } }, - "include": [ - "js/**/*", - "test/**/*", - "types/**/*", - "*.config.js" - ] + "include": ["js/**/*", "test/**/*", "types/**/*", "*.config.js"] }