mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
Harden Yomitan read-only logging and extract overlay options
- Redact skipped Yomitan write log values (paths to basename, titles hidden) - Extract overlay BrowserWindow option builder for direct unit testing - Document and test `externalProfilePath` tilde (`~`) home expansion
This commit is contained in:
@@ -951,7 +951,7 @@ For GameSentenceMiner on Linux, the default overlay profile path is typically `~
|
||||
|
||||
| Option | Values | Description |
|
||||
| --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `externalProfilePath` | string path | Optional absolute path to another app's Yomitan Electron profile. SubMiner loads that profile read-only and reuses its dictionaries/settings. |
|
||||
| `externalProfilePath` | string path | Optional absolute path, or a path beginning with `~` (expanded to your home directory), to another app's Yomitan Electron profile. SubMiner loads that profile read-only and reuses its dictionaries/settings. |
|
||||
|
||||
External-profile mode behavior:
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { createResolveContext } from './context';
|
||||
import { applyIntegrationConfig } from './integrations';
|
||||
|
||||
@@ -139,5 +140,8 @@ test('yomitan externalProfilePath expands leading tilde to the current home dire
|
||||
|
||||
applyIntegrationConfig(context);
|
||||
|
||||
assert.equal(context.resolved.yomitan.externalProfilePath, `${homeDir}/.config/gsm_overlay`);
|
||||
assert.equal(
|
||||
context.resolved.yomitan.externalProfilePath,
|
||||
path.join(homeDir, '.config', 'gsm_overlay'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { buildOverlayWindowOptions } from './overlay-window-options';
|
||||
|
||||
test('overlay window config explicitly disables renderer sandbox for preload compatibility', () => {
|
||||
const sourcePath = path.join(process.cwd(), 'src/core/services/overlay-window.ts');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
const options = buildOverlayWindowOptions('visible', {
|
||||
isDev: false,
|
||||
yomitanSession: null,
|
||||
});
|
||||
|
||||
assert.match(source, /webPreferences:\s*\{[\s\S]*sandbox:\s*false[\s\S]*\}/m);
|
||||
assert.equal(options.webPreferences?.sandbox, false);
|
||||
});
|
||||
|
||||
test('overlay window config uses the provided Yomitan session when available', () => {
|
||||
const sourcePath = path.join(process.cwd(), 'src/core/services/overlay-window.ts');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
const yomitanSession = { id: 'session' } as never;
|
||||
const withSession = buildOverlayWindowOptions('visible', {
|
||||
isDev: false,
|
||||
yomitanSession,
|
||||
});
|
||||
const withoutSession = buildOverlayWindowOptions('visible', {
|
||||
isDev: false,
|
||||
yomitanSession: null,
|
||||
});
|
||||
|
||||
assert.match(source, /session:\s*options\.yomitanSession\s*\?\?\s*undefined/);
|
||||
assert.equal(withSession.webPreferences?.session, yomitanSession);
|
||||
assert.equal(withoutSession.webPreferences?.session, undefined);
|
||||
});
|
||||
|
||||
39
src/core/services/overlay-window-options.ts
Normal file
39
src/core/services/overlay-window-options.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { BrowserWindowConstructorOptions, Session } from 'electron';
|
||||
import * as path from 'path';
|
||||
import type { OverlayWindowKind } from './overlay-window-input';
|
||||
|
||||
export function buildOverlayWindowOptions(
|
||||
kind: OverlayWindowKind,
|
||||
options: {
|
||||
isDev: boolean;
|
||||
yomitanSession?: Session | null;
|
||||
},
|
||||
): BrowserWindowConstructorOptions {
|
||||
const showNativeDebugFrame = process.platform === 'win32' && options.isDev;
|
||||
|
||||
return {
|
||||
show: false,
|
||||
width: 800,
|
||||
height: 600,
|
||||
x: 0,
|
||||
y: 0,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
hasShadow: false,
|
||||
focusable: true,
|
||||
acceptFirstMouse: true,
|
||||
...(process.platform === 'win32' ? { thickFrame: showNativeDebugFrame } : {}),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '..', '..', 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
webSecurity: true,
|
||||
session: options.yomitanSession ?? undefined,
|
||||
additionalArguments: [`--overlay-layer=${kind}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
handleOverlayWindowBeforeInputEvent,
|
||||
type OverlayWindowKind,
|
||||
} from './overlay-window-input';
|
||||
import { buildOverlayWindowOptions } from './overlay-window-options';
|
||||
|
||||
const logger = createLogger('main:overlay-window');
|
||||
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
||||
@@ -81,32 +82,7 @@ export function createOverlayWindow(
|
||||
yomitanSession?: Session | null;
|
||||
},
|
||||
): BrowserWindow {
|
||||
const showNativeDebugFrame = process.platform === 'win32' && options.isDev;
|
||||
const window = new BrowserWindow({
|
||||
show: false,
|
||||
width: 800,
|
||||
height: 600,
|
||||
x: 0,
|
||||
y: 0,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
hasShadow: false,
|
||||
focusable: true,
|
||||
acceptFirstMouse: true,
|
||||
...(process.platform === 'win32' ? { thickFrame: showNativeDebugFrame } : {}),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '..', '..', 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
webSecurity: true,
|
||||
session: options.yomitanSession ?? undefined,
|
||||
additionalArguments: [`--overlay-layer=${kind}`],
|
||||
},
|
||||
});
|
||||
const window = new BrowserWindow(buildOverlayWindowOptions(kind, options));
|
||||
|
||||
options.ensureOverlayWindowLevel(window);
|
||||
loadOverlayWindowLayer(window, kind);
|
||||
@@ -172,4 +148,5 @@ export function syncOverlayWindowLayer(window: BrowserWindow, layer: 'visible'):
|
||||
loadOverlayWindowLayer(window, layer);
|
||||
}
|
||||
|
||||
export { buildOverlayWindowOptions } from './overlay-window-options';
|
||||
export type { OverlayWindowKind } from './overlay-window-input';
|
||||
|
||||
11
src/main.ts
11
src/main.ts
@@ -376,6 +376,7 @@ import { createCharacterDictionaryAutoSyncRuntimeService } from './main/runtime/
|
||||
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
||||
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
|
||||
import {
|
||||
getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime,
|
||||
shouldForceOverrideYomitanAnkiServer,
|
||||
@@ -1348,7 +1349,7 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
||||
},
|
||||
importYomitanDictionary: async (zipPath) => {
|
||||
if (isYomitanExternalReadOnlyMode()) {
|
||||
logSkippedYomitanWrite(`importYomitanDictionary(${zipPath})`);
|
||||
logSkippedYomitanWrite(formatSkippedYomitanWriteAction('importYomitanDictionary', zipPath));
|
||||
return false;
|
||||
}
|
||||
await ensureYomitanExtensionLoaded();
|
||||
@@ -1359,7 +1360,9 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
||||
},
|
||||
deleteYomitanDictionary: async (dictionaryTitle) => {
|
||||
if (isYomitanExternalReadOnlyMode()) {
|
||||
logSkippedYomitanWrite(`deleteYomitanDictionary(${dictionaryTitle})`);
|
||||
logSkippedYomitanWrite(
|
||||
formatSkippedYomitanWriteAction('deleteYomitanDictionary', dictionaryTitle),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
await ensureYomitanExtensionLoaded();
|
||||
@@ -1370,7 +1373,9 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
||||
},
|
||||
upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => {
|
||||
if (isYomitanExternalReadOnlyMode()) {
|
||||
logSkippedYomitanWrite(`upsertYomitanDictionarySettings(${dictionaryTitle})`);
|
||||
logSkippedYomitanWrite(
|
||||
formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', dictionaryTitle),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
await ensureYomitanExtensionLoaded();
|
||||
|
||||
24
src/main/runtime/yomitan-read-only-log.test.ts
Normal file
24
src/main/runtime/yomitan-read-only-log.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { formatSkippedYomitanWriteAction } from './yomitan-read-only-log';
|
||||
|
||||
test('formatSkippedYomitanWriteAction redacts full filesystem paths to basenames', () => {
|
||||
assert.equal(
|
||||
formatSkippedYomitanWriteAction('importYomitanDictionary', '/tmp/private/merged.zip'),
|
||||
'importYomitanDictionary(merged.zip)',
|
||||
);
|
||||
});
|
||||
|
||||
test('formatSkippedYomitanWriteAction redacts dictionary titles', () => {
|
||||
assert.equal(
|
||||
formatSkippedYomitanWriteAction('deleteYomitanDictionary', 'SubMiner Character Dictionary'),
|
||||
'deleteYomitanDictionary(<redacted>)',
|
||||
);
|
||||
});
|
||||
|
||||
test('formatSkippedYomitanWriteAction falls back when value is blank', () => {
|
||||
assert.equal(
|
||||
formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', ' '),
|
||||
'upsertYomitanDictionarySettings(<redacted>)',
|
||||
);
|
||||
});
|
||||
25
src/main/runtime/yomitan-read-only-log.ts
Normal file
25
src/main/runtime/yomitan-read-only-log.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as path from 'path';
|
||||
|
||||
function redactSkippedYomitanWriteValue(
|
||||
actionName: 'importYomitanDictionary' | 'deleteYomitanDictionary' | 'upsertYomitanDictionarySettings',
|
||||
rawValue: string,
|
||||
): string {
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) {
|
||||
return '<redacted>';
|
||||
}
|
||||
|
||||
if (actionName === 'importYomitanDictionary') {
|
||||
const basename = path.basename(trimmed);
|
||||
return basename || '<redacted>';
|
||||
}
|
||||
|
||||
return '<redacted>';
|
||||
}
|
||||
|
||||
export function formatSkippedYomitanWriteAction(
|
||||
actionName: 'importYomitanDictionary' | 'deleteYomitanDictionary' | 'upsertYomitanDictionarySettings',
|
||||
rawValue: string,
|
||||
): string {
|
||||
return `${actionName}(${redactSkippedYomitanWriteValue(actionName, rawValue)})`;
|
||||
}
|
||||
Reference in New Issue
Block a user