fix(desktop): stop reading whole voice backgrounds to classify them (#1839)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:28:43 +02:00
1 parent c8ff177f85
commit 7ec155eac1
6 files changed
+58 -28

No files matched your search

@@ -319,7 +319,6 @@ export interface ElectronAPI {
clearThemeLocalFiles: () => Promise<void>;
importThemeDirectory: () => Promise<Array<ThemeDirectoryCssFile>>;
cacheVoiceBackgroundMedia: (options: VoiceBackgroundMediaCacheRequest) => Promise<VoiceBackgroundMediaCacheResult>;
resolveVoiceBackgroundMedia: (id: string) => Promise<VoiceBackgroundMediaCacheResult | null>;
readVoiceBackgroundMedia: (id: string) => Promise<VoiceBackgroundMediaReadResult | null>;
deleteVoiceBackgroundMedia: (id: string) => Promise<void>;
onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => () => void;
-1
View File
@@ -377,7 +377,6 @@ export interface ElectronAPI {
clearThemeLocalFiles?(): Promise<void>;
importThemeDirectory?(): Promise<Array<ThemeDirectoryCssFile>>;
cacheVoiceBackgroundMedia?(options: VoiceBackgroundMediaCacheRequest): Promise<VoiceBackgroundMediaCacheResult>;
resolveVoiceBackgroundMedia?(id: string): Promise<VoiceBackgroundMediaCacheResult | null>;
readVoiceBackgroundMedia?(id: string): Promise<VoiceBackgroundMediaReadResult | null>;
deleteVoiceBackgroundMedia?(id: string): Promise<void>;
getDesktopTroubleshootingSettings?(): Promise<DesktopTroubleshootingSettings>;
-1
View File
@@ -656,7 +656,6 @@ export interface ElectronAPI {
clearThemeLocalFiles: () => Promise<void>;
importThemeDirectory: () => Promise<Array<ThemeDirectoryCssFile>>;
cacheVoiceBackgroundMedia: (options: VoiceBackgroundMediaCacheRequest) => Promise<VoiceBackgroundMediaCacheResult>;
resolveVoiceBackgroundMedia: (id: string) => Promise<VoiceBackgroundMediaCacheResult | null>;
readVoiceBackgroundMedia: (id: string) => Promise<VoiceBackgroundMediaReadResult | null>;
deleteVoiceBackgroundMedia: (id: string) => Promise<void>;
getDesktopTroubleshootingSettings: () => Promise<DesktopTroubleshootingSettings>;
@@ -87,3 +87,19 @@ describe('ChromiumRuntime Windows capture policy', () => {
assert.deepEqual([...features], ['ExistingFeature']);
});
});
describe('ChromiumRuntime configured switch allowlist', () => {
test('rejects settings-supplied switches that would disable or shrink the HTTP cache', () => {
const {appendedSwitches, module} = loadChromiumRuntime('win32');
module.appendConfiguredChromiumSwitches([
'disable-http-cache',
'disk-cache-size',
'disk-cache-dir',
'disable-gpu-shader-disk-cache',
'disable_metal',
]);
assert.deepEqual(appendedSwitches, [{name: 'disable_metal', value: undefined}]);
});
});
@@ -15,6 +15,8 @@ const CACHE_DIR_NAME = 'voice-background-media';
const MAX_BACKGROUND_MEDIA_BYTES = 10 * 1024 * 1024;
const MAX_BACKGROUND_MEDIA_CACHE_FILES = 16;
const MAX_BACKGROUND_MEDIA_ID_LENGTH = 128;
const MAX_BACKGROUND_MEDIA_TEMP_AGE_MS = 60 * 60 * 1000;
const WEBP_HEADER_BYTES = 1024;
const MEDIA_TYPES = new Map<
string,
@@ -82,7 +84,7 @@ function isAnimatedWebp(data: Buffer): boolean {
if (data.subarray(12, 16).toString('ascii') === 'VP8X') {
return (data[20] & 0b0000_0010) !== 0;
}
return data.includes(Buffer.from('ANMF', 'ascii'));
return data.subarray(0, WEBP_HEADER_BYTES).includes(Buffer.from('ANMF', 'ascii'));
}
function classifyMediaKind(
@@ -95,6 +97,20 @@ function classifyMediaKind(
return mediaType.mediaKind;
}
async function readMediaHeader(filePath: string, byteLength: number): Promise<Buffer | null> {
const handle = await fs.open(filePath, 'r').catch(() => null);
if (!handle) return null;
try {
const header = Buffer.alloc(byteLength);
const {bytesRead} = await handle.read(header, 0, byteLength, 0);
return header.subarray(0, bytesRead);
} catch {
return null;
} finally {
await handle.close().catch(() => undefined);
}
}
async function findCachedVoiceBackgroundMedia(
id: string,
): Promise<{path: string; mimeType: string; mediaKind: VoiceBackgroundMediaKind} | null> {
@@ -110,9 +126,9 @@ async function findCachedVoiceBackgroundMedia(
const filePath = path.join(cacheDir, entry.name);
let mediaKind = mediaType.mediaKind;
if (mediaType.extension === 'webp') {
const data = await fs.readFile(filePath).catch(() => null);
if (!data) continue;
mediaKind = classifyMediaKind(mediaType, data);
const header = await readMediaHeader(filePath, WEBP_HEADER_BYTES);
if (!header) continue;
mediaKind = classifyMediaKind(mediaType, header);
}
return {
path: filePath,
@@ -124,20 +140,29 @@ async function findCachedVoiceBackgroundMedia(
}
async function trimVoiceBackgroundMediaCache(cacheDir: string): Promise<void> {
const entries = await fs.readdir(cacheDir, {withFileTypes: true});
const files = await Promise.all(
entries
.filter((entry) => entry.isFile())
.map(async (entry) => {
const filePath = path.join(cacheDir, entry.name);
const stats = await fs.stat(filePath);
return {filePath, mtimeMs: stats.mtimeMs};
}),
const entries = await fs.readdir(cacheDir, {withFileTypes: true}).catch(() => []);
const now = Date.now();
const cachedMedia: Array<{filePath: string; mtimeMs: number}> = [];
for (const entry of entries) {
if (!entry.isFile()) continue;
const filePath = path.join(cacheDir, entry.name);
const stats = await fs.stat(filePath).catch(() => null);
if (!stats) continue;
const extension = path.extname(entry.name).slice(1).toLowerCase();
if (!MEDIA_TYPES_BY_EXTENSION.has(extension)) {
if (now - stats.mtimeMs > MAX_BACKGROUND_MEDIA_TEMP_AGE_MS) {
await fs.rm(filePath, {force: true}).catch(() => undefined);
}
continue;
}
cachedMedia.push({filePath, mtimeMs: stats.mtimeMs});
}
if (cachedMedia.length <= MAX_BACKGROUND_MEDIA_CACHE_FILES) return;
cachedMedia.sort((a, b) => a.mtimeMs - b.mtimeMs);
const deleteCount = cachedMedia.length - MAX_BACKGROUND_MEDIA_CACHE_FILES;
await Promise.all(
cachedMedia.slice(0, deleteCount).map(({filePath}) => fs.rm(filePath, {force: true}).catch(() => undefined)),
);
if (files.length <= MAX_BACKGROUND_MEDIA_CACHE_FILES) return;
files.sort((a, b) => a.mtimeMs - b.mtimeMs);
const deleteCount = files.length - MAX_BACKGROUND_MEDIA_CACHE_FILES;
await Promise.all(files.slice(0, deleteCount).map(({filePath}) => fs.rm(filePath, {force: true})));
}
async function cacheVoiceBackgroundMedia(
@@ -165,12 +190,6 @@ async function cacheVoiceBackgroundMedia(
return {path: filePath, mediaKind};
}
async function resolveVoiceBackgroundMedia(id: string): Promise<VoiceBackgroundMediaCacheResult | null> {
const cached = await findCachedVoiceBackgroundMedia(id);
if (!cached) return null;
return {path: cached.path, mediaKind: cached.mediaKind};
}
async function readVoiceBackgroundMedia(id: string): Promise<VoiceBackgroundMediaReadResult | null> {
const cached = await findCachedVoiceBackgroundMedia(id);
if (!cached) return null;
@@ -200,7 +219,6 @@ export function registerVoiceBackgroundMediaCacheHandlers(): void {
ipcMain.handle('voice-background-media-cache:write', (_event, options: VoiceBackgroundMediaCacheRequest) =>
cacheVoiceBackgroundMedia(options),
);
ipcMain.handle('voice-background-media-cache:resolve', (_event, id: string) => resolveVoiceBackgroundMedia(id));
ipcMain.handle('voice-background-media-cache:read', (_event, id: string) => readVoiceBackgroundMedia(id));
ipcMain.handle('voice-background-media-cache:delete', (_event, id: string) => deleteVoiceBackgroundMedia(id));
}
-1
View File
@@ -357,7 +357,6 @@ const api: ElectronAPI = {
clearThemeLocalFiles: () => ipcRenderer.invoke('theme-local-files-clear'),
importThemeDirectory: () => ipcRenderer.invoke('theme-directory-import'),
cacheVoiceBackgroundMedia: (options) => ipcRenderer.invoke('voice-background-media-cache:write', options),
resolveVoiceBackgroundMedia: (id) => ipcRenderer.invoke('voice-background-media-cache:resolve', id),
readVoiceBackgroundMedia: (id) => ipcRenderer.invoke('voice-background-media-cache:read', id),
deleteVoiceBackgroundMedia: (id) => ipcRenderer.invoke('voice-background-media-cache:delete', id),
getDesktopTroubleshootingSettings: (): Promise<DesktopTroubleshootingSettings> =>