From 012a46167467ceb2a77024f7509cc873e960b655 Mon Sep 17 00:00:00 2001 From: kenjiuno Date: Sat, 9 Aug 2025 10:57:29 +0900 Subject: [PATCH 1/2] Fix Burner.ts so that it can respect RB-tree idea. --- .editorconfig | 4 + src/Burner.ts | 63 ++++++++++++++- test/test.js | 217 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 243 insertions(+), 41 deletions(-) diff --git a/.editorconfig b/.editorconfig index 3eb7d0f..50c5b93 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,3 +13,7 @@ indent_size = 2 [*.ts] indent_style = space indent_size = 2 + +[Burner.ts] +indent_style = space +indent_size = 4 diff --git a/src/Burner.ts b/src/Burner.ts index 0e95779..0843043 100644 --- a/src/Burner.ts +++ b/src/Burner.ts @@ -66,6 +66,8 @@ interface LiteEntry { firstSector: number; isMini?: boolean; + + isRed: boolean; } function RoundUpto4096(num: number) { @@ -144,6 +146,7 @@ class LiteBurner { child: -1, firstSector: 0, isMini: it.length < 4096, + isRed: false, }) ); @@ -275,7 +278,7 @@ class LiteBurner { ds.seek(pos + 0x40); ds.writeUint16(Math.min(64, numBytesName + 2)); ds.writeUint8(liteEnt.entry.type); - ds.writeUint8((x === 0) ? 0 : 1); + ds.writeUint8(liteEnt.isRed ? 0 : 1); ds.writeInt32(liteEnt.left); ds.writeInt32(liteEnt.right); ds.writeInt32(liteEnt.child); @@ -362,6 +365,11 @@ class LiteBurner { return t; } + /** + * Build the directory tree with RB-tree idea starting at specified index. + * + * @param dirIndex The index of the directory entry to be built. + */ private buildTree(dirIndex: number) { const { liteEnts } = this; const liteEntry = liteEnts[dirIndex]; @@ -381,10 +389,23 @@ class LiteBurner { } ); - liteEntry.child = children[0]; + // Select the middle node as the root of this subtree + const midIndex = Math.floor(children.length / 2); + liteEntry.child = children[midIndex]; + liteEnts[liteEntry.child].isRed = false; // Root of the subtree is always black + + // Recursively build left and right subtrees + const leftChildren = children.slice(0, midIndex); + const rightChildren = children.slice(midIndex + 1); - for (let x = 0; x < children.length - 1; x++) { - liteEnts[children[x]].right = children[x + 1]; + if (leftChildren.length !== 0) { + liteEnts[children[midIndex]].left = leftChildren[0]; + this.buildSubTree(leftChildren, children[midIndex]); + } + + if (rightChildren.length !== 0) { + liteEnts[children[midIndex]].right = rightChildren[0]; + this.buildSubTree(rightChildren, children[midIndex]); } for (let subIndex of children @@ -394,6 +415,40 @@ class LiteBurner { } } } + + private buildSubTree(children: number[], parentIndex: number) { + const { liteEnts } = this; + + if (children.length === 0) return; + + const midIndex = Math.floor(children.length / 2); + const rootIndex = children[midIndex]; + + liteEnts[rootIndex].isRed = true; // New nodes are red by default + liteEnts[rootIndex].left = -1; + liteEnts[rootIndex].right = -1; + + // Link to parent + if (rootIndex < parentIndex) { + liteEnts[parentIndex].left = rootIndex; + } else { + liteEnts[parentIndex].right = rootIndex; + } + + // Recursively build left and right subtrees + const leftChildren = children.slice(0, midIndex); + const rightChildren = children.slice(midIndex + 1); + + if (leftChildren.length !== 0) { + liteEnts[rootIndex].left = leftChildren[0]; + this.buildSubTree(leftChildren, rootIndex); + } + + if (rightChildren.length !== 0) { + liteEnts[rootIndex].right = rightChildren[0]; + this.buildSubTree(rightChildren, rootIndex); + } + } } /** diff --git a/test/test.js b/test/test.js index 18d3fe9..9924c4c 100644 --- a/test/test.js +++ b/test/test.js @@ -627,6 +627,65 @@ describe('Burner', function () { const burn = require('../lib/Burner').burn; const Reader = require('../lib/Reader').Reader; + /** + * + * path: `file`, `dir/file`, `dir1/dir2/file` + * + * @param { path: string, binary?: ArrayLike }[] entries + * @returns { array: Uint8Array } + */ + const burnByFsEntries = (entries) => { + const fsEntries = [ + { + name: "Root Entry", + type: TypeEnum.ROOT, + length: 0, + children: [], + }, + ]; + + for (const entry of entries) { + const { path, binary } = entry; + const elements = path.split('/'); + + let parentIndex = 0; + for (let index = 0; index < elements.length - 1; index++) { + const name = elements[index]; + const hitIndex = fsEntries[parentIndex].children.find(it => fsEntries[it].name === name); + if (hitIndex === undefined) { + const newIndex = fsEntries.length; + fsEntries.push({ + name, + type: TypeEnum.DIRECTORY, + length: 0, + children: [], + }); + fsEntries[parentIndex].children.push(newIndex); + parentIndex = newIndex; + } else { + parentIndex = hitIndex; + } + } + + { + const newIndex = fsEntries.length; + + fsEntries.push({ + name: elements[elements.length - 1], + type: TypeEnum.DOCUMENT, + length: binary ? binary.length : 0, + binaryProvider: binary ? () => new Uint8Array(binary) : undefined, + children: [], + }); + + fsEntries[parentIndex].children.push(newIndex); + } + } + + const array = burn(fsEntries); + return { array: array }; + }; + const burnAFileHavingLengthBy = (x) => { const writeData = new Uint8Array(x); for (let t = 0; t < writeData.length; t++) { @@ -653,50 +712,134 @@ describe('Burner', function () { return { writeData, array }; }; - const runReaderWith = ({ writeData, array }) => { - const reader = new Reader(array); - reader.parse(); - - const readData = reader.rootFolder().readFile("file"); - assert.deepStrictEqual(readData, writeData); - }; + const files10 = [ + { path: "file1", binary: new Uint8Array(Buffer.from("data1")) }, + { path: "file2", binary: new Uint8Array(Buffer.from("data2")) }, + { path: "file3", binary: new Uint8Array(Buffer.from("data3")) }, + { path: "file4", binary: new Uint8Array(Buffer.from("data4")) }, + { path: "file5", binary: new Uint8Array(Buffer.from("data5")) }, + { path: "file6", binary: new Uint8Array(Buffer.from("data6")) }, + { path: "file7", binary: new Uint8Array(Buffer.from("data7")) }, + { path: "file8", binary: new Uint8Array(Buffer.from("data8")) }, + { path: "file9", binary: new Uint8Array(Buffer.from("data9")) }, + { path: "file10", binary: new Uint8Array(Buffer.from("data10")) }, + ]; + + const dirs3_10 = [ + { path: "d1/e1/file1", binary: new Uint8Array(Buffer.from("data1")) }, + { path: "d1/e2/file2", binary: new Uint8Array(Buffer.from("data2")) }, + { path: "d2/e1/file3", binary: new Uint8Array(Buffer.from("data3")) }, + { path: "d2/e2/file4", binary: new Uint8Array(Buffer.from("data4")) }, + { path: "d3/e1/file5", binary: new Uint8Array(Buffer.from("data5")) }, + { path: "d3/e2/file6", binary: new Uint8Array(Buffer.from("data6")) }, + { path: "d1/e1/file7", binary: new Uint8Array(Buffer.from("data7")) }, + { path: "d2/e1/file8", binary: new Uint8Array(Buffer.from("data8")) }, + { path: "d3/e1/file9", binary: new Uint8Array(Buffer.from("data9")) }, + { path: "d1/e1/file10", binary: new Uint8Array(Buffer.from("data10")) }, + ]; describe('Compare file contents among Burner/Reader', function () { - const testIt = function (length) { - return runReaderWith( - burnAFileHavingLengthBy(length) - ); - } + describe('file size', function () { + const runReaderWith = ({ writeData, array }) => { + const reader = new Reader(array); + reader.parse(); - it('file size 0', function () { testIt(0); }); - it('file size 1', function () { testIt(1); }); - it('file size 63', function () { testIt(63); }); - it('file size 64 (minifat sector size)', function () { testIt(64); }); - it('file size 65', function () { testIt(65); }); - it('file size 511', function () { testIt(511); }); - it('file size 512 (fat sector size)', function () { testIt(512); }); - it('file size 513', function () { testIt(513); }); - it('file size 65537', function () { testIt(65537); }); + const readData = reader.rootFolder().readFile("file"); + assert.deepStrictEqual(readData, writeData); + }; + + const testIt = function (length) { + return runReaderWith( + burnAFileHavingLengthBy(length) + ); + }; + + it('file size 0', function () { testIt(0); }); + it('file size 1', function () { testIt(1); }); + it('file size 63', function () { testIt(63); }); + it('file size 64 (minifat sector size)', function () { testIt(64); }); + it('file size 65', function () { testIt(65); }); + it('file size 511', function () { testIt(511); }); + it('file size 512 (fat sector size)', function () { testIt(512); }); + it('file size 513', function () { testIt(513); }); + it('file size 65537', function () { testIt(65537); }); + }); + + describe('tree builder', function () { + const runReaderWith = (array, entries) => { + const reader = new Reader(array); + reader.parse(); + + const loadedEntries = []; + + function walk(folder, prefix) { + for (const subFolder of folder.subFolders()) { + walk(subFolder, prefix + subFolder.name + "/"); + } + + for (const fileSet of folder.fileNameSets()) { + const path = `${prefix}${fileSet.name}`; + loadedEntries.push({ + path: path, + binary: fileSet.provider(), + }); + } + } + + walk(reader.rootFolder(), ""); + + function sort(array) { + return [...array].sort((a, b) => a.path.localeCompare(b.path)); + } + + assert.deepStrictEqual(sort(loadedEntries), sort(entries)); + }; + + const testIt = function (entries) { + return runReaderWith( + burnByFsEntries(entries).array, + entries + ); + }; + + it('files10', function () { testIt(files10); }); + it('dirs3_10', function () { testIt(dirs3_10); }); + }); }); (useValidateCompoundFile ? describe : describe.skip)('validateCompoundFile', function () { - const testIt = async function (length) { - await runValidateCompoundFileAsync( - { - binary: burnAFileHavingLengthBy(length).array, - } - ); - } + describe("file size", function () { + const testIt = async function (length) { + await runValidateCompoundFileAsync( + { + binary: burnAFileHavingLengthBy(length).array, + } + ); + } - it('file size 0', function () { return testIt(0); }); - it('file size 1', function () { return testIt(1); }); - it('file size 63', function () { return testIt(63); }); - it('file size 64 (minifat sector size)', function () { return testIt(64); }); - it('file size 65', function () { return testIt(65); }); - it('file size 511', function () { return testIt(511); }); - it('file size 512 (fat sector size)', function () { return testIt(512); }); - it('file size 513', function () { return testIt(513); }); - it('file size 65537', function () { return testIt(65537); }); + it('file size 0', function () { return testIt(0); }); + it('file size 1', function () { return testIt(1); }); + it('file size 63', function () { return testIt(63); }); + it('file size 64 (minifat sector size)', function () { return testIt(64); }); + it('file size 65', function () { return testIt(65); }); + it('file size 511', function () { return testIt(511); }); + it('file size 512 (fat sector size)', function () { return testIt(512); }); + it('file size 513', function () { return testIt(513); }); + it('file size 65537', function () { return testIt(65537); }); + }); + + describe("tree builder", function () { + const testIt = async function (entries) { + await runValidateCompoundFileAsync( + { + binary: burnByFsEntries(entries).array, + } + ); + } + + it('files10', function () { testIt(files10); }); + it('dirs3_10', function () { testIt(dirs3_10); }); + }); }); }); From 72241af696567594329b60cab4c61127ede73359 Mon Sep 17 00:00:00 2001 From: kenjiuno Date: Sat, 9 Aug 2025 12:06:48 +0900 Subject: [PATCH 2/2] Improve node splitter --- src/Burner.ts | 98 ++++++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/src/Burner.ts b/src/Burner.ts index 0843043..9e0dc92 100644 --- a/src/Burner.ts +++ b/src/Burner.ts @@ -60,8 +60,16 @@ export interface Entry { interface LiteEntry { entry: Entry; + /** + * Lesser side of {@link Entry.name} + */ left: number; + + /** + * Greater side of {@link Entry.name} + */ right: number; + child: number; firstSector: number; @@ -366,7 +374,7 @@ class LiteBurner { } /** - * Build the directory tree with RB-tree idea starting at specified index. + * Build the directory tree. * * @param dirIndex The index of the directory entry to be built. */ @@ -378,8 +386,9 @@ class LiteBurner { throw new Error("It must be a storage!"); } + // Array.sort is destructive, so copy it by concat() before changing const children = liteEntry.entry.children.concat(); - if (children.length >= 1) { + if (1 <= children.length) { children.sort( (a, b) => { return this.compareName( @@ -389,24 +398,43 @@ class LiteBurner { } ); - // Select the middle node as the root of this subtree - const midIndex = Math.floor(children.length / 2); - liteEntry.child = children[midIndex]; - liteEnts[liteEntry.child].isRed = false; // Root of the subtree is always black - - // Recursively build left and right subtrees - const leftChildren = children.slice(0, midIndex); - const rightChildren = children.slice(midIndex + 1); - - if (leftChildren.length !== 0) { - liteEnts[children[midIndex]].left = leftChildren[0]; - this.buildSubTree(leftChildren, children[midIndex]); + // ( | 0 ) + // ( 0 | 1 ) + // ( 0 | 1 2 ) + + // (left, right), returns first right node + const split2 = (start: number, end: number, isRed: boolean): number => { + if (start < end) { + const midNum = Math.floor((start + end) / 2); + const entryIndex = children[midNum]; + const entry = liteEnts[entryIndex]; + entry.isRed = isRed; + entry.left = split2(start, midNum, !isRed); + entry.right = split2(midNum + 1, end, !isRed); + return entryIndex; + } else { + return -1; + } } - if (rightChildren.length !== 0) { - liteEnts[children[midIndex]].right = rightChildren[0]; - this.buildSubTree(rightChildren, children[midIndex]); - } + // ( | 0 | ) + // ( | 0 | 1 ) + // ( 0 | 1 | 2 ) + // ( 0 | 1 | 2 3 ) + // ( 0 1 | 2 | 3 4 ) + + // (left, root, right), returns root node + const split3 = (): number => { + const midNum = Math.floor(children.length / 2); + const entryIndex = children[midNum]; + const entry = liteEnts[entryIndex]; + entry.isRed = false; + entry.left = split2(0, midNum, true); + entry.right = split2(midNum + 1, children.length, true); + return entryIndex; + }; + + liteEntry.child = split3(); for (let subIndex of children .filter(it => liteEnts[it].entry.type === TypeEnum.DIRECTORY) @@ -415,40 +443,6 @@ class LiteBurner { } } } - - private buildSubTree(children: number[], parentIndex: number) { - const { liteEnts } = this; - - if (children.length === 0) return; - - const midIndex = Math.floor(children.length / 2); - const rootIndex = children[midIndex]; - - liteEnts[rootIndex].isRed = true; // New nodes are red by default - liteEnts[rootIndex].left = -1; - liteEnts[rootIndex].right = -1; - - // Link to parent - if (rootIndex < parentIndex) { - liteEnts[parentIndex].left = rootIndex; - } else { - liteEnts[parentIndex].right = rootIndex; - } - - // Recursively build left and right subtrees - const leftChildren = children.slice(0, midIndex); - const rightChildren = children.slice(midIndex + 1); - - if (leftChildren.length !== 0) { - liteEnts[rootIndex].left = leftChildren[0]; - this.buildSubTree(leftChildren, rootIndex); - } - - if (rightChildren.length !== 0) { - liteEnts[rootIndex].right = rightChildren[0]; - this.buildSubTree(rightChildren, rootIndex); - } - } } /**