mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -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 |
|
| 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:
|
External-profile mode behavior:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
import { createResolveContext } from './context';
|
import { createResolveContext } from './context';
|
||||||
import { applyIntegrationConfig } from './integrations';
|
import { applyIntegrationConfig } from './integrations';
|
||||||
|
|
||||||
@@ -139,5 +140,8 @@ test('yomitan externalProfilePath expands leading tilde to the current home dire
|
|||||||
|
|
||||||
applyIntegrationConfig(context);
|
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 test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs';
|
import { buildOverlayWindowOptions } from './overlay-window-options';
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
test('overlay window config explicitly disables renderer sandbox for preload compatibility', () => {
|
test('overlay window config explicitly disables renderer sandbox for preload compatibility', () => {
|
||||||
const sourcePath = path.join(process.cwd(), 'src/core/services/overlay-window.ts');
|
const options = buildOverlayWindowOptions('visible', {
|
||||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
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', () => {
|
test('overlay window config uses the provided Yomitan session when available', () => {
|
||||||
const sourcePath = path.join(process.cwd(), 'src/core/services/overlay-window.ts');
|
const yomitanSession = { id: 'session' } as never;
|
||||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
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,
|
handleOverlayWindowBeforeInputEvent,
|
||||||
type OverlayWindowKind,
|
type OverlayWindowKind,
|
||||||
} from './overlay-window-input';
|
} from './overlay-window-input';
|
||||||
|
import { buildOverlayWindowOptions } from './overlay-window-options';
|
||||||
|
|
||||||
const logger = createLogger('main:overlay-window');
|
const logger = createLogger('main:overlay-window');
|
||||||
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
||||||
@@ -81,32 +82,7 @@ export function createOverlayWindow(
|
|||||||
yomitanSession?: Session | null;
|
yomitanSession?: Session | null;
|
||||||
},
|
},
|
||||||
): BrowserWindow {
|
): BrowserWindow {
|
||||||
const showNativeDebugFrame = process.platform === 'win32' && options.isDev;
|
const window = new BrowserWindow(buildOverlayWindowOptions(kind, options));
|
||||||
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}`],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
options.ensureOverlayWindowLevel(window);
|
options.ensureOverlayWindowLevel(window);
|
||||||
loadOverlayWindowLayer(window, kind);
|
loadOverlayWindowLayer(window, kind);
|
||||||
@@ -172,4 +148,5 @@ export function syncOverlayWindowLayer(window: BrowserWindow, layer: 'visible'):
|
|||||||
loadOverlayWindowLayer(window, layer);
|
loadOverlayWindowLayer(window, layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { buildOverlayWindowOptions } from './overlay-window-options';
|
||||||
export type { OverlayWindowKind } from './overlay-window-input';
|
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 { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||||
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
||||||
|
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
|
||||||
import {
|
import {
|
||||||
getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime,
|
getPreferredYomitanAnkiServerUrl as getPreferredYomitanAnkiServerUrlRuntime,
|
||||||
shouldForceOverrideYomitanAnkiServer,
|
shouldForceOverrideYomitanAnkiServer,
|
||||||
@@ -1348,7 +1349,7 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
|||||||
},
|
},
|
||||||
importYomitanDictionary: async (zipPath) => {
|
importYomitanDictionary: async (zipPath) => {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (isYomitanExternalReadOnlyMode()) {
|
||||||
logSkippedYomitanWrite(`importYomitanDictionary(${zipPath})`);
|
logSkippedYomitanWrite(formatSkippedYomitanWriteAction('importYomitanDictionary', zipPath));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await ensureYomitanExtensionLoaded();
|
await ensureYomitanExtensionLoaded();
|
||||||
@@ -1359,7 +1360,9 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
|||||||
},
|
},
|
||||||
deleteYomitanDictionary: async (dictionaryTitle) => {
|
deleteYomitanDictionary: async (dictionaryTitle) => {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (isYomitanExternalReadOnlyMode()) {
|
||||||
logSkippedYomitanWrite(`deleteYomitanDictionary(${dictionaryTitle})`);
|
logSkippedYomitanWrite(
|
||||||
|
formatSkippedYomitanWriteAction('deleteYomitanDictionary', dictionaryTitle),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await ensureYomitanExtensionLoaded();
|
await ensureYomitanExtensionLoaded();
|
||||||
@@ -1370,7 +1373,9 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
|||||||
},
|
},
|
||||||
upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => {
|
upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => {
|
||||||
if (isYomitanExternalReadOnlyMode()) {
|
if (isYomitanExternalReadOnlyMode()) {
|
||||||
logSkippedYomitanWrite(`upsertYomitanDictionarySettings(${dictionaryTitle})`);
|
logSkippedYomitanWrite(
|
||||||
|
formatSkippedYomitanWriteAction('upsertYomitanDictionarySettings', dictionaryTitle),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await ensureYomitanExtensionLoaded();
|
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