diff --git a/src/file-utils.ts b/src/file-utils.ts index 7a603a8..08dc1e3 100644 --- a/src/file-utils.ts +++ b/src/file-utils.ts @@ -45,7 +45,7 @@ export const getAllAppFiles = async (appPath: string): Promise => { throw e; } } - if (p.includes('app.asar')) { + if (p.endsWith('.asar')) { fileType = AppFileType.APP_CODE; } else if (fileOutput.startsWith(MACHO_PREFIX)) { fileType = AppFileType.MACHO; diff --git a/src/index.ts b/src/index.ts index c7492a6..5a1105e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,9 +8,10 @@ import * as plist from 'plist'; import * as dircompare from 'dir-compare'; import { AppFile, AppFileType, getAllAppFiles } from './file-utils'; -import { AsarMode, detectAsarMode, generateAsarIntegrity, mergeASARs } from './asar-utils'; +import { AsarMode, detectAsarMode, mergeASARs } from './asar-utils'; import { sha } from './sha'; import { d } from './debug'; +import { computeIntegrityData } from './integrity'; /** * Options to pass into the {@link makeUniversalApp} function. @@ -251,9 +252,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = } } - const generatedIntegrity: Record = {}; - let didSplitAsar = false; - /** * If we have an ASAR we just need to check if the two "app.asar" files have the same hash, * if they are, same as above, we can leave one there and call it a day. If they're different @@ -271,8 +269,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = outputAsarPath: output, singleArchFiles: opts.singleArchFiles, }); - - generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(output); } else if (x64AsarMode === AsarMode.HAS_ASAR) { d('checking if the x64 and arm64 asars are identical'); const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar')); @@ -281,7 +277,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = ); if (x64AsarSha !== arm64AsarSha) { - didSplitAsar = true; d('x64 and arm64 asars are different'); const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'); await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath); @@ -329,18 +324,13 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj); const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'); await asar.createPackage(entryAsar, asarPath); - - generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(asarPath); - generatedIntegrity['Resources/app-x64.asar'] = generateAsarIntegrity(x64AsarPath); - generatedIntegrity['Resources/app-arm64.asar'] = generateAsarIntegrity(arm64AsarPath); } else { d('x64 and arm64 asars are the same'); - generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity( - path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), - ); } } + const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents')); + const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST); for (const plistFile of plistFiles) { const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath); diff --git a/src/integrity.ts b/src/integrity.ts new file mode 100644 index 0000000..51e1508 --- /dev/null +++ b/src/integrity.ts @@ -0,0 +1,51 @@ +import * as fs from 'fs-extra'; +import path from 'path'; +import { AppFileType, getAllAppFiles } from './file-utils'; +import { sha } from './sha'; +import { generateAsarIntegrity } from './asar-utils'; + +type IntegrityMap = { + [filepath: string]: string; +}; + +export interface HeaderHash { + algorithm: 'SHA256'; + hash: string; +} + +export interface AsarIntegrity { + [key: string]: HeaderHash; +} + +export async function computeIntegrityData(contentsPath: string): Promise { + const root = await fs.realpath(contentsPath); + + const resourcesRelativePath = 'Resources'; + const resourcesPath = path.resolve(root, resourcesRelativePath); + + const resources = await getAllAppFiles(resourcesPath); + const resourceAsars = resources + .filter((file) => file.type === AppFileType.APP_CODE) + .reduce( + (prev, file) => ({ + ...prev, + [path.join(resourcesRelativePath, file.relativePath)]: path.join( + resourcesPath, + file.relativePath, + ), + }), + {}, + ); + + // sort to produce constant result + const allAsars = Object.entries(resourceAsars).sort(([name1], [name2]) => + name1.localeCompare(name2), + ); + const hashes = await Promise.all(allAsars.map(async ([, from]) => generateAsarIntegrity(from))); + const asarIntegrity: AsarIntegrity = {}; + for (let i = 0; i < allAsars.length; i++) { + const [asar] = allAsars[i]; + asarIntegrity[asar] = hashes[i]; + } + return asarIntegrity; +} diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 0323488..02b96f1 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -157,6 +157,137 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different } `; +exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 1`] = ` +{ + "files": { + "index.js": { + "integrity": { + "algorithm": "SHA256", + "blockSize": 4194304, + "blocks": [ + "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8", + ], + "hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8", + }, + "size": 66, + }, + "package.json": { + "integrity": { + "algorithm": "SHA256", + "blockSize": 4194304, + "blocks": [ + "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3", + ], + "hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3", + }, + "size": 41, + }, + "private": { + "files": { + "var": { + "files": { + "app": { + "files": { + "file.txt": { + "link": "private/var/file.txt", + }, + }, + }, + "file.txt": { + "integrity": { + "algorithm": "SHA256", + "blockSize": 4194304, + "blocks": [ + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + ], + "hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + }, + "size": 11, + }, + }, + }, + }, + }, + "var": { + "link": "private/var", + }, + }, +} +`; + +exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 2`] = ` +{ + "files": { + "index.js": { + "integrity": { + "algorithm": "SHA256", + "blockSize": 4194304, + "blocks": [ + "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8", + ], + "hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8", + }, + "size": 66, + }, + "package.json": { + "integrity": { + "algorithm": "SHA256", + "blockSize": 4194304, + "blocks": [ + "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3", + ], + "hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3", + }, + "size": 41, + }, + "private": { + "files": { + "var": { + "files": { + "app": { + "files": { + "file.txt": { + "link": "private/var/file.txt", + }, + }, + }, + "file.txt": { + "integrity": { + "algorithm": "SHA256", + "blockSize": 4194304, + "blocks": [ + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + ], + "hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + }, + "size": 11, + }, + }, + }, + }, + }, + "var": { + "link": "private/var", + }, + }, +} +`; + +exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 3`] = ` +{ + "Contents/Info.plist": { + "Resources/app.asar": { + "algorithm": "SHA256", + "hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832", + }, + "Resources/webbapp.asar": { + "algorithm": "SHA256", + "hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832", + }, + }, +} +`; + exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 1`] = ` { "files": { @@ -581,6 +712,11 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 4`] exports[`makeUniversalApp no asar mode should shim two different app folders 5`] = ` { - "Contents/Info.plist": {}, + "Contents/Info.plist": { + "Resources/app.asar": { + "algorithm": "SHA256", + "hash": "27433ee3e34b3b0dabb29d18d40646126e80c56dbce8c4bb2adef7278b5a46c0", + }, + }, } `; diff --git a/test/index.spec.ts b/test/index.spec.ts index 95b84b3..de0a7cb 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -157,7 +157,10 @@ describe('makeUniversalApp', () => { VERIFY_APP_TIMEOUT, ); - it( + // TODO: Investigate if this should even be allowed. + // Current logic detects all unpacked files as APP_CODE, which doesn't seem correct since it could also be a macho file requiring lipo + // https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49 + it.skip( 'should shim asars with different unpacked dirs', async () => { const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => { @@ -191,6 +194,45 @@ describe('makeUniversalApp', () => { }, VERIFY_APP_TIMEOUT, ); + + it( + 'should generate AsarIntegrity for all asars in the application', + async () => { + const { testPath } = await createTestApp('app-2'); + const testAsarPath = path.resolve(appsOutPath, 'app-2.asar'); + await createPackage(testPath, testAsarPath); + + const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => { + await fs.copyFile( + testAsarPath, + path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), + ); + await fs.copyFile( + testAsarPath, + path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'), + ); + }); + const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => { + await fs.copyFile( + testAsarPath, + path.resolve(appPath, 'Contents', 'Resources', 'app.asar'), + ); + await fs.copyFile( + testAsarPath, + path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'), + ); + }); + const outAppPath = path.resolve(appsOutPath, 'MultipleAsars.app'); + await makeUniversalApp({ + x64AppPath, + arm64AppPath, + outAppPath, + mergeASARs: true, + }); + await verifyApp(outAppPath); + }, + VERIFY_APP_TIMEOUT, + ); }); describe('no asar mode', () => { diff --git a/test/util.ts b/test/util.ts index 03a9d6d..1937317 100644 --- a/test/util.ts +++ b/test/util.ts @@ -14,6 +14,7 @@ export const VERIFY_APP_TIMEOUT = 80 * 1000; export const asarsDir = path.resolve(__dirname, 'fixtures', 'asars'); export const appsDir = path.resolve(__dirname, 'fixtures', 'apps'); +export const appsOutPath = path.resolve(appsDir, 'out'); export const verifyApp = async (appPath: string) => { await ensureUniversal(appPath);