Skip to content

Add WASM hidden-scan engine for the scan page - #114

Merged
cursor[bot] merged 2 commits into
mainfrom
cursor/wasm-scan-engine-cdf8
Sep 2, 2026
Merged

Add WASM hidden-scan engine for the scan page#114
cursor[bot] merged 2 commits into
mainfrom
cursor/wasm-scan-engine-cdf8

Conversation

@byte271

@byte271 byte271 commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Compile fuckmark-hidden-scan-v1 to a no-dependency wasm32 module so the scan page (and the Chromium popup) can classify text on-device. scan.js remains the file:// 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

  • Rust crate crates/fuckmark-scan (same category table as editors/vscode/scan.js, language-aware Trojan Source roles, clean/autofix).
  • Committed fuckmark_scan.wasm (~45 KiB) plus scan_wasm.js loader, copied next to the scan page, packaged web UI, and browser popup.
  • fuckmark web serves .wasm as application/wasm.
  • CI job WASM hidden-scan runs cargo test and a wasm32 release build.
  • Tests replay frozen spec vectors on the module and compare classify to the JS port across the Unicode scalar space.

Codex follow-ups addressed

  • P1 lone surrogates: UTF-16 unpaired surrogates cannot ride in UTF-8; scanText / cleanText detect them and route through the JS fallback so surrogate findings and cleans still match.
  • P1 dense scans: classify_context reuses the already-parsed Vec<char> instead of rebuilding it per finding.
  • P2 extension CSP: MV3 content_security_policy.extension_pages includes wasm-unsafe-eval so Chromium can instantiate the module in the popup.
  • P2 empty categories: null encodes 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.html with 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.

Open in Web Open in Cursor 

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>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: bb1ad7e6-4626-4827-8c6f-bf6ffd33d0e0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@byte271
byte271 marked this pull request as ready for review September 2, 2026 02:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread editors/wasm/scan_wasm.js
const encoder = new TextEncoder();

function callScan(text, language, categories, maxFindings) {
const textBytes = encoder.encode(String(text || ""));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +306 to +307
pub fn classify_context(text: &str, index: usize, role: &str, category: &str) -> &'static str {
let chars: Vec<char> = text.chars().collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread editors/browser/popup.js
Comment on lines +161 to +164
if (typeof globalThis.loadFuckMarkScanWasm === "function") {
try {
await globalThis.loadFuckMarkScanWasm("fuckmark_scan.wasm");
} catch (_err) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +450 to +454
fn parse_categories(raw: &str) -> Option<Vec<&'static str>> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@cursor
cursor Bot changed the base branch from cursor/browser-extension-cdf8 to main September 2, 2026 03:12
@cursor
cursor Bot merged commit 04c9e36 into main Sep 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants