diff --git a/src/fs.ts b/src/fs.ts new file mode 100644 index 0000000..ab31d63 --- /dev/null +++ b/src/fs.ts @@ -0,0 +1,156 @@ +import * as fs from 'fs-extra'; +import { SENTINEL } from './constants'; + +export const getFuseHeaderPositions = async (fuseFilePath: string, firstOnly = false) => { + let fileLength = 0; + + const firstAndLast = await new Promise<[number, number]>((resolve, reject) => { + const firstAndLast: [number, number] = [-1, -1]; + + // Keep a rolling list of chunks to not miss sentinels on chunk boundaries. + let chunksPosition = 0; + let chunksLength = 0; + const chunks: Buffer[] = []; + + const readStream = fs.createReadStream(fuseFilePath); + + readStream.on('error', (error) => { + reject(error); + }); + + readStream.on('data', (chunk) => { + // Only process previous chunks if we can throw some away afterwards. + if (chunks.length >= 2 && chunksLength >= 2 * SENTINEL.length) { + const joined = Buffer.concat(chunks); + + if (firstAndLast[0] === -1) { + const firstIndex = joined.indexOf(SENTINEL); + if (firstIndex !== -1) { + firstAndLast[0] = chunksPosition + firstIndex; + if (firstOnly) { + readStream.destroy(); + resolve(firstAndLast); + return; + } + } + } + + const lastIndex = joined.lastIndexOf(SENTINEL); + if (lastIndex !== -1) { + firstAndLast[1] = chunksPosition + lastIndex; + } + + // Keep enough chunks to contain every possible starting position of a sentinel on a chunk boundary. + // This is almost always just one, but a chunk isn't actually guaranteed to be longer than our sentinel. + while (chunksLength - chunks[0].length >= SENTINEL.length - 1) { + chunksPosition += chunks[0].length; + chunksLength -= chunks[0].length; + chunks.shift(); + } + } + + // fs.createReadStream returns a Buffer if the encoding is not specified. + chunk = chunk as Buffer; + + fileLength += chunk.length; + chunksLength += chunk.length; + chunks.push(chunk); + }); + + readStream.on('end', () => { + const joined = Buffer.concat(chunks); + + if (firstAndLast[0] === -1) { + const firstIndex = joined.indexOf(SENTINEL); + if (firstIndex !== -1) { + firstAndLast[0] = chunksPosition + firstIndex; + } + } + + const lastIndex = joined.lastIndexOf(SENTINEL); + if (lastIndex !== -1) { + firstAndLast[1] = chunksPosition + lastIndex; + } + + resolve(firstAndLast); + }); + }); + + if ( + firstAndLast[0] === -1 || + firstAndLast[firstOnly ? 0 : 1] + SENTINEL.length + 2 > fileLength + ) { + throw new Error( + 'Could not find a fuse wire in the provided Electron binary. Fuses are only supported in Electron 12 and higher.', + ); + } + + // If there's more than one fuse wire, we are probably in a universal build. + // We should flip the fuses in both wires to affect both slices of the universal binary. + if (!firstOnly && firstAndLast[0] !== firstAndLast[1]) { + return firstAndLast.map((position) => position + SENTINEL.length); + } + + return [firstAndLast[0] + SENTINEL.length]; +}; + +export const readBytesOrClose = async (fileHandle: number, length: number, position: number) => { + const buffer = Buffer.alloc(length); + let bytesReadTotal = 0; + + while (bytesReadTotal < length) { + const { bytesRead } = await fs + .read(fileHandle, buffer, bytesReadTotal, length - bytesReadTotal, position + bytesReadTotal) + .catch((error) => { + return fs.close(fileHandle).then( + () => { + throw error; + }, + () => { + throw error; + }, + ); + }); + + if (bytesRead === 0) { + throw new Error('Reached the end of the Electron binary while trying to read the fuses.'); + } + + bytesReadTotal += bytesRead; + } + + return buffer; +}; + +export const writeBytesOrClose = async (fileHandle: number, buffer: Buffer, position: number) => { + let bytesWrittenTotal = 0; + + while (bytesWrittenTotal < buffer.length) { + const { bytesWritten } = await fs + .write( + fileHandle, + buffer, + bytesWrittenTotal, + buffer.length - bytesWrittenTotal, + position + bytesWrittenTotal, + ) + .catch((error) => { + console.error( + `Failed to write the fuses to the Electron binary. The fuse wire may be corrupted. Tried to write 0x${buffer.toString( + 'hex', + )} to position ${position}.`, + ); + + return fs.close(fileHandle).then( + () => { + throw error; + }, + () => { + throw error; + }, + ); + }); + + bytesWrittenTotal += bytesWritten; + } +}; diff --git a/src/index.ts b/src/index.ts index 427d9f1..842d0ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,8 @@ import * as cp from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; import { FuseConfig, FuseV1Config, FuseV1Options, FuseVersion } from './config'; -import { FuseState, SENTINEL } from './constants'; +import { FuseState } from './constants'; +import { getFuseHeaderPositions, readBytesOrClose, writeBytesOrClose } from './fs'; export * from './config'; @@ -64,25 +65,14 @@ const setFuseWire = async ( fuseNamer: (index: number) => string, ) => { const fuseFilePath = pathToFuseFile(pathToElectron); - const electron = await fs.readFile(fuseFilePath); + const headerPositions = await getFuseHeaderPositions(fuseFilePath); + const fileHandle = await fs.open(fuseFilePath, 'r+'); - const firstSentinel = electron.indexOf(SENTINEL); - const lastSentinel = electron.lastIndexOf(SENTINEL); - // If the last sentinel is different to the first sentinel we are probably in a universal build - // We should flip the fuses in both sentinels to affect both slices of the universal binary - const sentinels = - firstSentinel === lastSentinel ? [firstSentinel] : [firstSentinel, lastSentinel]; + for (const headerPosition of headerPositions) { + const header = await readBytesOrClose(fileHandle, 2, headerPosition); + const [fuseWireVersion, fuseWireLength] = [header[0], header[1]]; + const fuseWireBuffer = await readBytesOrClose(fileHandle, fuseWireLength, headerPosition + 2); - for (const indexOfSentinel of sentinels) { - if (indexOfSentinel === -1) { - throw new Error( - 'Could not find sentinel in the provided Electron binary, fuses are only supported in Electron 12 and higher', - ); - } - - const fuseWirePosition = indexOfSentinel + SENTINEL.length; - - const fuseWireVersion = electron[fuseWirePosition]; if (parseInt(fuseVersion, 10) !== fuseWireVersion) { throw new Error( `Provided fuse wire version "${parseInt( @@ -91,12 +81,12 @@ const setFuseWire = async ( )}" does not match watch was found in the binary "${fuseWireVersion}". You should update your usage of @electron/fuses.`, ); } - const fuseWireLength = electron[fuseWirePosition + 1]; const wire = fuseWireBuilder(fuseWireLength).slice(0, fuseWireLength); + let changesMade = false; + for (let i = 0; i < wire.length; i++) { - const idx = fuseWirePosition + 2 + i; - const currentState = electron[idx]; + const currentState = fuseWireBuffer[i]; const newState = wire[i]; if (currentState === FuseState.REMOVED && newState !== FuseState.INHERIT) { @@ -106,37 +96,43 @@ const setFuseWire = async ( )}" that has been marked as removed, setting this fuse is a noop`, ); } + if (newState === FuseState.INHERIT) continue; - electron[idx] = newState; + + fuseWireBuffer[i] = newState; + changesMade = true; + } + + if (changesMade) { + await writeBytesOrClose(fileHandle, fuseWireBuffer, headerPosition + 2); } } - await fs.writeFile(fuseFilePath, electron); + await fs.close(fileHandle); - return sentinels.length; + return headerPositions.length; }; export const getCurrentFuseWire = async ( pathToElectron: string, ): Promise> => { const fuseFilePath = pathToFuseFile(pathToElectron); - const electron = await fs.readFile(fuseFilePath); - const fuseWirePosition = electron.indexOf(SENTINEL) + SENTINEL.length; - if (fuseWirePosition - SENTINEL.length === -1) { - throw new Error( - 'Could not find sentinel in the provided Electron binary, fuses are only supported in Electron 12 and higher', - ); - } - const fuseWireVersion = (electron[fuseWirePosition] as any) as FuseVersion; - const fuseWireLength = electron[fuseWirePosition + 1]; + const headerPosition = (await getFuseHeaderPositions(fuseFilePath, true))[0]; + + const fileHandle = await fs.open(fuseFilePath, 'r'); + const header = await readBytesOrClose(fileHandle, 2, headerPosition); + const [fuseWireVersion, fuseWireLength] = [(header[0] as any) as FuseVersion, header[1]]; + const fuseWireBuffer = await readBytesOrClose(fileHandle, fuseWireLength, headerPosition + 2); + await fs.close(fileHandle); + const fuseConfig: FuseConfig = { version: `${fuseWireVersion}` as FuseVersion, }; for (let i = 0; i < fuseWireLength; i++) { - const idx = fuseWirePosition + 2 + i; - const currentState = electron[idx]; + const currentState = fuseWireBuffer[i]; + switch (fuseConfig.version) { case FuseVersion.V1: fuseConfig[i as FuseV1Options] = currentState as FuseState; @@ -151,11 +147,11 @@ export const flipFuses = async ( pathToElectron: string, fuseConfig: FuseConfig, ): Promise => { - let numSentinels: number; + let fuseWiresSeen: number; switch (fuseConfig.version) { case FuseVersion.V1: - numSentinels = await setFuseWire( + fuseWiresSeen = await setFuseWire( pathToElectron, fuseConfig.version, buildFuseV1Wire.bind(null, fuseConfig), @@ -183,5 +179,5 @@ export const flipFuses = async ( } } - return numSentinels; + return fuseWiresSeen; }; diff --git a/test/index.spec.ts b/test/index.spec.ts index 7179f80..7aad32f 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -48,11 +48,11 @@ describe('flipFuses()', () => { expect((await getCurrentFuseWire(electronPath))[FuseV1Options.EnableCookieEncryption]).toEqual( FuseState.DISABLE, ); - const sentinels = await flipFuses(electronPath, { + const fuseWiresSeen = await flipFuses(electronPath, { version: FuseVersion.V1, [FuseV1Options.EnableCookieEncryption]: true, }); - expect(sentinels).toEqual(1); + expect(fuseWiresSeen).toEqual(1); expect((await getCurrentFuseWire(electronPath))[FuseV1Options.EnableCookieEncryption]).toEqual( FuseState.ENABLE, ); @@ -97,11 +97,11 @@ describe('flipFuses()', () => { force: false, }); - const sentinels = await flipFuses(electronPathUniversal, { + const fuseWiresSeen = await flipFuses(electronPathUniversal, { version: FuseVersion.V1, [FuseV1Options.EnableCookieEncryption]: true, }); - expect(sentinels).toEqual(2); + expect(fuseWiresSeen).toEqual(2); }); } });