fix(desktop): keep non-ASCII desktop entries out of Open In matching (#3403)
desktopEntryMatchesApp normalized haystack values without dropping empty
ones, so a .desktop entry with no ASCII letters or digits in Name, id,
file name, or Exec (e.g. Name=抖音) normalized to "" and
needle.includes("") matched every requested app — hijacking the
installed-apps list and Open In launch specs. Empty normalized haystack
values are now filtered, matching the existing needles handling.
discovered-apps.json gains a version field (INSTALLED_APPS_CACHE_VERSION
= 2); caches written before the fix are treated as stale and refresh
through the existing TTL-expiry path instead of serving the poisoned
list for the rest of the 24h TTL.
This commit is contained in:
@@ -145,7 +145,11 @@ export const readLinuxDesktopEntries = async (options = {}) => {
|
|||||||
const desktopEntryMatchesApp = (entry, appName, appId = '') => {
|
const desktopEntryMatchesApp = (entry, appName, appId = '') => {
|
||||||
const needles = uniqueStrings([appName, appId]).flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)]).filter(Boolean);
|
const needles = uniqueStrings([appName, appId]).flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)]).filter(Boolean);
|
||||||
const haystacks = [entry.name, entry.id, path.basename(entry.filePath || ''), entry.exec]
|
const haystacks = [entry.name, entry.id, path.basename(entry.filePath || ''), entry.exec]
|
||||||
.flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)]);
|
.flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)])
|
||||||
|
// A value with no ASCII letters or digits (e.g. a CJK-only Name) normalizes to the empty
|
||||||
|
// string, and needle.includes('') is true for every app — drop it so such entries can
|
||||||
|
// only match through a field that still carries comparable text.
|
||||||
|
.filter(Boolean);
|
||||||
return needles.some((needle) => haystacks.some((haystack) => haystack === needle || haystack.includes(needle) || needle.includes(haystack)));
|
return needles.some((needle) => haystacks.some((haystack) => haystack === needle || haystack.includes(needle) || needle.includes(haystack)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildLinuxOpenSpecs,
|
||||||
|
filterLinuxInstalledApps,
|
||||||
|
parseDesktopEntry,
|
||||||
|
} from './linux-app-discovery.mjs';
|
||||||
|
|
||||||
|
const NON_ASCII_ONLY_ENTRY = `[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=抖音
|
||||||
|
Exec=/usr/bin/example --app-url=https://www.douyin.com/
|
||||||
|
Icon=example`;
|
||||||
|
|
||||||
|
const TEST_ENV = { PATH: '/nonexistent-openchamber-test-bin' };
|
||||||
|
|
||||||
|
const parseEntryAt = (content, id) => parseDesktopEntry(content, `/usr/share/applications/${id}.desktop`);
|
||||||
|
|
||||||
|
test('still parses desktop entries whose Name has no ASCII letters or digits', () => {
|
||||||
|
const entry = parseEntryAt(NON_ASCII_ONLY_ENTRY, 'example');
|
||||||
|
assert.ok(entry);
|
||||||
|
assert.equal(entry.name, '抖音');
|
||||||
|
assert.equal(entry.exec, '/usr/bin/example --app-url=https://www.douyin.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a non-ASCII-only desktop entry does not mark every app as installed', async () => {
|
||||||
|
const entry = parseEntryAt(NON_ASCII_ONLY_ENTRY, 'example');
|
||||||
|
const installed = await filterLinuxInstalledApps(
|
||||||
|
['Visual Studio Code', 'Cursor', 'Sublime Text'],
|
||||||
|
{ entries: [entry] },
|
||||||
|
);
|
||||||
|
assert.deepEqual(installed, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Open In specs never launch a non-ASCII-only entry for another app', () => {
|
||||||
|
const entry = parseEntryAt(NON_ASCII_ONLY_ENTRY, 'example');
|
||||||
|
const specs = buildLinuxOpenSpecs({
|
||||||
|
targetPath: '/tmp/project',
|
||||||
|
appId: 'vscode',
|
||||||
|
appName: 'Visual Studio Code',
|
||||||
|
entries: [entry],
|
||||||
|
env: TEST_ENV,
|
||||||
|
});
|
||||||
|
assert.deepEqual(specs, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ASCII desktop entries still match their own app and build their own launch spec', async () => {
|
||||||
|
const entry = parseEntryAt(`[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Visual Studio Code
|
||||||
|
Exec=/usr/bin/code %F
|
||||||
|
Icon=code`, 'code');
|
||||||
|
const installed = await filterLinuxInstalledApps(
|
||||||
|
['Visual Studio Code', 'Cursor'],
|
||||||
|
{ entries: [entry] },
|
||||||
|
);
|
||||||
|
assert.deepEqual(installed, ['Visual Studio Code']);
|
||||||
|
|
||||||
|
const specs = buildLinuxOpenSpecs({
|
||||||
|
targetPath: '/tmp/project',
|
||||||
|
appId: 'vscode',
|
||||||
|
appName: 'Visual Studio Code',
|
||||||
|
entries: [entry],
|
||||||
|
env: TEST_ENV,
|
||||||
|
});
|
||||||
|
assert.equal(specs.length, 1);
|
||||||
|
assert.equal(specs[0].program, '/usr/bin/code');
|
||||||
|
assert.deepEqual(specs[0].args, ['/tmp/project']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('entries mixing ASCII and non-ASCII still match through their ASCII part', () => {
|
||||||
|
const entry = parseEntryAt(`[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=VSCode 抖音版
|
||||||
|
Exec=/usr/local/bin/vscode-douyin %F
|
||||||
|
Icon=vscode-douyin`, 'vscode-douyin');
|
||||||
|
const specs = buildLinuxOpenSpecs({
|
||||||
|
targetPath: '/tmp/project',
|
||||||
|
appId: 'vscode',
|
||||||
|
appName: 'Visual Studio Code',
|
||||||
|
entries: [entry],
|
||||||
|
env: TEST_ENV,
|
||||||
|
});
|
||||||
|
assert.equal(specs.length, 1);
|
||||||
|
assert.equal(specs[0].program, '/usr/local/bin/vscode-douyin');
|
||||||
|
});
|
||||||
@@ -246,6 +246,9 @@ const GITHUB_FEATURE_REQUEST_URL = 'https://github.com/openchamber/openchamber/i
|
|||||||
const DISCORD_INVITE_URL = 'https://discord.gg/ZYRSdnwwKA';
|
const DISCORD_INVITE_URL = 'https://discord.gg/ZYRSdnwwKA';
|
||||||
const INSTALLED_APPS_CACHE_TTL_SECS = 60 * 60 * 24;
|
const INSTALLED_APPS_CACHE_TTL_SECS = 60 * 60 * 24;
|
||||||
const INSTALLED_APPS_CACHE_FILE = 'discovered-apps.json';
|
const INSTALLED_APPS_CACHE_FILE = 'discovered-apps.json';
|
||||||
|
// Bump when discovery results change shape or matching semantics change, so cached
|
||||||
|
// entries written by an older build are treated as stale and refresh immediately.
|
||||||
|
const INSTALLED_APPS_CACHE_VERSION = 2;
|
||||||
const LINUX_DESKTOP_ENTRIES_CACHE_TTL_MS = 30_000;
|
const LINUX_DESKTOP_ENTRIES_CACHE_TTL_MS = 30_000;
|
||||||
const OPENCODE_SHUTDOWN_GRACE_MS = 100;
|
const OPENCODE_SHUTDOWN_GRACE_MS = 100;
|
||||||
const { autoUpdater } = updaterPkg;
|
const { autoUpdater } = updaterPkg;
|
||||||
@@ -4428,11 +4431,13 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
|||||||
}
|
}
|
||||||
const cachedApps = Array.isArray(cache?.apps) ? cache.apps : [];
|
const cachedApps = Array.isArray(cache?.apps) ? cache.apps : [];
|
||||||
const hasCache = Boolean(cache);
|
const hasCache = Boolean(cache);
|
||||||
const isCacheStale = !cache || (now - Number(cache.updatedAt || 0)) > INSTALLED_APPS_CACHE_TTL_SECS;
|
const isCacheStale = !cache
|
||||||
|
|| cache.version !== INSTALLED_APPS_CACHE_VERSION
|
||||||
|
|| (now - Number(cache.updatedAt || 0)) > INSTALLED_APPS_CACHE_TTL_SECS;
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const apps = await buildPlatformInstalledApps(Array.isArray(args.apps) ? args.apps : []);
|
const apps = await buildPlatformInstalledApps(Array.isArray(args.apps) ? args.apps : []);
|
||||||
await fsp.mkdir(path.dirname(cachePath), { recursive: true });
|
await fsp.mkdir(path.dirname(cachePath), { recursive: true });
|
||||||
await fsp.writeFile(cachePath, JSON.stringify({ updatedAt: now, apps }, null, 2));
|
await fsp.writeFile(cachePath, JSON.stringify({ version: INSTALLED_APPS_CACHE_VERSION, updatedAt: now, apps }, null, 2));
|
||||||
emitToAllWindows('openchamber:installed-apps-updated', apps);
|
emitToAllWindows('openchamber:installed-apps-updated', apps);
|
||||||
};
|
};
|
||||||
if (process.platform !== 'darwin' && process.platform !== 'win32' && process.platform !== 'linux') {
|
if (process.platform !== 'darwin' && process.platform !== 'win32' && process.platform !== 'linux') {
|
||||||
|
|||||||
Reference in New Issue
Block a user