nufatfs
Version:
A new async-friendly library for accessing FAT16 and FAT32 filesystems
459 lines (458 loc) • 22.9 kB
JavaScript
"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