Skip to content

Commit

Permalink
feat: NSIS uninstaller reader (#4305) (#4355)
Browse files Browse the repository at this point in the history
  • Loading branch information
lutzroeder authored and develar committed Oct 30, 2019
1 parent fc31199 commit 6443179
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 8 deletions.
24 changes: 16 additions & 8 deletions packages/app-builder-lib/src/targets/nsis/NsisTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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 {
Expand Down
148 changes: 148 additions & 0 deletions packages/app-builder-lib/src/targets/nsis/nsisUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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<number>): 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)
}
}

0 comments on commit 6443179

Please sign in to comment.