From 1176b1a38387d7f990a320ff63560e11ebb12244 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 02:14:42 +0000 Subject: [PATCH 1/2] Add a WASM build of the hidden-scan classifier 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 --- .github/workflows/ci.yml | 19 + .gitignore | 1 + CHANGELOG.md | 1 + README.md | 5 +- crates/fuckmark-scan/Cargo.lock | 7 + crates/fuckmark-scan/Cargo.toml | 17 + crates/fuckmark-scan/README.md | 18 + crates/fuckmark-scan/build-wasm.sh | 22 + crates/fuckmark-scan/src/lib.rs | 788 +++++++++++++++++++++++++++++ docs/cli.md | 4 +- docs/fuckmark_scan.wasm | Bin 0 -> 45169 bytes docs/scan.html | 22 +- docs/scan_wasm.js | 176 +++++++ docs/website.md | 6 +- editors/browser/README.md | 9 +- editors/browser/fuckmark_scan.wasm | Bin 0 -> 45169 bytes editors/browser/popup.html | 3 +- editors/browser/popup.js | 7 + editors/browser/scan_wasm.js | 176 +++++++ editors/wasm/fuckmark_scan.wasm | Bin 0 -> 45169 bytes editors/wasm/scan_wasm.js | 176 +++++++ fuckmark/web.py | 5 + fuckmark/webui/fuckmark_scan.wasm | Bin 0 -> 45169 bytes fuckmark/webui/scan.html | 22 +- fuckmark/webui/scan_wasm.js | 176 +++++++ pyproject.toml | 2 +- tests/test_browser_extension.py | 6 + tests/test_scan_wasm.py | 164 ++++++ tests/test_web.py | 17 + 29 files changed, 1827 insertions(+), 22 deletions(-) create mode 100644 crates/fuckmark-scan/Cargo.lock create mode 100644 crates/fuckmark-scan/Cargo.toml create mode 100644 crates/fuckmark-scan/README.md create mode 100755 crates/fuckmark-scan/build-wasm.sh create mode 100644 crates/fuckmark-scan/src/lib.rs create mode 100755 docs/fuckmark_scan.wasm create mode 100644 docs/scan_wasm.js create mode 100755 editors/browser/fuckmark_scan.wasm create mode 100644 editors/browser/scan_wasm.js create mode 100755 editors/wasm/fuckmark_scan.wasm create mode 100644 editors/wasm/scan_wasm.js create mode 100755 fuckmark/webui/fuckmark_scan.wasm create mode 100644 fuckmark/webui/scan_wasm.js create mode 100644 tests/test_scan_wasm.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16d7e82f..14bb931d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,3 +167,22 @@ jobs: name: middev-calibration-readiness path: calibration-readiness/ if-no-files-found: error + + wasm-scan: + name: WASM hidden-scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Test crate and build wasm32 + run: | + cargo test --manifest-path crates/fuckmark-scan/Cargo.toml + cargo build --release --target wasm32-unknown-unknown --manifest-path crates/fuckmark-scan/Cargo.toml + test -f crates/fuckmark-scan/target/wasm32-unknown-unknown/release/fuckmark_scan.wasm + diff --git a/.gitignore b/.gitignore index b9e83619..b3bb9347 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ venv/ .env.* .DS_Store artifacts/ +target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 57455b41..6d2e27d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Language-aware Trojan Source analysis and autofix. `scan_hidden_characters(..., language=)` (and lint, from the file suffix) classifies source as `comment` / `string` / code. `autofix_trojan_source` / `autofixTrojanSource` strip only `bidi_control`. - Zero-install hidden-Unicode scan page: `docs/scan.html` (packaged as `fuckmark/webui/scan.html`, engine copy `scan.js`). `fuckmark web` serves it at `/scan.html`. After deploy: `mark.q1z.org/scan.html`. Scanning runs in the browser; a local Python `/api/scan` confirm is optional when the CLI server is up. - Chromium browser extension under `editors/browser`: popup scanner, on-page reveal (badges plus CSS highlights), context-menu scan, per-tab toolbar badge, and optional paste-safe stripping that keeps real emoji ZWJ sequences (both neighbors emoji) and stops page paste handlers from seeing the raw clipboard. Zero remote code; `scan.js` matches the VS Code engine copy. Load unpacked from `chrome://extensions`. +- WASM build of `fuckmark-hidden-scan-v1` (`crates/fuckmark-scan`, committed `fuckmark_scan.wasm`). The scan page and the Chromium popup load it when `fetch` works so classification stays on-device; `scan.js` remains the `file://` fallback. Page reveal and paste-safe keep the synchronous JS port. Rebuild with `crates/fuckmark-scan/build-wasm.sh`. - `fuckmark normalize` (`fuckmark-normalize-v1`): NFC, optional identifier lookalike fold (UTS #39-inspired subset, not a full confusable map), then strip of the security category set, plus a JSON receipt (`input_sha256` / `output_sha256` / `steps` / `report_hash`). `--clean` remains the blunt strip with no receipt. HTTP: `POST /api/normalize`. Python: `normalize_text`, `skeleton_fold`. Reference: `docs/normalize.md`. - VS Code / Cursor: `fuckmark.cleanOnSave` (default off) strips hidden Unicode on save. - Frozen confirmation files, hashes, mix mechanisms, and the published wheel are unchanged. Package version stays 0.4.1. diff --git a/README.md b/README.md index 87bd2e24..30f4d261 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Opens a local page at `http://127.0.0.1:8765/mark.html`. Paste text in the brows `fuckmark web` also serves a local Python API. Detect and strip POST to `/api/remove-marks` and run the same engine as `fuckmark --detect`. Opening `mark.html` with `file://` or the static website has no Python process, so those paths keep the in-browser fallback. - Product paste UI source: [`docs/mark.html`](docs/mark.html) (packaged as `fuckmark/webui/mark.html`) — after deploy: [mark.q1z.org/mark.html](https://mark.q1z.org/mark.html) -- Hidden-Unicode scan (no install): [`docs/scan.html`](docs/scan.html) — `file://` works; `fuckmark web` serves `/scan.html`; after deploy: [mark.q1z.org/scan.html](https://mark.q1z.org/scan.html) +- Hidden-Unicode scan (no install): [`docs/scan.html`](docs/scan.html) — `file://` works; `fuckmark web` serves `/scan.html`; after deploy: [mark.q1z.org/scan.html](https://mark.q1z.org/scan.html). The page prefers a local WASM copy of the same classifier (`fuckmark_scan.wasm`) and falls back to `scan.js`. - Research demo: [`docs/demo.html`](docs/demo.html) — `file://` works; after deploy: [mark.q1z.org/demo.html](https://mark.q1z.org/demo.html) `mark.html` runs a local closed-set FuckMark scan, strips approved insertions when found, and on a miss shows an English no-watermark card with contact to `Fhelp@q1z.org`. Under `fuckmark web` that scan is the Python detector; the JS copy is the fallback. @@ -181,7 +181,7 @@ The [`editors/vscode`](editors/vscode) extension reveals hidden Unicode inline a ## See it in the browser (Chrome / Edge / Brave) -The [`editors/browser`](editors/browser) extension reveals hidden Unicode on the page you are reading. Load it unpacked from `chrome://extensions` (Developer mode). The popup scans pasted text or the current tab, **Reveal** paints `U+XXXX` badges on the page, and **Paste-safe** (off by default) strips hidden characters when you paste into a field while keeping emoji sequences. Nothing is uploaded; the scanner is the same `scan.js` as the editor extension. Full notes: [`editors/browser/README.md`](editors/browser/README.md). +The [`editors/browser`](editors/browser) extension reveals hidden Unicode on the page you are reading. Load it unpacked from `chrome://extensions` (Developer mode). The popup scans pasted text or the current tab, **Reveal** paints `U+XXXX` badges on the page, and **Paste-safe** (off by default) strips hidden characters when you paste into a field while keeping emoji sequences. Nothing is uploaded. The popup prefers `fuckmark_scan.wasm` when it can fetch it; on-page reveal and paste-safe keep `scan.js`. Full notes: [`editors/browser/README.md`](editors/browser/README.md). ## Guard model input (LLM prompt-injection smuggling) @@ -255,6 +255,7 @@ Frozen evidence, hashes, and protocols: [`docs/research.md`](docs/research.md). ```text python -m pip install -e ".[dev]" python -m pytest +cargo test --manifest-path crates/fuckmark-scan/Cargo.toml ``` ## License diff --git a/crates/fuckmark-scan/Cargo.lock b/crates/fuckmark-scan/Cargo.lock new file mode 100644 index 00000000..cc63e444 --- /dev/null +++ b/crates/fuckmark-scan/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "fuckmark-scan" +version = "0.4.1" diff --git a/crates/fuckmark-scan/Cargo.toml b/crates/fuckmark-scan/Cargo.toml new file mode 100644 index 00000000..f9044667 --- /dev/null +++ b/crates/fuckmark-scan/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fuckmark-scan" +version = "0.4.1" +edition = "2021" +license = "MIT" +publish = false +description = "fuckmark-hidden-scan-v1 classifier for native and wasm32" + +[lib] +crate-type = ["cdylib", "rlib"] + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/crates/fuckmark-scan/README.md b/crates/fuckmark-scan/README.md new file mode 100644 index 00000000..1dbb4c44 --- /dev/null +++ b/crates/fuckmark-scan/README.md @@ -0,0 +1,18 @@ +# fuckmark-scan (Rust / WASM) + +Native and `wasm32` build of `fuckmark-hidden-scan-v1`. Same category table as +`editors/vscode/scan.js` and `fuckmark.product.scan`. No network, no extra +crate dependencies. + +```text +rustup target add wasm32-unknown-unknown +./crates/fuckmark-scan/build-wasm.sh +``` + +That writes `fuckmark_scan.wasm` next to the scan page, the packaged web UI, +and the Chromium popup. The page and popup load it when `fetch` works and keep +`scan.js` as the `file://` fallback. + +`cargo test` in this directory covers classification, Trojan Source roles, and +clean. Conformance vectors are also replayed from Python via Node on the +committed module. diff --git a/crates/fuckmark-scan/build-wasm.sh b/crates/fuckmark-scan/build-wasm.sh new file mode 100755 index 00000000..a7e6a39e --- /dev/null +++ b/crates/fuckmark-scan/build-wasm.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu +root="$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)" +crate="$root/crates/fuckmark-scan" +cd "$crate" +rustup target add wasm32-unknown-unknown >/dev/null +cargo test --offline --quiet 2>/dev/null || cargo test --quiet +cargo build --release --target wasm32-unknown-unknown --quiet +src="$crate/target/wasm32-unknown-unknown/release/fuckmark_scan.wasm" +test -f "$src" +for dest in \ + "$root/editors/wasm/fuckmark_scan.wasm" \ + "$root/docs/fuckmark_scan.wasm" \ + "$root/fuckmark/webui/fuckmark_scan.wasm" \ + "$root/editors/browser/fuckmark_scan.wasm" +do + cp "$src" "$dest" +done +cp "$root/editors/wasm/scan_wasm.js" "$root/docs/scan_wasm.js" +cp "$root/editors/wasm/scan_wasm.js" "$root/fuckmark/webui/scan_wasm.js" +cp "$root/editors/wasm/scan_wasm.js" "$root/editors/browser/scan_wasm.js" +wc -c "$src" diff --git a/crates/fuckmark-scan/src/lib.rs b/crates/fuckmark-scan/src/lib.rs new file mode 100644 index 00000000..e5502394 --- /dev/null +++ b/crates/fuckmark-scan/src/lib.rs @@ -0,0 +1,788 @@ +pub const SCAN_ALGORITHM_VERSION: &str = "fuckmark-hidden-scan-v1"; + +pub const CATEGORY_BIDI_CONTROL: &str = "bidi_control"; +pub const CATEGORY_ZERO_WIDTH: &str = "zero_width"; +pub const CATEGORY_VARIATION_SELECTOR: &str = "variation_selector"; +pub const CATEGORY_TAG: &str = "tag"; +pub const CATEGORY_ENCLOSING_MARK: &str = "enclosing_mark"; +pub const CATEGORY_LINE_SEPARATOR: &str = "line_separator"; +pub const CATEGORY_DEPRECATED: &str = "deprecated"; +pub const CATEGORY_FORMAT: &str = "format"; +pub const CATEGORY_CONTROL: &str = "control"; +pub const CATEGORY_PRIVATE_USE: &str = "private_use"; +pub const CATEGORY_NONCHARACTER: &str = "noncharacter"; +pub const CATEGORY_SURROGATE: &str = "surrogate"; + +pub const SCAN_CATEGORIES: [&str; 12] = [ + CATEGORY_BIDI_CONTROL, + CATEGORY_ZERO_WIDTH, + CATEGORY_VARIATION_SELECTOR, + CATEGORY_TAG, + CATEGORY_ENCLOSING_MARK, + CATEGORY_LINE_SEPARATOR, + CATEGORY_DEPRECATED, + CATEGORY_FORMAT, + CATEGORY_CONTROL, + CATEGORY_PRIVATE_USE, + CATEGORY_NONCHARACTER, + CATEGORY_SURROGATE, +]; + +const ALLOWED_WHITESPACE: [u32; 4] = [0x09, 0x0A, 0x0D, 0x20]; +const BIDI_EXTRA: [u32; 3] = [0x061C, 0x200E, 0x200F]; +const ZERO_WIDTH: [u32; 18] = [ + 0x00AD, 0x034F, 0x115F, 0x1160, 0x17B4, 0x17B5, 0x180E, 0x200B, 0x200C, 0x200D, 0x2060, 0x2061, + 0x2062, 0x2063, 0x2064, 0x3164, 0xFEFF, 0xFFA0, +]; +const FORMAT_RANGES: [(u32, u32); 10] = [ + (0x0600, 0x0605), + (0x06DD, 0x06DD), + (0x070F, 0x070F), + (0x0890, 0x0891), + (0x08E2, 0x08E2), + (0x110BD, 0x110BD), + (0x110CD, 0x110CD), + (0x13430, 0x1343F), + (0x1BCA0, 0x1BCA3), + (0x1D173, 0x1D17A), +]; +const EMOJI_BMP: [u32; 10] = [ + 0x00A9, 0x00AE, 0x203C, 0x2049, 0x2122, 0x2139, 0x3030, 0x303D, 0x3297, 0x3299, +]; +const ZWJ: u32 = 0x200D; + +fn in_range(cp: u32, lo: u32, hi: u32) -> bool { + cp >= lo && cp <= hi +} + +fn contains_u32(table: &[u32], cp: u32) -> bool { + table.iter().any(|item| *item == cp) +} + +pub fn classify(cp: u32) -> Option<&'static str> { + if contains_u32(&ALLOWED_WHITESPACE, cp) { + return None; + } + if contains_u32(&BIDI_EXTRA, cp) || in_range(cp, 0x202A, 0x202E) || in_range(cp, 0x2066, 0x2069) + { + return Some(CATEGORY_BIDI_CONTROL); + } + if in_range(cp, 0x206A, 0x206F) || cp == 0xFFF9 || cp == 0xFFFA || cp == 0xFFFB { + return Some(CATEGORY_DEPRECATED); + } + if contains_u32(&ZERO_WIDTH, cp) { + return Some(CATEGORY_ZERO_WIDTH); + } + if in_range(cp, 0xFE00, 0xFE0F) || in_range(cp, 0xE0100, 0xE01EF) { + return Some(CATEGORY_VARIATION_SELECTOR); + } + if in_range(cp, 0xE0000, 0xE007F) { + return Some(CATEGORY_TAG); + } + if in_range(cp, 0xFDD0, 0xFDEF) || (cp & 0xFFFF) == 0xFFFE || (cp & 0xFFFF) == 0xFFFF { + return Some(CATEGORY_NONCHARACTER); + } + if in_range(cp, 0xD800, 0xDFFF) { + return Some(CATEGORY_SURROGATE); + } + if in_range(cp, 0x0488, 0x0489) + || cp == 0x1ABE + || in_range(cp, 0x20DD, 0x20E0) + || in_range(cp, 0x20E2, 0x20E4) + || in_range(cp, 0xA670, 0xA672) + { + return Some(CATEGORY_ENCLOSING_MARK); + } + if cp == 0x2028 || cp == 0x2029 { + return Some(CATEGORY_LINE_SEPARATOR); + } + if in_range(cp, 0x00, 0x1F) || in_range(cp, 0x7F, 0x9F) { + return Some(CATEGORY_CONTROL); + } + if FORMAT_RANGES.iter().any(|(lo, hi)| in_range(cp, *lo, *hi)) { + return Some(CATEGORY_FORMAT); + } + if in_range(cp, 0xE000, 0xF8FF) + || in_range(cp, 0xF0000, 0xFFFFD) + || in_range(cp, 0x100000, 0x10FFFD) + { + return Some(CATEGORY_PRIVATE_USE); + } + None +} + +pub fn category_index(name: &str) -> Option { + SCAN_CATEGORIES + .iter() + .position(|item| *item == name) + .map(|index| index as i32) +} + +pub fn classify_index(cp: u32) -> i32 { + match classify(cp) { + Some(name) => category_index(name).unwrap_or(-1), + None => -1, + } +} + +pub fn normalize_language(language: &str) -> &'static str { + let key = language.trim().to_ascii_lowercase(); + match key.as_str() { + "" | "auto" => "auto", + "javascript" | "js" | "ts" | "typescript" | "jsx" | "tsx" => "javascript", + "c" | "h" | "cc" | "cpp" | "cxx" | "java" | "go" | "rs" | "rust" | "cs" | "css" + | "jsonc" => "c", + "python" | "py" | "pyi" | "hash" | "sh" | "bash" | "zsh" | "shell" | "shellscript" + | "yaml" | "yml" | "rb" | "ruby" | "toml" => "python", + "html" | "htm" | "xml" => "html", + "sql" => "sql", + _ => "auto", + } +} + +fn slash_comments(language: &str) -> bool { + matches!(language, "auto" | "javascript" | "c" | "sql") +} + +fn block_comments(language: &str) -> bool { + slash_comments(language) +} + +fn hash_comments(language: &str) -> bool { + language == "python" +} + +fn sql_line_comments(language: &str) -> bool { + language == "sql" +} + +fn html_comments(language: &str) -> bool { + language == "html" +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Role { + Code, + String, + Comment, +} + +fn role_name(role: Role) -> &'static str { + match role { + Role::Code => "code", + Role::String => "string", + Role::Comment => "comment", + } +} + +fn find_seq(chars: &[char], start: usize, needle: &[char]) -> Option { + if needle.is_empty() || start > chars.len() { + return None; + } + chars[start..] + .windows(needle.len()) + .position(|window| window == needle) + .map(|offset| start + offset) +} + +pub fn source_roles(text: &str, language: &str) -> Vec<&'static str> { + let lang = normalize_language(language); + let chars: Vec = text.chars().collect(); + let mut roles = vec![Role::Code; chars.len()]; + let length = chars.len(); + let mut index = 0; + let mut in_string: Option = None; + let mut escape = false; + while index < length { + let character = chars[index]; + if in_string.is_some() { + roles[index] = Role::String; + if escape { + escape = false; + index += 1; + continue; + } + if character == '\\' && index + 1 < length { + escape = true; + index += 1; + continue; + } + if Some(character) == in_string { + in_string = None; + } + index += 1; + continue; + } + let pair = if index + 1 < length { + Some((character, chars[index + 1])) + } else { + None + }; + if slash_comments(lang) + && pair == Some(('/', '/')) + && !(index > 0 && chars[index - 1] == ':') + { + let mut cursor = index; + while cursor < length && chars[cursor] != '\n' && chars[cursor] != '\r' { + roles[cursor] = Role::Comment; + cursor += 1; + } + index = cursor; + continue; + } + if sql_line_comments(lang) && pair == Some(('-', '-')) { + let mut cursor = index; + while cursor < length && chars[cursor] != '\n' && chars[cursor] != '\r' { + roles[cursor] = Role::Comment; + cursor += 1; + } + index = cursor; + continue; + } + if block_comments(lang) && pair == Some(('/', '*')) { + let close = find_seq(&chars, index + 2, &['*', '/']); + let stop = close.map(|pos| pos + 2).unwrap_or(length); + for role in roles.iter_mut().take(stop).skip(index) { + *role = Role::Comment; + } + index = stop; + continue; + } + if hash_comments(lang) && character == '#' { + let mut cursor = index; + while cursor < length && chars[cursor] != '\n' && chars[cursor] != '\r' { + roles[cursor] = Role::Comment; + cursor += 1; + } + index = cursor; + continue; + } + if html_comments(lang) && chars.get(index..index + 4) == Some(&['<', '!', '-', '-'][..]) { + let close = find_seq(&chars, index + 4, &['-', '-', '>']); + let stop = close.map(|pos| pos + 3).unwrap_or(length); + for role in roles.iter_mut().take(stop).skip(index) { + *role = Role::Comment; + } + index = stop; + continue; + } + if character == '"' || character == '\'' || character == '`' { + in_string = Some(character); + roles[index] = Role::String; + index += 1; + continue; + } + index += 1; + } + roles.into_iter().map(role_name).collect() +} + +fn is_emojiish(cp: i32) -> bool { + if cp < 0 { + return false; + } + let cp = cp as u32; + if in_range(cp, 0x1F1E6, 0x1F1FF) + || in_range(cp, 0x1F000, 0x1FAFF) + || in_range(cp, 0x2600, 0x27BF) + { + return true; + } + cp == ZWJ || contains_u32(&EMOJI_BMP, cp) +} + +fn is_ident_char(ch: Option) -> bool { + match ch { + Some('_') => true, + Some(character) => character.is_alphanumeric(), + None => false, + } +} + +fn is_bmp_vs(cp: i32) -> bool { + cp >= 0 && in_range(cp as u32, 0xFE00, 0xFE0F) +} + +pub fn classify_context(text: &str, index: usize, role: &str, category: &str) -> &'static str { + let chars: Vec = text.chars().collect(); + if index >= chars.len() { + return "prose"; + } + let prev = if index > 0 { + Some(chars[index - 1]) + } else { + None + }; + let next = chars.get(index + 1).copied(); + let prev_cp = prev.map(|ch| ch as u32 as i32).unwrap_or(-1); + let next_cp = next.map(|ch| ch as u32 as i32).unwrap_or(-1); + if (role == "comment" || role == "string") && category == CATEGORY_BIDI_CONTROL { + return if role == "comment" { + "comment" + } else { + "string" + }; + } + if is_emojiish(prev_cp) || is_emojiish(next_cp) || is_bmp_vs(prev_cp) || is_bmp_vs(next_cp) { + return "emoji"; + } + if role == "comment" { + return "comment"; + } + if role == "string" { + return "string"; + } + if is_ident_char(prev) || is_ident_char(next) { + return "identifier"; + } + "prose" +} + +pub fn score_severity(category: &str, context: &str) -> &'static str { + if category == CATEGORY_TAG { + return "critical"; + } + if category == CATEGORY_BIDI_CONTROL { + if matches!(context, "identifier" | "comment" | "string") { + return "critical"; + } + return "high"; + } + if category == CATEGORY_ZERO_WIDTH { + if context == "emoji" { + return "info"; + } + if context == "identifier" { + return "high"; + } + return "medium"; + } + if category == CATEGORY_VARIATION_SELECTOR { + return if context == "emoji" { "info" } else { "medium" }; + } + if matches!(category, "control" | "noncharacter" | "surrogate") { + return "high"; + } + "medium" +} + +pub fn explain_finding( + category: &str, + context: &str, + severity: &str, +) -> (&'static str, &'static str) { + if category == CATEGORY_BIDI_CONTROL && context == "identifier" { + return ( + "Bidirectional override sits inside an identifier, so the glyphs can read differently than the bytes (Trojan Source).", + "Strip the bidi control and keep the identifier left-to-right.", + ); + } + if category == CATEGORY_BIDI_CONTROL && context == "comment" { + return ( + "Bidirectional override sits inside a comment, so commented-out code can appear to run (Trojan Source commenting-out).", + "Strip the bidi control from the comment.", + ); + } + if category == CATEGORY_BIDI_CONTROL && context == "string" { + return ( + "Bidirectional override sits inside a string, so the literal can appear to close early (Trojan Source stretched-string).", + "Strip the bidi control from the string.", + ); + } + if category == CATEGORY_BIDI_CONTROL { + return ( + "Bidirectional override can reorder nearby glyphs (Trojan Source class, CVE-2021-42574).", + "Strip U+202A-U+202E / U+2066-U+2069 and rewrite the text left-to-right.", + ); + } + if category == CATEGORY_TAG { + return ( + "Unicode tag characters encode a second ASCII string that models read and humans do not.", + "Strip U+E0020-U+E007F; inspect tag_payload for the smuggled text.", + ); + } + if category == CATEGORY_ZERO_WIDTH && context == "emoji" { + return ( + "Zero-width joiner or invisible mark inside an emoji cluster; usually a legitimate emoji sequence.", + "Leave emoji ZWJ sequences unless you are sanitizing for a security boundary.", + ); + } + if category == CATEGORY_ZERO_WIDTH && context == "identifier" { + return ( + "Zero-width character splits an identifier, breaking search and some compilers while looking unchanged.", + "Strip the zero-width character from the identifier.", + ); + } + if category == CATEGORY_ZERO_WIDTH { + return ( + "Invisible spacing or joining character that changes the byte stream without changing the glyphs.", + "Strip the zero-width character.", + ); + } + if category == CATEGORY_VARIATION_SELECTOR && context == "emoji" { + return ( + "Variation selector tunes an emoji glyph; usually benign.", + "Keep emoji variation selectors unless you are stripping all hidden marks.", + ); + } + if severity == "high" { + return ( + "Hidden or non-text codepoint that should not appear in ordinary source or prompts.", + "Strip the character.", + ); + } + ( + "Hidden or format codepoint that is invisible or renderer-defined.", + "Strip the character if this text crosses a trust boundary.", + ) +} + +fn severity_rank(severity: &str) -> i32 { + match severity { + "info" => 0, + "medium" => 1, + "high" => 2, + "critical" => 3, + _ => -1, + } +} + +fn parse_categories(raw: &str) -> Option> { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return None; + } + let mut selected = Vec::new(); + for part in trimmed.split(',') { + let name = part.trim(); + if let Some(known) = SCAN_CATEGORIES.iter().copied().find(|item| *item == name) { + if !selected.contains(&known) { + selected.push(known); + } + } + } + Some(selected) +} + +fn json_escape(s: &str) -> String { + let mut out = String::from("\""); + for character in s.chars() { + match character { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)), + c => out.push(c), + } + } + out.push('"'); + out +} + +#[derive(Clone)] +pub struct HiddenFinding { + pub index: usize, + pub codepoint: u32, + pub category: &'static str, + pub context: &'static str, + pub severity: &'static str, + pub why: &'static str, + pub remedy: &'static str, +} + +pub struct ScanResult { + pub source_length: usize, + pub total: usize, + pub truncated: bool, + pub highest_severity: String, + pub counts: Vec<(&'static str, usize)>, + pub findings: Vec, +} + +pub fn scan_text(text: &str, language: &str, categories: &str, max_findings: i32) -> ScanResult { + let selected = parse_categories(categories); + let roles = source_roles(text, language); + let chars: Vec = text.chars().collect(); + let mut counts = [0usize; 12]; + let mut findings = Vec::new(); + let mut total = 0usize; + let mut truncated = false; + let mut peak = ""; + let cap = if max_findings < 0 { + usize::MAX + } else { + max_findings as usize + }; + for (index, character) in chars.iter().enumerate() { + let code = *character as u32; + let Some(category) = classify(code) else { + continue; + }; + if let Some(allowed) = selected.as_ref() { + if !allowed.contains(&category) { + continue; + } + } + total += 1; + if let Some(slot) = category_index(category) { + counts[slot as usize] += 1; + } + let role = roles.get(index).copied().unwrap_or("code"); + let context = classify_context(text, index, role, category); + let severity = score_severity(category, context); + let (why, remedy) = explain_finding(category, context, severity); + if severity_rank(severity) > severity_rank(peak) { + peak = severity; + } + if findings.len() < cap { + findings.push(HiddenFinding { + index, + codepoint: code, + category, + context, + severity, + why, + remedy, + }); + } else { + truncated = true; + } + } + let mut named_counts = Vec::new(); + for (index, name) in SCAN_CATEGORIES.iter().enumerate() { + if counts[index] > 0 { + named_counts.push((*name, counts[index])); + } + } + ScanResult { + source_length: chars.len(), + total, + truncated, + highest_severity: peak.to_string(), + counts: named_counts, + findings, + } +} + +pub fn clean_text(text: &str, categories: &str) -> (String, usize) { + let selected = parse_categories(categories); + let mut kept = String::new(); + let mut removed = 0usize; + for character in text.chars() { + let category = classify(character as u32); + let drop = match (category, selected.as_ref()) { + (Some(name), Some(allowed)) => allowed.contains(&name), + (Some(_), None) => true, + (None, _) => false, + }; + if drop { + removed += 1; + } else { + kept.push(character); + } + } + (kept, removed) +} + +pub fn scan_to_json(text: &str, language: &str, categories: &str, max_findings: i32) -> String { + let result = scan_text(text, language, categories, max_findings); + let mut out = String::from("{"); + out.push_str("\"algorithm_version\":"); + out.push_str(&json_escape(SCAN_ALGORITHM_VERSION)); + out.push_str(",\"source_length\":"); + out.push_str(&result.source_length.to_string()); + out.push_str(",\"total\":"); + out.push_str(&result.total.to_string()); + out.push_str(",\"truncated\":"); + out.push_str(if result.truncated { "true" } else { "false" }); + out.push_str(",\"highest_severity\":"); + out.push_str(&json_escape(&result.highest_severity)); + out.push_str(",\"counts\":{"); + for (index, (name, count)) in result.counts.iter().enumerate() { + if index > 0 { + out.push(','); + } + out.push_str(&json_escape(name)); + out.push(':'); + out.push_str(&count.to_string()); + } + out.push_str("},\"findings\":["); + for (index, finding) in result.findings.iter().enumerate() { + if index > 0 { + out.push(','); + } + out.push('{'); + out.push_str("\"index\":"); + out.push_str(&finding.index.to_string()); + out.push_str(",\"codepoint\":"); + out.push_str(&finding.codepoint.to_string()); + out.push_str(",\"category\":"); + out.push_str(&json_escape(finding.category)); + out.push_str(",\"context\":"); + out.push_str(&json_escape(finding.context)); + out.push_str(",\"severity\":"); + out.push_str(&json_escape(finding.severity)); + out.push_str(",\"why\":"); + out.push_str(&json_escape(finding.why)); + out.push_str(",\"remedy\":"); + out.push_str(&json_escape(finding.remedy)); + out.push('}'); + } + out.push_str("]}"); + out +} + +pub fn clean_to_json(text: &str, categories: &str) -> String { + let (cleaned, removed) = clean_text(text, categories); + let mut out = String::from("{\"removed\":"); + out.push_str(&removed.to_string()); + out.push_str(",\"cleaned\":"); + out.push_str(&json_escape(&cleaned)); + out.push('}'); + out +} + +fn pack_bytes(payload: &[u8]) -> *mut u8 { + let mut buffer = vec![0u8; 4 + payload.len()]; + buffer[..4].copy_from_slice(&(payload.len() as u32).to_le_bytes()); + buffer[4..].copy_from_slice(payload); + let ptr = buffer.as_mut_ptr(); + std::mem::forget(buffer); + ptr +} + +fn read_bytes<'a>(ptr: *const u8, len: u32) -> &'a [u8] { + if ptr.is_null() || len == 0 { + return &[]; + } + unsafe { std::slice::from_raw_parts(ptr, len as usize) } +} + +#[no_mangle] +pub extern "C" fn fm_alloc(size: u32) -> *mut u8 { + let mut buffer = vec![0u8; size as usize]; + let ptr = buffer.as_mut_ptr(); + std::mem::forget(buffer); + ptr +} + +#[no_mangle] +pub extern "C" fn fm_dealloc(ptr: *mut u8, size: u32) { + if ptr.is_null() { + return; + } + unsafe { + let _ = Vec::from_raw_parts(ptr, size as usize, size as usize); + } +} + +#[no_mangle] +pub extern "C" fn fm_classify(cp: u32) -> i32 { + classify_index(cp) +} + +#[no_mangle] +pub extern "C" fn fm_scan( + text_ptr: *const u8, + text_len: u32, + lang_ptr: *const u8, + lang_len: u32, + cats_ptr: *const u8, + cats_len: u32, + max_findings: i32, +) -> *mut u8 { + let text = match std::str::from_utf8(read_bytes(text_ptr, text_len)) { + Ok(value) => value.to_owned(), + Err(_) => return pack_bytes(br#"{"ok":false,"reason":"utf8"}"#), + }; + let language = std::str::from_utf8(read_bytes(lang_ptr, lang_len)) + .unwrap_or("auto") + .to_owned(); + let categories = std::str::from_utf8(read_bytes(cats_ptr, cats_len)) + .unwrap_or("") + .to_owned(); + let json = scan_to_json(&text, &language, &categories, max_findings); + pack_bytes(json.as_bytes()) +} + +#[no_mangle] +pub extern "C" fn fm_clean( + text_ptr: *const u8, + text_len: u32, + cats_ptr: *const u8, + cats_len: u32, +) -> *mut u8 { + let text = match std::str::from_utf8(read_bytes(text_ptr, text_len)) { + Ok(value) => value.to_owned(), + Err(_) => return pack_bytes(br#"{"ok":false,"reason":"utf8"}"#), + }; + let categories = std::str::from_utf8(read_bytes(cats_ptr, cats_len)) + .unwrap_or("") + .to_owned(); + let json = clean_to_json(&text, &categories); + pack_bytes(json.as_bytes()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn classifies_core_hidden_codepoints() { + assert_eq!(classify(0x202E), Some(CATEGORY_BIDI_CONTROL)); + assert_eq!(classify(0x200B), Some(CATEGORY_ZERO_WIDTH)); + assert_eq!(classify(0xE0061), Some(CATEGORY_TAG)); + assert_eq!(classify(0x20DD), Some(CATEGORY_ENCLOSING_MARK)); + assert_eq!(classify(0x13430), Some(CATEGORY_FORMAT)); + assert_eq!(classify(0x09), None); + assert_eq!(classify(0x61), None); + assert_eq!(classify(0x0301), None); + } + + #[test] + fn bidi_in_identifier_is_critical() { + let text: String = ['a', '\u{202E}', 'b'].into_iter().collect(); + let result = scan_text(&text, "auto", "", -1); + assert_eq!(result.total, 1); + assert_eq!(result.findings[0].context, "identifier"); + assert_eq!(result.findings[0].severity, "critical"); + } + + #[test] + fn url_slash_slash_is_not_a_comment() { + let text = "http://example.com/\u{202E}"; + let roles = source_roles(text, "auto"); + assert!(roles.iter().all(|role| *role == "code")); + let result = scan_text(text, "auto", "", -1); + assert_eq!(result.findings[0].context, "prose"); + assert_eq!(result.findings[0].severity, "high"); + } + + #[test] + fn python_hash_comment_is_critical_bidi() { + let text = "x = 1 # \u{202E} no"; + let result = scan_text(text, "python", "", -1); + assert_eq!(result.findings[0].context, "comment"); + assert_eq!(result.findings[0].severity, "critical"); + let auto = scan_text(text, "auto", "", -1); + assert_ne!(auto.findings[0].context, "comment"); + } + + #[test] + fn emoji_zwj_is_info() { + let text = "\u{1F468}\u{200D}\u{1F469}"; + let result = scan_text(text, "auto", "", -1); + assert_eq!(result.findings[0].context, "emoji"); + assert_eq!(result.findings[0].severity, "info"); + } + + #[test] + fn clean_strips_bidi_only() { + let text = "a\u{202E}b\u{200B}c"; + let (cleaned, removed) = clean_text(text, "bidi_control"); + assert_eq!(removed, 1); + assert_eq!(cleaned, "ab\u{200B}c"); + } +} diff --git a/docs/cli.md b/docs/cli.md index d7919a20..72109046 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -118,7 +118,7 @@ Existing files are read as UTF-8 bytes with no newline conversion. LF, CRLF, CR, Findings include context (`identifier`, `comment`, `string`, `emoji`, `prose`) and severity (`critical`, `high`, `medium`, `info`). A bidi override in an identifier, comment, or string is `critical` (Trojan Source); a ZWJ inside an emoji cluster is `info`. Comment syntax follows `language` (`auto` treats `//` and `/* */` as comments; `python` also treats `#`; `sql` treats `--`; `html` treats ``). The frozen table is [`specs/fuckmark-hidden-scan-v1.protocol.md`](../specs/fuckmark-hidden-scan-v1.protocol.md). -`--clean` removes every flagged category, so it reverses a FuckMark mix back to the visible text. It also removes emoji zero-width joiners and variation selectors; the Python `clean_hidden_characters(text, categories=...)` call accepts a category subset when emoji sequences must be preserved. `autofix_trojan_source` strips only `bidi_control`. The browser scan page is `docs/scan.html` (`fuckmark web` serves `/scan.html`); the API is `POST /api/scan` with optional `"language"`. +`--clean` removes every flagged category, so it reverses a FuckMark mix back to the visible text. It also removes emoji zero-width joiners and variation selectors; the Python `clean_hidden_characters(text, categories=...)` call accepts a category subset when emoji sequences must be preserved. `autofix_trojan_source` strips only `bidi_control`. The browser scan page is `docs/scan.html` (`fuckmark web` serves `/scan.html`); it prefers the WASM engine (`fuckmark_scan.wasm`) and falls back to `scan.js`. The API is `POST /api/scan` with optional `"language"`. ### `fuckmark normalize` @@ -143,7 +143,7 @@ fuckmark guard --refuse --receipt --json < messages.json ### `fuckmark web` -Open the local browser tool (same UI as `docs/mark.html`). Aimed at beginners who prefer a page over pipes and flags. The server also exposes a Python API: `GET /api/health`, `POST /api/remove-marks`, `POST /api/scan`, `POST /api/guard` (sanitize text or chat messages before a model call), and `POST /api/normalize`. Hidden-Unicode reveal is `/scan.html` (same engine as `--scan`; zero-install copy at `docs/scan.html`). +Open the local browser tool (same UI as `docs/mark.html`). Aimed at beginners who prefer a page over pipes and flags. The server also exposes a Python API: `GET /api/health`, `POST /api/remove-marks`, `POST /api/scan`, `POST /api/guard` (sanitize text or chat messages before a model call), and `POST /api/normalize`. Hidden-Unicode reveal is `/scan.html` (WASM engine with `scan.js` fallback; zero-install copy at `docs/scan.html`). ```text fuckmark web diff --git a/docs/fuckmark_scan.wasm b/docs/fuckmark_scan.wasm new file mode 100755 index 0000000000000000000000000000000000000000..b994a12c31eddaf90fab3baafcdc0cd02b6bb8b5 GIT binary patch literal 45169 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 Zc1nTgPI!Lhvrgt{>p8OKDE-2d{s*RKnZp18 literal 0 HcmV?d00001 diff --git a/docs/scan.html b/docs/scan.html index 0b007977..6457e557 100644 --- a/docs/scan.html +++ b/docs/scan.html @@ -288,7 +288,9 @@

See the bytes.

    - Scanning is fuckmark-hidden-scan-v1. Copy uses the security category set + Scanning is fuckmark-hidden-scan-v1. The page prefers the local WASM engine + (fuckmark_scan.wasm) and keeps scan.js as the file:// + fallback. Copy uses the security category set (bidi_control, zero_width, tag, control, noncharacter, surrogate). Fix Trojan Source strips only bidirectional controls. Nothing is uploaded from a static page or file://. @@ -298,6 +300,7 @@

    See the bytes.

    + + diff --git a/editors/browser/popup.js b/editors/browser/popup.js index 28c27ebc..d3b7979a 100644 --- a/editors/browser/popup.js +++ b/editors/browser/popup.js @@ -158,6 +158,13 @@ pasteSafe.addEventListener("change", async () => { } else { scanPage.classList.add("primary"); } + if (typeof globalThis.loadFuckMarkScanWasm === "function") { + try { + await globalThis.loadFuckMarkScanWasm("fuckmark_scan.wasm"); + } catch (_err) { + void _err; + } + } if (typeof chrome !== "undefined" && chrome.storage) { const stored = await chrome.storage.sync.get({ pasteSafe: false }); pasteSafe.checked = Boolean(stored.pasteSafe); diff --git a/editors/browser/scan_wasm.js b/editors/browser/scan_wasm.js new file mode 100644 index 00000000..6e60120d --- /dev/null +++ b/editors/browser/scan_wasm.js @@ -0,0 +1,176 @@ +"use strict"; + +const WASM_CATEGORIES = [ + "bidi_control", + "zero_width", + "variation_selector", + "tag", + "enclosing_mark", + "line_separator", + "deprecated", + "format", + "control", + "private_use", + "noncharacter", + "surrogate", +]; + +function utf16Offsets(text) { + const map = []; + let offset = 0; + while (offset < text.length) { + map.push(offset); + const cp = text.codePointAt(offset); + offset += cp > 0xffff ? 2 : 1; + } + return map; +} + +function codepointToken(cp) { + return "U+" + Number(cp).toString(16).toUpperCase().padStart(4, "0"); +} + +function writeUtf8(memory, alloc, bytes) { + const ptr = alloc(bytes.length); + new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes); + return ptr; +} + +function readPackedJson(memory, dealloc, ptr) { + const view = new DataView(memory.buffer, ptr, 4); + const length = view.getUint32(0, true); + const bytes = new Uint8Array(memory.buffer, ptr + 4, length); + const text = new TextDecoder("utf-8").decode(bytes); + dealloc(ptr, 4 + length); + return JSON.parse(text); +} + +function wrapInstance(instance, fallback) { + const { memory, fm_alloc, fm_dealloc, fm_classify, fm_scan, fm_clean } = instance.exports; + const encoder = new TextEncoder(); + + function callScan(text, language, categories, maxFindings) { + const textBytes = encoder.encode(String(text || "")); + const langBytes = encoder.encode(String(language || "auto")); + const catBytes = encoder.encode(Array.isArray(categories) ? categories.join(",") : String(categories || "")); + const textPtr = writeUtf8(memory, fm_alloc, textBytes); + const langPtr = writeUtf8(memory, fm_alloc, langBytes); + const catPtr = writeUtf8(memory, fm_alloc, catBytes); + const resultPtr = fm_scan( + textPtr, + textBytes.length, + langPtr, + langBytes.length, + catPtr, + catBytes.length, + maxFindings == null ? -1 : maxFindings + ); + fm_dealloc(textPtr, textBytes.length); + fm_dealloc(langPtr, langBytes.length); + fm_dealloc(catPtr, catBytes.length); + return readPackedJson(memory, fm_dealloc, resultPtr); + } + + function callClean(text, categories) { + const textBytes = encoder.encode(String(text || "")); + const catBytes = encoder.encode(Array.isArray(categories) ? categories.join(",") : String(categories || "")); + const textPtr = writeUtf8(memory, fm_alloc, textBytes); + const catPtr = writeUtf8(memory, fm_alloc, catBytes); + const resultPtr = fm_clean(textPtr, textBytes.length, catPtr, catBytes.length); + fm_dealloc(textPtr, textBytes.length); + fm_dealloc(catPtr, catBytes.length); + return readPackedJson(memory, fm_dealloc, resultPtr); + } + + function scanText(text, categories, language) { + const source = String(text || ""); + const payload = callScan(source, language || "auto", categories || "", -1); + const offsets = utf16Offsets(source); + const findings = (payload.findings || []).map((finding) => { + const offset = offsets[finding.index] || 0; + const length = finding.codepoint > 0xffff ? 2 : 1; + return { + offset, + length, + index: finding.index, + codepoint: finding.codepoint, + category: finding.category, + context: finding.context, + severity: finding.severity, + why: finding.why, + remedy: finding.remedy, + }; + }); + return { + total: payload.total || 0, + counts: payload.counts || {}, + findings, + truncated: Boolean(payload.truncated), + highest_severity: payload.highest_severity || "", + source_length: payload.source_length || 0, + }; + } + + function cleanText(text, categories) { + return callClean(text, categories || ""); + } + + const api = Object.assign({}, fallback || {}, { + engine: "wasm", + SCAN_ALGORITHM_VERSION: "fuckmark-hidden-scan-v1", + classify(cp) { + const index = fm_classify(cp >>> 0); + return index < 0 ? null : WASM_CATEGORIES[index]; + }, + scanText, + cleanText, + autofixTrojanSource(text) { + return cleanText(text, ["bidi_control"]); + }, + codepointToken: (fallback && fallback.codepointToken) || codepointToken, + }); + return api; +} + +async function instantiateWasm(source) { + let buffer; + if (source instanceof ArrayBuffer) { + buffer = source; + } else if (ArrayBuffer.isView(source)) { + buffer = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength); + } else if (typeof source === "string") { + const response = await fetch(source); + if (!response.ok) { + throw new Error("fuckmark_scan.wasm missing"); + } + buffer = await response.arrayBuffer(); + } else { + throw new Error("wasm source required"); + } + const result = await WebAssembly.instantiate(buffer, {}); + return result.instance; +} + +async function loadFuckMarkScanWasm(source) { + const fallback = typeof globalThis !== "undefined" ? globalThis.FuckMarkScan : undefined; + const instance = await instantiateWasm(source || "fuckmark_scan.wasm"); + const api = wrapInstance(instance, fallback); + if (typeof globalThis !== "undefined") { + globalThis.FuckMarkScan = api; + globalThis.FuckMarkScanWasm = api; + } + if (typeof module !== "undefined" && module.exports) { + module.exports.loadFuckMarkScanWasm = loadFuckMarkScanWasm; + module.exports.wrapInstance = wrapInstance; + } + return api; +} + +if (typeof module !== "undefined" && module.exports) { + module.exports.loadFuckMarkScanWasm = loadFuckMarkScanWasm; + module.exports.wrapInstance = wrapInstance; + module.exports.WASM_CATEGORIES = WASM_CATEGORIES; +} +if (typeof globalThis !== "undefined") { + globalThis.loadFuckMarkScanWasm = loadFuckMarkScanWasm; +} diff --git a/editors/wasm/fuckmark_scan.wasm b/editors/wasm/fuckmark_scan.wasm new file mode 100755 index 0000000000000000000000000000000000000000..b994a12c31eddaf90fab3baafcdc0cd02b6bb8b5 GIT binary patch literal 45169 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 Zc1nTgPI!Lhvrgt{>p8OKDE-2d{s*RKnZp18 literal 0 HcmV?d00001 diff --git a/editors/wasm/scan_wasm.js b/editors/wasm/scan_wasm.js new file mode 100644 index 00000000..6e60120d --- /dev/null +++ b/editors/wasm/scan_wasm.js @@ -0,0 +1,176 @@ +"use strict"; + +const WASM_CATEGORIES = [ + "bidi_control", + "zero_width", + "variation_selector", + "tag", + "enclosing_mark", + "line_separator", + "deprecated", + "format", + "control", + "private_use", + "noncharacter", + "surrogate", +]; + +function utf16Offsets(text) { + const map = []; + let offset = 0; + while (offset < text.length) { + map.push(offset); + const cp = text.codePointAt(offset); + offset += cp > 0xffff ? 2 : 1; + } + return map; +} + +function codepointToken(cp) { + return "U+" + Number(cp).toString(16).toUpperCase().padStart(4, "0"); +} + +function writeUtf8(memory, alloc, bytes) { + const ptr = alloc(bytes.length); + new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes); + return ptr; +} + +function readPackedJson(memory, dealloc, ptr) { + const view = new DataView(memory.buffer, ptr, 4); + const length = view.getUint32(0, true); + const bytes = new Uint8Array(memory.buffer, ptr + 4, length); + const text = new TextDecoder("utf-8").decode(bytes); + dealloc(ptr, 4 + length); + return JSON.parse(text); +} + +function wrapInstance(instance, fallback) { + const { memory, fm_alloc, fm_dealloc, fm_classify, fm_scan, fm_clean } = instance.exports; + const encoder = new TextEncoder(); + + function callScan(text, language, categories, maxFindings) { + const textBytes = encoder.encode(String(text || "")); + const langBytes = encoder.encode(String(language || "auto")); + const catBytes = encoder.encode(Array.isArray(categories) ? categories.join(",") : String(categories || "")); + const textPtr = writeUtf8(memory, fm_alloc, textBytes); + const langPtr = writeUtf8(memory, fm_alloc, langBytes); + const catPtr = writeUtf8(memory, fm_alloc, catBytes); + const resultPtr = fm_scan( + textPtr, + textBytes.length, + langPtr, + langBytes.length, + catPtr, + catBytes.length, + maxFindings == null ? -1 : maxFindings + ); + fm_dealloc(textPtr, textBytes.length); + fm_dealloc(langPtr, langBytes.length); + fm_dealloc(catPtr, catBytes.length); + return readPackedJson(memory, fm_dealloc, resultPtr); + } + + function callClean(text, categories) { + const textBytes = encoder.encode(String(text || "")); + const catBytes = encoder.encode(Array.isArray(categories) ? categories.join(",") : String(categories || "")); + const textPtr = writeUtf8(memory, fm_alloc, textBytes); + const catPtr = writeUtf8(memory, fm_alloc, catBytes); + const resultPtr = fm_clean(textPtr, textBytes.length, catPtr, catBytes.length); + fm_dealloc(textPtr, textBytes.length); + fm_dealloc(catPtr, catBytes.length); + return readPackedJson(memory, fm_dealloc, resultPtr); + } + + function scanText(text, categories, language) { + const source = String(text || ""); + const payload = callScan(source, language || "auto", categories || "", -1); + const offsets = utf16Offsets(source); + const findings = (payload.findings || []).map((finding) => { + const offset = offsets[finding.index] || 0; + const length = finding.codepoint > 0xffff ? 2 : 1; + return { + offset, + length, + index: finding.index, + codepoint: finding.codepoint, + category: finding.category, + context: finding.context, + severity: finding.severity, + why: finding.why, + remedy: finding.remedy, + }; + }); + return { + total: payload.total || 0, + counts: payload.counts || {}, + findings, + truncated: Boolean(payload.truncated), + highest_severity: payload.highest_severity || "", + source_length: payload.source_length || 0, + }; + } + + function cleanText(text, categories) { + return callClean(text, categories || ""); + } + + const api = Object.assign({}, fallback || {}, { + engine: "wasm", + SCAN_ALGORITHM_VERSION: "fuckmark-hidden-scan-v1", + classify(cp) { + const index = fm_classify(cp >>> 0); + return index < 0 ? null : WASM_CATEGORIES[index]; + }, + scanText, + cleanText, + autofixTrojanSource(text) { + return cleanText(text, ["bidi_control"]); + }, + codepointToken: (fallback && fallback.codepointToken) || codepointToken, + }); + return api; +} + +async function instantiateWasm(source) { + let buffer; + if (source instanceof ArrayBuffer) { + buffer = source; + } else if (ArrayBuffer.isView(source)) { + buffer = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength); + } else if (typeof source === "string") { + const response = await fetch(source); + if (!response.ok) { + throw new Error("fuckmark_scan.wasm missing"); + } + buffer = await response.arrayBuffer(); + } else { + throw new Error("wasm source required"); + } + const result = await WebAssembly.instantiate(buffer, {}); + return result.instance; +} + +async function loadFuckMarkScanWasm(source) { + const fallback = typeof globalThis !== "undefined" ? globalThis.FuckMarkScan : undefined; + const instance = await instantiateWasm(source || "fuckmark_scan.wasm"); + const api = wrapInstance(instance, fallback); + if (typeof globalThis !== "undefined") { + globalThis.FuckMarkScan = api; + globalThis.FuckMarkScanWasm = api; + } + if (typeof module !== "undefined" && module.exports) { + module.exports.loadFuckMarkScanWasm = loadFuckMarkScanWasm; + module.exports.wrapInstance = wrapInstance; + } + return api; +} + +if (typeof module !== "undefined" && module.exports) { + module.exports.loadFuckMarkScanWasm = loadFuckMarkScanWasm; + module.exports.wrapInstance = wrapInstance; + module.exports.WASM_CATEGORIES = WASM_CATEGORIES; +} +if (typeof globalThis !== "undefined") { + globalThis.loadFuckMarkScanWasm = loadFuckMarkScanWasm; +} diff --git a/fuckmark/web.py b/fuckmark/web.py index 865b90df..03fba62f 100644 --- a/fuckmark/web.py +++ b/fuckmark/web.py @@ -171,6 +171,11 @@ def _web_parser() -> argparse.ArgumentParser: class _MarkHandler(http.server.SimpleHTTPRequestHandler): + extensions_map = { + **http.server.SimpleHTTPRequestHandler.extensions_map, + ".wasm": "application/wasm", + } + def log_message(self, format: str, *args) -> None: return diff --git a/fuckmark/webui/fuckmark_scan.wasm b/fuckmark/webui/fuckmark_scan.wasm new file mode 100755 index 0000000000000000000000000000000000000000..b994a12c31eddaf90fab3baafcdc0cd02b6bb8b5 GIT binary patch literal 45169 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 Zc1nTgPI!Lhvrgt{>p8OKDE-2d{s*RKnZp18 literal 0 HcmV?d00001 diff --git a/fuckmark/webui/scan.html b/fuckmark/webui/scan.html index 0b007977..6457e557 100644 --- a/fuckmark/webui/scan.html +++ b/fuckmark/webui/scan.html @@ -288,7 +288,9 @@

    See the bytes.

      - Scanning is fuckmark-hidden-scan-v1. Copy uses the security category set + Scanning is fuckmark-hidden-scan-v1. The page prefers the local WASM engine + (fuckmark_scan.wasm) and keeps scan.js as the file:// + fallback. Copy uses the security category set (bidi_control, zero_width, tag, control, noncharacter, surrogate). Fix Trojan Source strips only bidirectional controls. Nothing is uploaded from a static page or file://. @@ -298,6 +300,7 @@

      See the bytes.

      +