diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeed56d8..dd793c29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: outputs: frontend: ${{ steps.filter.outputs.frontend }} backend: ${{ steps.filter.outputs.backend }} + wasm: ${{ steps.filter.outputs.wasm }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 @@ -44,6 +45,9 @@ jobs: - 'Cargo.toml' - 'Cargo.lock' - 'tests/**' + wasm: + - 'wasm/**' + - 'scripts/build-wasm.sh' frontend-check: name: Frontend — CSS and JS checks (format, lint, css-rules, types) @@ -122,6 +126,51 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-targets --all-features -- -D warnings + # Mirrors the `wasm-check` justfile recipe. The wasm crate is a + # standalone workspace under `wasm/oxicloud-hash/` and is NOT + # built by the server's `cargo build` — these checks have to run + # explicitly from inside the sub-workspace. clippy + tests run + # against the HOST target (no wasm32 target needed in CI) which + # is enough to cover the algorithmic logic; the wasm32 build is + # exercised by `scripts/build-wasm.sh` and the frontend tests. + wasm-check: + name: Wasm — fmt + clippy + needs: changes + if: needs.changes.outputs.wasm == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: wasm/oxicloud-hash + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: wasm/oxicloud-hash + - run: cargo fmt --all --check + - run: cargo clippy --all-features --release -- -D warnings + + # Mirrors the `wasm-test` justfile recipe. Tests run in release + # mode because the FastCDC + BLAKE3 workload takes minutes in + # the default debug profile (no inlining / no SIMD). + wasm-test: + name: Wasm — release tests + needs: changes + if: needs.changes.outputs.wasm == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: wasm/oxicloud-hash + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: wasm/oxicloud-hash + - run: cargo test --release + rust-test: name: Server Unit and Functionnal Tests needs: changes diff --git a/justfile b/justfile index a0622737..5fc59258 100644 --- a/justfile +++ b/justfile @@ -69,6 +69,13 @@ frontend-check: node scripts/check-dead-tokens.mjs node scripts/check-brand-drift.mjs +wasm-check: + cd wasm/oxicloud-hash; cargo fmt --all + cd wasm/oxicloud-hash; cargo clippy --all-features --release -- -D warnings + +wasm-test: + cd wasm/oxicloud-hash; cargo test --release + # audit security (condition: cargo install cargo-audit) audit: cargo audit diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 9a64cff7..55a52528 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -20,6 +20,19 @@ cd "$(dirname "$0")/.." CRATE=wasm/oxicloud-hash OUT=static/js/vendors/hash-wasm +if ! ( rustup target list --installed | grep -q wasm32-unknown-unknown ) +then + echo -e "install first wasm32-unknown-unknown via:\n\trustup target add wasm32-unknown-unknown" + exit 1 +fi + +if ! which wasm-bindgen >/dev/null 2>/dev/null +then + WASM_BINDGEN_VERSION=$(grep -A1 'name = "wasm-bindgen"' $CREATE/Cargo.lock | head -2 | grep version | sed 's/version = //') + echo -e "wasm-bindgen not found, install it via:\n\tcargo install wasm-bindgen-cli --version $WASM_BINDGEN_VERSION" + exit 1 +fi + # SIMD128 is baseline in every evergreen browser (Chrome 91+, Firefox 89+, # Safari 16.4+) and is worth ~3-4× in hashing throughput. Browsers without # it fail instantiation; the frontend detects that and falls back to a diff --git a/static/js/vendors/hash-wasm/oxicloud_hash_wasm_bg.wasm b/static/js/vendors/hash-wasm/oxicloud_hash_wasm_bg.wasm index 0f09d2a6..66b6097f 100644 Binary files a/static/js/vendors/hash-wasm/oxicloud_hash_wasm_bg.wasm and b/static/js/vendors/hash-wasm/oxicloud_hash_wasm_bg.wasm differ diff --git a/wasm/oxicloud-hash/src/lib.rs b/wasm/oxicloud-hash/src/lib.rs index 627c379b..b7c2955a 100644 --- a/wasm/oxicloud-hash/src/lib.rs +++ b/wasm/oxicloud-hash/src/lib.rs @@ -153,7 +153,7 @@ impl DeltaChunker { let bytes = &self.buf[chunk.offset..chunk.offset + chunk.length]; push_chunk_json( &mut out, - &blake3::hash(bytes).to_hex().to_string(), + blake3::hash(bytes).to_hex().as_ref(), chunk.length, ); consumed = chunk.offset + chunk.length; @@ -175,13 +175,13 @@ impl DeltaChunker { if !self.buf.is_empty() { push_chunk_json( &mut out, - &blake3::hash(&self.buf).to_hex().to_string(), + blake3::hash(&self.buf).to_hex().as_ref(), self.buf.len(), ); self.buf.clear(); } out.push_str("],\"file_hash\":\""); - out.push_str(&self.file_hasher.finalize().to_hex().to_string()); + out.push_str(self.file_hasher.finalize().to_hex().as_ref()); out.push_str("\",\"total\":"); out.push_str(&self.total.to_string()); out.push('}'); @@ -238,7 +238,7 @@ mod tests { fn run_chunker(data: &[u8], slice: usize) -> (Vec<(String, usize)>, String) { let mut chunker = DeltaChunker::new(); let mut chunks: Vec<(String, usize)> = Vec::new(); - let mut parse = |json: &str, into: &mut Vec<(String, usize)>| { + let parse = |json: &str, into: &mut Vec<(String, usize)>| { // items look like ["",N] — split on '[' groups. for item in json.split("[\"").skip(1) { let hash = &item[..64];