Add Disk Explorer
This commit is contained in:
parent
d011580074
commit
663a1f01dd
147
src/components/DiskExplorer.vue
Normal file
147
src/components/DiskExplorer.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user