diff --git a/Cargo.lock b/Cargo.lock index 6484811a9..45ebd2326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,6 +675,7 @@ dependencies = [ "tree-sitter", "tree-sitter-dscexpression", "tree-sitter-rust", + "urlencoding", "uuid", "which", ] @@ -3216,6 +3217,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 74639ac0f..dc5761a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,6 +194,8 @@ utfx = { version = "0.1" } # dsc-lib uuid = { version = "1.18", features = ["v4"] } # dsc-lib +urlencoding = { version = "2.1" } +# dsc-lib which = { version = "8.0" } # build-only dependencies diff --git a/docs/reference/schemas/config/functions/uriComponent.md b/docs/reference/schemas/config/functions/uriComponent.md new file mode 100644 index 000000000..bcf0325d5 --- /dev/null +++ b/docs/reference/schemas/config/functions/uriComponent.md @@ -0,0 +1,271 @@ +--- +description: Reference for the 'uriComponent' DSC configuration document function +ms.date: 01/10/2025 +ms.topic: reference +title: uriComponent +--- + +# uriComponent + +## Synopsis + +Encodes a string for use as a URI component using percent-encoding. + +## Syntax + +```Syntax +uriComponent() +``` + +## Description + +The `uriComponent()` function encodes a string using percent-encoding (also known as URL encoding) +to make it safe for use as a component of a URI. The function encodes all characters except the +unreserved characters defined in RFC 3986: + +- **Unreserved characters** (not encoded): `A-Z`, `a-z`, `0-9`, `-`, `_`, `.`, `~` +- **All other characters** are percent-encoded as `%XX` where `XX` is the hexadecimal value + +Use this function when you need to include user-provided data, special characters, or spaces in +URLs, query strings, or other URI components. This ensures that the resulting URI is valid and that +special characters don't break the URI structure. + +## Examples + +### Example 1 - Encode query parameter value + +The following example shows how to encode a string containing spaces for use in a URL query +parameter. + +```yaml +# uricomponent.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + searchTerm: + type: string + defaultValue: hello world +resources: +- name: Build search URL + type: Microsoft.DSC.Debug/Echo + properties: + output: + original: "[parameters('searchTerm')]" + encoded: "[uriComponent(parameters('searchTerm'))]" + fullUrl: "[concat('https://example.com/search?q=', uriComponent(parameters('searchTerm')))]" +``` + +```bash +dsc config get --file uricomponent.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Build search URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + original: hello world + encoded: hello%20world + fullUrl: https://example.com/search?q=hello%20world +messages: [] +hadErrors: false +``` + +### Example 2 - Encode email address + +The following example demonstrates encoding an email address that contains special characters. + +```yaml +# uricomponent.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + email: + type: string + defaultValue: user+tag@example.com +resources: +- name: Encode email for URL + type: Microsoft.DSC.Debug/Echo + properties: + output: + encoded: "[uriComponent(parameters('email'))]" + mailtoLink: "[concat('mailto:', uriComponent(parameters('email')))]" +``` + +```bash +dsc config get --file uricomponent.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Encode email for URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + encoded: user%2Btag%40example.com + mailtoLink: mailto:user%2Btag%40example.com +messages: [] +hadErrors: false +``` + +### Example 3 - Encode complete URL + +The following example shows how `uriComponent()` encodes an entire URL, including the protocol, +slashes, and special characters. + +```yaml +# uricomponent.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Encode complete URL + type: Microsoft.DSC.Debug/Echo + properties: + output: + originalUrl: https://example.com/path?query=value + encodedUrl: "[uriComponent('https://example.com/path?query=value')]" +``` + +```bash +dsc config get --file uricomponent.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Encode complete URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + originalUrl: https://example.com/path?query=value + encodedUrl: https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue +messages: [] +hadErrors: false +``` + +### Example 4 - Build API request with encoded parameters + +The following example demonstrates using `uriComponent()` with [`concat()`][01] and [`uri()`][02] +to build an API URL with safely encoded query parameters. + +```yaml +# uricomponent.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + apiBase: + type: string + defaultValue: https://api.example.com + resourcePath: + type: string + defaultValue: /users/search + nameFilter: + type: string + defaultValue: John Doe + ageFilter: + type: string + defaultValue: '30' +resources: +- name: Build API URL with query string + type: Microsoft.DSC.Debug/Echo + properties: + output: + apiUrl: >- + [concat( + uri(parameters('apiBase'), parameters('resourcePath')), + '?name=', + uriComponent(parameters('nameFilter')), + '&age=', + parameters('ageFilter') + )] +``` + +```bash +dsc config get --file uricomponent.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Build API URL with query string + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + apiUrl: https://api.example.com/users/search?name=John%20Doe&age=30 +messages: [] +hadErrors: false +``` + +### Example 5 - Unreserved characters remain unchanged + +The following example shows that unreserved characters (letters, numbers, hyphen, underscore, +period, and tilde) are not encoded. + +```yaml +# uricomponent.example.5.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Unreserved character handling + type: Microsoft.DSC.Debug/Echo + properties: + output: + original: ABCabc123-_.~ + encoded: "[uriComponent('ABCabc123-_.~')]" + identical: "[equals(uriComponent('ABCabc123-_.~'), 'ABCabc123-_.~')]" +``` + +```bash +dsc config get --file uricomponent.example.5.dsc.config.yaml +``` + +```yaml +results: +- name: Unreserved character handling + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + original: ABCabc123-_.~ + encoded: ABCabc123-_.~ + identical: true +messages: [] +hadErrors: false +``` + +## Parameters + +### stringToEncode + +The string value to encode using percent-encoding. All characters except unreserved characters +(A-Z, a-z, 0-9, -, _, ., ~) are encoded. + +```yaml +Type: string +Required: true +Position: 1 +``` + +## Output + +The `uriComponent()` function returns a string with all characters except unreserved characters +replaced with their percent-encoded equivalents (e.g., space becomes `%20`, `@` becomes `%40`). + +```yaml +Type: string +``` + +## Related functions + +- [`uri()`][02] - Combines base and relative URIs +- [`concat()`][01] - Concatenates multiple strings together +- [`format()`][03] - Creates a formatted string from a template +- [`base64()`][04] - Encodes a string to base64 +- [`parameters()`][05] - Retrieves parameter values +- [`equals()`][06] - Compares two values for equality + + +[01]: ./concat.md +[02]: ./uri.md +[03]: ./format.md +[04]: ./base64.md +[05]: ./parameters.md +[06]: ./equals.md diff --git a/docs/reference/schemas/config/functions/uriComponentToString.md b/docs/reference/schemas/config/functions/uriComponentToString.md new file mode 100644 index 000000000..84f425d65 --- /dev/null +++ b/docs/reference/schemas/config/functions/uriComponentToString.md @@ -0,0 +1,208 @@ +--- +description: Reference for the 'uriComponentToString' DSC configuration document function +ms.date: 10/10/2025 +ms.topic: reference +title: uriComponentToString function +--- + +# uriComponentToString + +## Synopsis + +Returns a decoded string from a URI-encoded value. + +## Syntax + +```yaml +uriComponentToString() +``` + +## Description + +The `uriComponentToString()` function decodes a URI-encoded string back to its original form. +It converts percent-encoded sequences (like `%20` for space or `%40` for `@`) back to their +original characters. This function is the inverse of [`uriComponent()`][01]. + +This function is useful when you need to decode URI components that were previously encoded, +such as query parameters, path segments, or other URI parts. + +## Examples + +### Example 1 - Decode a URI-encoded query parameter + +This example decodes a URI-encoded query parameter value back to its original string. + +```yaml +# example1.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded value + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('John%20Doe')]" +``` + +```bash +dsc config get --document example1.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded value + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: John Doe +``` + +### Example 2 - Decode a URI-encoded email address + +This example decodes a URI-encoded email address with special characters. + +```yaml +# example2.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded email + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('user%2Btag%40example.com')]" +``` + +```bash +dsc config get --document example2.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded email + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: user+tag@example.com +``` + +### Example 3 - Decode a complete URI-encoded URL + +This example decodes a completely URI-encoded URL back to its readable form. + +```yaml +# example3.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded URL + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('https%3A%2F%2Fapi.example.com%2Fusers%3Fstatus%3Dactive')]" +``` + +```bash +dsc config get --document example3.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: https://api.example.com/users?status=active +``` + +### Example 4 - Round-trip encoding and decoding + +This example demonstrates encoding a string with [`uriComponent()`][01] and then decoding it +back with `uriComponentToString()`, showing that they are inverse operations. + +```yaml +# example4.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo round-trip result + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString(uriComponent('Hello, World!'))]" +``` + +```bash +dsc config get --document example4.dsc.yaml config get +``` + +```yaml +results: +- name: Echo round-trip result + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: Hello, World! +``` + +### Example 5 - Decode Unicode characters + +This example decodes a URI-encoded string containing UTF-8 encoded Unicode characters. + +```yaml +# example5.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded Unicode + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('caf%C3%A9')]" +``` + +```bash +dsc config get --document example5.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded Unicode + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: café +``` + +## Parameters + +### uriEncodedString + +The `uriComponentToString()` function expects a single string argument representing a +URI-encoded value. The function decodes any percent-encoded sequences (like `%20`, `%40`, etc.) +back to their original characters. + +If the encoded string contains invalid percent-encoding sequences (such as incomplete sequences +or invalid hexadecimal digits), the function returns an error. + +```yaml +Type: string +Required: true +Position: 1 +``` + +## Output + +The `uriComponentToString()` function returns the decoded string with all percent-encoded +sequences converted back to their original characters. The output is always a string. + +```yaml +Type: string +``` + +## Related functions + +The following functions are related to `uriComponentToString()`: + +- [`uriComponent()`][01] - Encodes a string for safe use in URI components (inverse operation) +- [`uri()`][02] - Combines a base URI and relative URI with intelligent path handling +- [`base64ToString()`][03] - Decodes a base64-encoded string +- [`concat()`][04] - Combines multiple strings +- [`parameters()`][05] - Returns the value of a parameter + + +[01]: uriComponent.md +[02]: uri.md +[03]: base64ToString.md +[04]: concat.md +[05]: parameters.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 477db73da..5149c40b5 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -121,9 +121,10 @@ Describe 'tests for function expressions' { @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ - shared = [pscustomobject]@{ value = 42; flag = $true } - level = 1 - } } + shared = [pscustomobject]@{ value = 42; flag = $true } + level = 1 + } + } @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } ) { @@ -738,7 +739,7 @@ Describe 'tests for function expressions' { $out.results[0].result.actualState.output | Should -BeExactly $expected } - It 'base64ToString function works for: ' -TestCases @( + It 'base64ToString function works for: ' -TestCases @( @{ expression = "[base64ToString('aGVsbG8gd29ybGQ=')]"; expected = 'hello world' } @{ expression = "[base64ToString('')]"; expected = '' } @{ expression = "[base64ToString('aMOpbGxv')]"; expected = 'héllo' } @@ -930,4 +931,133 @@ Describe 'tests for function expressions' { $out.results[0].result.actualState.output | Should -BeExactly $expected } } + + It 'uriComponent function works for: ' -TestCases @( + @{ testInput = 'hello world' } + @{ testInput = 'hello@example.com' } + @{ testInput = 'https://example.com/path?query=value' } + @{ testInput = '' } + @{ testInput = 'ABCabc123-_.~' } + @{ testInput = ':/?#[]@!$&()*+,;=' } + @{ testInput = 'café' } + @{ testInput = 'name=John Doe&age=30' } + @{ testInput = '/path/to/my file.txt' } + @{ testInput = 'user+tag@example.com' } + @{ testInput = '1234567890' } + @{ testInput = '100%' } + @{ testInput = ' ' } + ) { + param($testInput) + + $expected = [Uri]::EscapeDataString($testInput) + $expression = "[uriComponent('$($testInput -replace "'", "''")')]" + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } + + It 'uriComponent function works with concat' { + $input1 = 'hello' + $input2 = ' ' + $input3 = 'world' + $expected = [Uri]::EscapeDataString($input1 + $input2 + $input3) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponent(concat('hello', ' ', 'world'))]" +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } + + It 'uriComponentToString function works for: ' -TestCases @( + @{ testInput = 'hello%20world' } + @{ testInput = 'hello%40example.com' } + @{ testInput = 'https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue' } + @{ testInput = '' } + @{ testInput = 'ABCabc123-_.~' } + @{ testInput = '%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D' } + @{ testInput = 'caf%C3%A9' } + @{ testInput = 'name%3DJohn%20Doe%26age%3D30' } + @{ testInput = '%2Fpath%2Fto%2Fmy%20file.txt' } + @{ testInput = '100%25' } + ) { + param($testInput) + + $expected = [Uri]::UnescapeDataString($testInput) + $expression = "[uriComponentToString('$($testInput -replace "'", "''")')]" + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } + + It 'uriComponentToString function works with round-trip encoding' { + $original = 'hello world' + $expected = $original + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString(uriComponent('hello world'))]" +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } + + It 'uriComponentToString function works with nested round-trip' { + $original = 'user+tag@example.com' + $expected = $original + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString(uriComponent('user+tag@example.com'))]" +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } + + It 'uriComponentToString function works with concat' { + $input1 = 'hello' + $input2 = '%20' + $input3 = 'world' + $expected = [Uri]::UnescapeDataString($input1 + $input2 + $input3) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString(concat('hello', '%20', 'world'))]" +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } } diff --git a/lib/dsc-lib/Cargo.toml b/lib/dsc-lib/Cargo.toml index 881bcd6d7..058c4a370 100644 --- a/lib/dsc-lib/Cargo.toml +++ b/lib/dsc-lib/Cargo.toml @@ -37,6 +37,7 @@ tracing-indicatif = { workspace = true } tree-sitter = { workspace = true } tree-sitter-rust = { workspace = true} uuid = { workspace = true } +urlencoding = { workspace = true } which = { workspace = true } # workspace crate dependencies dsc-lib-osinfo = { workspace = true } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 2c6429df4..6fe75107d 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -538,6 +538,13 @@ invalidArgType = "All arguments must either be arrays or objects" description = "Returns a deterministic unique string from the given strings" invoked = "uniqueString function" +[functions.uriComponent] +description = "Encodes a URI component using percent-encoding" + +[functions.uriComponentToString] +description = "Returns a string of a URI encoded value" +invalidUtf8 = "Invalid UTF-8 in decoded string: %{error}" + [functions.userFunction] expectedNoParameters = "User function '%{name}' does not accept parameters" unknownUserFunction = "Unknown user function '%{name}'" diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index ab4561299..dae8e9578 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -71,6 +71,8 @@ pub mod r#true; pub mod try_get; pub mod union; pub mod unique_string; +pub mod uri_component; +pub mod uri_component_to_string; pub mod user_function; pub mod utc_now; pub mod variables; @@ -196,6 +198,9 @@ impl FunctionDispatcher { Box::new(utc_now::UtcNow{}), Box::new(union::Union{}), Box::new(unique_string::UniqueString{}), + Box::new(uri_component::UriComponent{}), + Box::new(uri_component_to_string::UriComponentToString{}), + Box::new(utc_now::UtcNow{}), Box::new(variables::Variables{}), ]; for function in function_list { diff --git a/lib/dsc-lib/src/functions/uri_component.rs b/lib/dsc-lib/src/functions/uri_component.rs new file mode 100644 index 000000000..a31fdab8d --- /dev/null +++ b/lib/dsc-lib/src/functions/uri_component.rs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::Value; +use super::Function; + +#[derive(Debug, Default)] +pub struct UriComponent {} + +impl Function for UriComponent { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "uriComponent".to_string(), + description: t!("functions.uriComponent.description").to_string(), + category: vec![FunctionCategory::String], + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::String], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let string_to_encode = args[0].as_str().unwrap(); + let result = urlencoding::encode(string_to_encode); + Ok(Value::String(result.into_owned())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn test_uri_component_basic_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('hello world')]", &Context::new()).unwrap(); + assert_eq!(result, "hello%20world"); + } + + #[test] + fn test_uri_component_special_characters() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('hello@example.com')]", &Context::new()).unwrap(); + assert_eq!(result, "hello%40example.com"); + } + + #[test] + fn test_uri_component_url() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('https://example.com/path?query=value')]", &Context::new()).unwrap(); + assert_eq!(result, "https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue"); + } + + #[test] + fn test_uri_component_empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('')]", &Context::new()).unwrap(); + assert_eq!(result, ""); + } + + #[test] + fn test_uri_component_unreserved_characters() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('ABCabc123-_.~')]", &Context::new()).unwrap(); + assert_eq!(result, "ABCabc123-_.~"); + } + + #[test] + fn test_uri_component_reserved_characters() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent(':/?#[]@!$&()*+,;=')]", &Context::new()).unwrap(); + assert_eq!(result, "%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D"); + } + + #[test] + fn test_uri_component_unicode() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('café')]", &Context::new()).unwrap(); + assert_eq!(result, "caf%C3%A9"); + } + + #[test] + fn test_uri_component_query_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('name=John Doe&age=30')]", &Context::new()).unwrap(); + assert_eq!(result, "name%3DJohn%20Doe%26age%3D30"); + } + + #[test] + fn test_uri_component_path_with_spaces() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('/path/to/my file.txt')]", &Context::new()).unwrap(); + assert_eq!(result, "%2Fpath%2Fto%2Fmy%20file.txt"); + } + + #[test] + fn test_uri_component_nested_function() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent(concat('hello', ' ', 'world'))]", &Context::new()).unwrap(); + assert_eq!(result, "hello%20world"); + } + + #[test] + fn test_uri_component_email() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('user+tag@example.com')]", &Context::new()).unwrap(); + assert_eq!(result, "user%2Btag%40example.com"); + } + + #[test] + fn test_uri_component_numbers_only() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('1234567890')]", &Context::new()).unwrap(); + assert_eq!(result, "1234567890"); + } + + #[test] + fn test_uri_component_percent_sign() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('100%')]", &Context::new()).unwrap(); + assert_eq!(result, "100%25"); + } +} diff --git a/lib/dsc-lib/src/functions/uri_component_to_string.rs b/lib/dsc-lib/src/functions/uri_component_to_string.rs new file mode 100644 index 000000000..f66cfcfce --- /dev/null +++ b/lib/dsc-lib/src/functions/uri_component_to_string.rs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::Value; +use super::Function; + +#[derive(Debug, Default)] +pub struct UriComponentToString {} + +impl Function for UriComponentToString { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "uriComponentToString".to_string(), + description: t!("functions.uriComponentToString.description").to_string(), + category: vec![FunctionCategory::String], + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::String], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let uri_encoded_string = args[0].as_str().unwrap(); + let result = urlencoding::decode(uri_encoded_string) + .map_err(|e| DscError::Parser( + t!("functions.uriComponentToString.invalidUtf8", error = e).to_string() + ))?; + Ok(Value::String(result.into_owned())) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn test_uri_component_to_string_basic() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('hello%20world')]", &Context::new()).unwrap(); + assert_eq!(result, "hello world"); + } + + #[test] + fn test_uri_component_to_string_email() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('hello%40example.com')]", &Context::new()).unwrap(); + assert_eq!(result, "hello@example.com"); + } + + #[test] + fn test_uri_component_to_string_url() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/path?query=value"); + } + + #[test] + fn test_uri_component_to_string_empty() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('')]", &Context::new()).unwrap(); + assert_eq!(result, ""); + } + + #[test] + fn test_uri_component_to_string_no_encoding() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('ABCabc123-_.~')]", &Context::new()).unwrap(); + assert_eq!(result, "ABCabc123-_.~"); + } + + #[test] + fn test_uri_component_to_string_reserved_chars() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D')]", &Context::new()).unwrap(); + assert_eq!(result, ":/?#[]@!$&()*+,;="); + } + + #[test] + fn test_uri_component_to_string_unicode() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('caf%C3%A9')]", &Context::new()).unwrap(); + assert_eq!(result, "café"); + } + + #[test] + fn test_uri_component_to_string_query_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('name%3DJohn%20Doe%26age%3D30')]", &Context::new()).unwrap(); + assert_eq!(result, "name=John Doe&age=30"); + } + + #[test] + fn test_uri_component_to_string_path() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('%2Fpath%2Fto%2Fmy%20file.txt')]", &Context::new()).unwrap(); + assert_eq!(result, "/path/to/my file.txt"); + } + + #[test] + fn test_uri_component_to_string_roundtrip() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString(uriComponent('hello world'))]", &Context::new()).unwrap(); + assert_eq!(result, "hello world"); + } + + #[test] + fn test_uri_component_to_string_roundtrip_email() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString(uriComponent('user+tag@example.com'))]", &Context::new()).unwrap(); + assert_eq!(result, "user+tag@example.com"); + } + + #[test] + fn test_uri_component_to_string_percent_sign() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('100%25')]", &Context::new()).unwrap(); + assert_eq!(result, "100%"); + } + + #[test] + fn test_uri_component_to_string_mixed() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('hello%20world%21')]", &Context::new()).unwrap(); + assert_eq!(result, "hello world!"); + } +}