diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e69043f..1a89b27 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -43,6 +43,8 @@ jobs: run: | cd backend cargo test --verbose + cd sync_lib + wasm-pack test --node - name: Lint frontend run: | diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 78b2aa6..dbeff8b 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2102,6 +2102,7 @@ dependencies = [ "base64 0.22.1", "console_error_panic_hook", "getrandom", + "insta", "reconcile", "thiserror", "wasm-bindgen", diff --git a/backend/sync_lib/Cargo.toml b/backend/sync_lib/Cargo.toml index 11c1f5c..864443f 100644 --- a/backend/sync_lib/Cargo.toml +++ b/backend/sync_lib/Cargo.toml @@ -22,6 +22,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } [dev-dependencies] wasm-bindgen-test = "0.3.34" +insta = "1.41.1" [features] default = ["console_error_panic_hook"] diff --git a/backend/sync_lib/src/errors.rs b/backend/sync_lib/src/errors.rs index 68bc71c..c09eafb 100644 --- a/backend/sync_lib/src/errors.rs +++ b/backend/sync_lib/src/errors.rs @@ -1,5 +1,3 @@ -use std::str::Utf8Error; - use base64::DecodeError; use thiserror::Error; use wasm_bindgen::JsValue; @@ -8,9 +6,6 @@ use wasm_bindgen::JsValue; pub enum SyncLibError { #[error("Base64 decoding error because of {}", .reason)] Base64DecodingError { reason: String }, - - #[error("Bytes cannot be decoded as UTF-8 string because of {}", .reason)] - StringDecodingError { reason: String }, } impl From for SyncLibError { @@ -21,14 +16,6 @@ impl From for SyncLibError { } } -impl From for SyncLibError { - fn from(e: Utf8Error) -> Self { - SyncLibError::StringDecodingError { - reason: e.to_string(), - } - } -} - impl From for SyncLibError { fn from(e: std::string::FromUtf8Error) -> Self { SyncLibError::Base64DecodingError { diff --git a/backend/sync_lib/src/lib.rs b/backend/sync_lib/src/lib.rs index 9832c5b..2258dee 100644 --- a/backend/sync_lib/src/lib.rs +++ b/backend/sync_lib/src/lib.rs @@ -6,53 +6,57 @@ use wasm_bindgen::prelude::*; pub mod errors; +/// Encode binary data for easy transport over HTTP. Inverse of +/// `base64_to_bytes`. #[wasm_bindgen(js_name = bytesToBase64)] pub fn bytes_to_base64(input: &[u8]) -> String { STANDARD_NO_PAD.encode(input) } -#[wasm_bindgen(js_name = stringToBase64)] -pub fn string_to_base64(input: &str) -> String { bytes_to_base64(input.as_bytes()) } - +/// Inverse of `bytes_to_base64`. #[wasm_bindgen(js_name = base64ToBytes)] pub fn base64_to_bytes(input: &str) -> Result, SyncLibError> { STANDARD_NO_PAD.decode(input).map_err(SyncLibError::from) } -#[wasm_bindgen(js_name = base64ToString)] -pub fn base64_to_string(input: &str) -> Result { - let bytes = base64_to_bytes(input)?; - String::from_utf8(bytes).map_err(SyncLibError::from) -} - +/// Merge two documents with a common parent. Relies on `reconcile::reconcile` +/// for texts and returns the right document as-is if either of the updated +/// documents is binary. #[wasm_bindgen] -pub fn merge(parent: &[u8], left: &[u8], right: &[u8]) -> Result, SyncLibError> { - Ok(if is_binary(right) { +pub fn merge(parent: &[u8], left: &[u8], right: &[u8]) -> Vec { + if is_binary(parent) || is_binary(left) || is_binary(right) { right.to_vec() } else { reconcile::reconcile( - str::from_utf8(parent).map_err(SyncLibError::from)?, - str::from_utf8(left).map_err(SyncLibError::from)?, - str::from_utf8(right).map_err(SyncLibError::from)?, + str::from_utf8(parent).expect("parent must be valid UTF-8 because it's not binary"), + str::from_utf8(left).expect("left must be valid UTF-8 because it's not binary"), + str::from_utf8(right).expect("right must be valid UTF-8 because it's not binary"), ) .into_bytes() - }) + } } +/// WASM wrapper around `reconcile::reconcile` for text merging. #[wasm_bindgen(js_name = mergeText)] pub fn merge_text(parent: &str, left: &str, right: &str) -> String { reconcile::reconcile(parent, left, right) } +/// Heuristically determine if the given data is a binary or a text file's +/// content. #[wasm_bindgen(js_name = isBinary)] -pub fn is_binary(data: &[u8]) -> bool { std::str::from_utf8(data).is_ok() } +pub fn is_binary(data: &[u8]) -> bool { + if data.iter().any(|&b| b == 0) { + // Even though the NUL character is valid in UTF-8, it's highly suspicious in + // human-readable text. + return true; + } + std::str::from_utf8(data).is_err() +} + +/// Set up panic hook for better error messages in the browser console. #[cfg(feature = "console_error_panic_hook")] #[wasm_bindgen(js_name = setPanicHook)] pub fn set_panic_hook() { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see // https://github.com/rustwasm/console_error_panic_hook#readme console_error_panic_hook::set_once(); } diff --git a/backend/sync_lib/tests/snapshots/web__base64_to_bytes_error.snap b/backend/sync_lib/tests/snapshots/web__base64_to_bytes_error.snap new file mode 100644 index 0000000..fa17876 --- /dev/null +++ b/backend/sync_lib/tests/snapshots/web__base64_to_bytes_error.snap @@ -0,0 +1,10 @@ +--- +source: sync_lib/tests/web.rs +expression: base64_to_bytes(input) +snapshot_kind: text +--- +Err( + Base64DecodingError { + reason: "Invalid symbol 61, offset 0.", + }, +) diff --git a/backend/sync_lib/tests/web.rs b/backend/sync_lib/tests/web.rs index de5c1da..642ceaa 100644 --- a/backend/sync_lib/tests/web.rs +++ b/backend/sync_lib/tests/web.rs @@ -1,13 +1,46 @@ //! Test suite for the Web and headless browsers. -#![cfg(target_arch = "wasm32")] - -extern crate wasm_bindgen_test; +use insta::assert_debug_snapshot; +use sync_lib::*; use wasm_bindgen_test::*; -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen_test] -fn pass() { - assert_eq!(1 + 1, 2); +#[wasm_bindgen_test(unsupported = test)] +fn test_bytes_to_base64() { + let input = b"hello"; + let expected = "aGVsbG8"; + assert_eq!(bytes_to_base64(input), expected); +} + +#[wasm_bindgen_test(unsupported = test)] +fn test_base64_to_bytes() { + let input = "aGVsbG8"; + let expected = b"hello".to_vec(); + assert_eq!(base64_to_bytes(input).unwrap(), expected); +} + +#[test] // insta doesn't support wasm-bindgen-test +fn test_base64_to_bytes_error() { + let input = "==="; + assert_debug_snapshot!(base64_to_bytes(input)); +} + +#[wasm_bindgen_test(unsupported = test)] +fn merge_text() { + let left = b"hello "; + let right = b"world"; + assert_eq!(merge(b"", left, right), b"hello world".to_vec()); +} + +#[wasm_bindgen_test(unsupported = test)] +fn merge_binary() { + let left = [0, 1, 2]; + let right = [3, 4, 5]; + assert_eq!(merge(b"", &left, &right), right); +} + +#[wasm_bindgen_test(unsupported = test)] +fn test_is_binary() { + assert!(is_binary(&[0, 159, 146, 150])); + assert!(is_binary(&[0, 12])); + assert!(!is_binary(b"hello")); }