Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,27 @@ jobs:
with:
targets: wasm32-unknown-unknown

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"

- 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

- name: Build host library and language bindings
run: |
./crates/fuckmark-scan/build-native.sh
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
python -m pytest -q tests/test_language_bindings.py tests/test_scan_wasm.py

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- 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. Lone UTF-16 surrogates route through the JS engine (UTF-8 cannot carry them). Empty category arrays stay empty selections. The extension CSP allows `wasm-unsafe-eval` so Chromium can instantiate the module. Page reveal and paste-safe keep the synchronous JS port. Rebuild with `crates/fuckmark-scan/build-wasm.sh`.
- Language bindings for the same Rust scan ABI: C header (`crates/fuckmark-scan/include/fuckmark_scan.h`), optional Python ctypes loader (`fuckmark.native_scan`), and a Node package under `bindings/node` (WASM + JS fallback). Host library build: `crates/fuckmark-scan/build-native.sh`. Lone UTF-16 surrogates cannot cross the UTF-8 ABI, so Python `scan_text` / `clean_text` delegate those strings to the Python reference scanner, and the WASM wrapper normalizes the JS fallback result to the same `index` / `truncated` / `highest_severity` / `source_length` schema. The C example is linked with `-rpath,$ORIGIN` and written next to `libfuckmark_scan.so`. Overview: `bindings/README.md`.
- `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.
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,16 @@ Eligible Latin, Greek, Cyrillic, Han, Kana, Hangul syllable, and emoji clusters

Frozen evidence, hashes, and protocols: [`docs/research.md`](docs/research.md). Product contract: [`docs/product-contract.md`](docs/product-contract.md). CLI: [`docs/cli.md`](docs/cli.md). Limits matrix: [`docs/limits.md`](docs/limits.md). Demo: [`docs/demo.html`](docs/demo.html).

## Language bindings (Rust scan ABI)

The hidden-scan classifier also ships as a small Rust crate with a C ABI, an optional Python native loader (`fuckmark.native_scan`), and a Node package (`bindings/node`). Overview: [`bindings/README.md`](bindings/README.md).

```text
./crates/fuckmark-scan/build-native.sh
python -c "from fuckmark.native_scan import scan_text; print(scan_text('a\u202eb')['total'])"
cd bindings/node && node sync-assets.js && node test.js
```

## Development

```text
Expand Down
57 changes: 57 additions & 0 deletions bindings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Language bindings

The classifier core lives in `crates/fuckmark-scan` (`fuckmark-hidden-scan-v1`).
Python remains the reference implementation in `fuckmark.product.scan`. These
bindings call the same table through the shared C / WASM ABI.

| Binding | Path | Engine |
| --- | --- | --- |
| C / FFI header | `crates/fuckmark-scan/include/fuckmark_scan.h` | host `cdylib` |
| Python (optional native) | `fuckmark/native_scan.py` | host `cdylib` via ctypes |
| Node / JS | `bindings/node` | WASM + JS fallback |
| Browser / extension | `editors/wasm`, `docs/scan.html` | WASM + JS fallback |

## Build the host library

```text
./crates/fuckmark-scan/build-native.sh
```

That prints the path to `libfuckmark_scan.so` (Linux), `.dylib` (macOS), or
`fuckmark_scan.dll` (Windows). Override discovery with `FUCKMARK_SCAN_LIB`.

Categories ABI: pass `*` for every category, an empty string for an empty
selection, or a comma-separated list such as `bidi_control,tag`.

Packed return values begin with a little-endian `u32` length followed by UTF-8
JSON.

## Python

```text
./crates/fuckmark-scan/build-native.sh
python -c "from fuckmark.native_scan import scan_text; print(scan_text('a\u202eb'))"
```

If the library is missing, `available()` is false and callers should use
`fuckmark.product.scan`.

## Node

```text
cd bindings/node
node sync-assets.js
node test.js
```

## C example

```text
./crates/fuckmark-scan/build-native.sh
cc -I crates/fuckmark-scan/include \
crates/fuckmark-scan/examples/scan_cli.c \
-L crates/fuckmark-scan/target/release -lfuckmark_scan \
-Wl,-rpath,\$ORIGIN \
-o crates/fuckmark-scan/target/release/fm-scan
./crates/fuckmark-scan/target/release/fm-scan $'a\u202eb'
```
21 changes: 21 additions & 0 deletions bindings/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @fuckmark/scan (Node)

Node binding for `fuckmark-hidden-scan-v1`. Loads the committed WASM module and
keeps the JS port as a fallback for lone UTF-16 surrogates / `file`-style
environments.

```text
cd bindings/node
node sync-assets.js
node test.js
```

```js
const scan = require("./index.js");

const result = await scan.scanText(source, null, "javascript");
const cleaned = await scan.cleanText(source, scan.fallback.DEFAULT_SECURITY_CATEGORIES);
```

Assets are copied from `editors/wasm` and `editors/vscode/scan.js`. Re-run
`npm run sync` after rebuilding the WASM module.
Binary file added bindings/node/fuckmark_scan.wasm
Binary file not shown.
56 changes: 56 additions & 0 deletions bindings/node/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use strict";

const fs = require("fs");
const path = require("path");
const loader = require("./scan_wasm.js");
const fallback = require("./scan.js");

let ready = null;

function wasmPath() {
return path.join(__dirname, "fuckmark_scan.wasm");
}

async function createScanEngine() {
globalThis.FuckMarkScan = fallback;
const bytes = fs.readFileSync(wasmPath());
return loader.loadFuckMarkScanWasm(bytes);
}

function getEngine() {
if (!ready) {
ready = createScanEngine();
}
return ready;
}

async function scanText(text, categories, language) {
const api = await getEngine();
return api.scanText(text, categories, language);
}

async function cleanText(text, categories) {
const api = await getEngine();
return api.cleanText(text, categories);
}

async function autofixTrojanSource(text) {
const api = await getEngine();
return api.autofixTrojanSource(text);
}

async function classify(codepoint) {
const api = await getEngine();
return api.classify(codepoint);
}

module.exports = {
SCAN_ALGORITHM_VERSION: "fuckmark-hidden-scan-v1",
createScanEngine,
getEngine,
scanText,
cleanText,
autofixTrojanSource,
classify,
fallback,
};
34 changes: 34 additions & 0 deletions bindings/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@fuckmark/scan",
"version": "0.4.1",
"description": "fuckmark-hidden-scan-v1 Node binding (WASM engine with JS fallback)",
"license": "MIT",
"type": "commonjs",
"main": "index.js",
"files": [
"index.js",
"scan_wasm.js",
"fuckmark_scan.wasm",
"scan.js",
"README.md"
],
"engines": {
"node": ">=18"
},
"scripts": {
"test": "node test.js",
"sync": "node sync-assets.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/byte271/FuckMark.git",
"directory": "bindings/node"
},
"keywords": [
"unicode",
"trojan-source",
"hidden-text",
"wasm",
"security"
]
}
Loading