From 496cf5a989e8b884ff5d71df8d3c70fc1d8359e8 Mon Sep 17 00:00:00 2001 From: Kenny Kerr Date: Wed, 24 Jan 2024 14:03:28 -0600 Subject: [PATCH] Add JSON validator sample (#2815) --- .github/workflows/clippy.yml | 1 + .github/workflows/test.yml | 3 +- .github/workflows/windows-sys.yml | 2 +- .github/workflows/windows-version.yml | 2 +- crates/libs/sys/Cargo.toml | 2 +- crates/libs/targets/Cargo.toml | 2 +- crates/libs/version/Cargo.toml | 2 +- .../components/json_validator/Cargo.toml | 19 ++ .../components/json_validator/readme.md | 1 + .../components/json_validator/src/lib.rs | 315 ++++++++++++++++++ crates/targets/aarch64_gnullvm/Cargo.toml | 2 +- crates/targets/aarch64_msvc/Cargo.toml | 2 +- crates/targets/i686_gnu/Cargo.toml | 2 +- crates/targets/i686_msvc/Cargo.toml | 2 +- crates/targets/x86_64_gnu/Cargo.toml | 2 +- crates/targets/x86_64_gnullvm/Cargo.toml | 2 +- crates/targets/x86_64_msvc/Cargo.toml | 2 +- crates/tools/riddle/Cargo.toml | 2 +- 18 files changed, 351 insertions(+), 14 deletions(-) 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/.github/workflows/windows-sys.yml b/.github/workflows/windows-sys.yml index a3aad7efa9..3598e1fd8b 100644 --- a/.github/workflows/windows-sys.yml +++ b/.github/workflows/windows-sys.yml @@ -16,7 +16,7 @@ jobs: name: Check strategy: matrix: - rust: [1.56.0, stable, nightly] + rust: [1.60.0, stable, nightly] runs-on: - windows-latest - ubuntu-latest diff --git a/.github/workflows/windows-version.yml b/.github/workflows/windows-version.yml index a64c4a6cf9..6cc98d97ab 100644 --- a/.github/workflows/windows-version.yml +++ b/.github/workflows/windows-version.yml @@ -16,7 +16,7 @@ jobs: name: Check strategy: matrix: - rust: [1.56.0, stable, nightly] + rust: [1.60.0, stable, nightly] runs-on: - windows-latest - ubuntu-latest diff --git a/crates/libs/sys/Cargo.toml b/crates/libs/sys/Cargo.toml index 2e5a976623..9d36d5f16a 100644 --- a/crates/libs/sys/Cargo.toml +++ b/crates/libs/sys/Cargo.toml @@ -3,7 +3,7 @@ name = "windows-sys" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Rust for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/libs/targets/Cargo.toml b/crates/libs/targets/Cargo.toml index 950cae37ab..952cb6304b 100644 --- a/crates/libs/targets/Cargo.toml +++ b/crates/libs/targets/Cargo.toml @@ -4,7 +4,7 @@ name = "windows-targets" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import libs for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/libs/version/Cargo.toml b/crates/libs/version/Cargo.toml index 644ba93716..864eeb27c5 100644 --- a/crates/libs/version/Cargo.toml +++ b/crates/libs/version/Cargo.toml @@ -3,7 +3,7 @@ name = "windows-version" version = "0.1.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Windows version information" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/samples/components/json_validator/Cargo.toml b/crates/samples/components/json_validator/Cargo.toml new file mode 100644 index 0000000000..242fdb03e7 --- /dev/null +++ b/crates/samples/components/json_validator/Cargo.toml @@ -0,0 +1,19 @@ +[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", + "Win32_System_Com", +] 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..cd5d35fa63 --- /dev/null +++ b/crates/samples/components/json_validator/src/lib.rs @@ -0,0 +1,315 @@ +use jsonschema::JSONSchema; +use windows::{core::*, Win32::Foundation::*, Win32::System::Com::*}; + +// Creates a JSON validator object with the given schema. The returned handle must be freed +// by calling `CloseJsonValidator`. +#[no_mangle] +unsafe 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] +unsafe extern "system" fn ValidateJson( + handle: usize, + value: *const u8, + value_len: usize, + sanitized_value: *mut *mut u8, + sanitized_value_len: *mut usize, +) -> HRESULT { + validate( + handle, + value, + value_len, + sanitized_value, + sanitized_value_len, + ) + .into() +} + +// Closes a JSON validator object. +#[no_mangle] +unsafe extern "system" fn CloseJsonValidator(handle: usize) { + if handle != 0 { + _ = Box::from_raw(handle as *mut JSONSchema); + } +} + +// Implementation of the `CreateJsonValidator` function so we can use `Result` for simplicity. +unsafe fn create_validator(schema: *const u8, schema_len: usize, handle: *mut usize) -> Result<()> { + let schema = json_from_raw_parts(schema, schema_len)?; + + let compiled = JSONSchema::compile(&schema) + .map_err(|error| Error::new(E_INVALIDARG, error.to_string().into()))?; + + if handle.is_null() { + return Err(E_POINTER.into()); + } + + // The handle is not null so we can safely dereference it here. + *handle = Box::into_raw(Box::new(compiled)) as usize; + + Ok(()) +} + +// Implementation of the `ValidateJson` function so we can use `Result` for simplicity. +unsafe fn validate( + handle: usize, + value: *const u8, + value_len: usize, + sanitized_value: *mut *mut u8, + sanitized_value_len: *mut 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 = &*(handle as *const JSONSchema); + + if schema.is_valid(&value) { + if !sanitized_value.is_null() && !sanitized_value_len.is_null() { + let value = value.to_string(); + + *sanitized_value = CoTaskMemAlloc(value.len()) as _; + + if (*sanitized_value).is_null() { + return Err(E_OUTOFMEMORY.into()); + } + + (*sanitized_value).copy_from(value.as_ptr(), value.len()); + *sanitized_value_len = value.len(); + } + + 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. +unsafe fn json_from_raw_parts(value: *const u8, value_len: usize) -> Result { + if value.is_null() { + return Err(E_POINTER.into()); + } + + let value = std::slice::from_raw_parts(value, value_len); + + let value = + std::str::from_utf8(value).map_err(|_| Error::from(ERROR_NO_UNICODE_TRANSLATION))?; + + serde_json::from_str(value).map_err(|error| Error::new(E_INVALIDARG, format!("{error}").into())) +} + +#[test] +fn simple() { + unsafe { + // 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(), + std::ptr::null_mut(), + std::ptr::null_mut() + ) + ); + + // Check check validation failure provides reasonable error information. + let value = r#""Hello World""#; + let code = ValidateJson( + handle, + value.as_ptr(), + value.len(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + 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(), + std::ptr::null_mut(), + std::ptr::null_mut() + ) + ); + + // Close the validator with the given handle. + CloseJsonValidator(handle); + + // Closing a "zero" handle is harmless. + CloseJsonValidator(0); + } +} + +#[test] +fn invalid_create_params() { + unsafe { + // 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() { + unsafe { + // 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(), + std::ptr::null_mut(), + std::ptr::null_mut() + ) + ); + + // Check that a value null pointer is caught. + assert_eq!( + E_POINTER, + ValidateJson( + handle, + std::ptr::null(), + value.len(), + std::ptr::null_mut(), + std::ptr::null_mut() + ) + ); + + // Check that JSON parsing failure provides reasonable error information. + let value = r#""Hello"#; + let code = ValidateJson( + handle, + value.as_ptr(), + value.len(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + 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); + } +} + +#[test] +fn sanitized_value() { + unsafe { + // Create a validator with the given schema. + let schema = r#" + { + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + } + "#; + + let mut handle = 0; + assert_eq!( + S_OK, + CreateJsonValidator(schema.as_ptr(), schema.len(), &mut handle) + ); + + // Validate and check the sanitized return value. + let value = r#" + { + "name": "Kenny", + "age": 21 + } + "#; + let mut sanitized_alloc = std::ptr::null_mut(); + let mut sanitized_len = 0; + + assert_eq!( + S_OK, + ValidateJson( + handle, + value.as_ptr(), + value.len(), + &mut sanitized_alloc, + &mut sanitized_len + ) + ); + let sanitized = std::slice::from_raw_parts(sanitized_alloc, sanitized_len); + let sanitized = String::from_utf8_lossy(sanitized); + CoTaskMemFree(Some(sanitized_alloc as _)); + assert_eq!(sanitized, r#"{"age":21,"name":"Kenny"}"#); + + // Close the validator with the given handle. + CloseJsonValidator(handle); + } +} diff --git a/crates/targets/aarch64_gnullvm/Cargo.toml b/crates/targets/aarch64_gnullvm/Cargo.toml index 44d4f92943..694b440577 100644 --- a/crates/targets/aarch64_gnullvm/Cargo.toml +++ b/crates/targets/aarch64_gnullvm/Cargo.toml @@ -3,7 +3,7 @@ name = "windows_aarch64_gnullvm" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import lib for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/targets/aarch64_msvc/Cargo.toml b/crates/targets/aarch64_msvc/Cargo.toml index f16aeeefd8..3c936275bc 100644 --- a/crates/targets/aarch64_msvc/Cargo.toml +++ b/crates/targets/aarch64_msvc/Cargo.toml @@ -3,7 +3,7 @@ name = "windows_aarch64_msvc" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import lib for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/targets/i686_gnu/Cargo.toml b/crates/targets/i686_gnu/Cargo.toml index 4200436f3b..e5a7d38f75 100644 --- a/crates/targets/i686_gnu/Cargo.toml +++ b/crates/targets/i686_gnu/Cargo.toml @@ -3,7 +3,7 @@ name = "windows_i686_gnu" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import lib for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/targets/i686_msvc/Cargo.toml b/crates/targets/i686_msvc/Cargo.toml index 9a2b612b44..db707034a7 100644 --- a/crates/targets/i686_msvc/Cargo.toml +++ b/crates/targets/i686_msvc/Cargo.toml @@ -3,7 +3,7 @@ name = "windows_i686_msvc" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import lib for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/targets/x86_64_gnu/Cargo.toml b/crates/targets/x86_64_gnu/Cargo.toml index f45204b9a0..a847fcf4fb 100644 --- a/crates/targets/x86_64_gnu/Cargo.toml +++ b/crates/targets/x86_64_gnu/Cargo.toml @@ -3,7 +3,7 @@ name = "windows_x86_64_gnu" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import lib for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/targets/x86_64_gnullvm/Cargo.toml b/crates/targets/x86_64_gnullvm/Cargo.toml index 7cadad8954..1cc813c4bc 100644 --- a/crates/targets/x86_64_gnullvm/Cargo.toml +++ b/crates/targets/x86_64_gnullvm/Cargo.toml @@ -3,7 +3,7 @@ name = "windows_x86_64_gnullvm" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import lib for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/targets/x86_64_msvc/Cargo.toml b/crates/targets/x86_64_msvc/Cargo.toml index 4156aefe85..57aef6246b 100644 --- a/crates/targets/x86_64_msvc/Cargo.toml +++ b/crates/targets/x86_64_msvc/Cargo.toml @@ -3,7 +3,7 @@ name = "windows_x86_64_msvc" version = "0.52.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Import lib for Windows" repository = "https://github.com/microsoft/windows-rs" diff --git a/crates/tools/riddle/Cargo.toml b/crates/tools/riddle/Cargo.toml index 0a8cde3ff5..c4e4ffb39c 100644 --- a/crates/tools/riddle/Cargo.toml +++ b/crates/tools/riddle/Cargo.toml @@ -3,7 +3,7 @@ name = "riddle" version = "0.1.0" authors = ["Microsoft"] edition = "2021" -rust-version = "1.56" +rust-version = "1.60" license = "MIT OR Apache-2.0" description = "Windows metadata compiler" repository = "https://github.com/microsoft/windows-rs"