Files
SubMiner/src/main/electron-runtime-guard.ts
T
sudacode 06b21a68fa fix(storage): protect Yomitan profiles across Electron runtime changes
- Block unsupported or downgraded Electron runtimes before loading profile storage
- Isolate development profiles and guard against unexpected dictionary loss
2026-08-18 22:19:47 -07:00

171 lines
5.3 KiB
TypeScript

import fs from 'node:fs';
import path from 'node:path';
import { writeTextFileAtomicallyDurable } from '../shared/fs-utils';
export const SUPPORTED_ELECTRON_MAJOR = 43;
const RUNTIME_STATE_FILE_NAME = 'electron-runtime.json';
type ElectronRuntimeState = {
highestElectronMajor: number;
lastElectronVersion: string;
};
type ParsedElectronVersion = {
major: number;
minor: number;
patch: number;
};
type ValidatedElectronRuntimeState = {
state: ElectronRuntimeState;
version: ParsedElectronVersion;
};
export type ElectronRuntimeGuardResult =
| { ok: true; statePath: string }
| { ok: false; title: string; details: string; statePath: string };
function parseElectronVersion(version: string): ParsedElectronVersion | null {
const match = /^(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$/.exec(version.trim());
if (!match) return null;
const major = Number.parseInt(match[1]!, 10);
const minor = Number.parseInt(match[2]!, 10);
const patch = Number.parseInt(match[3]!, 10);
if (![major, minor, patch].every((part) => Number.isSafeInteger(part) && part >= 0)) {
return null;
}
if (major === 0) return null;
return { major, minor, patch };
}
function compareElectronVersions(
left: ParsedElectronVersion,
right: ParsedElectronVersion,
): number {
return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
}
function readRuntimeState(statePath: string): ValidatedElectronRuntimeState | null {
if (!fs.existsSync(statePath)) return null;
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8')) as Partial<ElectronRuntimeState>;
const highestElectronMajor = parsed.highestElectronMajor;
const lastElectronVersion =
typeof parsed.lastElectronVersion === 'string'
? parseElectronVersion(parsed.lastElectronVersion)
: null;
if (
typeof highestElectronMajor !== 'number' ||
!Number.isSafeInteger(highestElectronMajor) ||
highestElectronMajor <= 0 ||
lastElectronVersion === null
) {
throw new Error('The runtime safety record has an invalid format.');
}
return {
state: {
highestElectronMajor,
lastElectronVersion: parsed.lastElectronVersion!,
},
version: lastElectronVersion,
};
}
function writeRuntimeState(statePath: string, state: ElectronRuntimeState): void {
writeTextFileAtomicallyDurable(statePath, `${JSON.stringify(state, null, 2)}\n`);
}
export function enforceElectronRuntimeGuard(options: {
electronVersion: string;
userDataPath: string;
supportedElectronMajor?: number;
}): ElectronRuntimeGuardResult {
const supportedElectronMajor = options.supportedElectronMajor ?? SUPPORTED_ELECTRON_MAJOR;
const statePath = path.join(options.userDataPath, RUNTIME_STATE_FILE_NAME);
const currentVersion = parseElectronVersion(options.electronVersion);
if (currentVersion === null) {
return {
ok: false,
title: 'SubMiner could not verify Electron',
details: `Electron reported an invalid version: ${JSON.stringify(options.electronVersion)}. SubMiner did not load Yomitan storage.`,
statePath,
};
}
if (currentVersion.major !== supportedElectronMajor) {
return {
ok: false,
title: 'Unsupported Electron runtime',
details: [
`This SubMiner build requires Electron ${supportedElectronMajor}.`,
`The current runtime is Electron ${options.electronVersion}.`,
'',
'Launch SubMiner through its packaged application or the repository package scripts. Yomitan storage was not loaded.',
].join('\n'),
statePath,
};
}
let previousState: ValidatedElectronRuntimeState | null;
try {
previousState = readRuntimeState(statePath);
} catch (error) {
return {
ok: false,
title: 'SubMiner profile safety check failed',
details: [
`SubMiner could not read the runtime safety record at ${statePath}.`,
(error as Error).message,
'',
'Yomitan storage was not loaded. Repair or remove only this safety record after verifying the profile backup.',
].join('\n'),
statePath,
};
}
if (
previousState &&
(currentVersion.major < previousState.state.highestElectronMajor ||
compareElectronVersions(currentVersion, previousState.version) < 0)
) {
return {
ok: false,
title: 'Electron downgrade blocked',
details: [
`This profile was previously opened with Electron ${previousState.state.lastElectronVersion}.`,
`The current runtime is Electron ${options.electronVersion}.`,
'',
'Opening Chromium storage with an older Electron version can destroy Yomitan dictionaries. Upgrade SubMiner before using this profile.',
].join('\n'),
statePath,
};
}
try {
writeRuntimeState(statePath, {
highestElectronMajor: Math.max(
currentVersion.major,
previousState?.state.highestElectronMajor ?? 0,
),
lastElectronVersion: options.electronVersion,
});
} catch (error) {
return {
ok: false,
title: 'SubMiner profile safety check failed',
details: [
`SubMiner could not update the runtime safety record at ${statePath}.`,
(error as Error).message,
'',
'Yomitan storage was not loaded.',
].join('\n'),
statePath,
};
}
return { ok: true, statePath };
}