Add Disk Explorer

This commit is contained in:
Jordan Goulder 2025-01-24 22:30:01 -05:00
parent d011580074
commit 663a1f01dd
4 changed files with 236 additions and 12 deletions

View File

@ -0,0 +1,147 @@
<script lang="ts" setup>
import { FloppyDisk } from '@/floppy/disk.ts'
import { computed, ref } from 'vue'
import HexDump from '@/components/HexDump.vue'
const { floppyDisk = null } = defineProps<{ floppyDisk: FloppyDisk | null }>()
const currentPath = ref([''])
const currentFileData = ref(new ArrayBuffer(0))
const currentFileName = ref<string>('')
const directories = computed(() => {
const fileList = floppyDisk?.getFileList()
let directories = fileList
?.filter((file) => {
return file.isDirectory && arraysEqual(currentPath.value, file.path)
})
.sort()
if (directories) {
directories = [
{
name: '.',
isDirectory: true,
path: [],
firstCuster: -1,
size: 0,
},
{
name: '..',
isDirectory: true,
path: [],
firstCuster: -1,
size: 0,
},
...directories,
]
}
return directories
})
const files = computed(() => {
const fileList = floppyDisk?.getFileList()
return fileList
?.filter((file) => {
return !file.isDirectory && arraysEqual(currentPath.value, file.path)
})
.sort()
})
function arraysEqual<T>(a1: T[], a2: T[]): boolean {
return a1.length === a2.length && a1.every((value, index) => value === a2[index])
}
function selectDirectory(name: string) {
if (name === '.') {
currentFileData.value = new ArrayBuffer(0)
currentFileName.value = ''
} else if (name === '..') {
currentFileData.value = new ArrayBuffer(0)
currentFileName.value = ''
if (currentPath.value.length > 1) {
currentPath.value.pop()
}
} else {
currentPath.value.push(name)
}
}
function loadFile(name: string, firstCluster: number, size: number) {
currentFileData.value = floppyDisk?.getFileData(firstCluster, size) ?? new ArrayBuffer(0)
currentFileName.value = name
}
</script>
<template>
<div class="path">A:{{ currentPath.join('\\') + '\\' + currentFileName }}</div>
<div class="cols">
<div class="file-list">
<ul class="directories">
<li v-for="(dir, index) in directories" :key="index">
<a href="" @click.prevent="selectDirectory(dir.name)">{{ dir.name }}</a>
</li>
</ul>
<ul class="files">
<li v-for="(file, index) in files" :key="index">
<a href="" @click.prevent="loadFile(file.name, file.firstCuster, file.size)">{{
file.name
}}</a>
</li>
</ul>
</div>
<div class="hex">
<HexDump :buffer="currentFileData" />
</div>
</div>
</template>
<style scoped>
div.path {
margin-left: 1em;
}
div.cols {
display: flex;
}
div.file-list {
box-sizing: border-box;
min-width: 12em;
background: rgba(255, 255, 255, 0.075);
margin: 1em;
max-height: 40em;
overflow-y: auto;
padding: 1em 1em 0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.directories a {
color: cornflowerblue;
text-decoration: none;
}
.directories a:hover {
text-decoration: underline;
}
.files a {
color: white;
text-decoration: none;
}
.files a:hover {
text-decoration: underline;
}
ul {
margin: 0;
line-height: 1.75em;
padding: 0;
list-style: none;
}
</style>

View File

@ -2,19 +2,15 @@
import { computed } from 'vue'
import { FloppyDisk } from '@/floppy/disk.ts'
const { data = new ArrayBuffer(0) } = defineProps<{ data: ArrayBuffer }>()
const floppyDisk = computed(() => {
return new FloppyDisk(data)
})
const { floppyDisk = null } = defineProps<{ floppyDisk: FloppyDisk | null }>()
const fileListing = computed(() => {
return floppyDisk.value.buildFileListing()
return floppyDisk?.buildFileListing()
})
</script>
<template>
<section v-if="floppyDisk.bootSectorInfo">
<section v-if="floppyDisk">
<h3>File Listing</h3>
<div v-memo="fileListing" v-html="fileListing"></div>
<h3>Boot Sector</h3>

View File

@ -1,8 +1,10 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { computed, ref } from 'vue'
import DiskReader from '@/components/DiskReader.vue'
import HexDump from '@/components/HexDump.vue'
import DiskInfo from '@/components/DiskInfo.vue'
import { FloppyDisk } from '@/floppy/disk.ts'
import DiskExplorer from '@/components/DiskExplorer.vue'
type DiskReadyState = 'empty' | 'read-started' | 'read-success' | 'read-failed'
@ -12,6 +14,13 @@ const diskTotalBytes = ref(0)
const diskReadBytes = ref(0)
const diskReadError = ref('')
const diskData = ref(new ArrayBuffer(0))
const floppyDisk = computed(() => {
if (diskData.value.byteLength === 0) {
return null
} else {
return new FloppyDisk(diskData.value)
}
})
function onReadStart(name: string) {
console.log(`Reading disk from ${name}`)
@ -84,9 +93,13 @@ function onUnload() {
<div v-else>No disk loaded</div>
</div>
</section>
<section v-if="diskReadyState === 'read-success'">
<h2>Disk Explorer</h2>
<DiskExplorer :floppyDisk />
</section>
<section v-if="diskReadyState === 'read-success'">
<h2>Disk Info</h2>
<DiskInfo :data="diskData" />
<DiskInfo :floppyDisk />
</section>
<section v-if="diskReadyState === 'read-success'">
<h2>Disk Hex Dump</h2>

View File

@ -68,6 +68,14 @@ export interface IUnusedDirEntry {
export type TDirEntry = IStandardDirEntry | ILongFileNameDirEntry | IFinalDirEntry | IUnusedDirEntry
export interface IFile {
name: string
path: string[]
isDirectory: boolean
firstCuster: number
size: number
}
export class FloppyDisk {
buffer = new ArrayBuffer(0)
private _bootSector: IBootSectorInfo | null = null
@ -192,7 +200,7 @@ export class FloppyDisk {
return chain
}
addDirectory(listing: string, path: string[], entries: TDirEntry[]) {
addDirectoryListing(listing: string, path: string[], entries: TDirEntry[]) {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
if (
@ -208,7 +216,7 @@ export class FloppyDisk {
if (entry.attributes.directory) {
listing += '\\<br/>'
listing = this.addDirectory(listing, [...path, entry.name], entry.subDirEntries)
listing = this.addDirectoryListing(listing, [...path, entry.name], entry.subDirEntries)
} else {
listing += '<br/>'
}
@ -219,10 +227,70 @@ export class FloppyDisk {
buildFileListing(): string {
let listing = '<p>\\<br/>'
listing += this.addDirectory('', [''], this.rootDirEntries ?? [])
listing += this.addDirectoryListing('', [''], this.rootDirEntries ?? [])
listing += '</p>'
return listing
}
addDirectory(list: IFile[], path: string[], entries: TDirEntry[]) {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
if (
entry.type !== 'standard-entry' ||
entry.attributes.volumeId ||
entry.name === '..' ||
entry.name === '.'
) {
continue
}
if (entry.attributes.directory) {
list.push({
name: entry.name,
path,
isDirectory: true,
firstCuster: entry.firstCluster,
size: 0,
})
this.addDirectory(list, [...path, entry.name], entry.subDirEntries)
} else {
list.push({
name: entry.name,
path,
isDirectory: false,
firstCuster: entry.firstCluster,
size: entry.size,
})
}
}
}
getFileList(): IFile[] {
const list: IFile[] = []
this.addDirectory(list, [''], this.rootDirEntries ?? [])
return list
}
getFileData(firstCluster: number, size: number): ArrayBuffer {
const clusterChain = this.clusterChain(firstCluster)
const dataSize = clusterChain.length * this.bytesPerCluster
if (dataSize === 0 || size < 1) {
return new ArrayBuffer(0)
}
const entryData = new Uint8Array(dataSize)
for (let i = 0; i < clusterChain.length; i++) {
const offset = (clusterChain[i] - 2) * this.bytesPerCluster
const view = new Uint8Array(this.buffer, this.dataOffset + offset, this.bytesPerCluster)
entryData.set(view, i * this.bytesPerCluster)
}
return entryData.slice(0, Math.min(size, entryData.byteLength))
}
}
function decodeBootSector(data: DataView): IBootSectorInfo | null {