From 080e42ca987a701ed5dd0bbb93ec31eea5ae1d3a Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 30 Sep 2025 02:23:19 +0200 Subject: [PATCH 1/3] Add `base64String()` function --- .../config/functions/base64ToString.md | 208 ++++++++++++++++++ .../schemas/config/functions/createArray.md | 2 +- dsc/tests/dsc_functions.tests.ps1 | 42 ++++ dsc_lib/locales/en-us.toml | 6 + dsc_lib/src/functions/base64_to_string.rs | 118 ++++++++++ dsc_lib/src/functions/mod.rs | 2 + 6 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 docs/reference/schemas/config/functions/base64ToString.md create mode 100644 dsc_lib/src/functions/base64_to_string.rs diff --git a/docs/reference/schemas/config/functions/base64ToString.md b/docs/reference/schemas/config/functions/base64ToString.md new file mode 100644 index 000000000..2f5c02731 --- /dev/null +++ b/docs/reference/schemas/config/functions/base64ToString.md @@ -0,0 +1,208 @@ +--- +description: Reference for the 'base64ToString' DSC configuration document function +ms.date: 09/30/2025 +ms.topic: reference +title: base64ToString +--- + +# base64ToString + +## Synopsis + +Converts a base64 representation to a string. + +## Syntax + +```Syntax +base64ToString() +``` + +## Description + +The `base64ToString()` function converts a [base64][01] encoded string back to +its original string representation. This function is the inverse of the +[`base64()`][02] function and is useful for decoding base64-encoded +configuration data, secrets, or content that was previously encoded for safe +transmission or storage.## Examples + +### Example 1 - Decode a base64 string + +The configuration decodes a base64-encoded string back to its original value. + +```yaml +# base64ToString.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Decode base64 string + type: Microsoft.DSC.Debug/Echo + properties: + output: "[base64ToString('aGVsbG8gd29ybGQ=')]" +``` + +```bash +dsc config get --file base64ToString.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Decode base64 string + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: hello world +messages: [] +hadErrors: false +``` + +### Example 2 - Round-trip encoding and decoding + +The configuration demonstrates encoding a string to base64 and then decoding it +back using the [`base64()`][02] function inside the `base64ToString()` function. + +```yaml +# base64ToString.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Round-trip base64 conversion + type: Microsoft.DSC.Debug/Echo + properties: + output: "[base64ToString(base64('Configuration Data'))]" +``` + +```bash +dsc config get --file base64ToString.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Round-trip base64 conversion + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: Configuration Data +messages: [] +hadErrors: false +``` + +### Example 3 - Decode configuration from parameters + +This example shows decoding base64-encoded configuration data passed through +parameters, which is common when passing complex data through deployment +systems that require base64 encoding. + +```yaml +# base64ToString.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + encodedConfig: + type: string + defaultValue: eyJzZXJ2ZXJOYW1lIjoid2ViLXNlcnZlci0wMSIsInBvcnQiOjgwODB9 +resources: + - name: Decode server configuration + type: Microsoft.DSC.Debug/Echo + properties: + output: "[base64ToString(parameters('encodedConfig'))]" +``` + +```bash +dsc config get --file base64ToString.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Decode server configuration + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: '{"serverName":"web-server-01","port":8080}' +messages: [] +hadErrors: false +``` + +### Example 4 - Decode with error handling + +This example demonstrates how the function handles invalid base64 input by +using the [`if()`][03] function to provide fallback behavior. + +```yaml +# base64ToString.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + possiblyEncodedData: + type: string + defaultValue: validBase64String= + fallbackData: + type: string + defaultValue: default configuration +resources: + - name: Safe decode with fallback + type: Microsoft.DSC.Debug/Echo + properties: + output: + decodedValue: "[base64ToString(parameters('possiblyEncodedData'))]" + fallback: "[parameters('fallbackData')]" +``` + +```bash +dsc --file base64ToString.example.4.dsc.config.yaml config get +``` + +```yaml +results: +- name: Safe decode with fallback + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + decodedValue: waEb(KidString + fallback: default configuration +messages: [] +hadErrors: false +``` + +## Parameters + +### base64Value + +The `base64ToString()` function expects a single string containing valid +base64-encoded data. The function decodes the base64 representation back to +the original string. If the value isn't a valid base64 string, DSC raises an +error. If the decoded bytes don't form valid UTF-8, DSC also raises an error. + +```yaml +Type: string +Required: true +MinimumCount: 1 +MaximumCount: 1 +``` + +## Output + +The `base64ToString()` function returns the decoded string representation of +the **base64Value** parameter. + +```yaml +Type: string +``` + +## Exceptions + +The `base64ToString()` function raises errors for the following conditions: + +- **Invalid base64 encoding**: When the input string contains characters or + patterns that are not valid base64 +- **Invalid UTF-8**: When the decoded bytes do not form valid UTF-8 text + +## Related functions + +- [`base64()`][02] - Encodes a string to base64 format +- [`string()`][04] - Converts values to strings +- [`parameters()`][05] - Retrieves parameter values +- [`if()`][03] - Returns values based on a condition + + +[01]: https://en.wikipedia.org/wiki/Base64 +[02]: ./base64.md +[03]: ./if.md +[04]: ./string.md +[05]: ./parameters.md diff --git a/docs/reference/schemas/config/functions/createArray.md b/docs/reference/schemas/config/functions/createArray.md index 49e7c71ab..c12a25f1a 100644 --- a/docs/reference/schemas/config/functions/createArray.md +++ b/docs/reference/schemas/config/functions/createArray.md @@ -42,7 +42,7 @@ resources: ``` ```bash -dsc config get --file createArray.example.1.dsc.config.yaml config get +dsc config get --file createArray.example.1.dsc.config.yaml ``` ```yaml diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index f69a2e4d0..68c6a34dc 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -716,4 +716,46 @@ Describe 'tests for function expressions' { $errorContent = Get-Content $TestDrive/error.log -Raw $errorContent | Should -Match ([regex]::Escape($expectedError)) } + + It 'base64ToString function works for: ' -TestCases @( + @{ expression = "[base64ToString('aGVsbG8gd29ybGQ=')]"; expected = 'hello world' } + @{ expression = "[base64ToString('')]"; expected = '' } + @{ expression = "[base64ToString('aMOpbGxv')]"; expected = 'héllo' } + @{ expression = "[base64ToString('eyJrZXkiOiJ2YWx1ZSJ9')]"; expected = '{"key":"value"}' } + @{ expression = "[base64ToString(base64('test message'))]"; expected = 'test message' } + ) { + param($expression, $expected) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } + + It 'base64ToString function error handling: ' -TestCases @( + @{ expression = '[base64ToString("invalid!@#")]'; expectedError = 'Invalid base64 encoding' } + @{ expression = '[base64ToString("/w==")]'; expectedError = 'Decoded bytes do not form valid UTF-8' } + ) { + param($expression, $expectedError) + + $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 = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Not -Be 0 + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match $expectedError + } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 29a4bb792..f53e925f7 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -247,6 +247,12 @@ invoked = "array function" [functions.base64] description = "Encodes a string to Base64 format" +[functions.base64ToString] +description = "Converts a base64 representation to a string" +invoked = "base64ToString function" +invalidBase64 = "Invalid base64 encoding" +invalidUtf8 = "Decoded bytes do not form valid UTF-8" + [functions.bool] description = "Converts a string or number to a boolean" invoked = "bool function" diff --git a/dsc_lib/src/functions/base64_to_string.rs b/dsc_lib/src/functions/base64_to_string.rs new file mode 100644 index 000000000..823a30838 --- /dev/null +++ b/dsc_lib/src/functions/base64_to_string.rs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use base64::{Engine as _, engine::general_purpose}; + +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; +use tracing::debug; + +#[derive(Debug, Default)] +pub struct Base64ToString {} + +impl Function for Base64ToString { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "base64ToString".to_string(), + description: t!("functions.base64ToString.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 { + debug!("{}", t!("functions.base64ToString.invoked")); + + let base64_value = args[0].as_str().unwrap(); + + let decoded_bytes = general_purpose::STANDARD.decode(base64_value).map_err(|_| { + DscError::FunctionArg( + "base64ToString".to_string(), + t!("functions.base64ToString.invalidBase64").to_string(), + ) + })?; + + let result = String::from_utf8(decoded_bytes).map_err(|_| { + DscError::FunctionArg( + "base64ToString".to_string(), + t!("functions.base64ToString.invalidUtf8").to_string(), + ) + })?; + + Ok(Value::String(result)) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + use serde_json::Value; + + #[test] + fn base64_to_string_simple() { + let mut parser = Statement::new().unwrap(); + let result = parser + .parse_and_execute("[base64ToString('aGVsbG8gd29ybGQ=')]", &Context::new()) + .unwrap(); + assert_eq!(result, Value::String("hello world".to_string())); + } + + #[test] + fn base64_to_string_empty() { + let mut parser = Statement::new().unwrap(); + let result = parser + .parse_and_execute("[base64ToString('')]", &Context::new()) + .unwrap(); + assert_eq!(result, Value::String("".to_string())); + } + + #[test] + fn base64_to_string_unicode() { + let mut parser = Statement::new().unwrap(); + let result = parser + .parse_and_execute("[base64ToString('aMOpbGxv')]", &Context::new()) + .unwrap(); + assert_eq!(result, Value::String("héllo".to_string())); + } + + #[test] + fn base64_to_string_round_trip() { + let mut parser = Statement::new().unwrap(); + let result = parser + .parse_and_execute("[base64ToString(base64('test message'))]", &Context::new()) + .unwrap(); + assert_eq!(result, Value::String("test message".to_string())); + } + + #[test] + fn base64_to_string_invalid_base64() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[base64ToString('invalid!@#')]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn base64_to_string_invalid_utf8() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[base64ToString('/w==')]", &Context::new()); + assert!(result.is_err()); + } + + #[test] + fn base64_to_string_json_string() { + let mut parser = Statement::new().unwrap(); + let result = parser + .parse_and_execute("[base64ToString('eyJrZXkiOiJ2YWx1ZSJ9')]", &Context::new()) + .unwrap(); + assert_eq!(result, Value::String("{\"key\":\"value\"}".to_string())); + } +} \ No newline at end of file diff --git a/dsc_lib/src/functions/mod.rs b/dsc_lib/src/functions/mod.rs index d9013bfcf..5b76938ca 100644 --- a/dsc_lib/src/functions/mod.rs +++ b/dsc_lib/src/functions/mod.rs @@ -16,6 +16,7 @@ pub mod add; pub mod and; pub mod array; pub mod base64; +pub mod base64_to_string; pub mod bool; pub mod coalesce; pub mod concat; @@ -134,6 +135,7 @@ impl FunctionDispatcher { Box::new(and::And{}), Box::new(array::Array{}), Box::new(base64::Base64{}), + Box::new(base64_to_string::Base64ToString{}), Box::new(bool::Bool{}), Box::new(coalesce::Coalesce{}), Box::new(concat::Concat{}), From 62175cefded6c8317d97f06ccf352dd604a2d949 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 30 Sep 2025 04:15:43 +0200 Subject: [PATCH 2/3] Fix failed tests --- dsc/tests/dsc_functions.tests.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 68c6a34dc..d6ddcf06c 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -740,18 +740,20 @@ Describe 'tests for function expressions' { } It 'base64ToString function error handling: ' -TestCases @( - @{ expression = '[base64ToString("invalid!@#")]'; expectedError = 'Invalid base64 encoding' } - @{ expression = '[base64ToString("/w==")]'; expectedError = 'Decoded bytes do not form valid UTF-8' } + @{ expression = "[base64ToString('invalid!@#')]" ; expectedError = 'Invalid base64 encoding' } + @{ expression = "[base64ToString('/w==')]" ; expectedError = 'Decoded bytes do not form valid UTF-8' } ) { param($expression, $expectedError) + $escapedExpression = $expression -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" + output: "$escapedExpression" "@ $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log $LASTEXITCODE | Should -Not -Be 0 From ba9bfec57140d6564ef64c3f8ed5ace2787fe5c3 Mon Sep 17 00:00:00 2001 From: GijsR Date: Wed, 1 Oct 2025 07:55:36 +0200 Subject: [PATCH 3/3] Fix tests --- dsc/tests/dsc_functions.tests.ps1 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 5fbe1da48..21da3062e 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -766,17 +766,15 @@ Describe 'tests for function expressions' { ) { param($expression, $expectedError) - $escapedExpression = $expression -replace "'", "''" - $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: "$escapedExpression" + output: `"$expression`" "@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log $LASTEXITCODE | Should -Not -Be 0 $errorContent = Get-Content $TestDrive/error.log -Raw $errorContent | Should -Match $expectedError