From 6c559be9d8d146ccfbfa59f853ea41f2174d2e58 Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:01:36 +0800 Subject: [PATCH 1/3] fix(windows): harden file index and UserAssist parsing --- .../windows/src/main/fileIndex/scanRules.test.ts | 5 +++++ desktop/windows/src/main/fileIndex/scanRules.ts | 4 ++-- desktop/windows/src/main/usage/userAssist.test.ts | 15 ++++++++++++--- desktop/windows/src/main/usage/userAssist.ts | 6 +++--- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/desktop/windows/src/main/fileIndex/scanRules.test.ts b/desktop/windows/src/main/fileIndex/scanRules.test.ts index 76380ba6ad..a58b92b535 100644 --- a/desktop/windows/src/main/fileIndex/scanRules.test.ts +++ b/desktop/windows/src/main/fileIndex/scanRules.test.ts @@ -8,6 +8,11 @@ describe('shouldVisitDir', () => { expect(shouldVisitDir('__pycache__', 1)).toBe(false) expect(shouldVisitDir('.Trash', 1)).toBe(false) }) + it('skips noise directories case-insensitively on Windows paths', () => { + expect(shouldVisitDir('Node_Modules', 1)).toBe(false) + expect(shouldVisitDir('.GIT', 1)).toBe(false) + expect(shouldVisitDir('__PYCACHE__', 1)).toBe(false) + }) it('visits normal dirs within depth', () => { expect(shouldVisitDir('src', 1)).toBe(true) expect(shouldVisitDir('src', MAX_DEPTH)).toBe(true) diff --git a/desktop/windows/src/main/fileIndex/scanRules.ts b/desktop/windows/src/main/fileIndex/scanRules.ts index 9b8f8d6810..7d223c0272 100644 --- a/desktop/windows/src/main/fileIndex/scanRules.ts +++ b/desktop/windows/src/main/fileIndex/scanRules.ts @@ -1,10 +1,10 @@ export const MAX_DEPTH = 3 export const MAX_FILE_SIZE = 500 * 1024 * 1024 // 500 MB, matching macOS -export const SKIP_DIRS = new Set(['.Trash', 'node_modules', '.git', '__pycache__']) +export const SKIP_DIRS = new Set(['.trash', 'node_modules', '.git', '__pycache__']) // True when a subdirectory at `depth` should be descended into. export function shouldVisitDir(name: string, depth: number): boolean { - return depth <= MAX_DEPTH && !SKIP_DIRS.has(name) + return depth <= MAX_DEPTH && !SKIP_DIRS.has(name.toLowerCase()) } // True when a file of `sizeBytes` should be recorded. diff --git a/desktop/windows/src/main/usage/userAssist.test.ts b/desktop/windows/src/main/usage/userAssist.test.ts index 1805d6026a..62dcea7e43 100644 --- a/desktop/windows/src/main/usage/userAssist.test.ts +++ b/desktop/windows/src/main/usage/userAssist.test.ts @@ -10,9 +10,9 @@ function blob(opts: { len?: number }): Buffer { const b = Buffer.alloc(opts.len ?? 72) - if (b.length >= 8) b.writeInt32LE(opts.runCount ?? 0, 4) - if (b.length >= 12) b.writeInt32LE(opts.focusCount ?? 0, 8) - if (b.length >= 16) b.writeInt32LE(opts.focusMs ?? 0, 12) + if (b.length >= 8) b.writeUInt32LE(opts.runCount ?? 0, 4) + if (b.length >= 12) b.writeUInt32LE(opts.focusCount ?? 0, 8) + if (b.length >= 16) b.writeUInt32LE(opts.focusMs ?? 0, 12) if (b.length >= 68 && opts.lastUsedMs != null) { // ms epoch -> Windows FILETIME (100ns ticks since 1601-01-01) const ticks = (BigInt(opts.lastUsedMs) + 11644473600000n) * 10000n @@ -39,6 +39,15 @@ describe('parseUserAssistData', () => { expect(p!.focusCount).toBe(757) expect(p!.focusSeconds).toBe(43_147) // rounded }) + it('reads high-bit DWORD counters as unsigned values', () => { + const p = parseUserAssistData( + blob({ runCount: 3_000_000_000, focusCount: 4_000_000_000, focusMs: 3_000_000_000 }) + ) + expect(p).not.toBeNull() + expect(p!.runCount).toBe(3_000_000_000) + expect(p!.focusCount).toBe(4_000_000_000) + expect(p!.focusSeconds).toBe(3_000_000) + }) it('reads last-used from the FILETIME at offset 60', () => { const when = Date.UTC(2026, 5, 3, 12, 0, 0) const p = parseUserAssistData(blob({ focusMs: 1000, lastUsedMs: when })) diff --git a/desktop/windows/src/main/usage/userAssist.ts b/desktop/windows/src/main/usage/userAssist.ts index 0199980cb9..9a24333da4 100644 --- a/desktop/windows/src/main/usage/userAssist.ts +++ b/desktop/windows/src/main/usage/userAssist.ts @@ -50,15 +50,15 @@ export function rot13(s: string): string { // hold the focus-time field (control/sentinel entries can be tiny). export function parseUserAssistData(data: Buffer): ParsedUserAssist | null { if (data.length < OFF_FOCUS_MS + 4) return null - const focusMs = data.readInt32LE(OFF_FOCUS_MS) + const focusMs = data.readUInt32LE(OFF_FOCUS_MS) let lastUsed = 0 if (data.length >= OFF_LAST_USED_FILETIME + 8) { const ticks = data.readBigUInt64LE(OFF_LAST_USED_FILETIME) if (ticks > 0n) lastUsed = Number(ticks / 10_000n - FILETIME_UNIX_OFFSET_MS) } return { - runCount: data.readInt32LE(OFF_RUN_COUNT), - focusCount: data.readInt32LE(OFF_FOCUS_COUNT), + runCount: data.readUInt32LE(OFF_RUN_COUNT), + focusCount: data.readUInt32LE(OFF_FOCUS_COUNT), focusSeconds: Math.round(focusMs / 1000), lastUsed } From b7b9ad5f49651941b668e463cd21bd15fe994ec2 Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:08:22 +0800 Subject: [PATCH 2/3] fix(windows): keep skip dir export verbatim --- desktop/windows/src/main/fileIndex/scanRules.test.ts | 3 ++- desktop/windows/src/main/fileIndex/scanRules.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/desktop/windows/src/main/fileIndex/scanRules.test.ts b/desktop/windows/src/main/fileIndex/scanRules.test.ts index a58b92b535..abad454c38 100644 --- a/desktop/windows/src/main/fileIndex/scanRules.test.ts +++ b/desktop/windows/src/main/fileIndex/scanRules.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { shouldVisitDir, shouldIndexFile, MAX_DEPTH, MAX_FILE_SIZE } from './scanRules' +import { shouldVisitDir, shouldIndexFile, SKIP_DIRS, MAX_DEPTH, MAX_FILE_SIZE } from './scanRules' describe('shouldVisitDir', () => { it('skips noise directories', () => { @@ -7,6 +7,7 @@ describe('shouldVisitDir', () => { expect(shouldVisitDir('.git', 1)).toBe(false) expect(shouldVisitDir('__pycache__', 1)).toBe(false) expect(shouldVisitDir('.Trash', 1)).toBe(false) + expect(SKIP_DIRS.has('.Trash')).toBe(true) }) it('skips noise directories case-insensitively on Windows paths', () => { expect(shouldVisitDir('Node_Modules', 1)).toBe(false) diff --git a/desktop/windows/src/main/fileIndex/scanRules.ts b/desktop/windows/src/main/fileIndex/scanRules.ts index 7d223c0272..eeaa4b247c 100644 --- a/desktop/windows/src/main/fileIndex/scanRules.ts +++ b/desktop/windows/src/main/fileIndex/scanRules.ts @@ -1,10 +1,11 @@ export const MAX_DEPTH = 3 export const MAX_FILE_SIZE = 500 * 1024 * 1024 // 500 MB, matching macOS -export const SKIP_DIRS = new Set(['.trash', 'node_modules', '.git', '__pycache__']) +export const SKIP_DIRS = new Set(['.Trash', 'node_modules', '.git', '__pycache__']) +const NORMALIZED_SKIP_DIRS = new Set([...SKIP_DIRS].map((name) => name.toLowerCase())) // True when a subdirectory at `depth` should be descended into. export function shouldVisitDir(name: string, depth: number): boolean { - return depth <= MAX_DEPTH && !SKIP_DIRS.has(name.toLowerCase()) + return depth <= MAX_DEPTH && !NORMALIZED_SKIP_DIRS.has(name.toLowerCase()) } // True when a file of `sizeBytes` should be recorded. From 86dfe2e724fe724d1d8985e64ee7d79c023e0d00 Mon Sep 17 00:00:00 2001 From: 491034170 <142008960+491034170@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:43:42 +0800 Subject: [PATCH 3/3] Format UserAssist edge-case tests --- .../windows/src/main/usage/userAssist.test.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/desktop/windows/src/main/usage/userAssist.test.ts b/desktop/windows/src/main/usage/userAssist.test.ts index 62dcea7e43..9938c922d8 100644 --- a/desktop/windows/src/main/usage/userAssist.test.ts +++ b/desktop/windows/src/main/usage/userAssist.test.ts @@ -72,7 +72,9 @@ describe('friendlyAppName', () => { expect(friendlyAppName('Telegram.TelegramDesktop')).toBe('TelegramDesktop') }) it('strips the package-family hash and !App suffix from packaged AUMIDs', () => { - expect(friendlyAppName('Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic')).toBe('ZuneMusic') + expect(friendlyAppName('Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic')).toBe( + 'ZuneMusic' + ) expect(friendlyAppName('5319275A.WhatsAppDesktop_cv1g1gvanyjgm!App')).toBe('WhatsAppDesktop') // Uses the package-name segment, not the !Activatable id. "SpotifyMusic" // still matches an indexed "Spotify" via rankApps' containment rule. @@ -83,11 +85,15 @@ describe('friendlyAppName', () => { }) it('uses the exe basename (without .exe) for full paths', () => { expect(friendlyAppName('C:\\Users\\me\\AppData\\Local\\Programs\\Warp\\Warp.exe')).toBe('Warp') - expect(friendlyAppName('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe')).toBe('chrome') + expect(friendlyAppName('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe')).toBe( + 'chrome' + ) }) it('ignores a leading KNOWNFOLDERID GUID segment in a path', () => { expect( - friendlyAppName('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe') + friendlyAppName( + '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe' + ) ).toBe('powershell') }) it('returns null for empty / GUID-only names', () => { @@ -100,9 +106,15 @@ describe('aggregateUserAssist', () => { it('decodes, drops control entries, and sums focus time per friendly name', () => { const raw = [ { name: rot13('UEME_CTLSESSION'), data: blob({ focusMs: 999_999 }) }, - { name: rot13('dev.warp.Warp'), data: blob({ focusMs: 60_000, runCount: 3, lastUsedMs: 100 }) }, + { + name: rot13('dev.warp.Warp'), + data: blob({ focusMs: 60_000, runCount: 3, lastUsedMs: 100 }) + }, // same friendly name via a full path -> merged - { name: rot13('C:\\x\\Warp\\Warp.exe'), data: blob({ focusMs: 30_000, runCount: 2, lastUsedMs: 200 }) }, + { + name: rot13('C:\\x\\Warp\\Warp.exe'), + data: blob({ focusMs: 30_000, runCount: 2, lastUsedMs: 200 }) + }, { name: rot13('Chrome'), data: blob({ focusMs: 120_000, lastUsedMs: 50 }) } ] const out = aggregateUserAssist(raw)