Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use streaming I/O to avoid reading entire binary into memory #34

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions src/fs.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
72 changes: 34 additions & 38 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand All @@ -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<FuseConfig<FuseState>> => {
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<FuseState> = {
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;
Expand All @@ -151,11 +147,11 @@ export const flipFuses = async (
pathToElectron: string,
fuseConfig: FuseConfig,
): Promise<number> => {
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),
Expand Down Expand Up @@ -183,5 +179,5 @@ export const flipFuses = async (
}
}

return numSentinels;
return fuseWiresSeen;
};
8 changes: 4 additions & 4 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down Expand Up @@ -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);
});
}
});
Loading