UNPKG

nufatfs

Version:

A new async-friendly library for accessing FAT16 and FAT32 filesystems

459 lines (458 loc) 22.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LowLevelFatFilesystem = exports.FatType = exports.CachedDirectory = exports.FORBIDDEN_ATTRIBUTES_FOR_FILE = exports.FAT_MARKER_DELETED = exports.FatError = void 0; const chained_structures_1 = require("./chained-structures"); const cluster_allocator_1 = require("./cluster-allocator"); const cluster_chain_1 = require("./cluster-chain"); const constructors_1 = require("./constructors"); const types_1 = require("./types"); const utils_1 = require("./utils"); class FatError extends Error { } exports.FatError = FatError; const textEncoder = new TextEncoder(); exports.FAT_MARKER_DELETED = 0xe5; exports.FORBIDDEN_ATTRIBUTES_FOR_FILE = types_1.FatFSDirectoryEntryAttributes.Directory | types_1.FatFSDirectoryEntryAttributes.VolumeLabel; class CachedDirectory { constructor(fat, initialCluster, underlying) { this.fat = fat; this.initialCluster = initialCluster; this.underlying = underlying; } async getEntries() { if (!this.rawDirectoryEntries) { // Load it all first const initialCluster = this.underlying._firstCluster; const rawEntries = await this.fat.readAndConsumeAllDirectoryEntries(initialCluster); this.rawDirectoryEntries = rawEntries.map((e) => { if (e.attribs & types_1.FatFSDirectoryEntryAttributes.Directory) { let entryInitialCluster = e._firstCluster; return new CachedDirectory(this.fat, entryInitialCluster, e); } return e; }); } return this.rawDirectoryEntries; } async findEntry(name, typeRequired) { var _a; const entries = await this.getEntries(); return (_a = entries.find(e => { if (e instanceof CachedDirectory) { if (typeRequired !== 'file') { return (0, utils_1.namesEqual)(name, e.underlying._filenameStr); } return false; } return !(e.attribs & exports.FORBIDDEN_ATTRIBUTES_FOR_FILE) && (0, utils_1.namesEqual)(name, e._filenameStr) && e.attribs !== types_1.FatFSDirectoryEntryAttributes.EqLFN; })) !== null && _a !== void 0 ? _a : null; } async listDir() { const entries = await this.getEntries(); return entries.map(e => { if (e instanceof CachedDirectory) { if (["..", "."].includes((0, utils_1.name83toNormal)(e.underlying._filenameStr))) return null; return (0, utils_1.name83toNormal)(e.underlying._filenameStr) + '/'; } if (e.attribs === types_1.FatFSDirectoryEntryAttributes.EqLFN) return null; if (e.attribs & exports.FORBIDDEN_ATTRIBUTES_FOR_FILE) return null; return (0, utils_1.name83toNormal)(e._filenameStr); }).filter(e => typeof e === 'string'); } static readyMade(fat, entries, initialCluster, underlying) { const entry = new CachedDirectory(fat, initialCluster, underlying !== null && underlying !== void 0 ? underlying : null); entry.rawDirectoryEntries = entries.map((e) => { if (e.attribs & types_1.FatFSDirectoryEntryAttributes.Directory) { let entryInitialCluster = e._firstCluster; if (entryInitialCluster === 0) { // Root. return CachedDirectory.readyMade(fat, fat.root.rawDirectoryEntries.map(e => (e instanceof CachedDirectory) ? e.underlying : e), 0, e); } return new CachedDirectory(fat, entryInitialCluster, e); } return e; }); return entry; } } exports.CachedDirectory = CachedDirectory; var FatType; (function (FatType) { FatType[FatType["Fat12"] = 0] = "Fat12"; FatType[FatType["Fat16"] = 1] = "Fat16"; FatType[FatType["Fat32"] = 2] = "Fat32"; })(FatType || (exports.FatType = FatType = {})); class LowLevelFatFilesystem { get isFat16Or12() { return this.fatType === FatType.Fat16 || this.fatType === FatType.Fat12; } clusterToSector(cluster) { // cluster - 2: // FAT16 and FAT32 reserve two first clusters - cluster 0 means "No data", and 1 is reserved for the FAT itself. // Therefore, we need to decrease the value by two. return this.bootsectorInfo.logicalSectorsPerCluster * (cluster - 2) + this.dataSectorOffset + this.fat16ClusterAreaOffset; } readFATClusterEntry(number) { if (this.fatType === FatType.Fat12) { const base = Math.floor(number / 2) * 3; const baseContents = (this.fatContents.getUint16(base, true) | (this.fatContents.getUint8(base + 2) << 16)) >>> 0; if (number & 1) { return (baseContents & 0xFFF000) >>> 12; } else { return (baseContents & 0xFFF) >>> 0; } } else if (this.fatType === FatType.Fat16) { return this.fatContents.getUint16(number * 2, true); } else { return this.fatContents.getUint32(number * 4, true); } } writeFATClusterEntry(number, next) { let sector; if (this.fatType === FatType.Fat12) { const base = Math.floor(number / 2) * 3; let baseContents = (this.fatContents.getUint16(base, true) | (this.fatContents.getUint8(base + 2) << 16)) >>> 0; if (number & 1) { baseContents = baseContents & 0x000FFF | (next << 12); } else { baseContents = baseContents & 0xFFF000 | next; } this.fatContents.setUint16(base, baseContents & 0xFFFF, true); this.fatContents.setUint8(base + 2, baseContents >>> 16); this.alteredFATSectors.add(Math.floor(base / this.bootsectorInfo.bytesPerLogicalSector)); this.alteredFATSectors.add(Math.floor((base + 2) / this.bootsectorInfo.bytesPerLogicalSector)); this.fatAltered = true; return; } else if (this.fatType === FatType.Fat16) { sector = Math.floor((number * 2) / this.bootsectorInfo.bytesPerLogicalSector); this.fatContents.setUint16(number * 2, next, true); } else { sector = Math.floor((number * 4) / this.bootsectorInfo.bytesPerLogicalSector); this.fatContents.setUint32(number * 4, next, true); } this.alteredFATSectors.add(sector); this.fatAltered = true; } get dataSectorOffset() { return this.bootsectorInfo.reservedLogicalSectors + this.bootsectorInfo.fatCount * this.logicalSectorsPerFat; } ; get logicalSectorsPerFat() { return this.isFat16Or12 ? this.bootsectorInfo.deprecatedLogicalSectorsPerFat : this.fat32Extension.logicalSectorsPerFat; } get clusterSizeInBytes() { return this.bootsectorInfo.logicalSectorsPerCluster * this.bootsectorInfo.bytesPerLogicalSector; } constructor(driver) { this.driver = driver; this.maxCluster = 0; this.maxDataCluster = 0; this.fatType = FatType.Fat16; this.fat16ClusterAreaOffset = 0; this.endOfChain = []; this.alteredFATSectors = new Set(); this.fatAltered = false; this.alteredDirectoryEntries = []; this.isWritable = !!driver.writeSectors; } ; async load(bypassCoherencyCheck = false, forceFSType) { const firstSector = await this.driver.readSectors(0, 1); this.bootsectorInfo = (0, constructors_1.createBootSectorInfo)(firstSector); let offset = 0x24; // Fat 12 has to be forced on. if (this.bootsectorInfo.deprecatedLogicalSectorsPerFat === 0) { this.fat32Extension = (0, constructors_1.createFat32ExtendedInfo)(firstSector, offset); offset += 28; this.fatType = FatType.Fat32; } else { this.fatType = FatType.Fat16; } if (forceFSType !== undefined) { this.fatType = forceFSType; } switch (this.fatType) { case FatType.Fat12: this.endOfChain = Array(8).fill(0).map((e, i) => 0xFF8 + i); break; case FatType.Fat16: this.endOfChain = Array(8).fill(0).map((e, i) => 0xFFF8 + i); break; case FatType.Fat32: this.endOfChain = Array(8).fill(0).map((e, i) => 0x0FFFFFF8 + i); break; } this.fatBootInfo = (0, constructors_1.createFatBootInfo)(firstSector, offset); if (this.bootsectorInfo.bytesPerLogicalSector !== this.driver.sectorSize) { throw new FatError("The number of bytes per logical sector doesn't match driver's declaration!"); } if (!Number.isInteger(this.bootsectorInfo.bytesPerLogicalSector / 128)) throw new FatError(`Expected logical sector size to be a multiple of 128, got ${this.bootsectorInfo.bytesPerLogicalSector}`); if (!Number.isInteger(Math.log2(this.bootsectorInfo.logicalSectorsPerCluster))) throw new FatError(`Expected sectors per cluster ot be a power of 2. Got ${this.bootsectorInfo.logicalSectorsPerCluster}`); if (this.fatBootInfo.extendedBootSignature === 0x28) { this.fatBootInfo.label = textEncoder.encode("NO NAME "); this.fatBootInfo.fsType = textEncoder.encode("FAT16 "); } else if (this.fatBootInfo.extendedBootSignature !== 0x29) { console.log(`[NUFATFS]: Warning: Found invalid extended boot signature: 0x${this.fatBootInfo.extendedBootSignature.toString(16)}`); } if (this.fatType === FatType.Fat32) { const fsInfoSector = await this.driver.readSectors(this.fat32Extension.fsInformationSectorNum, 1); this.fsInfo = (0, constructors_1.createFatFsInformation)(fsInfoSector); if (!((0, utils_1.arraysEq)(this.fsInfo.signature1, textEncoder.encode("RRaA")) && (0, utils_1.arraysEq)(this.fsInfo.signature2, textEncoder.encode("rrAa")) && (0, utils_1.arraysEq)(this.fsInfo.signature3, new Uint8Array([0x00, 0x00, 0x55, 0xaa])))) { console.log(`[NUFATFS]: Warning: Found invalid values in fat32 signatures. Ignoring values.`); this.fsInfo.lastKnownFreeDataClusters = 0xFFFFFFFF; this.fsInfo.lastKnownAllocatedDataCluster = 0xFFFFFFFF; } } const totalLogicalSectors = this.bootsectorInfo.deprecatedTotalLogicalSectors || this.bootsectorInfo.totalLogicalSectors; this.maxCluster = Math.floor((totalLogicalSectors - this.dataSectorOffset) / this.bootsectorInfo.logicalSectorsPerCluster); if (this.maxCluster > 268435447) { console.log("[NUFATFS]: Warning: FAT Device is too big. Some data will be inaccessible."); this.maxCluster = 268435447; } // Carve out space for reserved data. (see clusterToSector for explanation) this.maxDataCluster = this.maxCluster - Math.ceil((this.dataSectorOffset + this.fat16ClusterAreaOffset) / this.bootsectorInfo.logicalSectorsPerCluster) - 2; if (this.isFat16Or12) { this.fat16ClusterAreaOffset = (this.bootsectorInfo.deprecatedMaxRootDirEntries * 32) / this.bootsectorInfo.bytesPerLogicalSector; } let rawFat = await this.driver.readSectors(this.bootsectorInfo.reservedLogicalSectors, this.logicalSectorsPerFat); this.fatContents = new DataView(rawFat.buffer); if (!bypassCoherencyCheck) { for (let alternativeFat = 1; alternativeFat < this.bootsectorInfo.fatCount; alternativeFat++) { let altFatContents = await this.driver.readSectors(this.bootsectorInfo.reservedLogicalSectors + this.logicalSectorsPerFat * alternativeFat, this.logicalSectorsPerFat); if (!(0, utils_1.arraysEq)(altFatContents, rawFat)) { throw new FatError("Fat backup invalid - filesystem damaged. Run CHKDSK or fsck!"); } } } this.root = CachedDirectory.readyMade(this, await this.getRootDirectoryData(), this.isFat16Or12 ? -1 : this.fat32Extension.rootDirCluster, null); this.allocator = await cluster_allocator_1.ClusterAllocator.create(this); } async getRootDirectoryData() { // If we're dealing with FAT16, this.dataSectorOffset points to the root directory. // Else, read the directory table from 32extension if (this.isFat16Or12) { let rootSectorLength = (this.bootsectorInfo.deprecatedMaxRootDirEntries * 32) / this.driver.sectorSize; return this.consumeAllDirectoryEntries(await this.driver.readSectors(this.dataSectorOffset, rootSectorLength)); } else { return await this.readAndConsumeAllDirectoryEntries(this.fat32Extension.rootDirCluster); } } markAsAltered(entry) { if (!this.alteredDirectoryEntries.includes(entry)) { this.alteredDirectoryEntries.push(entry); } } markFatAsAltered() { this.fatAltered = true; } consumeAllDirectoryEntries(data, includeDeleted = false) { let output = []; let offset = 0; let lfnCounter = 0; for (;;) { let entry = (0, constructors_1.createFatFSDirectoryEntry)(data, offset); if (entry.attribs === types_1.FatFSDirectoryEntryAttributes.EqLFN) lfnCounter++; else { entry._lfns = lfnCounter; lfnCounter = 0; } offset += 32; if (entry.filename[0] == exports.FAT_MARKER_DELETED && !includeDeleted) continue; if (entry.filename[0] == 0x00) break; output.push(entry); } if (lfnCounter) { console.log(`[NUFATFS]: Warning! Encountered unused LFNs while traversing directory trees.`); } return output; } getClusterChainFromFAT(initialCluster) { let link = initialCluster; const links = [link]; // console.log("Constructing chain for " + initialCluster); // 0x00 can be EoC as well. while (!([...this.endOfChain, 0x00].includes(link = this.readFATClusterEntry(link)))) { // console.log("... " + link); if (links.includes(link)) throw new Error("Infinite allocation loop!"); links.push(link); } // console.log("Chain complete. There are " + links.length + " links"); return links; } constructClusterChain(initialCluster, enableAllocator = true, limitLength) { if (!this.allocator && enableAllocator) { throw new FatError("Cannot use the allocator when mid-initialization!"); } const defaultLength = this.bootsectorInfo.logicalSectorsPerCluster * this.driver.sectorSize; const links = []; // InitialCluster === 0 denotes file created, but space unallocated. It is zero bytes long. if (initialCluster !== 0) { const clusterChain = this.getClusterChainFromFAT(initialCluster); for (let link of clusterChain) { links.push(new cluster_chain_1.ClusterChainLink(this, link, defaultLength)); } } return new chained_structures_1.Chain(links, enableAllocator ? (link, size) => Promise.resolve(this.allocator.allocate(link, size)) : undefined, limitLength); } async readAndConsumeAllDirectoryEntries(initialCluster) { return this.consumeAllDirectoryEntries(await this.constructClusterChain(initialCluster, false).readAll()); } async readClusters(clusterNumber, count) { if (clusterNumber + count > this.driver.numSectors) { throw new FatError("Corrupted FAT - reading outside of volume!"); } return this.driver.readSectors(this.clusterToSector(clusterNumber), count * this.bootsectorInfo.logicalSectorsPerCluster); } async writeClusters(clusterNumber, data) { if (!this.isWritable) throw new FatError("Cannot write to a read-only volume!"); if (clusterNumber + (data.length / this.clusterSizeInBytes) > this.driver.numSectors) { throw new FatError("Corrupted FAT - writing outside of volume!"); } await this.driver.writeSectors(this.clusterToSector(clusterNumber), data); } async redefineClusterChain(oldInitialCluster, newChain) { // Free the previous chain, then rewrite it. // ( Get initial cluster from the new chain ) const previousChain = this.getClusterChainFromFAT(oldInitialCluster); for (let link of previousChain) { if (newChain.indexOf(link) === -1) { // Give it to the allocator (mark as free) this.allocator.freemap[link] = true; } } for (let link of newChain) { if (previousChain.indexOf(link) === -1) { // Take it from the allocator (mark as nonfree) this.allocator.freemap[link] = false; } } // Make the allocator recompute the freelist this.allocator.convertFreemapToFreelist(); for (let link of previousChain) { this.writeFATClusterEntry(link, 0x00); } let previous = newChain[0]; for (let link of newChain.slice(1)) { this.writeFATClusterEntry(previous, link); previous = link; } // Write End-of-Chain this.writeFATClusterEntry(previous, this.endOfChain[this.endOfChain.length - 1]); } async flush() { var _a; // Since we're not altering any format-related infortmation // only the FAT and all changed directory entries will need to be flushed. if (!this.isWritable) { throw new FatError("Cannot flush a read-only volume."); } // Reserialize the FAT tables, and write all the copies. if (this.fatAltered) { const fatContents = new Uint8Array(this.fatContents.buffer); for (let fat = 0; fat < this.bootsectorInfo.fatCount; fat++) { for (const usedFATSector of this.alteredFATSectors) { const sectorSlice = fatContents.subarray(usedFATSector * this.bootsectorInfo.bytesPerLogicalSector, (usedFATSector + 1) * this.bootsectorInfo.bytesPerLogicalSector); await this.driver.writeSectors(this.bootsectorInfo.reservedLogicalSectors + fat * this.logicalSectorsPerFat + usedFATSector, sectorSlice); } } this.alteredFATSectors = new Set(); this.fatAltered = false; } // Rebuild all the altered directory entries. const toFree = []; for (let entry of this.alteredDirectoryEntries) { let writingChain; if (entry.initialCluster === -1) { // Root cluster on FAT16. Do not use an allocator. Instead fake this chain. let rootSectorLength = (this.bootsectorInfo.deprecatedMaxRootDirEntries * 32); const that = this; writingChain = new chained_structures_1.Chain([{ length: rootSectorLength, async read() { // This will be the structure on which the new data will be overlayed // By returning zeros here, we make sure there's no outdated data in the root directory return new Uint8Array(rootSectorLength).fill(0); }, async write(data) { await that.driver.writeSectors(that.dataSectorOffset, data); } }]); } else { writingChain = this.constructClusterChain(entry.initialCluster); } let remaining = writingChain.length(); for (let subentry of await entry.getEntries()) { const raw = subentry instanceof CachedDirectory ? subentry.underlying : subentry; const data = (0, constructors_1.serializeFatFSDirectoryEntry)(raw); remaining -= data.length; await writingChain.write(data); } if (remaining > 0) { while (remaining > writingChain.links[writingChain.links.length - 1].length && entry.initialCluster !== -1) { // Remove the last chain. const lastLink = writingChain.links.splice(writingChain.links.length - 1, 1)[0]; toFree.push(lastLink.index); this.writeFATClusterEntry(lastLink.index, 0); if (writingChain.links.length) this.writeFATClusterEntry(writingChain.links[writingChain.links.length - 1].index, this.endOfChain[this.endOfChain.length - 1]); remaining -= lastLink.length; } // Zero out the rest. await writingChain.write(new Uint8Array(remaining).fill(0)); } await writingChain.flushChanges(); } (_a = this.allocator) === null || _a === void 0 ? void 0 : _a.addClusterListToFreelist(toFree); this.alteredDirectoryEntries = []; } // TODO: LFN support! async traverseEntries(path) { while (path.startsWith("/")) path = path.substring(1); const pathEntries = path.split("/").filter(e => e); let currentRoot = this.root; let roots = [currentRoot]; for (let i = 0; i < pathEntries.length; i++) { const next = await currentRoot.findEntry((0, utils_1.name83toNormal)((0, utils_1.nameNormalTo83)(pathEntries[i]))); if (!next) return null; if (i !== pathEntries.length - 1 && !(next instanceof CachedDirectory)) { return null; } currentRoot = next; roots.push(currentRoot); } return roots; } async traverse(path) { const entries = await this.traverseEntries(path); return entries ? entries[entries.length - 1] : null; } static async _create(driver, bypassCoherencyCheck = false, forceFSType) { const fs = new LowLevelFatFilesystem(driver); await fs.load(bypassCoherencyCheck, forceFSType); return fs; } } exports.LowLevelFatFilesystem = LowLevelFatFilesystem; // Normal API: // await fs.open() => FileHandle (number) // await fs.read(handle, 0x100) => Uint8Array // fs.getUnderlying() => FATAllocation