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..9e0dc92 100644 --- a/src/Burner.ts +++ b/src/Burner.ts @@ -60,12 +60,22 @@ 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; isMini?: boolean; + + isRed: boolean; } function RoundUpto4096(num: number) { @@ -144,6 +154,7 @@ class LiteBurner { child: -1, firstSector: 0, isMini: it.length < 4096, + isRed: false, }) ); @@ -275,7 +286,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 +373,11 @@ class LiteBurner { return t; } + /** + * Build the directory tree. + * + * @param dirIndex The index of the directory entry to be built. + */ private buildTree(dirIndex: number) { const { liteEnts } = this; const liteEntry = liteEnts[dirIndex]; @@ -370,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( @@ -381,12 +398,44 @@ class LiteBurner { } ); - liteEntry.child = children[0]; - - for (let x = 0; x < children.length - 1; x++) { - liteEnts[children[x]].right = children[x + 1]; + // ( | 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; + } } + // ( | 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) ) { 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); }); + }); }); });