Add WASM hidden-scan engine for the scan page - #114
Conversation
Compile fuckmark-hidden-scan-v1 to a 45 KiB wasm32 module with no crate dependencies. The scan page and Chromium popup load it when fetch works so text never leaves the machine; scan.js stays the file:// fallback. Replay frozen vectors and the full Unicode plane against the JS port. Co-authored-by: Byte271 <byte271@users.noreply.github.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1176b1a383
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const encoder = new TextEncoder(); | ||
|
|
||
| function callScan(text, language, categories, maxFindings) { | ||
| const textBytes = encoder.encode(String(text || "")); |
There was a problem hiding this comment.
Detect lone surrogates before UTF-8 encoding
When the scanned JavaScript string contains an unpaired UTF-16 surrogate, TextEncoder replaces it with U+FFFD before Rust sees it. The WASM engine therefore reports zero findings for the high-severity surrogate category, while cleanText("\uD800") returns the replacement character instead of removing it as the fallback engine does. Detect and handle lone surrogates before encoding, or route such strings through the JS fallback.
Useful? React with 👍 / 👎.
| pub fn classify_context(text: &str, index: usize, role: &str, category: &str) -> &'static str { | ||
| let chars: Vec<char> = text.chars().collect(); |
There was a problem hiding this comment.
Reuse the parsed character buffer for context checks
For input containing many hidden characters, this rebuilds a Vec<char> for every finding because scan_text invokes classify_context inside its scan loop. Dense input such as a file of zero-width characters therefore takes quadratic time; the unbounded file picker makes even tens of thousands of characters freeze the scan page for seconds. Pass the already-created character buffer or neighboring characters into this function so scanning remains linear.
Useful? React with 👍 / 👎.
| if (typeof globalThis.loadFuckMarkScanWasm === "function") { | ||
| try { | ||
| await globalThis.loadFuckMarkScanWasm("fuckmark_scan.wasm"); | ||
| } catch (_err) { |
There was a problem hiding this comment.
Allow WebAssembly in the extension CSP
In the Chromium MV3 extension popup, this instantiation runs under the default extension-page CSP, while editors/browser/manifest.json does not opt into script-src 'wasm-unsafe-eval'. Chromium consequently rejects WebAssembly.instantiate; the exception is silently caught and the popup always remains on scan.js, so the advertised extension WASM path is never used. Add the permitted extension-page CSP directive to the manifest.
Useful? React with 👍 / 👎.
| fn parse_categories(raw: &str) -> Option<Vec<&'static str>> { | ||
| let trimmed = raw.trim(); | ||
| if trimmed.is_empty() { | ||
| return None; | ||
| } |
There was a problem hiding this comment.
Preserve empty category selections
When a caller passes [] to scanText or cleanText, the wrapper serializes it as an empty string and this parser converts that to None, which means “all categories” in the Rust scan and clean paths. The existing JS engine instead treats an empty array as an empty selection, so switching to WASM unexpectedly reports every category or removes every hidden character. Distinguish an explicitly empty list from the null/all-categories case in the ABI.
Useful? React with 👍 / 👎.
Route lone UTF-16 surrogates through the JS fallback, allow wasm-unsafe-eval in the extension CSP, treat empty category arrays as empty selections, and reuse the parsed char buffer so dense scans stay linear. Co-authored-by: Byte271 <byte271@users.noreply.github.com>
Compile
fuckmark-hidden-scan-v1to a no-dependencywasm32module so the scan page (and the Chromium popup) can classify text on-device.scan.jsremains thefile://fallback. Page reveal and paste-safe stay on the synchronous JS port.Stack: merge #112, then #113, then this. Base is
cursor/browser-extension-cdf8.What landed
crates/fuckmark-scan(same category table aseditors/vscode/scan.js, language-aware Trojan Source roles, clean/autofix).fuckmark_scan.wasm(~45 KiB) plusscan_wasm.jsloader, copied next to the scan page, packaged web UI, and browser popup.fuckmark webserves.wasmasapplication/wasm.WASM hidden-scanrunscargo testand a wasm32 release build.classifyto the JS port across the Unicode scalar space.Codex follow-ups addressed
scanText/cleanTextdetect them and route through the JS fallback sosurrogatefindings and cleans still match.classify_contextreuses the already-parsedVec<char>instead of rebuilding it per finding.content_security_policy.extension_pagesincludeswasm-unsafe-evalso Chromium can instantiate the module in the popup.nullencodes as*(all categories);[]encodes as an empty selection and finds/removes nothing.Rebuild:
crates/fuckmark-scan/build-wasm.sh.Package version stays 0.4.1. Frozen confirmation artifacts are unchanged.
Demo
Verified locally at
/scan.htmlwith engine badge wasm:scan_page_wasm_engine_examples.mp4
WASM engine on Trojan Source identifier
WASM engine on Python hash comment
WASM engine on emoji ZWJ info
To show artifacts inline, enable in settings.