// tools/privacy-scan.ts: the release gate over every desktop artifact. Made-up terms only (the real ones live in a // file outside the repository). import { describe, expect, test } from 'bun:test'; import { spawnSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { collectTerms, kindOf, mask, parseTerms, scan, scanBuffer, type Term } from '../tools/privacy-scan'; const T: Term[] = [{ text: 'Quillfeather', short: false }, { text: 'qz', short: true }]; // home folders are put together at run time, so tools/scrub.ts (which forbids them in the repository) passes const home = (root: string, name: string): string => `${root}${name}`; describe('privacy scan', () => { test('terms: comments and blanks skipped, never printed in full', () => { expect(parseTerms('# names\nQuillfeather\n\n qz \n')).toEqual(['Quillfeather', 'qz']); expect(mask('Quillfeather')).toBe('Q***'); const dir = mkdtempSync(join(tmpdir(), 'sb-terms-')); writeFileSync(join(dir, 't.txt'), `Quillfeather\nCRT\n${['hi', 'crt.fyi'].join('@')}\n`); // the release identity is never a forbidden term expect(collectTerms(join(dir, 't.txt'), false).map(t => t.text)).toEqual(['Quillfeather']); }); test('finds a term in any case, in UTF-8 and UTF-16LE', () => { const hits = scanBuffer('bin', Buffer.concat([Buffer.from('xx QUILLFEATHER yy'), Buffer.from('built by quillFeather', 'utf16le')]), T); expect(hits.map(h => h.encoding)).toEqual(['utf8', 'utf16le']); expect(hits.every(h => h.what === 'term Q***')).toBe(true); expect(scanBuffer('bin', Buffer.from('nothing to see'), T)).toEqual([]); }); test('a short term counts only in a path', () => { expect(scanBuffer('bin', Buffer.from('jazz quiz qz-mode'), T)).toEqual([]); expect(scanBuffer('bin', Buffer.from('/opt/qz/src'), T).some(h => h.what === 'term q***')).toBe(true); }); test('home folders of any account, generic ones allowed', () => { const hits = scanBuffer('bin', Buffer.from(`${home('/Users/', 'someone')}/work /home/runner/work ${home('C:\\Users\\', 'other')}\\x /Users/you/app /home/$USER`), []); expect(hits.map(h => h.what)).toEqual(['home folder of an account (s***)', 'home folder of an account (o***)']); }); test('folders, archives and debug files', () => { const dir = mkdtempSync(join(tmpdir(), 'sb-scan-')); mkdirSync(join(dir, 'pkg/My.app/Contents'), { recursive: true }); writeFileSync(join(dir, 'pkg/My.app/Contents/clean.txt'), 'all good'); writeFileSync(join(dir, 'pkg/My.app/Contents/leak.bin'), Buffer.from(`panic at ${home('/Users/', 'quillfeather')}/src/main.rs`, 'utf8')); mkdirSync(join(dir, 'pkg/My.app.dSYM')); expect(spawnSync('tar', ['-czf', join(dir, 'bundle.tar.gz'), '-C', join(dir, 'pkg'), '.']).status).toBe(0); const r = scan([join(dir, 'bundle.tar.gz')], null, false); expect(r.errors).toEqual([]); const shown = r.hits.map(h => `${h.file}: ${h.what}`); expect(shown.some(s => s.startsWith('bundle.tar.gz!/') && s.includes('leak.bin') && s.includes('home folder'))).toBe(true); expect(shown.some(s => s.includes('My.app.dSYM: debug file'))).toBe(true); expect(r.files).toBeGreaterThanOrEqual(3); const clean = scan([join(dir, 'pkg/My.app/Contents/clean.txt')], null, false); expect(clean.hits).toEqual([]); expect(scan([join(dir, 'missing.dmg')], null, false).errors).toHaveLength(1); }); test('container kinds', () => { expect(kindOf('a/storebound_0.1.0_universal.dmg', new Uint8Array())).toBe('dmg'); expect(kindOf('storebound_0.1.0_universal.app.tar.gz', new Uint8Array())).toBe('tar'); expect(kindOf('storebound_0.1.0_amd64.deb', new Uint8Array())).toBe('deb'); expect(kindOf('storebound_0.1.0_amd64.AppImage', new Uint8Array())).toBe('appimage'); expect(kindOf('storebound_0.1.0_x64-setup.exe', Buffer.from('MZ....Nullsoft Install System'))).toBe('nsis'); expect(kindOf('storebound-desktop.exe', Buffer.from('MZ'))).toBe('file'); }); });