From 496cf5a989e8b884ff5d71df8d3c70fc1d8359e8 Mon Sep 17 00:00:00 2001
From: Kenny Kerr <kenny@kennykerr.ca>
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<serde_json::Value> {
+    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"