From 608c56bce672a98f8112dff19d31730d2218cd40 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 6 Apr 2022 09:38:39 -0700 Subject: [PATCH] Initial files --- .gitignore | 120 ++ README.md | 3 +- littlefs.js | 3342 +++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 14 + package.json | 29 + 5 files changed, 3507 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 littlefs.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1140251 --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +*.DS_Store diff --git a/README.md b/README.md index 71a0bfd..d7fbdb2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # littlefs-pure-js -LittleFS partially ported over to pure JavaScript + +LittleFS partially ported over to pure JavaScript. This was ported over just enough to generate a LittleFS partition and to add files and folders to it. diff --git a/littlefs.js b/littlefs.js new file mode 100644 index 0000000..a302977 --- /dev/null +++ b/littlefs.js @@ -0,0 +1,3342 @@ +/*********************************************************************/ +// 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"); +} + +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) { + 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 + let err; + 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 File({ + 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 Config { + 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 File { + 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; + } +}; + +export { LittleFS, Config }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e39003d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "@adafruit/littlefs-pure-js", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@adafruit/littlefs-pure-js", + "version": "1.0.0", + "license": "MIT", + "devDependencies": {} + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dfc49e5 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "@adafruit/littlefs-pure-js", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, + "version": "1.0.0", + "description": "LittleFS partially ported over to pure JavaScript", + "main": "littlefs.js", + "dependencies": { + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git://github.com/adafruit/littlefs-pure-js.git" + }, + "keywords": [ + "BLE", + "WebBluetooth" + ], + "author": "Melissa LeBlanc-Williams", + "license": "MIT", + "bugs": { + "url": "https://github.com/adafruit/littlefs-pure-js/issues" + }, + "homepage": "https://github.com/adafruit/littlefs-pure-js#readme" +}