From 928717e24de5ab73436905fe6199469aee28325f Mon Sep 17 00:00:00 2001 From: Kenny Kerr Date: Tue, 23 Jan 2024 15:30:14 -0600 Subject: [PATCH] json_validator --- .github/workflows/clippy.yml | 1 + .github/workflows/test.yml | 3 +- .../components/json_validator/Cargo.toml | 18 ++ .../components/json_validator/readme.md | 1 + .../components/json_validator/src/lib.rs | 189 ++++++++++++++++++ 5 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 crates/samples/components/json_validator/Cargo.toml create mode 100644 crates/samples/components/json_validator/readme.md create mode 100644 crates/samples/components/json_validator/src/lib.rs diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 2c9460330e..d56fcbbfc6 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -38,6 +38,7 @@ jobs: cargo clippy -p sample_bits && cargo clippy -p sample_com_uri && cargo clippy -p sample_component_hello_world && + cargo clippy -p sample_component_json_validator && cargo clippy -p sample_consent && cargo clippy -p sample_core_app && cargo clippy -p sample_counter && diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 502246fc44..7a50357e54 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,7 @@ jobs: cargo test -p sample_bits && cargo test -p sample_com_uri && cargo test -p sample_component_hello_world && + cargo test -p sample_component_json_validator && cargo test -p sample_consent && cargo test -p sample_core_app && cargo test -p sample_counter && @@ -102,8 +103,8 @@ jobs: cargo test -p test_dispatch && cargo test -p test_does_not_return && cargo test -p test_enums && - cargo test -p test_error && cargo clean && + cargo test -p test_error && cargo test -p test_event && cargo test -p test_extensions && cargo test -p test_handles && diff --git a/crates/samples/components/json_validator/Cargo.toml b/crates/samples/components/json_validator/Cargo.toml new file mode 100644 index 0000000000..2b50a09604 --- /dev/null +++ b/crates/samples/components/json_validator/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sample_component_json_validator" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +jsonschema = "0.17" +serde_json = "1.0" + +[dependencies.windows] +path = "../../../libs/windows" +features = [ + "Win32_Foundation", +] diff --git a/crates/samples/components/json_validator/readme.md b/crates/samples/components/json_validator/readme.md new file mode 100644 index 0000000000..4ff7601da1 --- /dev/null +++ b/crates/samples/components/json_validator/readme.md @@ -0,0 +1 @@ +Sample for upcoming entry in [Getting Started with Rust](https://kennykerr.ca/rust-getting-started). diff --git a/crates/samples/components/json_validator/src/lib.rs b/crates/samples/components/json_validator/src/lib.rs new file mode 100644 index 0000000000..69d7de9e2e --- /dev/null +++ b/crates/samples/components/json_validator/src/lib.rs @@ -0,0 +1,189 @@ +use jsonschema::JSONSchema; +use windows::{core::*, Win32::Foundation::*}; + +// Creates a JSON validator object with the given schema. The returned handle must be freed +// by calling `CloseJsonValidator`. +#[no_mangle] +extern "system" fn CreateJsonValidator( + schema: *const u8, + schema_len: usize, + handle: *mut usize, +) -> HRESULT { + create_validator(schema, schema_len, handle).into() +} + +// Validates a JSON value against a previously-compiled schema. +#[no_mangle] +extern "system" fn ValidateJson(handle: usize, value: *const u8, value_len: usize) -> HRESULT { + validate(handle, value, value_len).into() +} + +// Closes a JSON validator object. +#[no_mangle] +extern "system" fn CloseJsonValidator(handle: usize) { + if handle != 0 { + unsafe { + _ = Box::from_raw(handle as *mut JSONSchema); + } + } +} + +// Implementation of the `CreateJsonValidator` function so we can use `Result` for simplicity. +fn create_validator(schema: *const u8, schema_len: usize, handle: *mut usize) -> Result<()> { + let schema = json_from_raw_parts(schema, schema_len)?; + + match JSONSchema::compile(&schema) { + Ok(compiled) => { + if handle.is_null() { + return Err(E_POINTER.into()); + } + + // The handle is not null so we can safely dereference it here. + unsafe { + *handle = Box::into_raw(Box::new(compiled)) as usize; + } + + Ok(()) + } + Err(error) => Err(Error::new(E_INVALIDARG, error.to_string().into())), + } +} + +// Implementation of the `ValidateJson` function so we can use `Result` for simplicity. +fn validate(handle: usize, value: *const u8, value_len: usize) -> Result<()> { + if handle == 0 { + return Err(E_HANDLE.into()); + } + + let value = json_from_raw_parts(value, value_len)?; + + // This looks a bit tricky but we're just turning the opaque handle into `JSONSchema` pointer + // and then returning a reference to avoid taking ownership of it. + let schema = unsafe { &*(handle as *const JSONSchema) }; + + if schema.is_valid(&value) { + Ok(()) + } else { + let mut message = String::new(); + + // The `validate` method returns a collection of errors. We'll just return the first + // for simplicity. + if let Some(error) = schema.validate(&value).unwrap_err().next() { + message = error.to_string(); + } + + Err(Error::new(E_INVALIDARG, message.into())) + } +} + +// Takes care of all the JSON parsing and parameter validation. +fn json_from_raw_parts(value: *const u8, value_len: usize) -> Result { + if value.is_null() { + return Err(E_POINTER.into()); + } + + let value = unsafe { std::slice::from_raw_parts(value, value_len) }; + + let Ok(value) = std::str::from_utf8(value) else { + return Err(ERROR_NO_UNICODE_TRANSLATION.into()); + }; + + match serde_json::from_str(value) { + Ok(value) => Ok(value), + Err(error) => Err(Error::new(E_INVALIDARG, format!("{error}").into())), + } +} + +#[test] +fn simple() { + // Create a validator with the given schema. + let schema = r#"{"maxLength": 5}"#; + let mut handle = 0; + assert_eq!( + S_OK, + CreateJsonValidator(schema.as_ptr(), schema.len(), &mut handle) + ); + + // Validate the json against the schema. + let value = r#""Hello""#; + assert_eq!(S_OK, ValidateJson(handle, value.as_ptr(), value.len())); + + // Check check validation failure provides reasonable error information. + let value = r#""Hello World""#; + let code = ValidateJson(handle, value.as_ptr(), value.len()); + assert_eq!(E_INVALIDARG, code); + assert_eq!( + r#""Hello World" is longer than 5 characters"#, + Error::from(code).message() + ); + + // The schema validator is reusable. + let value = r#""World""#; + assert_eq!(S_OK, ValidateJson(handle, value.as_ptr(), value.len())); + + // Close the validator with the given handle. + CloseJsonValidator(handle); + + // Closing a "zero" handle is harmless. + CloseJsonValidator(0); +} + +#[test] +fn invalid_create_params() { + // Check schema parsing failure provides reasonable error information. + let schema = r#"{ "invalid"#; + let mut handle = 0; + let code = CreateJsonValidator(schema.as_ptr(), schema.len(), &mut handle); + assert_eq!(E_INVALIDARG, code); + assert_eq!( + "EOF while parsing a string at line 1 column 10", + Error::from(code).message() + ); + + // Check that schema null pointer is caught. + let schema = r#"{"maxLength": 5}"#; + let mut handle = 0; + assert_eq!( + E_POINTER, + CreateJsonValidator(std::ptr::null(), schema.len(), &mut handle) + ); + + // Check that handle null pointer is caught. + assert_eq!( + E_POINTER, + CreateJsonValidator(schema.as_ptr(), schema.len(), std::ptr::null_mut()) + ); +} + +#[test] +fn invalid_validate_params() { + // Create a validator with the given schema. + let schema = r#"{"maxLength": 5}"#; + let mut handle = 0; + assert_eq!( + S_OK, + CreateJsonValidator(schema.as_ptr(), schema.len(), &mut handle) + ); + + // Check that a zero handle is caught. + let value = r#""Hello""#; + assert_eq!(E_HANDLE, ValidateJson(0, value.as_ptr(), value.len())); + + // Check that a value null pointer is caught. + assert_eq!( + E_POINTER, + ValidateJson(handle, std::ptr::null(), value.len()) + ); + + // Check that JSON parsing failure provides reasonable error information. + let value = r#""Hello"#; + let code = ValidateJson(handle, value.as_ptr(), value.len()); + assert_eq!(E_INVALIDARG, code); + assert_eq!( + "EOF while parsing a string at line 1 column 6", + Error::from(code).message() + ); + + // Close the validator with the given handle. + CloseJsonValidator(handle); +}