diff --git a/src/__tests__/assets/dll/BSML.dll b/src/__tests__/assets/dll/BSML.dll new file mode 100644 index 00000000..691b9f29 Binary files /dev/null and b/src/__tests__/assets/dll/BSML.dll differ diff --git a/src/__tests__/assets/dll/Bugsmash.dll b/src/__tests__/assets/dll/Bugsmash.dll new file mode 100644 index 00000000..fb2ce53c Binary files /dev/null and b/src/__tests__/assets/dll/Bugsmash.dll differ diff --git a/src/__tests__/assets/dll/PlaylistManager.dll b/src/__tests__/assets/dll/PlaylistManager.dll new file mode 100644 index 00000000..5fbf01df Binary files /dev/null and b/src/__tests__/assets/dll/PlaylistManager.dll differ diff --git a/src/__tests__/assets/dll/SongCore.dll b/src/__tests__/assets/dll/SongCore.dll new file mode 100644 index 00000000..aa181463 Binary files /dev/null and b/src/__tests__/assets/dll/SongCore.dll differ diff --git a/src/__tests__/unit/mods.helpers.test.ts b/src/__tests__/unit/mods.helpers.test.ts new file mode 100644 index 00000000..43ec0276 --- /dev/null +++ b/src/__tests__/unit/mods.helpers.test.ts @@ -0,0 +1,62 @@ +import { parseMetadata } from "main/helpers/mods.helpers"; +import path from "path"; + +const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "dll"); + +describe("Test mods.helpers.test", () => { + + test("parseMetadata of BSML.dll", async () => { + const filepath = path.join(TEST_FOLDER, "BSML.dll"); + const data = await parseMetadata(filepath); + + expect(data).toBeTruthy(); + + expect(data.name).toBe("BeatSaberMarkupLanguage"); + expect(data.id).toBe("BeatSaberMarkupLanguage"); + expect(data.author).toBeTruthy(); + expect(data.version).toBeTruthy(); + expect(data.gameVersion).toBeTruthy(); + }); + + test("parseMetadata of Bugsmash.dll", async () => { + const filepath = path.join(TEST_FOLDER, "Bugsmash.dll"); + const data = await parseMetadata(filepath); + + expect(data).toBeTruthy(); + + expect(data.name).toBe("Bugsmash"); + expect(data.id).toBe("Bugsmash"); + expect(data.author).toBeTruthy(); + expect(data.version).toBeTruthy(); + expect(data.gameVersion).toBeTruthy(); + }); + + test("parseMetadata of PlaylistManager.dll", async () => { + const filepath = path.join(TEST_FOLDER, "PlaylistManager.dll"); + const data = await parseMetadata(filepath); + + expect(data).toBeTruthy(); + + expect(data.name).toBe("PlaylistManager"); + expect(data.id).toBe("PlaylistManager"); + expect(data.author).toBeTruthy(); + expect(data.version).toBeTruthy(); + expect(data.gameVersion).toBeTruthy(); + + }); + + test("parseMetadata of SongCore.dll", async () => { + const filepath = path.join(TEST_FOLDER, "SongCore.dll"); + const data = await parseMetadata(filepath); + + expect(data).toBeTruthy(); + + expect(data.name).toBe("SongCore"); + expect(data.id).toBe("SongCore"); + expect(data.author).toBeTruthy(); + expect(data.version).toBeTruthy(); + expect(data.gameVersion).toBeTruthy(); + }); + +}); + diff --git a/src/main/helpers/mods.helpers.ts b/src/main/helpers/mods.helpers.ts new file mode 100644 index 00000000..20d01644 --- /dev/null +++ b/src/main/helpers/mods.helpers.ts @@ -0,0 +1,57 @@ +import { readFile } from "fs-extra"; +import { ModMetadata } from "shared/models/mods/mod.interface"; + +// NOTE: getMetadata is slow since its using a search string and object parsing. +// Ways to optimize: +// - Find a way to locate the metadata string within the pdb/dll file +// - Correct object sizing, since the first byte does not match sometimes with the size somehow + +// ref: https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json +const SEARCH_ARRAY = [ + 0x22, 0x24, 0x73, 0x63, 0x68, + 0x65, 0x6D, 0x61, 0x22 +].reverse(); // '"$schema"' + +export async function parseMetadata(filepath: string): Promise { + const data = await readFile(filepath); + + // Find the word SEARCH_ARRAY from end to start of the buffer data + // NOTE: Inefficient algorithm + let start = data.length - 1; + for (let i = 0; start >= 0; --start) { + if (data[start] !== SEARCH_ARRAY[i]) { + i = 0; + } else if (++i === SEARCH_ARRAY.length) { + break; + } + } + + if (start < 4) { + return null; + } + + // Search for the starting left curly brace '{' + while (data[--start] !== 0x7B); + + // Find the ending right curly brace '}' + // NOTE: Inefficient algorithm + let braces = 1; + let end = start + 1; + for (; end < data.length && braces > 0; ++end) { + if (data[end] === 0x7B) { // { + ++braces; + } else if (data[end] === 0x7D) { // } + --braces; + } + } + + // Does not work with every dll (eg. Bugsmash.dll) + // const length = (data[start - 1] << 24) + // + (data[start - 2] << 16) + // + (data[start - 3] << 8) + // + (data[start - 4]); + // const end = start + length; + + return JSON.parse(data.toString(undefined, start, end)) as ModMetadata; +} + diff --git a/src/shared/models/mods/mod.interface.ts b/src/shared/models/mods/mod.interface.ts index e58daf94..f8177118 100644 --- a/src/shared/models/mods/mod.interface.ts +++ b/src/shared/models/mods/mod.interface.ts @@ -100,3 +100,27 @@ export interface BbmUserAPIResponse { createdAt?: Date; updatedAt?: Date; } + +// https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json +export interface ModMetadata { + name: string; + id: string | null; + version: string; + gameVersion: string; + description: string; + author: string; + dependsOn?: Record; + conflictsWith?: Record; + loadAfter?: string[]; + loadBefore?: string[]; + feature?: Record; + icon?: string; + files?: string[]; + links?: { + "project-home"?: string; + "project-source"?: string; + donate?: string; + }; + misc?: Record; +} +