/*********************************************************************/ // LittleFS JavaScript Port based on LittleFS 2.4.2 // Written by Melissa LeBlanc-Williams for Adafruit Industries /*********************************************************************/ // Error constants const LFS_ERR_OK = 0 // No error const LFS_ERR_IO = -5 // Error during device operation const LFS_ERR_CORRUPT = -84 // Corrupted const LFS_ERR_NOENT = -2 // No directory entry const LFS_ERR_EXIST = -17 // Entry already exists const LFS_ERR_NOTDIR = -20 // Entry is not a dir const LFS_ERR_ISDIR = -21 // Entry is a dir const LFS_ERR_NOTEMPTY = -39 // Dir is not empty const LFS_ERR_BADF = -9 // Bad file number const LFS_ERR_FBIG = -27 // File too large const LFS_ERR_INVAL = -22 // Invalid parameter const LFS_ERR_NOSPC = -28 // No space left on device const LFS_ERR_NOMEM = -12 // No more memory available const LFS_ERR_NOATTR = -61 // No data/attr available const LFS_ERR_NAMETOOLONG = -36 // File name too long const LFS_DISK_VERSION = 0x00020000 const LFS_DISK_VERSION_MAJOR = (0xffff & (LFS_DISK_VERSION >>> 16)) const LFS_DISK_VERSION_MINOR = (0xffff & (LFS_DISK_VERSION >>> 0)) const LFS_NAME_MAX = 32 const LFS_FILE_MAX = 2147483647 const LFS_ATTR_MAX = 1022 const LFS_BLOCK_NULL = -1 const LFS_BLOCK_INLINE = -2 const LFS_TYPE_REG = 0x001 const LFS_TYPE_DIR = 0x002 // internally used types const LFS_TYPE_SPLICE = 0x400 const LFS_TYPE_NAME = 0x000 const LFS_TYPE_STRUCT = 0x200 const LFS_TYPE_USERATTR = 0x300 const LFS_TYPE_FROM = 0x100 const LFS_TYPE_TAIL = 0x600 const LFS_TYPE_GLOBALS = 0x700 const LFS_TYPE_CRC = 0x500 // internally used type specializations const LFS_TYPE_CREATE = 0x401 const LFS_TYPE_DELETE = 0x4ff const LFS_TYPE_SUPERBLOCK = 0x0ff const LFS_TYPE_DIRSTRUCT = 0x200 const LFS_TYPE_CTZSTRUCT = 0x202 const LFS_TYPE_INLINESTRUCT = 0x201 const LFS_TYPE_SOFTTAIL = 0x600 const LFS_TYPE_HARDTAIL = 0x601 const LFS_TYPE_MOVESTATE = 0x7ff // internal chip sources const LFS_FROM_NOOP = 0x000 const LFS_FROM_MOVE = 0x101 const LFS_FROM_USERATTRS = 0x102 // Comparison Constants const LFS_CMP_EQ = 0 const LFS_CMP_LT = 1 const LFS_CMP_GT = 2 const LFS_F_DIRTY = 0x010000 // File does not match storage const LFS_F_WRITING = 0x020000 // File has been written since last flush const LFS_F_READING = 0x040000 // File has been read since last flush const LFS_F_ERRED = 0x080000 // An error occurred during write const LFS_F_INLINE = 0x100000 // Currently inlined in directory entry // File open flags const LFS_O_RDONLY = 1 // Open a file as read only const LFS_O_WRONLY = 2 // Open a file as write only const LFS_O_RDWR = 3 // Open a file as read and write const LFS_O_CREAT = 0x0100 // Create a file if it does not exist const LFS_O_EXCL = 0x0200 // Fail if a file already exists const LFS_O_TRUNC = 0x0400 // Truncate the existing file to zero size const LFS_O_APPEND = 0x0800 // Move to end of file on every write function toHex(value, size=2) { return "0x" + value.toString(16).toUpperCase().padStart(size, "0"); } function toByteArray(str) { let byteArray = []; for (let i = 0; i < str.length; i++) { let charcode = str.charCodeAt(i); if (charcode <= 0xFF) { byteArray.push(charcode); } } return byteArray; } class LittleFS { constructor(config) { this.rcache = new Cache(); this.pcache = new Cache(); this.root = new Uint8Array(2); this.mList = new MList(); this.seed = 0; this.gstate = new GState({tag: 0}); this.gdisk = new GState({tag: 0}); this.gdelta = new GState({tag: 0}); this.free = { off: LFS_BLOCK_NULL, size: LFS_BLOCK_NULL, i: LFS_BLOCK_NULL, ack: LFS_BLOCK_NULL, buffer: null }; this.nameMax = 0; this.fileMax = 0; this.attrMax = 0; this.init(config); } init(config) { let err = 0; if (config) { this.cfg = config; } // validate that the lfs-cfg sizes were initiated properly before // performing any arithmetic logics with them console.assert(this.cfg.readSize != 0); console.assert(this.cfg.progSize != 0); console.assert(this.cfg.cacheSize != 0); // check that block size is a multiple of cache size is a multiple // of prog and read sizes console.assert(this.cfg.cacheSize % this.cfg.readSize == 0); console.assert(this.cfg.cacheSize % this.cfg.progSize == 0); console.assert(this.cfg.blockSize % this.cfg.cacheSize == 0); // check that the block size is large enough to fit ctz pointers console.assert(4*this.npw2(0xffffffff / (this.cfg.blockSize-2*4)) <= this.cfg.blockSize); // block_cycles = 0 is no longer supported. // // block_cycles is the number of erase cycles before littlefs evicts // metadata logs as a part of wear leveling. Suggested values are in the // range of 100-1000, or set block_cycles to -1 to disable block-level // wear-leveling. console.assert(this.cfg.blockCycles != 0); // setup read cache if (this.cfg.readBuffer) { this.rcache.buffer = this.cfg.readBuffer; } else { this.rcache.buffer = new Uint8Array(this.cfg.cacheSize); } // setup program cache if (this.cfg.progBuffer) { this.pcache.buffer = this.cfg.progBuffer; } else { this.pcache.buffer = new Uint8Array(this.cfg.cacheSize); } // zero to avoid information leaks this.cacheZero(this.rcache); this.cacheZero(this.pcache); // setup lookahead, must be multiple of 64-bits, 32-bit aligned console.assert(this.cfg.lookaheadSize > 0); console.assert(this.cfg.lookaheadSize % 8 == 0 && this.cfg.lookaheadBuffer % 4 == 0); if (this.cfg.lookaheadBuffer) { this.free.buffer = this.cfg.lookaheadBuffer } else { this.free.buffer = new Uint8Array(this.cfg.lookaheadSize); } if (!this.nameMax) { this.nameMax = this.cfg.nameMax; } if (!this.fileMax) { this.fileMax = this.cfg.fileMax; } if (!this.attrMax) { this.attrMax = this.cfg.attrMax; } this.root[0] = LFS_BLOCK_NULL; this.root[1] = LFS_BLOCK_NULL; this.mList = new MList(); this.seed = 0; this.gdisk.tag = 0; this.gstate.tag = 0; this.gdelta.tag = 0; } deinit() { if (!this.cfg.readBuffer) { this.rcache.buffer = null; } if (!this.cfg.progBuffer) { this.pcache.buffer = null; } if (!this.cfg.lookaheadBuffer) { this.free.buffer = null; } } format() { let err; main_block: if(true) { this.init() this.free.off = 0; this.free.size = Math.min(this.cfg.lookaheadSize, this.cfg.blockSize) this.free.i = 0; this.allocAck(); let root = new MDir(); err = this.dirAlloc(root); if (err) { return; } let superblock = new SuperBlock({ version: LFS_DISK_VERSION, blockSize: this.cfg.blockSize, blockCount: this.cfg.blockCount, nameMax: this.nameMax, fileMax: this.fileMax, attrMax: this.attrMax }); this.tagId(0x6000); this.superblockTole32(superblock); this.dirCommit(root, this.mkAttrs( [this.mkTag(LFS_TYPE_CREATE, 0, 0), null], [this.mkTag(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"], [this.mkTag(LFS_TYPE_INLINESTRUCT, 0, struct.calcsize("IIIIII")), superblock] )); err = this.dirCommit(root, null); if (err) { break main_block; } err = this.dirFetch(root, [0, 1]); if (err) { break main_block; } } this.deinit() return err; } allocAck() { this.free.ack = this.cfg.blockCount; } dirAlloc(dir) { let err; for (let i = 0; i < 2; i++) { let blockObj = new ByRef(); err = this.alloc(blockObj); dir.pair[(i+1) % 2] = blockObj.get(); if (err) return err; } dir.rev = 0; let bufferByRef = new ByRef(dir.rev) err = this.bdRead(null, this.rcache, struct.calcsize("I"), dir.pair[0], 0, bufferByRef, struct.calcsize("I")); dir.rev = bufferByRef.get(); dir.rev = this.fromle32(dir.rev); if (err && err != LFS_ERR_CORRUPT) { return err; } if (this.cfg.blockCycles > 0) { dir.rev = this.alignup(dir.rev, ((this.cfg.blockCycles+1)|1)); } // set defaults dir.off = struct.calcsize("I"); dir.etag = 0xffffffff; dir.count = 0; dir.tail[0] = LFS_BLOCK_NULL; dir.tail[1] = LFS_BLOCK_NULL; dir.erased = false; dir.split = false; // don't write out yet, let caller take care of that return LFS_ERR_OK; } // Find the smallest power of 2 greater than or equal to a npw2(a) { let r = 0; let s; a -= 1; s = (a > 0xffff) << 4; a >>>= s; r |= s; s = (a > 0xff ) << 3; a >>>= s; r |= s; s = (a > 0xf ) << 2; a >>>= s; r |= s; s = (a > 0x3 ) << 1; a >>>= s; r |= s; return (r | (a >>> 1)) + 1; } aligndown(value, alignment) { return (value - (value % alignment)) >>> 0; } alignup(value, alignment) { return this.aligndown((value + alignment - 1) >>> 0, alignment); } alloc(blockObj) { while (true) { while (this.free.i != this.free.size) { let off = this.free.i; this.free.i += 1; this.free.ack -= 1; if (!(this.free.buffer[parseInt(this.free.i / 32)] & (1 << (off % 32)))) { // found a free block blockObj.set((this.free.off + off) % this.cfg.blockCount); // eagerly find next off so an alloc ack can // discredit old lookahead blocks while (this.free.i != this.free.size && (this.free.buffer[parseInt(this.free.i / 32)] & (1 << (this.free.i % 32)))) { this.free.i += 1; this.free.ack -= 1; } return 0; } } // check if we have looked at all blocks since last ack if (this.free.ack == 0) { console.warn("No more free space " + this.free.i + this.free.off); return LFS_ERR_NOSPC; } this.free.off = (this.free.off + this.free.size) % this.cfg.blockCount; this.free.size = Math.min(8 * this.cfg.lookaheadSize, this.free.ack); this.free.i = 0; // find mask of free blocks from tree let err = this.fsTraverse(this.allocLookahead.bind(this), null, true); if (err) { this.allocDrop(); return err; } } } pairIsnull(pair) { return pair[0] == LFS_BLOCK_NULL || pair[1] == LFS_BLOCK_NULL; } fsTraverse(callback, data, includeOrphans) { let err; // iterate over metadata pairs let dir = new MDir(); dir.tail = [0, 1]; let cycle = 0; while (!this.pairIsnull(dir.tail)) { if (cycle >= this.cfg.blockCount/2) { // loop detected return LFS_ERR_CORRUPT; } cycle += 1; for (let i = 0; i < 2; i++) { err = callback(dir.tail[i]); if (err) { return err; } } // iterate through ids in directory err = this.dirFetch(dir, dir.tail); if (err) { return err; } for (let id = 0; id < dir.count; id++) { let ctz = new Ctz(); let bufferObj = new ByRef(new Uint8Array(struct.calcsize("II"))); let tag = this.dirGet(dir, this.mkTag(0x700, 0x3ff, 0), this.mkTag(LFS_TYPE_STRUCT, id, struct.calcsize("II")), bufferObj); ctz.setFromBuffer(bufferObj.get()) if (tag < 0) { if (tag == LFS_ERR_NOENT) { continue; } return tag; } this.ctzFromle32(ctz); if (this.tagType3(tag) == LFS_TYPE_CTZSTRUCT) { err = this.ctzTraverse(this.rcache, ctz.head, ctz.size, callback, data); if (err) { return err; } } else if (includeOrphans && this.tagType3(tag) == LFS_TYPE_DIRSTRUCT) { for (let i = 0; i < 2; i++) { err = callback(data, (ctz.head)[i]); if (err) { return err; } } } } } // iterate over any open files for (let f = this.mList; f; f = f.next) { if (f.type != LFS_TYPE_REG) { continue; } if ((f.flags & LFS_F_DIRTY) && !(f.flags & LFS_F_INLINE)) { let err = this.ctzTraverse(f.cache, this.rcache, f.ctz.head, f.ctz.size, callback, data); if (err) { return err; } } if ((f.flags & LFS_F_WRITING) && !(f.flags & LFS_F_INLINE)) { let err = this.ctzTraverse(f.cache, this.rcache, f.block, f.pos, callback, data); if (err) { return err; } } } return 0; } dirFetch(dir, pair) { // note, mask=-1, tag=-1 can never match a tag since this // pattern has the invalid bit set return this.dirFetchmatch(dir, pair, -1, -1, new ByRef(null), null, null); } dirFetchmatch(dir, pair, fmask, ftag, idByRef, callback, data) { // we can find tag very efficiently during a fetch, since we're already // scanning the entire directory let besttag = -1; // if either block address is invalid we return LFS_ERR_CORRUPT here, // otherwise later writes to the pair could fail if (pair[0] >= this.cfg.blockCount || pair[1] >= this.cfg.blockCount) { return LFS_ERR_CORRUPT; } // find the block with the most recent revision let revs = [0, 0]; let r = 0; let bufferByRef = new ByRef(); for (let i = 0; i < 2; i++) { let err = this.bdRead(null, this.rcache, struct.calcsize("I"), pair[i], 0, bufferByRef, struct.calcsize("I")); revs[i] = bufferByRef.get(); revs[i] = this.fromle32(revs[i]); if (err && err != LFS_ERR_CORRUPT) { return err; } if (err != LFS_ERR_CORRUPT && this.scmp(revs[i], revs[(i+1)%2]) > 0) { r = i; } } dir.pair[0] = pair[(r+0)%2]; dir.pair[1] = pair[(r+1)%2]; dir.rev = revs[(r+0)%2]; dir.off = 0; // nonzero = found some commits // now scan tags to fetch the actual dir and find possible match for (let i = 0; i < 2; i++) { let off = 0; let ptag = 0xffffffff; let tempcount = 0; let temptail = [LFS_BLOCK_NULL, LFS_BLOCK_NULL]; let tempsplit = false; let tempbesttag = besttag; dir.rev = this.tole32(dir.rev); let crc = this.crc(0xffffffff, dir.rev, struct.calcsize("I")); dir.rev = this.fromle32(dir.rev); while (true) { // extract next tag let tag; bufferByRef = new ByRef(); off += this.tagDsize(ptag); let err = this.bdRead(null, this.rcache, this.cfg.blockSize, dir.pair[0], off, bufferByRef, struct.calcsize("I")); tag = bufferByRef.get(); if (err) { if (err == LFS_ERR_CORRUPT) { // can't continue? dir.erased = false; break; } return err; } crc = this.crc(crc, tag, struct.calcsize("I")); tag = (this.fromBe32(tag) ^ ptag) >>> 0; // next commit not yet programmed or we're not in valid range if (!this.tagIsvalid(tag)) { dir.erased = (this.tagType1(ptag) == LFS_TYPE_CRC && dir.off % this.cfg.progSize == 0); break; } else if (off + this.tagDsize(tag) > this.cfg.blockSize) { dir.erased = false; break; } ptag = tag; if (this.tagType1(tag) == LFS_TYPE_CRC) { // check the crc attr let dcrc; bufferByRef = new ByRef(); err = this.bdRead(null, this.rcache, this.cfg.blockSize, dir.pair[0], off + struct.calcsize("I"), bufferByRef, struct.calcsize("I")); if (err) { if (err == LFS_ERR_CORRUPT) { dir.erased = false; break; } return err; } dcrc = bufferByRef.get(); dcrc = this.fromle32(dcrc); if (crc != dcrc) { dir.erased = false; break; } // reset the next bit if we need to ptag ^= (this.tagChunk(tag) & 1) << 31; ptag = ptag >>> 0; // toss our crc into the filesystem seed for // pseudorandom numbers, note we use another crc here // as a collection function because it is sufficiently // random and convenient this.seed = this.crc(this.seed, crc, struct.calcsize("I")); // update with what's found so far besttag = tempbesttag; dir.off = off + this.tagDsize(tag); dir.etag = ptag; dir.count = tempcount; dir.tail[0] = temptail[0]; dir.tail[1] = temptail[1]; dir.split = tempsplit; // reset crc crc = 0xffffffff; continue; } // crc the entry first, hopefully leaving it in the cache for (let j = struct.calcsize("I"); j < this.tagDsize(tag); j++) { let dat; bufferByRef = new ByRef(); err = this.bdRead(null, this.rcache, this.cfg.blockSize, dir.pair[0], off+j, bufferByRef, 1); if (err) { if (err == LFS_ERR_CORRUPT) { dir.erased = false; break; } return err; } dat = bufferByRef.get(); crc = this.crc(crc, dat, 1); } // directory modification tags? if (this.tagType1(tag) == LFS_TYPE_NAME) { // increase count of files if necessary if (this.tagId(tag) >= tempcount) { tempcount = this.tagId(tag) + 1; } } else if (this.tagType1(tag) == LFS_TYPE_SPLICE) { tempcount += this.tagSplice(tag); if (tag == (this.mkTag(LFS_TYPE_DELETE, 0, 0) | (this.mkTag(0, 0x3ff, 0) & tempbesttag))) { tempbesttag |= 0x80000000; } else if (tempbesttag != -1 && this.tagId(tag) <= this.tagId(tempbesttag)) { tempbesttag += this.mkTag(0, this.tagSplice(tag), 0); } } else if (this.tagType1(tag) == LFS_TYPE_TAIL) { tempsplit = (this.tagChunk(tag) & 1); bufferByRef = new ByRef(); err = this.bdRead(null, this.rcache, this.cfg.blockSize, dir.pair[0], off + struct.calcsize("I"), bufferByRef, 8); if (err) { if (err == LFS_ERR_CORRUPT) { dir.erased = false; break; } } temptail = bufferByRef.get(); this.pairFromle32(temptail); } // found a match for our fetcher? if ((fmask & tag) == (fmask & ftag)) { let res = callback(data, tag, new Diskoff({block: dir.pair[0], off: off+struct.calcsize("I")})); if (res < 0) { if (res == LFS_ERR_CORRUPT) { dir.erased = false; break; } return res; } if (res == LFS_CMP_EQ) { // found a match tempbesttag = tag; } else if ((this.mkTag(0x7ff, 0x3ff, 0) & tag) == (this.mkTag(0x7ff, 0x3ff, 0) & tempbesttag)) { // found an identical tag, but contents didn't match // this must mean that our besttag has been overwritten tempbesttag = -1; } else if (res == LFS_CMP_GT && this.tagId(tag) <= this.tagId(tempbesttag)) { // found a greater match, keep track to keep things sorted tempbesttag = tag | 0x80000000; } } } // consider what we have good enough if (dir.off > 0) { // synthetic move if (this.gstateHasmovehere(this.gdisk, dir.pair)) { if (this.tagId(this.gdisk.tag) == this.tagId(besttag)) { besttag |= 0x80000000; } else if (besttag != -1 && this.tagId(this.gdisk.tag) < this.tagId(besttag)) { besttag -= this.mkTag(0, 1, 0); } } // found tag? or found best id? if (idByRef.get()) { idByRef.set(Math.min(this.tagId(besttag), dir.count)); } if (this.tagIsvalid(besttag)) { return besttag; } else if (this.tagId(besttag) < dir.count) { return LFS_ERR_NOENT; } else { return 0; } } // failed, try the other block? this.pairSwap(dir.pair); dir.rev = revs[(r+1)%2]; } console.warn("Corrupted dir pair at {" +dir.pair[0]+ ", " + dir.pair[1] + "}"); return LFS_ERR_CORRUPT; } dirFindMatch(name, tag, disk) { // compare with disk let diff = Math.min(name.size, this.tagSize(tag)); let res = this.bdCmp(null, this.rcache, diff, disk.block, disk.off, name.name, diff); if (res != LFS_CMP_EQ) { return res; } // only equal if our size is still the same if (name.size != this.tagSize(tag)) { return (name.size < this.tagSize(tag)) ? LFS_CMP_LT : LFS_CMP_GT; } // found a match! return LFS_CMP_EQ; } allocLookahead(block) { let off = ((block - this.free.off) + this.cfg.blockCount) % this.cfg.blockCount if (off < this.free.size) { let offset = parseInt(off / 32); this.free.buffer.set(this.free.buffer.slice(offset, offset + 1) | 1 << (off % 32), offset); } } tagType1(tag) { return (tag & 0x70000000) >>> 20; } tagType3(tag) { return (tag & 0x7ff00000) >>> 20; } tagId(tag) { return (tag & 0x000ffc00) >>> 10; } tagDsize(tag) { return struct.calcsize("I") + this.tagSize(tag + this.tagIsdelete(tag)); } tagSize(tag) { return tag & 0x000003ff; } tagIsdelete(tag) { return ((tag << 22) >> 22) == -1; } tagIsvalid(tag) { return !(tag & 0x80000000); } tagChunk(tag) { return ((tag & 0x0ff00000) >> 20) & 0xff; } tagSplice(tag) { return (this.tagChunk(tag) << 24) >> 24; } crc(crc, buffer, size) { let data; const rtable = [ 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, ]; if (buffer == null) { data = []; } else if (typeof buffer === 'string') { data = toByteArray(buffer); } else if (typeof buffer === 'object') { data = new Uint8Array(size); const dataSize = parseInt(size / Object.entries(buffer).length); Object.values(buffer).forEach((value, index) => { data.set(LittleFS.UintToBuffer(value, dataSize), index * dataSize); }); data = Array.from(data); } else { data = Array.from(LittleFS.UintToBuffer(buffer, size)); } for (let i = 0; i < size; i++) { crc = ((crc >>> 4) ^ rtable[(crc ^ (data[i] >>> 0)) & 0xf]) >>> 0; crc = ((crc >>> 4) ^ rtable[(crc ^ (data[i] >>> 4)) & 0xf]) >>> 0; } return crc; } allocDrop() { this.free.size = 0; this.free.i = 0; this.allocAck(); } pairSwap(pair) { let t = pair[0]; pair[0] = pair[1]; pair[1] = t; } gstateHasorphans(a) { return !!this.tagSize(a.tag); } gstateGetorphans(a) { return this.tagSize(a.tag); } gstateHasmove(a) { return this.tagType1(a.tag); } gstateHasmovehere(a, pair) { return this.tagType1(a.tag) && this.pairCmp(a.pair, pair) == 0; } pairCmp(pairA, pairB) { return !(pairA[0] == pairB[0] || pairA[1] == pairB[1] || pairA[0] == pairB[1] || pairA[1] == pairB[0]); } static bufferToUint(buffer, size) { const view = new DataView(buffer.buffer) let ret = []; let offset = 0; // If size > 4, we will return an array of Uints while(size > 0) { if (size >= 4) { ret.push(view.getUint32(offset, true)); size -= 4; offset += 4; } else if (size >= 2) { ret.push(view.getUint16(offset, true)); size -= 2; offset += 2; } else if (size >= 1) { ret.push(view.getUint8(offset, true)); size -= 1; offset += 1; } } if (ret.length == 1) { return ret[0]; } return ret; } static UintToBuffer(data, size) { let buffer = new Uint8Array(size); const view = new DataView(buffer.buffer); let offset = 0; while(size > 0) { if (size >= 4) { view.setUint32(offset, data, true); size -= 4; offset += 4; } else if (size >= 2) { view.setUint16(offset, data, true); size -= 2; offset += 2; } else if (size >= 1) { view.setUint8(offset, data, true); size -= 1; offset += 1; } } return buffer; } // Block Device Read bdRead(pcache, rcache, hint, block, off, bufferByRef, size, returnRaw=false) { let data = new Uint8Array(size); let dataPtr = 0; if (block >= this.cfg.blockCount || off+size > this.cfg.blockSize) { return LFS_ERR_CORRUPT; } let bufferSize = size; while (size > 0) { let diff = size; if (pcache && block == pcache.block && off < pcache.off + pcache.size) { if (off >= pcache.off) { // is already in pcache? diff = Math.min(diff, pcache.size - (off-pcache.off)); //memcpy(data, pcache.buffer[off-pcache.off], diff); data.set(pcache.buffer.slice(off-pcache.off, off-pcache.off + diff), dataPtr); dataPtr += diff; off += diff; size -= diff; continue; } // pcache takes priority diff = Math.min(diff, pcache.off-off); } if (block == rcache.block && off < rcache.off + rcache.size) { if (off >= rcache.off) { // is already in rcache? diff = Math.min(diff, rcache.size - (off-rcache.off)); //memcpy(data, rcache.buffer[off-rcache.off], diff); data.set(rcache.buffer.slice(off-rcache.off, off-rcache.off + diff), dataPtr); dataPtr += diff; off += diff; size -= diff; continue; } // rcache takes priority diff = Math.min(diff, rcache.off-off); } if (size >= hint && off % this.cfg.readSize == 0 && size >= this.cfg.readSize) { // bypass cache? diff = this.aligndown(diff, this.cfg.readSize); let err = this.cfg.read(block, off, data, diff); if (err) { return err; } dataPtr += diff; off += diff; size -= diff; continue; } // load to cache, first condition can no longer fail console.assert(block < this.cfg.blockCount); rcache.block = block; rcache.off = this.aligndown(off, this.cfg.readSize); rcache.size = Math.min( Math.min( this.alignup(off+hint, this.cfg.readSize), this.cfg.blockSize) - rcache.off, this.cfg.cacheSize); let err = this.cfg.read(rcache.block, rcache.off, rcache.buffer, rcache.size); console.assert(err <= 0); if (err) { return err; } } bufferByRef.set(data); if (!returnRaw) { bufferByRef.set(LittleFS.bufferToUint(bufferByRef.get(), bufferSize)); } return 0; } bdFlush(pcache, rcache, validate) { if (pcache.block != LFS_BLOCK_NULL && pcache.block != LFS_BLOCK_INLINE) { console.assert(pcache.block < this.cfg.blockCount); let diff = this.alignup(pcache.size, this.cfg.progSize); let err = this.cfg.prog(pcache.block, pcache.off, pcache.buffer, diff); console.assert(err <= 0); if (err) { return err; } if (validate) { // check data on disk this.cacheDrop(rcache); let res = this.bdCmp(null, rcache, diff, pcache.block, pcache.off, pcache.buffer, diff); if (res < 0) { return res; } if (res != LFS_CMP_EQ) { return LFS_ERR_CORRUPT; } } this.cacheZero(pcache); } return 0; } bdCmp(pcache, rcache, hint, block, off, buffer, size) { let data; if (typeof buffer === 'string') { data = toByteArray(buffer); } else { data = buffer; } let diff = 0; for (let i = 0; i < size; i += diff) { let bufferByRef = new ByRef(); diff = Math.min(size-i, struct.calcsize("BBBBBBBB")); let res = this.bdRead(pcache, rcache, hint-i, block, off+i, bufferByRef, diff, true); let dat = bufferByRef.get(); if (res) { return res; } res = this.memcmp(dat, data.slice(i), diff); if (res) { return res < 0 ? LFS_CMP_LT : LFS_CMP_GT; } } return LFS_CMP_EQ; } memcmp(array1, array2, size) { size = Math.min(array1.length, array2.length, size); for (let i = 0; i < size; i++) { if (array1[i] === array2[i]) { continue; } else if (array1[i] < array2[i]) { return -1; } else { return 1; } } return 0; } bdErase(block) { console.assert(block < this.cfg.blockCount); let err = this.cfg.erase(block); console.assert(err <= 0); return err; } bdProg(pcache, rcache, validate, block, off, buffer, size) { let data; let dataPtr = 0; if (buffer == null) { data = new Uint8Array(size); } else if (Array.isArray(buffer)) { data = new Uint8Array(size); const dataSize = parseInt(size / buffer.length); for(let [index, item] of buffer.entries()) { data.set(LittleFS.UintToBuffer(item, dataSize), index * dataSize); } } else if (typeof buffer === 'object') { data = new Uint8Array(size); const dataSize = parseInt(size / Object.entries(buffer).length); Object.values(buffer).forEach((value, index) => { data.set(LittleFS.UintToBuffer(value, dataSize), index * dataSize); }); } else if (typeof buffer === 'string') { data = new Uint8Array(size); data.set(toByteArray(buffer)); } else { data = LittleFS.UintToBuffer(buffer, size); } console.assert(block == LFS_BLOCK_INLINE || block < this.cfg.blockCount); console.assert(off + size <= this.cfg.blockSize); while (size > 0) { if (block == pcache.block && off >= pcache.off && off < pcache.off + this.cfg.cacheSize) { // already fits in pcache? let diff = Math.min(size, this.cfg.cacheSize - (off-pcache.off)); //memcpy(&pcache->buffer[off-pcache->off], data, diff); pcache.buffer.set(data.slice(dataPtr, dataPtr + diff), off-pcache.off); dataPtr += diff; off += diff; size -= diff; pcache.size = Math.max(pcache.size, off - pcache.off); if (pcache.size == this.cfg.cacheSize) { // eagerly flush out pcache if we fill up let err = this.bdFlush(pcache, rcache, validate); if (err) { return err; } } continue; } // pcache must have been flushed, either by programming an // entire block or manually flushing the pcache console.assert(pcache.block == LFS_BLOCK_NULL); // prepare pcache, first condition can no longer fail pcache.block = block; pcache.off = this.aligndown(off, this.cfg.progSize); pcache.size = 0; } return 0; } bdSync(pcache, rcache, validate) { this.cacheDrop(rcache); let err = this.bdFlush(pcache, rcache, validate); if (err) { return err; } err = this.cfg.sync(); console.assert(err <= 0); return err; } mount() { // scan directory blocks for superblock and any global updates let dir = new MDir() dir.tail = [0, 1]; let cycle = 0; let err; while (!this.pairIsnull(dir.tail)) { if (cycle >= this.cfg.blockCount/2) { // loop detected err = LFS_ERR_CORRUPT; } cycle += 1; // fetch next block in tail list let tag = this.dirFetchmatch(dir, dir.tail, this.mkTag(0x7ff, 0x3ff, 0), this.mkTag(LFS_TYPE_SUPERBLOCK, 0, 8), new ByRef(null), this.dirFindMatch.bind(this), new DirFindMatch({name: "littlefs", size: 8})); if (tag < 0) { err = tag; } // has superblock? if (tag && !this.tagIsdelete(tag)) { // update root this.root[0] = dir.pair[0]; this.root[1] = dir.pair[1]; // grab superblock let superBlockByRef = new ByRef(new Uint8Array(struct.calcsize("IIIIII"))); tag = this.dirGet(dir, this.mkTag(0x7ff, 0x3ff, 0), this.mkTag(LFS_TYPE_INLINESTRUCT, 0, superBlockByRef.get().byteLength), superBlockByRef); if (tag < 0) { err = tag; } let superblock = new SuperBlock(); superblock.setFromBuffer(superBlockByRef.get()); this.superblockFromle32(superblock); // check version let major_version = (0xffff & (superblock.version >> 16)); let minor_version = (0xffff & (superblock.version >> 0)); if ((major_version != LFS_DISK_VERSION_MAJOR || minor_version > LFS_DISK_VERSION_MINOR)) { console.warn("Invalid version v" + major_version + "." + minor_version); err = LFS_ERR_INVAL; } // check superblock configuration if (superblock.nameMax) { if (superblock.nameMax > this.nameMax) { console.error("Unsupported nameMax (" + superblock.nameMax + " > " + this.nameMax + ")"); err = LFS_ERR_INVAL; } this.nameMax = superblock.nameMax; } if (superblock.fileMax) { if (superblock.fileMax > this.fileMax) { console.error("Unsupported fileMax (" + superblock.fileMax + " > " + this.fileMax + ")"); err = LFS_ERR_INVAL; } this.fileMax = superblock.fileMax; } if (superblock.attrMax) { if (superblock.attrMax > this.attrMax) { console.error("Unsupported attrMax (" + superblock.attrMax + " > " + this.attrMax + ")"); err = LFS_ERR_INVAL; } this.attrMax = superblock.attrMax; } } // has gstate? err = this.dirGetgstate(dir, this.gstate); } // found superblock? if (this.pairIsnull(this.root)) { err = LFS_ERR_INVAL; } // update littlefs with gstate if (!this.gstateIszero(this.gstate)) { console.warn("Found pending gstate " + toHex(this.gstate.tag, 8) + " " + toHex(this.gstate.pair[0], 8) + " " + toHex(this.gstate.pair[1], 8)); } this.gstate.tag += !this.tagIsvalid(this.gstate.tag); this.gdisk = this.gstate; // setup free lookahead, to distribute allocations uniformly across // boots, we start the allocator at a random location this.free.off = this.seed % this.cfg.blockCount; this.allocDrop(); return 0; } unmount() { return this.deinit(); } deinit() { // Nothing to do here cause this is JavaScript return 0; } dirCommit(dir, attrs) { // check for any inline files that aren't RAM backed and // forcefully evict them, needed for filesystem consistency if (attrs === null) { attrs = []; } for (let f = this.mList; f; f = f.next) { if (dir != f.m && this.pairCmp(f.m.pair, dir.pair) == 0 && f.type == LFS_TYPE_REG && (f.flags & LFS_F_INLINE) && f.ctz.size > this.cfg.cacheSize) { let err = this.fileOutline(f); if (err) { return err; } err = this.fileFlush(f); if (err) { return err; } } } // calculate changes to the directory let olddir = dir; let hasdelete = false; for (let i = 0; i < attrs.length; i++) { if (this.tagType3(attrs[i].tag) == LFS_TYPE_CREATE) { dir.count += 1; } else if (this.tagType3(attrs[i].tag) == LFS_TYPE_DELETE) { console.assert(dir.count > 0); dir.count -= 1; hasdelete = true; } else if (this.tagType1(attrs[i].tag) == LFS_TYPE_TAIL) { dir.tail[0] = attrs[i].buffer[0]; dir.tail[1] = attrs[i].buffer[1]; dir.split = (this.tagChunk(attrs[i].tag) & 1); this.pairFromle32(dir.tail); } } // should we actually drop the directory block? if (hasdelete && dir.count == 0) { let pdir; let err = this.fsPred(lfs, dir.pair, pdir); if (err && err != LFS_ERR_NOENT) { this.copyObjProps(dir, olddir); return err; } if (err != LFS_ERR_NOENT && pdir.split) { err = this.dirDrop(pdir, dir); if (err) { this.copyObjProps(dir, olddir); return err; } } } let doCompact = false; attempt_commit: if (dir.erased || dir.count >= 0xff) { // try to commit let commit = new Commit({ block: dir.pair[0], off: dir.off, ptag: dir.etag, crc: 0xffffffff, begin: dir.off, end: (this.cfg.metadataMax ? this.cfg.metadataMax : this.cfg.blockSize) - 8, }); // traverse attrs that need to be written out this.pairTole32(dir.tail); let err = this.dirTraverse(dir, dir.off, dir.etag, attrs, attrs.length, 0, 0, 0, 0, 0, this.dirCommitCommit.bind(this), commit); this.pairFromle32(dir.tail); if (err) { if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { doCompact = true; break attempt_commit; } this.copyObjProps(dir, olddir); return err; } // commit any global diffs if we have any let delta = new GState({tag: 0}); this.gstateXor(delta, this.gstate); this.gstateXor(delta, this.gdisk); this.gstateXor(delta, this.gdelta); delta.tag &= ~this.mkTag(0, 0, 0x3ff); if (!this.gstateIszero(delta)) { let err = this.dirGetgstate(dir, delta); if (err) { this.copyObjProps(dir, olddir); return err; } this.gstateTole32(delta); err = this.dirCommitattr( commit, this.mkTag(LFS_TYPE_MOVESTATE, 0x3ff, struct.calcsize("I")), delta ); if (err) { if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { doCompact = true; break attempt_commit; } this.copyObjProps(dir, olddir); return err; } } // finalize commit with the crc err = this.dirCommitcrc(commit); if (err) { if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { doCompact = true; break attempt_commit; } this.copyObjProps(dir, olddir); return err; } // successful commit, update dir console.assert(commit.off % this.cfg.progSize == 0); dir.off = commit.off; dir.etag = commit.ptag; // and update gstate this.gdisk = this.gstate; this.gdelta = new GState({tag: 0}); } else { doCompact = true; break attempt_commit; } if (doCompact) { // fall back to compaction this.cacheDrop(this.pcache); let err = this.dirCompact(dir, attrs, attrs.length, dir, 0, dir.count); if (err) { this.copyObjProps(dir, olddir); } } // this complicated bit of logic is for fixing up any active // metadata-pairs that we may have affected // // note we have to make two passes since the mdir passed to // lfs_dir_commit could also be in this list, and even then // we need to copy the pair so they don't get clobbered if we refetch // our mdir. for (let d = this.mList; d; d = d.next) { if (d.m != dir && this.pairCmp(d.m.pair, olddir.pair) == 0) { d.m = dir; for (let i = 0; i < attrs.length; i++) { if (this.tagType3(attrs[i].tag) == LFS_TYPE_DELETE && d.id == this.tagId(attrs[i].tag)) { d.m.pair[0] = LFS_BLOCK_NULL; d.m.pair[1] = LFS_BLOCK_NULL; } else if (this.tagType3(attrs[i].tag) == LFS_TYPE_DELETE && d.id > this.tagId(attrs[i].tag)) { d.id -= 1; if (d.type == LFS_TYPE_DIR) { d.pos -= 1; } } else if (this.tagType3(attrs[i].tag) == LFS_TYPE_CREATE && d.id >= this.tagId(attrs[i].tag)) { d.id += 1; if (d.type == LFS_TYPE_DIR) { d.pos += 1; } } } } } for (let d = this.mList; d; d = d.next) { if (this.pairCmp(d.m.pair, olddir.pair) == 0) { while (d.id >= d.m.count && d.m.split) { // we split and id is on tail now d.id -= d.m.count; let err = this.dirFetch(d.m, d.m.tail); if (err) { return err; } } } } return 0; } dirCompact(dir, attrs, attrcount, source, begin, end) { // save some state in case block is bad const oldpair = [dir.pair[0], dir.pair[1]]; let relocated = false; let tired = false; let doRelocate = false; // should we split? while (end - begin > 1) { // find size let size = 0; let sizeObj = {size: size} let err = this.dirTraverse(source, 0, 0xffffffff, attrs, attrcount, this.mkTag(0x400, 0x3ff, 0), this.mkTag(LFS_TYPE_NAME, 0, 0), begin, end, -begin, this.dirCommitSize.bind(this), sizeObj); size = sizeObj.size; if (err) { return err; } // space is complicated, we need room for tail, crc, gstate, // cleanup delete, and we cap at half a block to give room // for metadata updates. if (end - begin < 0xff && size <= Math.min(this.cfg.blockSize - 36, this.alignup((this.cfg.metadataMax ? this.cfg.metadataMax : this.cfg.blockSize)/2, this.cfg.progSize))) { break; } // can't fit, need to split, we should really be finding the // largest size that fits with a small binary search, but right now // it's not worth the code size let split = (end - begin) / 2; err = this.dirSplit(dir, attrs, attrcount, source, begin+split, end); if (err) { // if we fail to split, we may be able to overcompact, unless // we're too big for even the full block, in which case our // only option is to error if (err == LFS_ERR_NOSPC && size <= this.cfg.blockSize - 36) { break; } return err; } end = begin + split; } // increment revision count dir.rev += 1; // If our revision count == n * block_cycles, we should force a relocation, // this is how littlefs wear-levels at the metadata-pair level. Note that we // actually use (block_cycles+1)|1, this is to avoid two corner cases: // 1. block_cycles = 1, which would prevent relocations from terminating // 2. block_cycles = 2n, which, due to aliasing, would only ever relocate // one metadata block in the pair, effectively making this useless if (this.cfg.blockCycles > 0 && (dir.rev % ((this.cfg.blockCycles+1)|1) == 0)) { if (this.pairCmp(dir.pair, [0, 1]) == 0) { // oh no! we're writing too much to the superblock, // should we expand? let res = this.fsRawsize(); if (res < 0) { return res; } // do we have extra space? littlefs can't reclaim this space // by itself, so expand cautiously if (res < this.cfg.blockCount/2) { console.warn("Expanding superblock at rev " + dir.rev); let err = this.dirSplit(dir, attrs, attrcount, source, begin, end); if (err && err != LFS_ERR_NOSPC) { return err; } // welp, we tried, if we ran out of space there's not much // we can do, we'll error later if we've become frozen if (!err) { end = begin; } } } else { // we're writing too much, time to relocate tired = true; doRelocate = true; } } // begin loop to commit compaction to blocks until a compact sticks main_while_loop: while (true) { compaction_block: if (!doRelocate) { // setup commit state let commit = new Commit ({ block: dir.pair[1], off: 0, ptag: 0xffffffff, crc: 0xffffffff, begin: 0, end: (this.cfg.metadataMax ? this.cfg.metadataMax : this.cfg.blockSize) - 8, }); // erase block to write to let err = this.bdErase(dir.pair[1]); if (err) { if (err == LFS_ERR_CORRUPT) { break compaction_block; } return err; } // write out header dir.rev = this.tole32(dir.rev); err = this.dirCommitprog(commit, dir.rev, struct.calcsize("I")); dir.rev = this.fromle32(dir.rev); if (err) { if (err == LFS_ERR_CORRUPT) { break compaction_block; } return err; } // traverse the directory, this time writing out all unique tags err = this.dirTraverse(source, 0, 0xffffffff, attrs, attrcount, this.mkTag(0x400, 0x3ff, 0), this.mkTag(LFS_TYPE_NAME, 0, 0), begin, end, -begin, this.dirCommitCommit.bind(this), commit); if (err) { if (err == LFS_ERR_CORRUPT) { break compaction_block; } return err; } // commit tail, which may be new after last size check if (!this.pairIsnull(dir.tail)) { this.pairTole32(dir.tail); err = this.dirCommitattr(commit, this.mkTag(LFS_TYPE_TAIL + dir.split, 0x3ff, 8), dir.tail); this.pairFromle32(dir.tail); if (err) { if (err == LFS_ERR_CORRUPT) { break compaction_block; } return err; } } // bring over gstate? let delta = new GState({tag: 0}); if (!relocated) { this.gstateXor(delta, this.gdisk); this.gstateXor(delta, this.gstate); } this.gstateXor(delta, this.gdelta); delta.tag &= ~this.mkTag(0, 0, 0x3ff); err = this.dirGetgstate(dir, delta); if (err) { return err; } if (!this.gstateIszero(delta)) { this.gstateTole32(delta); err = this.dirCommitattr(commit, this.mkTag(LFS_TYPE_MOVESTATE, 0x3ff, struct.calcsize("III")), delta); if (err) { if (err == LFS_ERR_CORRUPT) { break compaction_block; } return err; } } // complete commit with crc err = this.dirCommitcrc(commit); if (err) { if (err == LFS_ERR_CORRUPT) { break compaction_block; } return err; } // successful compaction, swap dir pair to indicate most recent console.assert(commit.off % this.cfg.progSize == 0); this.pairSwap(dir.pair); dir.count = end - begin; dir.off = commit.off; dir.etag = commit.ptag; // update gstate this.gdelta = new GState({tag: 0}); if (!relocated) { this.gdisk = this.gstate; } break main_while_loop; } // end compaction_block // relocate: // commit was corrupted, drop caches and prepare to relocate block relocated = true; this.cacheDrop(this.pcache); if (!tired) { // Block is detected as unusable in normal Flash console.warn("Bad block at " + toHex(dir.pair[1], 8)); } // can't relocate superblock, filesystem is now frozen if (this.pairCmp(dir.pair, [0, 1]) == 0) { console.warn("Superblock " + toHex(dir.pair[1], 8) + " has become unwritable"); return LFS_ERR_NOSPC; } // relocate half of pair let blockObj = new ByRef(); let err = this.alloc(blockObj); dir.pair[1] = blockObj.get(); if (err && (err != LFS_ERR_NOSPC || !tired)) { return err; } tired = false; continue; } // end main_while_loop if (relocated) { // update references if we relocated console.warn("Relocating {" + toHex(oldpair[0], 8) + ", " + toHex(oldpair[1], 8) + "} " + "-> {" + toHex(dir.pair[0], 8) + ", " + toHex(dir.pair[1], 8) + "}"); let err = this.fsRelocate(oldpair, dir.pair); if (err) { return err; } } return 0; } dirTraverse(dir, off, ptag, attrs, attrcount, tmask, ttag, begin, end, diff, callback, data) { // iterate over directory and attrs let attrPtr = 0; while (true) { let tag; let buffer; let disk = new Diskoff(); if (off+this.tagDsize(ptag) < dir.off) { off += this.tagDsize(ptag); let bufferByRef = new ByRef(); let err = this.bdRead(null, this.rcache, struct.calcsize("I"), dir.pair[0], off, bufferByRef, struct.calcsize("I")); tag = bufferByRef.get(); if (err) { return err; } tag = ((this.fromBe32(tag) ^ ptag) >>> 0) | 0x80000000; disk.block = dir.pair[0]; disk.off = off + struct.calcsize("I"); buffer = disk; ptag = tag; } else if (attrcount > 0) { tag = attrs[attrPtr].tag; buffer = attrs[attrPtr].buffer; attrPtr += 1; attrcount -= 1; } else { return 0; } let mask = this.mkTag(0x7ff, 0, 0); if ((mask & tmask & tag) != (mask & tmask & ttag)) { continue; } // do we need to filter? inlining the filtering logic here allows // for some minor optimizations if (this.tagId(tmask) != 0) { // scan for duplicates and update tag based on creates/deletes let filter = this.dirTraverse(dir, off, ptag, attrs, attrcount, 0, 0, 0, 0, 0, this.dirTraverseFilter.bind(this), tag); if (filter < 0) { return filter; } if (filter) { continue; } // in filter range? if (!(this.tagId(tag) >= begin && this.tagId(tag) < end)) { continue; } } // handle special cases for mcu-side operations if (this.tagType3(tag) == LFS_FROM_NOOP) { // do nothing } else if (this.tagType3(tag) == LFS_FROM_MOVE) { let fromid = this.tagSize(tag); let toid = this.tagId(tag); let err = this.dirTraverse(buffer, 0, 0xffffffff, null, 0, this.mkTag(0x600, 0x3ff, 0), this.mkTag(LFS_TYPE_STRUCT, 0, 0), fromid, fromid+1, toid-fromid+diff, callback, data); if (err) { return err; } } else if (this.tagType3(tag) == LFS_FROM_USERATTRS) { let a = buffer; for (let i = 0; i < this.tagSize(tag); i++) { let err = callback(data, this.mkTag(LFS_TYPE_USERATTR + a[i].type, this.tagId(tag) + diff, a[i].size), a[i].buffer); if (err) { return err; } } } else { let err = callback(data, tag + this.mkTag(0, diff, 0), buffer); if (err) { return err; } } } } dirTraverseFilter(p, tag, buffer) { let filtertag = p; // which mask depends on unique bit in tag structure let mask = (tag & this.mkTag(0x100, 0, 0)) ? this.mkTag(0x7ff, 0x3ff, 0) : this.mkTag(0x700, 0x3ff, 0); // check for redundancy if ((mask & tag) == (mask & filtertag) || this.tagIsdelete(filtertag) || (this.mkTag(0x7ff, 0x3ff, 0) & tag) == ( this.mkTag(LFS_TYPE_DELETE, 0, 0) | (this.mkTag(0, 0x3ff, 0) & filtertag))) { return true; } // check if we need to adjust for created/deleted tags if (this.tagType1(tag) == LFS_TYPE_SPLICE && this.tagId(tag) <= this.tagId(filtertag)) { filtertag += this.mkTag(0, this.tagSplice(tag), 0); // Likely ref error } return false; } dirSplit() { console.warn("dirSplit not implemented"); } dirFind(dir, pathByRef, idByRef) { // we reduce path to a single name if we can find it if (idByRef.get()) { idByRef.set(0x3ff); } let name = 0; let path = pathByRef.get(); // default to root dir let tag = this.mkTag(LFS_TYPE_DIR, 0x3ff, 0); dir.tail[0] = this.root[0]; dir.tail[1] = this.root[1]; while (true) { // skip slashes name += path.indexOf("/") + 1; let namelen = path.slice(name).indexOf("/"); if (namelen == -1) { namelen = path.slice(name).length; } // skip '.' and root '..' if ((namelen == 1 && path.substr(name, 1) == ".") || (namelen == 2 && path.substr(name, 2) == "..")) { name += namelen; continue; } // skip if matched by '..' in name let suffix = path.substr(name + namelen) let sufflen; let depth = 1; while (true) { suffix += path.indexOf("/") + 1; sufflen = path.slice(suffix).indexOf("/"); if (sufflen <= 0) { break; } if (sufflen == 2 && path.substr(suffix, 2) == "..") { depth -= 1; if (depth == 0) { name = suffix + sufflen; continue; } } else { depth += 1; } suffix += sufflen; } // found path if (name == path.length) { return tag; } // update what we've found so far path = path.slice(name); pathByRef.set(path); name = 0; // only continue if we hit a directory if (this.tagType3(tag) != LFS_TYPE_DIR) { return LFS_ERR_NOTDIR; } // grab the entry data if (this.tagId(tag) != 0x3ff) { let res = this.dirGet(dir, this.mkTag(0x700, 0x3ff, 0), this.mkTag(LFS_TYPE_STRUCT, this.tagId(tag), 8), dir.tail); if (res < 0) { return res; } this.pairFromle32(dir.tail); } // find entry matching name while (true) { tag = this.dirFetchmatch(dir, dir.tail, this.mkTag(0x780, 0, 0), this.mkTag(LFS_TYPE_NAME, 0, namelen), // are we last name? path.slice(name).indexOf('/') < 0 ? idByRef : new ByRef(null), this.dirFindMatch.bind(this), new DirFindMatch({name: path.slice(name), size: namelen}) ); if (tag < 0) { return tag; } if (tag) { break; } if (!dir.split) { return LFS_ERR_NOENT; } } // to next name name += namelen; } } dirCommitCommit(p, tag, buffer) { return this.dirCommitattr(p, tag, buffer); } dirCommitcrc(commit) { // align to program units const end = this.alignup(commit.off + 2 * struct.calcsize("I"), this.cfg.progSize); let off1 = 0; let crc1 = 0; // create crc tags to fill up remainder of commit, note that // padding is not crc'd, which lets fetches skip padding but // makes committing a bit more complicated while (commit.off < end) { let off = commit.off + struct.calcsize("I"); let noff = Math.min(end - off, 0x3fe) + off; if (noff < end) { noff = Math.min(noff, end - struct.calcsize("II")); } // read erased state from next program unit let tag = 0xffffffff; let bufferByRef = new ByRef(); let err = this.bdRead(null, this.rcache, struct.calcsize("I"), commit.block, noff, bufferByRef, struct.calcsize("I")); if (err && err != LFS_ERR_CORRUPT) { return err; } tag = bufferByRef.get(); // build crc tag let reset = !!(~this.fromBe32(tag) >>> 31) ? 1 : 0; tag = this.mkTag(LFS_TYPE_CRC + reset, 0x3ff, noff - off); // write out crc let footer = new Array(2); footer[0] = this.toBe32((tag ^ commit.ptag) >>> 0); commit.crc = this.crc(commit.crc, footer[0], struct.calcsize("I")); footer[1] = this.tole32(commit.crc); err = this.bdProg(this.pcache, this.rcache, false, commit.block, commit.off, footer, struct.calcsize("II")); if (err) { return err; } // keep track of non-padding checksum to verify if (off1 == 0) { off1 = commit.off + struct.calcsize("I"); crc1 = commit.crc; } commit.off += struct.calcsize("I") + this.tagSize(tag); commit.ptag = (tag ^ (reset << 31)) >>> 0; commit.crc = 0xffffffff; // reset crc for next "commit" } // write buffers to flash and flush let err = this.bdSync(this.pcache, this.rcache, false); if (err) { return err; } // successful commit, check checksums to make sure let off = commit.begin; let noff = off1; while (off < end) { let crc = 0xffffffff; for (let i = off; i < noff + struct.calcsize("I"); i++) { // check against written crc, may catch blocks that // become readonly and match our commit size exactly if (i == off1 && crc != crc1) { return LFS_ERR_CORRUPT; } // leave it up to caching to make this efficient let bufferByRef = new ByRef(); err = this.bdRead(null, this.rcache, noff + struct.calcsize("I") - i, commit.block, i, bufferByRef, 1); if (err) { return err; } let dat = bufferByRef.get(); crc = this.crc(crc, dat, 1); } // detected write error? if (crc != 0) { return LFS_ERR_CORRUPT; } // skip padding off = Math.min(end - noff, 0x3fe) + noff; if (off < end) { off = Math.min(off, end - 2 * struct.calcsize("I")); } noff = off + struct.calcsize("I"); } return 0; } fsRawsize() { console.warn("fsRawsize not implemented"); } dirCommitSize() { console.warn("dirCommitSize not implemented"); } dirCommitattr(commit, tag, buffer) { // check if we fit let dsize = this.tagDsize(tag); if (commit.off + dsize > commit.end) { return LFS_ERR_NOSPC; } // write out tag let ntag = this.toBe32(((tag & 0x7fffffff) ^ commit.ptag) >>> 0); let err = this.dirCommitprog(commit, ntag, struct.calcsize("I")); // ntag by ref if (err) { return err; } if (!(tag & 0x80000000)) { // from memory err = this.dirCommitprog(commit, buffer, dsize - struct.calcsize("I")); if (err) { return err; } } else { // from disk let disk = buffer; for (let i = 0; i < dsize-struct.calcsize("I"); i++) { // rely on caching to make this efficient let bufferByRef = new ByRef(); err = this.bdRead(null, this.rcache, dsize-struct.calcsize("I")-i, disk.block, disk.off+i, bufferByRef, 1); let dat = bufferByRef.get(); if (err) { return err; } err = this.dirCommitprog(commit, dat, 1); if (err) { return err; } } } commit.ptag = tag & 0x7fffffff; return 0; } dirDrop() { console.warn("dirDrop not implemented"); } dirGetread() { console.warn("dirGetread not implemented"); } dirGet(dir, gmask, gtag, buffer) { return this.dirGetslice(dir, gmask, gtag, 0, buffer, this.tagSize(gtag)); } dirGetslice(dir, gmask, gtag, goff, gbuffer, gsize) { let off = dir.off; let ntag = dir.etag; let gdiff = 0; if (this.gstateHasmovehere(this.gdisk, dir.pair) && this.tagId(gmask) != 0 && this.tagId(this.gdisk.tag) <= this.tagId(gtag)) { // synthetic moves gdiff -= this.mkTag(0, 1, 0); } // iterate over dir block backwards (for faster lookups) while (off >= struct.calcsize("I") + this.tagDsize(ntag)) { off -= this.tagDsize(ntag); let tag = ntag; let bufferByRef = new ByRef(); let err = this.bdRead(null, this.rcache, struct.calcsize("I"), dir.pair[0], off, bufferByRef, struct.calcsize("I")); if (err) { return err; } ntag = bufferByRef.get(); ntag = (this.fromBe32(ntag) ^ tag) & 0x7fffffff; if (this.tagId(gmask) != 0 && this.tagType1(tag) == LFS_TYPE_SPLICE && this.tagId(tag) <= this.tagId(gtag - gdiff)) { if (tag == (this.mkTag(LFS_TYPE_CREATE, 0, 0) | (this.mkTag(0, 0x3ff, 0) & (gtag - gdiff)))) { // found where we were created return LFS_ERR_NOENT; } // move around splices gdiff += this.mkTag(0, this.tagSplice(tag), 0); } if ((gmask & tag) == (gmask & (gtag - gdiff))) { if (this.tagIsdelete(tag)) { return LFS_ERR_NOENT; } let diff = Math.min(this.tagSize(tag), gsize); let bufferByRef = new ByRef(); err = this.bdRead(null, this.rcache, diff, dir.pair[0], off + struct.calcsize("I") + goff, bufferByRef, diff, true); if (err) { return err; } gbuffer.data.set(bufferByRef.get()); //memset((uint8_t*)gbuffer + diff, 0, gsize - diff); gbuffer.data.set(new Uint8Array(gsize-diff).fill(0), diff) return tag + gdiff; } } return LFS_ERR_NOENT; } dirGetgstate(dir, gstate) { let temp = new GState(); let res = this.dirGet(dir, this.mkTag(0x7ff, 0, 0), this.mkTag(LFS_TYPE_MOVESTATE, 0, struct.calcsize("III")), temp); if (res < 0 && res != LFS_ERR_NOENT) { return res; } if (res != LFS_ERR_NOENT) { // xor together to find resulting gstate this.gstateFromle32(temp); this.gstateXor(gstate, temp); } return 0; } cacheDrop(cache) { cache.block = LFS_BLOCK_NULL; } cacheZero(cache) { cache.buffer.fill(0xFF); cache.block = LFS_BLOCK_NULL; } fsPred() { console.warn("fsPred not implemented"); } fsRelocate() { console.warn("fsRelocate not implemented"); } gstateXor(gstateA, gstateB) { gstateA.tag ^= gstateB.tag gstateA.pair[0] ^= gstateB.pair[0] gstateA.pair[1] ^= gstateB.pair[1] } gstateIszero(gstate) { return gstate.tag === 0 && gstate.pair[0] === 0 && gstate.pair[1] === 0; } gstateTole32(gstate) { gstate.tag = this.tole32(gstate.tag); gstate.pair[0] = this.tole32(gstate.pair[0]); gstate.pair[1] = this.tole32(gstate.pair[1]); } gstateFromle32(gstate) { gstate.tag = this.fromle32(gstate.tag); gstate.pair[0] = this.fromle32(gstate.pair[0]); gstate.pair[1] = this.fromle32(gstate.pair[1]); } scmp(a, b) { return parseInt(a - b); } tole32(value) { return this.fromle32(value); } fromle32(value) { const data = Array.from(LittleFS.UintToBuffer(value, 4)); value = (data[0] << 0) | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); return value >>> 0; } toBe32(value) { return this.fromBe32(value); } fromBe32(value) { const data = Array.from(LittleFS.UintToBuffer(value, 4)); value = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3] << 0); return value >>> 0; } ctzFromle32(ctz) { ctz.head = this.fromle32(ctz.head); ctz.size = this.fromle32(ctz.size); } ctzTole32(ctz) { ctz.head = this.tole32(ctz.head); ctz.size = this.tole32(ctz.size); } pairFromle32(pair) { pair[0] = this.fromle32(pair[0]); pair[1] = this.fromle32(pair[1]); } pairTole32(pair) { pair[0] = this.tole32(pair[0]); pair[1] = this.tole32(pair[1]); } superblockTole32(superblock) { superblock.version = this.tole32(superblock.version); superblock.blockSize = this.tole32(superblock.blockSize); superblock.blockCount = this.tole32(superblock.blockCount); superblock.nameMax = this.tole32(superblock.nameMax); superblock.fileMax = this.tole32(superblock.fileMax); superblock.attrMax = this.tole32(superblock.attrMax); } superblockFromle32(superblock) { superblock.version = this.fromle32(superblock.version); superblock.blockSize = this.fromle32(superblock.blockSize); superblock.blockCount = this.fromle32(superblock.blockCount); superblock.nameMax = this.fromle32(superblock.nameMax); superblock.fileMax = this.fromle32(superblock.fileMax); superblock.attrMax = this.fromle32(superblock.attrMax); } fileOpen(file, path, flags) { let defaults = new FileConfig({attrCount: 0}); let err = this.fileOpencfg(file, path, flags, defaults); return err; } fileOpencfg(file, path, flags, cfg) { let err; main_block: if (true) { // deorphan if we haven't yet, needed at most once after poweron if ((flags & LFS_O_WRONLY) == LFS_O_WRONLY) { let err = this.fsForceconsistency(); if (err) { return err; } } // setup simple file details file.cfg = cfg; file.flags = flags; file.pos = 0; file.off = 0; file.cache.buffer = null; // allocate entry for file if it doesn't exist let pathByRef = new ByRef(path); let idByRef = new ByRef(file.id); let tag = this.dirFind(file.m, pathByRef, idByRef); file.id = idByRef.get(); path = pathByRef.get(); if (tag < 0 && !(tag == LFS_ERR_NOENT && file.id != 0x3ff)) { err = tag; break main_block; } // get id, add to list of mdirs to catch update changes file.type = LFS_TYPE_REG; this.mlistAppend(file); if (tag == LFS_ERR_NOENT) { if (!(flags & LFS_O_CREAT)) { err = LFS_ERR_NOENT; break main_block; } // check that name fits let nlen = path.length; if (nlen > this.nameMax) { err = LFS_ERR_NAMETOOLONG; break main_block; } // get next slot and create entry to remember name err = this.dirCommit(file.m, this.mkAttrs( [this.mkTag(LFS_TYPE_CREATE, file.id, 0), null], [this.mkTag(LFS_TYPE_REG, file.id, nlen), path], [this.mkTag(LFS_TYPE_INLINESTRUCT, file.id, 0), null] )); if (err) { err = LFS_ERR_NAMETOOLONG; break main_block; } tag = this.mkTag(LFS_TYPE_INLINESTRUCT, 0, 0); } else if (flags & LFS_O_EXCL) { err = LFS_ERR_EXIST; break main_block; } else if (this.tagType3(tag) != LFS_TYPE_REG) { err = LFS_ERR_ISDIR; break main_block; } else if (flags & LFS_O_TRUNC) { // truncate if requested tag = this.mkTag(LFS_TYPE_INLINESTRUCT, file.id, 0); file.flags |= LFS_F_DIRTY; } else { // try to load what's on disk, if it's inlined we'll fix it later tag = this.dirGet(file.m, this.mkTag(0x700, 0x3ff, 0), this.mkTag(LFS_TYPE_STRUCT, file.id, 8), file.ctz); if (tag < 0) { err = tag; break main_block; } this.ctzFromle32(file.ctz); } // fetch attrs for (let i = 0; i < file.cfg.attr_count; i++) { // if opened for read / read-write operations if ((file.flags & LFS_O_RDONLY) == LFS_O_RDONLY) { let res = this.dirGet(file.m, this.mkTag(0x7ff, 0x3ff, 0), this.mkTag(LFS_TYPE_USERATTR + file.cfg.attrs[i].type, file.id, file.cfg.attrs[i].size), file.cfg.attrs[i].buffer); if (res < 0 && res != LFS_ERR_NOENT) { err = res; break main_block; } } // if opened for write / read-write operations if ((file.flags & LFS_O_WRONLY) == LFS_O_WRONLY) { if (file.cfg.attrs[i].size > this.attrMax) { err = LFS_ERR_NOSPC; break main_block; } file.flags |= LFS_F_DIRTY; } } // allocate buffer if needed if (file.cfg.buffer) { file.cache.buffer = file.cfg.buffer; } else { file.cache.buffer = new Uint8Array(this.cfg.cacheSize); if (!file.cache.buffer) { err = LFS_ERR_NOMEM; break main_block; } } // zero to avoid information leak this.cacheZero(file.cache); if (this.tagType3(tag) == LFS_TYPE_INLINESTRUCT) { // load inline files file.ctz.head = LFS_BLOCK_INLINE; file.ctz.size = this.tagSize(tag); file.flags |= LFS_F_INLINE; file.cache.block = file.ctz.head; file.cache.off = 0; file.cache.size = this.cfg.cacheSize; // don't always read (may be new/trunc file) if (file.ctz.size > 0) { let res = this.dirGet(file.m, this.mkTag(0x700, 0x3ff, 0), this.mkTag(LFS_TYPE_STRUCT, file.id, Math.min(file.cache.size, 0x3fe)), file.cache.buffer); if (res < 0) { err = res; break main_block; } } } return 0; } //cleanup: // clean up lingering resources file.flags |= LFS_F_ERRED; this.fileClose(file); return err; } fileWrite(file, buffer, size) { console.assert((file.flags & LFS_O_WRONLY) == LFS_O_WRONLY); let data = 0; let nsize = size; if (file.flags & LFS_F_READING) { // drop any reads let err = this.fileFlush(file); if (err) { return err; } } if ((file.flags & LFS_O_APPEND) && file.pos < file.ctz.size) { file.pos = file.ctz.size; } if (file.pos + size > this.fileMax) { // Larger than file limit? return LFS_ERR_FBIG; } if (!(file.flags & LFS_F_WRITING) && file.pos > file.ctz.size) { // fill with zeros let pos = file.pos; file.pos = file.ctz.size; while (file.pos < pos) { let res = this.fileWrite(file, 0, 1); if (res < 0) { return res; } } } if ((file.flags & LFS_F_INLINE) && Math.max(file.pos+nsize, file.ctz.size) > Math.min(0x3fe, Math.min( this.cfg.cacheSize, (this.cfg.metadataMax ? this.cfg.metadataMax : this.cfg.blockSize) / 8))) { // inline file doesn't fit anymore let err = this.fileOutline(file); if (err) { file.flags |= LFS_F_ERRED; return err; } } while (nsize > 0) { // check if we need a new block if (!(file.flags & LFS_F_WRITING) || file.off == this.cfg.blockSize) { if (!(file.flags & LFS_F_INLINE)) { if (!(file.flags & LFS_F_WRITING) && file.pos > 0) { // find out which block we're extending from let err = this.ctzFind(null, file.cache, file.ctz.head, file.ctz.size, file.pos-1, file.block, file.off); if (err) { file.flags |= LFS_F_ERRED; return err; } // mark cache as dirty since we may have read data into it this.cacheZero(file.cache); } // extend file with new blocks this.allocAck(); let err = this.ctzExtend(file.cache, this.rcache, file.block, file.pos, file.block, file.off); if (err) { file.flags |= LFS_F_ERRED; return err; } } else { file.block = LFS_BLOCK_INLINE; file.off = file.pos; } file.flags |= LFS_F_WRITING; } // program as much as we can in current block let diff = Math.min(nsize, this.cfg.blockSize - file.off); program_loop: while (true) { program_block: if (true) { let err = this.bdProg(file.cache, this.rcache, true, file.block, file.off, buffer.slice(data), diff); if (err) { if (err == LFS_ERR_CORRUPT) { break program_block; } file.flags |= LFS_F_ERRED; return err; } break program_loop; } //relocate: let err = this.fileRelocate(file); if (err) { file.flags |= LFS_F_ERRED; return err; } } file.pos += diff; file.off += diff; data += diff; nsize -= diff; this.allocAck(); } file.flags &= ~LFS_F_ERRED; return size; } fileClose(file) { let err = this.fileSync(file); // remove from list of mdirs this.mlistRemove(file); // clean up memory if (!file.cfg.buffer) { file.cache.buffer = null; } return err; } fileSync(file) { if (file.flags & LFS_F_ERRED) { // it's not safe to do anything if our file errored return 0; } let err = this.fileFlush(file); if (err) { file.flags |= LFS_F_ERRED; return err; } if ((file.flags & LFS_F_DIRTY) && !this.pairIsnull(file.m.pair)) { // update dir entry let type; let buffer; let size; let ctz = new Ctz(); if (file.flags & LFS_F_INLINE) { // inline the whole file type = LFS_TYPE_INLINESTRUCT; buffer = file.cache.buffer; size = file.ctz.size; } else { // update the ctz reference type = LFS_TYPE_CTZSTRUCT; // copy ctz so alloc will work during a relocate ctz = file.ctz; this.ctzTole32(ctz); buffer = ctz; size = struct.calcsize("II"); } // commit file data and attributes err = this.dirCommit(file.m, this.mkAttrs( [this.mkTag(type, file.id, size), buffer], [this.mkTag(LFS_FROM_USERATTRS, file.id, file.cfg.attrCount), file.cfg.attrs])); if (err) { file.flags |= LFS_F_ERRED; return err; } file.flags &= ~LFS_F_DIRTY; } return 0; } setattr(path, type, buffer, size) { if (size > this.attrMax) { return LFS_ERR_NOSPC; } return this.commitattr(path, type, buffer, size); } fileRelocate(file) { while (true) { main_block: if (true) { // just relocate what exists into new block let blockObj = new ByRef(); let err = this.alloc(blockObj); let nblock = blockObj.get(); if (err) { return err; } err = this.bdErase(nblock); if (err) { if (err == LFS_ERR_CORRUPT) { break main_block; } return err; } // either read from dirty cache or disk for (let i = 0; i < file.off; i++) { let data; if (file.flags & LFS_F_INLINE) { err = this.dirGetread(file.m, // note we evict inline files before they can be dirty null, file.cache, file.off-i, this.mkTag(0xfff, 0x1ff, 0), this.mkTag(LFS_TYPE_INLINESTRUCT, file.id, 0), i, data, 1); if (err) { return err; } } else { var bufferByRef = new ByRef(); err = this.bdRead(file.cache, this.rcache, file.off-i, file.block, i, bufferByRef, 1); data = bufferByRef.get(); if (err) { return err; } } err = this.bdProg(this.pcache, this.rcache, true, nblock, i, data, 1); if (err) { if (err == LFS_ERR_CORRUPT) { break main_block; } return err; } } // copy over new state of file //memcpy(file.cache.buffer, this.pcache.buffer, this.cfg.cacheSize); file.cache.buffer.set(this.pcache.buffer.slice(0, this.cfg.cacheSize)); file.cache.block = this.pcache.block; file.cache.off = this.pcache.off; file.cache.size = this.pcache.size; this.cacheZero(this.pcache); file.block = nblock; file.flags |= LFS_F_WRITING; return 0; } //relocate: console.warn("Bad block at " + toHex(nblock, 8)); // just clear cache and try a new block this.cacheDrop(this.pcache); } } fileOutline(file) { file.off = file.pos; this.allocAck(); let err = this.fileRelocate(file); if (err) { return err; } file.flags &= ~LFS_F_INLINE; return 0; } fileFlush(file) { if (file.flags & LFS_F_READING) { if (!(file.flags & LFS_F_INLINE)) { this.cacheDrop(file.cache); } file.flags &= ~LFS_F_READING; } if (file.flags & LFS_F_WRITING) { let pos = file.pos; if (!(file.flags & LFS_F_INLINE)) { // copy over anything after current branch let orig = new LfsFile({ ctz: file.ctz, flags: LFS_O_RDONLY, pos: file.pos, cache: this.rcache, }); this.cacheDrop(this.rcache); while (file.pos < file.ctz.size) { // copy over a byte at a time, leave it up to caching // to make this efficient let data; let res = this.fileRead(orig, data, 1); if (res < 0) { return res; } res = this.fileWrite(file, data, 1); if (res < 0) { return res; } // keep our reference to the rcache in sync if (this.rcache.block != LFS_BLOCK_NULL) { this.cacheDrop(orig.cache); this.cacheDrop(this.rcache); } } // write out what we have while (true) { let err = this.bdFlush(file.cache, this.rcache, true); if (!err) { break; } if (err != LFS_ERR_CORRUPT) { return err; } console.warn("Bad block at " + toHex(file.block, 8)); err = this.fileRelocate(file); if (err) { return err; } } } else { file.pos = Math.max(file.pos, file.ctz.size); } // actual file updates file.ctz.head = file.block; file.ctz.size = file.pos; file.flags &= ~LFS_F_WRITING; file.flags |= LFS_F_DIRTY; file.pos = pos; } return 0; } mlistRemove(mlist) { for (let p = this.mlist; p; p = p.next) { if (p == mlist) { p = p.next; break; } } } mlistAppend(mList) { mList.next = this.mList; this.mList = mList; } commitattr(path, type, buffer, size) { let cwd = new MDir(); let pathByRef = new ByRef(path); let tag = this.dirFind(cwd, pathByRef, new ByRef(null)); path = pathByRef.get(); if (tag < 0) { return tag; } let id = this.tagId(tag); if (id == 0x3ff) { // special case for root id = 0; let err = this.dirFetch(cwd, this.root); if (err) { return err; } } return this.dirCommit(cwd, this.mkAttrs([this.mkTag(LFS_TYPE_USERATTR + type.charCodeAt(0), id, size), buffer])); } dirCommitprog(commit, buffer, size) { let err = this.bdProg(this.pcache, this.rcache, false, commit.block, commit.off, buffer, size); if (err) { return err; } commit.crc = this.crc(commit.crc, buffer, size); commit.off += size; return 0; } fsForceconsistency() { let err = this.fsDemove(); if (err) { return err; } err = this.fsDeorphan(); if (err) { return err; } return 0; } fsDemove() { if (!this.gstateHasmove(this.gdisk)) { return 0; } // Fix bad moves console.warn("Fixing move {" + toHex(this.gdisk.pair[0], 8) + ", " + toHex(this.gdisk.pair[1], 8) + "} " + toHex(this.tagId(this.gdisk.tag), 4)); // fetch and delete the moved entry let movedir = new MDir(); let err = this.dirFetch(movedir, this.gdisk.pair); if (err) { return err; } // prep gstate and delete move id let moveid = this.tagId(this.gdisk.tag); this.fsPrepmove(0x3ff, null); err = this.dirCommit(movedir, this.mkAttrs( [this.mkTag(LFS_TYPE_DELETE, moveid, 0), null])); if (err) { return err; } return 0; } fsDeorphan() { if (!this.gstateHasorphans(this.gstate)) { return 0; } // Fix any orphans let pdir = new MDir({split: true, tail: [0, 1]}); let dir = new MDir(); // iterate over all directory directory entries while (!this.pairIsnull(pdir.tail)) { let err = this.dirFetch(dir, pdir.tail); if (err) { return err; } // check head blocks for orphans if (!pdir.split) { // check if we have a parent let parent = new MDir(); let tag = this.fsParent(pdir.tail, parent); if (tag < 0 && tag != LFS_ERR_NOENT) { return tag; } if (tag == LFS_ERR_NOENT) { // we are an orphan console.warn("Fixing orphan {" + toHex(pdir.tail[0], 8) + ", " + toHex(pdir.tail[1], 8) + "}"); err = this.dirDrop(pdir, dir); if (err) { return err; } // refetch tail continue; } let pair = new Array(2); let res = this.dirGet(parent, this.mkTag(0x7ff, 0x3ff, 0), tag, pair); if (res < 0) { return res; } this.pairFromle32(pair); if (!this.pairSync(pair, pdir.tail)) { // we have desynced console.warn("Fixing half-orphan {" + toHex(pdir.tail[0], 8) + ", " + toHex(pdir.tail[1], 8) + "} -> {" + toHex(pair[0], 8) + ", " + toHex(pair[1], 8) + "}"); this.pairTole32(pair); err = this.dirCommit(pdir, this.mkAttrs( [this.mkTag(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pair])); this.pairFromle32(pair); if (err) { return err; } // refetch tail continue; } } pdir = dir; } // mark orphans as fixed return this.fsPreporphans(-this.gstateGetorphans(this.gstate)); } ctzFind() { console.warn("ctzFind not implemented"); } ctzExtend() { console.warn("ctzExtend not implemented"); } ctzTraverse() { console.warn("ctzTraverse not implemented"); } fsPreporphans() { console.warn("fsPreporphans not implemented"); } fsPrepmove() { console.warn("fsPrepmove not implemented"); } fsParent() { console.warn("fsParent not implemented"); } pairSync() { console.warn("pairSync not implemented"); } mkdir(path) { // deorphan if we haven't yet, needed at most once after poweron let err = this.fsForceconsistency(); if (err) { return err; } let cwd = new MList(); cwd.next = this.mlist; let idByRef = new ByRef(); let pathByRef = new ByRef(path); err = this.dirFind(cwd.m, pathByRef, idByRef); path = pathByRef.get(); let id = idByRef.get(); if (!(err == LFS_ERR_NOENT && id != 0x3ff)) { return (err < 0) ? err : LFS_ERR_EXIST; } // check that name fits let nlen = path.length; if (nlen > this.nameMax) { return LFS_ERR_NAMETOOLONG; } // build up new directory this.allocAck(); dir = new MDir(); err = this.dirAlloc(dir); if (err) { return err; } // find end of list let pred = cwd.m; while (pred.split) { err = lfs_dir_fetch(pred, pred.tail); if (err) { return err; } } // setup dir this.pairTole32(pred.tail); err = this.dirCommit(dir, this.mkAttrs( [this.mkTag(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pred.tail])); this.pairFromle32(pred.tail); if (err) { return err; } // current block end of list? if (cwd.m.split) { // update tails, this creates a desync err = this.fsPreporphans(+1); if (err) { return err; } // it's possible our predecessor has to be relocated, and if // our parent is our predecessor's predecessor, this could have // caused our parent to go out of date, fortunately we can hook // ourselves into littlefs to catch this cwd.type = 0; cwd.id = 0; this.mlist = cwd; this.pairTole32(dir.pair); err = this.dirCommit(pred, this.mkAttrs( [this.mkTag(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair])); this.pairFromle32(dir.pair); if (err) { this.mlist = cwd.next; return err; } this.mlist = cwd.next; err = this.fsPreporphans(-1); if (err) { return err; } } // now insert into our parent block this.pairTole32(dir.pair); err = this.dirCommit(cwd.m, this.mkAttrs( [this.mkTag(LFS_TYPE_CREATE, id, 0), null], [this.mkTag(LFS_TYPE_DIR, id, nlen), path], [this.mkTag(LFS_TYPE_DIRSTRUCT, id, 8), dir.pair], [this.mkTagIf(!cwd.m.split, LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair] )); this.pairFromle32(dir.pair); if (err) { return err; } return 0; } mkTag(type, id, size) { return ((type << 20) | (id << 10) | size) >>> 0; } mkTagIf(cond, type, id, size) { return cond ? this.mkTag(type, id, size) : this.mkTag(LFS_FROM_NOOP, 0, 0); } mkTagIfElse(cond, type1, id1, size1, type2, id2, size2) { return cond ? this.mkTag(type1, id1, size1) : this.mkTag(type2, id2, size2); } mkAttrs(...args) { let attrs = []; for (let [tag, buffer] of args) { attrs.push(new MAttr({tag: tag, buffer: buffer})); } return attrs; } copyObjProps(toObject, fromObject) { for (var key in fromObject) { if (fromObject.hasOwnProperty(key)) { toObject[key] = fromObject[key]; } } } } class LfsConfig { constructor({ context = null, readSize = 64, progSize = 64, blockSize = 0, blockCount = 0, blockCycles = 16, cacheSize = 64, lookaheadSize = 64, nameMax = LFS_NAME_MAX, fileMax = LFS_FILE_MAX, attrMax = LFS_ATTR_MAX, flash = flash }={}) { this.context = context; this.readSize = readSize; this.progSize = progSize; this.blockSize = blockSize; this.blockCount = blockCount; this.blockCycles = blockCycles; this.cacheSize = cacheSize; this.lookaheadSize = lookaheadSize; this.nameMax = nameMax; this.fileMax = fileMax; this.attrMax = attrMax; this.metadataMax = this.blockSize; this.flash = flash } // Read a region in a block. Negative error codes are propagated // to the user. read(block, off, buffer, size) { //memcpy(buffer, this.flash[0] + this.blockSize * block + off, size); buffer.set(this.flash.data.slice(this.blockSize * block + off, this.blockSize * block + off + size)); return 0; } // Program a region in a block. The block must have previously // been erased. Negative error codes are propagated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. prog(block, off, buffer, size) { //memcpy(&s_flashmem[0] + block * this.blockSize + off, buffer, size); this.flash.data.set(buffer.slice(0, size), this.blockSize * block + off); return 0; } // Erase a block. A block must be erased before being programmed. // The state of an erased block is undefined. Negative error codes // are propagated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. erase(block) { //memset(&s_flashmem[0] + block * this.blockSize, 0, this.blockSize); this.flash.data.set(new Uint8Array(this.blockSize).fill(0), this.blockSize * block); return 0; } // Sync the state of the underlying block device. Negative error codes // are propagated to the user. sync() { return 0; } // Optional statically allocated read buffer. Must be cache_size. // By default lfs_malloc is used to allocate this buffer. readBuffer = null; // Optional statically allocated program buffer. Must be cache_size. // By default lfs_malloc is used to allocate this buffer. progBuffer = null; // Optional statically allocated lookahead buffer. Must be lookahead_size // and aligned to a 32-bit boundary. By default lfs_malloc is used to // allocate this buffer. lookaheadBuffer = null; } class SuperBlock { constructor({ version = 0, blockSize = 0, blockCount = 0, nameMax = 0, fileMax = 0, attrMax = 0 } = {}) { this.version = version; this.blockSize = blockSize; this.blockCount = blockCount; this.nameMax = nameMax; this.fileMax = fileMax; this.attrMax = attrMax; } setFromBuffer(buffer) { let items = LittleFS.bufferToUint(buffer, struct.calcsize("IIIIII")); Object.keys(this).forEach((key, index) => { this[key] = items[index]; }); } } class ByRef { constructor(data = undefined) { this.data = data; } set(value) { this.data = value; } get() { return this.data; } } class GState { constructor({ tag = 0, pair = [0, 0] } = {}) { this.tag = tag; this.pair = pair; } } class MList { constructor({ id = 0, type = 0, m = new MDir() } = {}) { this.id = id; this.type = type; this.m = m; this.next = null; } } class MDir { constructor({ pair = [0, 0], rev = 0, off = 0, etag = 0, count = 0, erased = false, split = false, tail = [0, 0] } = {}) { this.pair = pair; this.rev = rev; this.off = off; this.etag = etag; this.count = count; this.erased = erased; this.split = split; this.tail = tail; } } class MAttr { constructor({ tag = null, buffer = null } = {}) { this.tag = tag; this.buffer = buffer; } } class Attr { constructor({ type = 0, buffer = null, size = 0 } = {}) { this.type = type; this.buffer = buffer; this.size = size; } } class Diskoff { constructor({ block, off } = {}) { this.block = block; this.off = off; } }; class Ctz { constructor({head = null, size = 0} = {}) { this.head = head; this.size = size; } setFromBuffer(buffer) { let items = LittleFS.bufferToUint(buffer, struct.calcsize("II")); Object.keys(this).forEach((key, index) => { this[key] = items[index]; }); } } class DirFindMatch { constructor({name = null, size = 0} = {}) { this.name = name; this.size = size; } }; class Commit { constructor({ block = 0, off = 0, ptag = 0, crc = 0, begin = 0, end = 0 } = {}) { this.block = block; this.off = off; this.ptag = ptag; this.crc = crc; this.begin = begin; this.end = end; } }; class Cache { constructor({ block = LFS_BLOCK_NULL, off = 0, size = 0, buffer = null, } = {}) { this.block = block; this.off = off; this.size = size; this.buffer = buffer; } }; class FileConfig { constructor({ buffer = null, attrs = [], attrCount = 0 } = {}) { this.buffer = buffer; this.attrs = attrs; this.attrCount = attrCount; } } class LfsFile { constructor({ id = 1, type = 0, m = new MDir(), ctz = new Ctz(), flags = 0, pos = 0, block = 0, off = 0, cache = new Cache(), cfg = new FileConfig() } = {}) { this.id = id; this.type = type; this.m = m; this.ctz = ctz; this.flags = flags; this.pos = pos; this.block = block; this.off = off; this.cache = cache; this.cfg = cfg; this.next = null; } }; class struct { static lut = { "b": {u: DataView.prototype.getInt8, p: DataView.prototype.setInt8, bytes: 1}, "B": {u: DataView.prototype.getUint8, p: DataView.prototype.setUint8, bytes: 1}, "h": {u: DataView.prototype.getInt16, p: DataView.prototype.setInt16, bytes: 2}, "H": {u: DataView.prototype.getUint16, p: DataView.prototype.setUint16, bytes: 2}, "i": {u: DataView.prototype.getInt32, p: DataView.prototype.setInt32, bytes: 4}, "I": {u: DataView.prototype.getUint32, p: DataView.prototype.setUint32, bytes: 4}, "q": {u: DataView.prototype.getInt64, p: DataView.prototype.setInt64, bytes: 8}, "Q": {u: DataView.prototype.getUint64, p: DataView.prototype.setUint64, bytes: 8}, } static pack(...args) { let format = args[0]; let pointer = 0; let data = args.slice(1); if (format.replace(/[<>]/, '').length != data.length) { throw("Pack format to Argument count mismatch"); return; } let bytes = []; let littleEndian = true; for (let i = 0; i < format.length; i++) { if (format[i] == "<") { littleEndian = true; } else if (format[i] == ">") { littleEndian = false; } else { pushBytes(format[i], data[pointer]); pointer++; } } function pushBytes(formatChar, value) { if (!(formatChar in struct.lut)) { throw("Unhandled character '" + formatChar + "' in pack format"); } let dataSize = struct.lut[formatChar].bytes; let view = new DataView(new ArrayBuffer(dataSize)); let dataViewFn = struct.lut[formatChar].p.bind(view); dataViewFn(0, value, littleEndian); for (let i = 0; i < dataSize; i++) { bytes.push(view.getUint8(i)); } } return bytes; }; static unpack(format, bytes) { let pointer = 0; let data = []; let littleEndian = true; for (let c of format) { if (c == "<") { littleEndian = true; } else if (c == ">") { littleEndian = false; } else { pushData(c); } } function pushData(formatChar) { if (!(formatChar in struct.lut)) { throw("Unhandled character '" + formatChar + "' in unpack format"); } let dataSize = struct.lut[formatChar].bytes; let view = new DataView(new ArrayBuffer(dataSize)); for (let i = 0; i < dataSize; i++) { view.setUint8(i, bytes[pointer + i] & 0xFF); } let dataViewFn = struct.lut[formatChar].u.bind(view); data.push(dataViewFn(0, littleEndian)); pointer += dataSize; } return data; }; static calcsize(format) { let size = 0; for (let i = 0; i < format.length; i++) { if (format[i] != "<" && format[i] != ">") { size += struct.lut[format[i]].bytes; } } return size; } } export { LittleFS, LfsConfig, LfsFile, LFS_O_RDONLY, LFS_O_WRONLY, LFS_O_RDWR, LFS_O_CREAT, LFS_O_EXCL, LFS_O_TRUNC, LFS_O_APPEND };