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 { computed } from 'vue'
|
||||||
import { FloppyDisk } from '@/floppy/disk.ts'
|
import { FloppyDisk } from '@/floppy/disk.ts'
|
||||||
|
|
||||||
const { data = new ArrayBuffer(0) } = defineProps<{ data: ArrayBuffer }>()
|
const { floppyDisk = null } = defineProps<{ floppyDisk: FloppyDisk | null }>()
|
||||||
|
|
||||||
const floppyDisk = computed(() => {
|
|
||||||
return new FloppyDisk(data)
|
|
||||||
})
|
|
||||||
|
|
||||||
const fileListing = computed(() => {
|
const fileListing = computed(() => {
|
||||||
return floppyDisk.value.buildFileListing()
|
return floppyDisk?.buildFileListing()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section v-if="floppyDisk.bootSectorInfo">
|
<section v-if="floppyDisk">
|
||||||
<h3>File Listing</h3>
|
<h3>File Listing</h3>
|
||||||
<div v-memo="fileListing" v-html="fileListing"></div>
|
<div v-memo="fileListing" v-html="fileListing"></div>
|
||||||
<h3>Boot Sector</h3>
|
<h3>Boot Sector</h3>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import DiskReader from '@/components/DiskReader.vue'
|
import DiskReader from '@/components/DiskReader.vue'
|
||||||
import HexDump from '@/components/HexDump.vue'
|
import HexDump from '@/components/HexDump.vue'
|
||||||
import DiskInfo from '@/components/DiskInfo.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'
|
type DiskReadyState = 'empty' | 'read-started' | 'read-success' | 'read-failed'
|
||||||
|
|
||||||
@ -12,6 +14,13 @@ const diskTotalBytes = ref(0)
|
|||||||
const diskReadBytes = ref(0)
|
const diskReadBytes = ref(0)
|
||||||
const diskReadError = ref('')
|
const diskReadError = ref('')
|
||||||
const diskData = ref(new ArrayBuffer(0))
|
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) {
|
function onReadStart(name: string) {
|
||||||
console.log(`Reading disk from ${name}`)
|
console.log(`Reading disk from ${name}`)
|
||||||
@ -84,9 +93,13 @@ function onUnload() {
|
|||||||
<div v-else>No disk loaded</div>
|
<div v-else>No disk loaded</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section v-if="diskReadyState === 'read-success'">
|
||||||
|
<h2>Disk Explorer</h2>
|
||||||
|
<DiskExplorer :floppyDisk />
|
||||||
|
</section>
|
||||||
<section v-if="diskReadyState === 'read-success'">
|
<section v-if="diskReadyState === 'read-success'">
|
||||||
<h2>Disk Info</h2>
|
<h2>Disk Info</h2>
|
||||||
<DiskInfo :data="diskData" />
|
<DiskInfo :floppyDisk />
|
||||||
</section>
|
</section>
|
||||||
<section v-if="diskReadyState === 'read-success'">
|
<section v-if="diskReadyState === 'read-success'">
|
||||||
<h2>Disk Hex Dump</h2>
|
<h2>Disk Hex Dump</h2>
|
||||||
|
|||||||
@ -68,6 +68,14 @@ export interface IUnusedDirEntry {
|
|||||||
|
|
||||||
export type TDirEntry = IStandardDirEntry | ILongFileNameDirEntry | IFinalDirEntry | IUnusedDirEntry
|
export type TDirEntry = IStandardDirEntry | ILongFileNameDirEntry | IFinalDirEntry | IUnusedDirEntry
|
||||||
|
|
||||||
|
export interface IFile {
|
||||||
|
name: string
|
||||||
|
path: string[]
|
||||||
|
isDirectory: boolean
|
||||||
|
firstCuster: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
export class FloppyDisk {
|
export class FloppyDisk {
|
||||||
buffer = new ArrayBuffer(0)
|
buffer = new ArrayBuffer(0)
|
||||||
private _bootSector: IBootSectorInfo | null = null
|
private _bootSector: IBootSectorInfo | null = null
|
||||||
@ -192,7 +200,7 @@ export class FloppyDisk {
|
|||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
addDirectory(listing: string, path: string[], entries: TDirEntry[]) {
|
addDirectoryListing(listing: string, path: string[], entries: TDirEntry[]) {
|
||||||
for (let i = 0; i < entries.length; i++) {
|
for (let i = 0; i < entries.length; i++) {
|
||||||
const entry = entries[i]
|
const entry = entries[i]
|
||||||
if (
|
if (
|
||||||
@ -208,7 +216,7 @@ export class FloppyDisk {
|
|||||||
|
|
||||||
if (entry.attributes.directory) {
|
if (entry.attributes.directory) {
|
||||||
listing += '\\<br/>'
|
listing += '\\<br/>'
|
||||||
listing = this.addDirectory(listing, [...path, entry.name], entry.subDirEntries)
|
listing = this.addDirectoryListing(listing, [...path, entry.name], entry.subDirEntries)
|
||||||
} else {
|
} else {
|
||||||
listing += '<br/>'
|
listing += '<br/>'
|
||||||
}
|
}
|
||||||
@ -219,10 +227,70 @@ export class FloppyDisk {
|
|||||||
|
|
||||||
buildFileListing(): string {
|
buildFileListing(): string {
|
||||||
let listing = '<p>\\<br/>'
|
let listing = '<p>\\<br/>'
|
||||||
listing += this.addDirectory('', [''], this.rootDirEntries ?? [])
|
listing += this.addDirectoryListing('', [''], this.rootDirEntries ?? [])
|
||||||
listing += '</p>'
|
listing += '</p>'
|
||||||
return listing
|
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 {
|
function decodeBootSector(data: DataView): IBootSectorInfo | null {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user