diff --git a/assets/tests/manifest.zip b/assets/tests/manifest.zip new file mode 100644 index 00000000..3af4d77b Binary files /dev/null and b/assets/tests/manifest.zip differ diff --git a/src/__tests__/unit/zip.test.ts b/src/__tests__/unit/zip.test.ts index 9f2f382d..4dc123c7 100644 --- a/src/__tests__/unit/zip.test.ts +++ b/src/__tests__/unit/zip.test.ts @@ -1,11 +1,12 @@ import path from "path"; -import { mkdir, pathExistsSync, rm } from "fs-extra"; -import { extractZip } from "main/helpers/zip.helpers"; +import { mkdir, pathExistsSync, readFile, rm } from "fs-extra"; +import { extractZip, getFilesFromZip } from "main/helpers/zip.helpers"; const TEST_FOLDER = path.resolve(__dirname, "../../..", "assets", "tests"); const STANDARD_ZIP = path.join(TEST_FOLDER, "standard.zip"); const WINDOWS_LEGACY_MAP_ZIP = path.join(TEST_FOLDER, "windows_legacy.zip"); const SPECIAL_ZIP = path.join(TEST_FOLDER, "special.zip"); +const MANIFEST_ZIP = path.join(TEST_FOLDER, "manifest.zip"); const DESTINATION_FOLDER = path.join(TEST_FOLDER, "out"); describe("Zip Server Service Test", () => { @@ -82,7 +83,19 @@ describe("Zip Server Service Test", () => { ] expect(beforeExtracted).toEqual(expected); expect(afterExtracted).toEqual(expected); - }) + }); + + it("Extract manifest.json from zip file", async () => { + const zipBuffer = await readFile(MANIFEST_ZIP); + const buffers = await getFilesFromZip(zipBuffer, ["manifest.json"]); + const manifest = JSON.parse(buffers["manifest.json"].toString()); + expect(manifest).toBeTruthy(); + + // Check some of the fields if they are correct + expect(manifest.appId).toBe("some-app-id"); + expect(manifest.canonicalName).toBe("some-canonical-name"); + expect(manifest.isCore).toBe(true); + }); afterAll(async () => { if (pathExistsSync(DESTINATION_FOLDER)) { diff --git a/src/main/helpers/zip.helpers.ts b/src/main/helpers/zip.helpers.ts index a5444fd4..e2644978 100644 --- a/src/main/helpers/zip.helpers.ts +++ b/src/main/helpers/zip.helpers.ts @@ -28,19 +28,28 @@ export interface ZipProcessOptions { getBuffer?: boolean; }; +const YAUZL_OPTIONS: yauzl.Options = { + lazyEntries: true, + decodeStrings: true, +}; + +function openZip(data: string | Buffer, callback: (error: Error, zip: yauzl.ZipFile) => void) { + if (typeof data === "string") { + yauzl.open(data, YAUZL_OPTIONS, callback); + } else { + yauzl.fromBuffer(data, YAUZL_OPTIONS, callback); + } +} + export async function extractZip( - zipPath: string, + data: string | Buffer, destination: string, options?: Readonly ): Promise { await ensureFolderExist(destination); return new Promise((resolve, reject) => { - yauzl.open(zipPath, { - lazyEntries: true, - decodeStrings: true, - }, - (openError, zip) => { + openZip(data, (openError: Error, zip: yauzl.ZipFile) => { if (openError) return reject(openError); handleExtractZip(zip, destination, options, resolve, reject); }); @@ -99,7 +108,8 @@ function handleExtractZip( if (options?.terminate?.(zipEntry)) { readStream.destroy(); - return zip.close(); + zip.close(); + return resolve(files); } if (options?.condition) { @@ -130,6 +140,64 @@ function handleExtractZip( }); } +export function getFilesFromZip( + data: string | Buffer, + files: string[] +): Promise> { + return new Promise((resolve, reject) => { + openZip(data, (openError: Error, zip: yauzl.ZipFile) => { + if (openError) return reject(openError); + handleGetFilesFromZip(zip, files, resolve, reject); + }); + }); +} + +function handleGetFilesFromZip( + zip: yauzl.ZipFile, + files: string[], + resolve: (buffers: Record) => void, + reject: (error: Error) => void +) { + const buffers: Record = {}; + let count = 0; + + zip.readEntry(); + + zip.on("entry", (entry: yauzl.Entry) => { + // Entry is a directory / folder + if (entry.fileName.endsWith("/") || !files.includes(entry.fileName)) { + return zip.readEntry(); + } + + // Entry is in the files list + zip.openReadStream(entry, (readError, readStream) => { + if (readError) return reject(readError); + + const chunks: Buffer[] = []; + readStream.on("data", data => { + chunks.push(data); + }); + + readStream.on("end", () => { + buffers[entry.fileName] = Buffer.concat(chunks); + + ++count; + if (count === files.length) { + zip.close(); + return resolve(buffers); + } + + zip.readEntry(); + }); + }); + }); + + zip.once("end", () => { + zip.close(); + resolve(buffers); + }); +} + // @params loop - this should not throw an error export function processZip( data: string | Buffer, @@ -138,32 +206,23 @@ export function processZip( initValue: T ): Promise { return new Promise((resolve, reject) => { - const callback = (openError: Error, zip: yauzl.ZipFile) => { + openZip(data, (openError: Error, zip: yauzl.ZipFile) => { if (openError) return reject(openError); handleProcessZip( zip, options, loop, initValue, resolve, reject ); - } - - if (typeof data === "string") { - yauzl.open(data, { - lazyEntries: true, - decodeStrings: true, - }, callback); - } else { - yauzl.fromBuffer(data, callback); - } + }); }); } -async function handleProcessZip( +function handleProcessZip( zip: yauzl.ZipFile, options: Readonly, loop: (accumulator: T, entry: ZipEntry) => T, accumulator: T, resolve: (result: T) => void, reject: (error: Error) => void -): Promise { +): void { const zipEntry: ZipEntry = { name: "", directory: false, @@ -191,7 +250,7 @@ async function handleProcessZip( if (options?.terminate?.(zipEntry)) { readStream.destroy(); zip.close(); - return; + return resolve(accumulator); } const chunks: Buffer[] = []; diff --git a/src/main/models/oculus-downloader.class.ts b/src/main/models/oculus-downloader.class.ts index 3078ef0a..50fba9e2 100644 --- a/src/main/models/oculus-downloader.class.ts +++ b/src/main/models/oculus-downloader.class.ts @@ -6,7 +6,7 @@ import { inflate } from "pako" import { EMPTY, Observable, ReplaySubject, Subscriber, catchError, filter, from, lastValueFrom, mergeMap, scan, share, tap } from "rxjs"; import { Progression, hashFile } from "../helpers/fs.helpers"; import { OculusDownloaderErrorCodes } from "../../shared/models/bs-version-download/oculus-download.model"; -import { processZip } from "main/helpers/zip.helpers"; +import { getFilesFromZip } from "main/helpers/zip.helpers"; export class OculusDownloader { @@ -34,24 +34,14 @@ export class OculusDownloader { const buffer = await this.downloadManifestZip(downloadUrl) .catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_FAILED")); - let found = false; - let manifestString: string | null = null; - - // NOTE: Might be better to do get file in zip.helpers - await processZip(buffer, { - terminate: () => found, - getBuffer: true, - }, (_, entry): void => { - if (entry.name !== "manifest.json") return; - found = true; - manifestString = entry.buffer.toString(); - }, undefined); - - if(!manifestString) { + const manifestName = "manifest.json"; + const buffers = await getFilesFromZip(buffer, [manifestName]); + const manifest = buffers[manifestName]; + if(!manifest) { throw new CustomError("Manifest file not found", "MANIFEST_FILE_NOT_FOUND"); } - return JSON.parse(manifestString) + return JSON.parse(manifest.toString()) .catch((err: Error) => CustomError.throw(err, "PARSE_MANIFEST_FILE_FAILED")); }