diff --git a/src/backend/electron/manager/vvppManager.ts b/src/backend/electron/manager/vvppManager.ts index bb76c9ceeb..0f8d3fa008 100644 --- a/src/backend/electron/manager/vvppManager.ts +++ b/src/backend/electron/manager/vvppManager.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { moveFile } from "move-file"; -import { dialog } from "electron"; +import { app, dialog } from "electron"; import AsyncLock from "async-lock"; import { EngineId, @@ -114,8 +114,11 @@ export class VvppManager { callbacks?: { onProgress?: ProgressCallback }, ) { const { outputDir, manifest } = await extractVvpp( - vvppPath, - this.vvppEngineDir, + { + vvppLikeFilePath: vvppPath, + vvppEngineDir: this.vvppEngineDir, + tmpDir: app.getPath("temp"), + }, callbacks, ); diff --git a/src/backend/electron/vvppFile.ts b/src/backend/electron/vvppFile.ts index d4d3568adc..94a1898488 100644 --- a/src/backend/electron/vvppFile.ts +++ b/src/backend/electron/vvppFile.ts @@ -17,13 +17,6 @@ import { createLogger } from "@/helpers/log"; const log = createLogger("vvppFile"); -// https://www.garykessler.net/library/file_sigs.html#:~:text=7-zip%20compressed%20file -const SEVEN_ZIP_MAGIC_NUMBER = Buffer.from([ - 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c, -]); - -const ZIP_MAGIC_NUMBER = Buffer.from([0x50, 0x4b, 0x03, 0x04]); - /** VVPPファイルが分割されている場合、それらのファイルを取得する */ async function getArchiveFileParts( vvppLikeFilePath: string, @@ -61,21 +54,14 @@ async function getArchiveFileParts( /** 分割されているVVPPファイルを連結して返す */ async function concatenateVvppFiles( - format: "zip" | "7z", archiveFileParts: string[], + outputFilePath: string, ) { - // -siオプションでの7z解凍はサポートされていないため、 - // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 log.info(`Concatenating ${archiveFileParts.length} files...`); - const tmpConcatenatedFile = path.join( - app.getPath("temp"), // TODO: archiveFilePartsと同じディレクトリにしてappの依存をなくす - `vvpp-${new Date().getTime()}.${format}`, - ); - log.info("Temporary file:", tmpConcatenatedFile); + await new Promise((resolve, reject) => { - if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); const inputStreams = archiveFileParts.map((f) => fs.createReadStream(f)); - const outputStream = fs.createWriteStream(tmpConcatenatedFile); + const outputStream = fs.createWriteStream(outputFilePath); new MultiStream(inputStreams) .pipe(outputStream) .on("close", () => { @@ -85,7 +71,6 @@ async function concatenateVvppFiles( .on("error", reject); }); log.info("Concatenated"); - return tmpConcatenatedFile; } /** 7zでファイルを解凍する */ @@ -107,13 +92,7 @@ async function unarchive( "-bsp1", // 進捗出力 ]; - let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; - if (!sevenZipPath) { - throw new Error("7z path is not defined"); - } - if (import.meta.env.PROD) { - sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath); // TODO: helperに移動してappの依存をなくす - } + const sevenZipPath = getSevenZipPath(); log.info("Spawning 7z:", sevenZipPath, args.join(" ")); await new Promise((resolve, reject) => { const child = spawn(sevenZipPath, args, { @@ -152,13 +131,24 @@ async function unarchive( // FIXME: rejectが2回呼ばれることがある child.on("error", reject); }); + + function getSevenZipPath() { + let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; + if (!sevenZipPath) { + throw new Error("7z path is not defined"); + } + if (import.meta.env.PROD) { + sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath); + } + return sevenZipPath; + } } export async function extractVvpp( - vvppLikeFilePath: string, - vvppEngineDir: string, // TODO: payload objectに変える + payload: { vvppLikeFilePath: string; vvppEngineDir: string; tmpDir: string }, callbacks?: { onProgress?: ProgressCallback }, ): Promise<{ outputDir: string; manifest: MinimumEngineManifestType }> { + const { vvppLikeFilePath, vvppEngineDir, tmpDir } = payload; callbacks?.onProgress?.({ progress: 0 }); const nonce = new Date().getTime().toString(); @@ -177,10 +167,12 @@ export async function extractVvpp( let archiveFile: string; try { if (archiveFileParts.length > 1) { - tmpConcatenatedFile = await concatenateVvppFiles( - format, - archiveFileParts, - ); + // -siオプションでの7z解凍はサポートされていないため、 + // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 + tmpConcatenatedFile = createTmpConcatenatedFilePath(); + log.info("Temporary file:", tmpConcatenatedFile); + + await concatenateVvppFiles(archiveFileParts, tmpConcatenatedFile); archiveFile = tmpConcatenatedFile; } else { archiveFile = archiveFileParts[0]; @@ -214,6 +206,10 @@ export async function extractVvpp( } throw e; } + + function createTmpConcatenatedFilePath(): string { + return path.join(tmpDir, `vvpp-${new Date().getTime()}.${format}`); + } } async function detectFileFormat( @@ -225,6 +221,13 @@ async function detectFileFormat( await file.read(buffer, 0, 8, 0); await file.close(); + // https://www.garykessler.net/library/file_sigs.html#:~:text=7-zip%20compressed%20file + const SEVEN_ZIP_MAGIC_NUMBER = Buffer.from([ + 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c, + ]); + + const ZIP_MAGIC_NUMBER = Buffer.from([0x50, 0x4b, 0x03, 0x04]); + if (buffer.compare(SEVEN_ZIP_MAGIC_NUMBER, 0, 6, 0, 6) === 0) { return "7z"; } else if (buffer.compare(ZIP_MAGIC_NUMBER, 0, 4, 0, 4) === 0) { diff --git a/tests/unit/backend/electron/vvppFile.node.spec.ts b/tests/unit/backend/electron/vvppFile.node.spec.ts index bc4d071c1a..3dd04933f2 100644 --- a/tests/unit/backend/electron/vvppFile.node.spec.ts +++ b/tests/unit/backend/electron/vvppFile.node.spec.ts @@ -20,11 +20,17 @@ test("正しいVVPPファイルからエンジンを切り出せる", async () = const sourceDir = path.join(__dirname, "vvpps", targetName); const outputFilePath = path.join(tmpDir, uuid4() + targetName); await createZipFile(sourceDir, outputFilePath); - await extractVvpp(outputFilePath, tmpDir); + + const vvppEngineDir = createVvppEngineDir(); + await extractVvpp({ + vvppLikeFilePath: outputFilePath, + vvppEngineDir, + tmpDir, + }); + expectManifestExists(vvppEngineDir); }); -test.fails("分割されたVVPPファイルからエンジンを切り出せる", async () => { - // TODO: electronのappに依存しているのでテストが通らない。依存がなくなり次第.failsを失くす +test("分割されたVVPPファイルからエンジンを切り出せる", async () => { const targetName = "perfect.vvpp"; const sourceDir = path.join(__dirname, "vvpps", targetName); const outputFilePath = path.join(tmpDir, uuid4() + targetName); @@ -33,7 +39,14 @@ test.fails("分割されたVVPPファイルからエンジンを切り出せる" const outputFilePath1 = outputFilePath + ".1.vvppp"; const outputFilePath2 = outputFilePath + ".2.vvppp"; splitFile(outputFilePath, outputFilePath1, outputFilePath2); - await extractVvpp(outputFilePath1, tmpDir); + + const vvppEngineDir = createVvppEngineDir(); + await extractVvpp({ + vvppLikeFilePath: outputFilePath1, + vvppEngineDir, + tmpDir, + }); + expectManifestExists(vvppEngineDir); }); test.each([ @@ -46,19 +59,38 @@ test.each([ const sourceDir = path.join(__dirname, "vvpps", targetName); const outputFilePath = path.join(tmpDir, uuid4() + targetName); await createZipFile(sourceDir, outputFilePath); - await expect(extractVvpp(outputFilePath, tmpDir)).rejects.toThrow( - expectedError, - ); + await expect( + extractVvpp({ + vvppLikeFilePath: outputFilePath, + vvppEngineDir: tmpDir, + tmpDir, + }), + ).rejects.toThrow(expectedError); }, ); /** 7zを使って指定したフォルダからzipファイルを作成する */ async function createZipFile(sourceDir: string, outputFilePath: string) { - const zipBin = import.meta.env.VITE_7Z_BIN_NAME; - const command = `"${zipBin}" a -tzip "${outputFilePath}" "${sourceDir}\\*"`; + const sevenZipBin = import.meta.env.VITE_7Z_BIN_NAME; + const command = `"${sevenZipBin}" a -tzip "${outputFilePath}" "${path.join(sourceDir, "*")}"`; await promisify(exec)(command); } +function createVvppEngineDir() { + const dir = path.join(tmpDir, uuid4()); + fs.mkdirSync(dir); + return dir; +} + +function expectManifestExists(vvppEngineDir: string) { + const files = fs.readdirSync(vvppEngineDir, { recursive: true }); + const manifestExists = files.some( + (file) => + typeof file === "string" && path.basename(file) == "engine_manifest.json", + ); + expect(manifestExists).toBe(true); +} + /** ファイルを2つに分割する */ function splitFile( inputFilePath: string,