diff --git a/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts b/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts index 77760f26bf7..16f81daf60d 100644 --- a/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts +++ b/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts @@ -23,7 +23,7 @@ import { addCustomMessageFileInclude, createAddLangsMacro, LangConfigurator } fr import { computeLicensePage } from "./nsisLicense" import { NsisOptions, PortableOptions } from "./nsisOptions" import { NsisScriptGenerator } from "./nsisScriptGenerator" -import { AppPackageHelper, NSIS_PATH, nsisTemplatesDir } from "./nsisUtil" +import { AppPackageHelper, NSIS_PATH, nsisTemplatesDir, UninstallerReader } from "./nsisUtil" const debug = _debug("electron-builder:nsis") @@ -324,13 +324,21 @@ export class NsisTarget extends Target { // http://forums.winamp.com/showthread.php?p=3078545 if (isMacOsCatalina()) { - (await packager.vm.value).exec(installerPath, []) - - // Parallels VM can exit after command execution, but NSIS continue to be running - let i = 0 - while (!(await exists(uninstallerPath)) && i++ < 100) { - // noinspection JSUnusedLocalSymbols - await new Promise((resolve, _reject) => setTimeout(resolve, 300)) + try { + UninstallerReader.exec(installerPath, uninstallerPath) + } + catch (error) { + log.warn(error.message) + log.warn("packager.vm is used") + + const vm = await packager.vm.value + vm.exec(installerPath, []) + // Parallels VM can exit after command execution, but NSIS continue to be running + let i = 0 + while (!(await exists(uninstallerPath)) && i++ < 100) { + // noinspection JSUnusedLocalSymbols + await new Promise((resolve, _reject) => setTimeout(resolve, 300)) + } } } else { diff --git a/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts b/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts index 146b7973626..c41de0246c4 100644 --- a/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts +++ b/packages/app-builder-lib/src/targets/nsis/nsisUtil.ts @@ -8,6 +8,8 @@ import { Lazy } from "lazy-val" import * as path from "path" import { getTemplatePath } from "../../util/pathManager" import { NsisTarget } from "./NsisTarget" +import fs from "fs" +import zlib from "zlib" export const nsisTemplatesDir = getTemplatePath("nsis") @@ -101,3 +103,149 @@ export class CopyElevateHelper { return promise } } + +class BinaryReader { + + private _buffer: Buffer + private _position: number + + constructor(buffer: Buffer) { + this._buffer = buffer + this._position = 0 + } + + get length(): number { + return this._buffer.length + } + + get position(): number { + return this._position + } + + match(signature: Array): boolean { + if (signature.every((v, i) => this._buffer[this._position + i] === v)) { + this._position += signature.length + return true + } + return false + } + + skip(offset: number) { + this._position += offset + } + + bytes(size: number): Buffer { + const value = this._buffer.subarray(this._position, this._position + size) + this._position += size + return value + } + + uint16(): number { + const value = this._buffer[this._position] | (this._buffer[this._position + 1] << 8) + this._position += 2 + return value + } + + uint32(): number { + return this.uint16() | (this.uint16() << 16) + } + + string(length: number): string { + let value = "" + for (let i = 0; i < length; i++) { + const c = this._buffer[this._position + i] + if (c === 0x00) { + break + } + value += String.fromCharCode(c) + } + this._position += length + return value + } +} + +export class UninstallerReader { + + static exec(installerPath: string, uninstallerPath: string) { + const buffer = fs.readFileSync(installerPath) + const reader = new BinaryReader(buffer) + // IMAGE_DOS_HEADER + if (!reader.match([ 0x4D, 0x5A ])) { + throw new Error("Invalid 'MZ' signature.") + } + reader.skip(58) + reader.skip(reader.uint32() - reader.position) // e_lfanew + // IMAGE_FILE_HEADER + if (!reader.match([ 0x50, 0x45, 0x00, 0x00 ])) { + throw new Error("Invalid 'PE' signature.") + } + reader.skip(2) + const numberOfSections = reader.uint16() + reader.skip(12) + const sizeOfOptionalHeader = reader.uint16() + reader.skip(2) + reader.skip(sizeOfOptionalHeader) + // IMAGE_SECTION_HEADER + let nsisOffset = 0 + for (let i = 0; i < numberOfSections; i++) { + const name = reader.string(8) + reader.skip(8) + const rawSize = reader.uint32() + const rawPointer = reader.uint32() + reader.skip(16) + switch (name) { + case ".text": + case ".rdata": + case ".data": + case ".rsrc": { + nsisOffset = Math.max(rawPointer + rawSize, nsisOffset) + break + } + default: { + if (rawPointer !== 0 && rawSize !== 0) { + throw new Error("Unsupported section '" + name + "'.") + } + break + } + } + } + const executable = buffer.subarray(0, nsisOffset) + const nsisSize = buffer.length - nsisOffset + const nsisReader = new BinaryReader(buffer.subarray(nsisOffset, nsisOffset + nsisSize)) + const nsisSignature = [ 0xEF, 0xBE, 0xAD, 0xDE, 0x4E, 0x75, 0x6C, 0x6C, 0x73, 0x6F, 0x66, 0x74, 0x49, 0x6E, 0x73, 0x74 ] + nsisReader.uint32() // ? + if (!nsisReader.match(nsisSignature)) { + throw new Error("Invalid signature.") + } + nsisReader.uint32() // ? + if (nsisSize !== nsisReader.uint32()) { + throw new Error("Size mismatch.") + } + let innerBuffer = null + while (true) { + let size = nsisReader.uint32() + if ((size & 0x80000000) === 0) { + break + } + size &= 0x7FFFFFFF + if (size === 0 || nsisReader.position >= nsisReader.length) { + break + } + const compressedData = nsisReader.bytes(size) + const buffer = zlib.inflateRawSync(compressedData) + const innerReader = new BinaryReader(buffer) + innerReader.uint32() // ? + if (innerReader.match(nsisSignature)) { + if (innerBuffer) { + throw new Error("Multiple inner blocks.") + } + innerBuffer = buffer + } + } + if (!innerBuffer) { + throw new Error("Inner block not found.") + } + fs.writeFileSync(uninstallerPath, executable) + fs.appendFileSync(uninstallerPath, innerBuffer) + } +}