// A NUL byte in a source file is invisible in an editor and changes what git thinks the file IS: // git classifies any file containing one as binary, and from then on a diff of it carries no body. // That is not cosmetic. A forge that ships pull requests as patches — tangled builds every round // with `git format-patch` — then emits `Binary files a/x and b/x differ` for a `.ts` file, and the // round cannot be applied by the other side. `packages/ui/src/lib/store.ts` reached `main` with // three of them, written as literal bytes where `\u0000` was meant, and the first sign of it was a // pull request reporting a conflict against a branch that merged as a fast-forward. // // So: every tracked file that is not a known binary asset must be free of NUL bytes. Escapes are // unaffected — `"\u0000"` is six ASCII characters and produces the identical string at runtime. import { spawnSync } from 'node:child_process' import { readFileSync } from 'node:fs' /** Extensions whose CONTENT is legitimately binary. Everything else must be text. */ const BINARY = new Set([ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico', 'icns', 'bmp', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'wasm', 'gz', 'zip', 'tar', 'br', 'zst', 'pdf', 'mp4', 'webm', 'mp3', 'wav', 'ogg', ]) const listed = spawnSync('git', ['ls-files', '-z'], { encoding: 'buffer', maxBuffer: 1 << 28 }) if (listed.status !== 0) { console.error(`check-no-nul: git ls-files failed: ${listed.stderr?.toString() ?? ''}`) process.exit(2) } const paths = listed.stdout .toString('utf8') .split('\0') .filter((path) => path.length > 0) const offenders = [] for (const path of paths) { const dot = path.lastIndexOf('.') const extension = dot === -1 ? '' : path.slice(dot + 1).toLowerCase() if (BINARY.has(extension)) continue let bytes try { bytes = readFileSync(path) } catch { continue // a submodule, a broken symlink, or a path removed since ls-files ran } const at = bytes.indexOf(0) if (at === -1) continue const line = bytes.subarray(0, at).toString('utf8').split('\n').length const total = bytes.reduce((count, byte) => (byte === 0 ? count + 1 : count), 0) offenders.push({ path, line, total }) } if (offenders.length > 0) { console.error('check-no-nul: NUL bytes in files git will therefore treat as BINARY:\n') for (const { path, line, total } of offenders) { console.error(` ${path}:${line} (${total} NUL byte${total === 1 ? '' : 's'})`) } console.error('\nWrite the escape instead of the raw byte — `\\u0000` in a JS/TS string or template') console.error('literal produces the identical value and keeps the file text.') console.error('If the file really is binary, add its extension to BINARY in scripts/check-no-nul.mjs.') process.exit(1) } console.log(`check-no-nul: ${paths.length} tracked files, no NUL bytes outside binary assets`)