mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-30 17:40:53 -07:00
- Compare only major and minor versions, so same-minor patch downgrades are allowed - Keep blocking Electron minor and major downgrades before Yomitan storage loads - Add tests for patch downgrades, minor downgrades and Electron 42 reopening a 43 profile - Document the new rules and the SubMiner-dev profile workaround in troubleshooting
172 lines
5.4 KiB
TypeScript
172 lines
5.4 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 compareElectronMajorMinorVersions(
|
|
left: ParsedElectronVersion,
|
|
right: ParsedElectronVersion,
|
|
): number {
|
|
return left.major - right.major || left.minor - right.minor;
|
|
}
|
|
|
|
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 ||
|
|
compareElectronMajorMinorVersions(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}.`,
|
|
`Runtime safety record: ${statePath}.`,
|
|
'',
|
|
'Opening Chromium storage with an older Electron major or minor version can destroy Yomitan dictionaries. Upgrade SubMiner before using this profile. Patch downgrades within the same major and minor version are allowed.',
|
|
].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 };
|
|
}
|