From c4889d4beb2c81150c8c7e7ade8e5dc4f3d34d4c Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 1 May 2026 08:09:29 +0200 Subject: [PATCH 1/2] Support array for Microsoft.Windows/Registry resource --- lib/dsc-lib-registry/src/config.rs | 28 +++- lib/dsc-lib-registry/src/lib.rs | 51 +++--- resources/registry/src/main.rs | 153 ++++++++++-------- .../tests/registry.config.get.tests.ps1 | 58 +++++++ .../tests/registry.config.set.tests.ps1 | 63 ++++++++ .../tests/registry.config.whatif.tests.ps1 | 31 ++++ resources/sshdconfig/src/get.rs | 8 +- 7 files changed, 300 insertions(+), 92 deletions(-) diff --git a/lib/dsc-lib-registry/src/config.rs b/lib/dsc-lib-registry/src/config.rs index 372180360..d58059a72 100644 --- a/lib/dsc-lib-registry/src/config.rs +++ b/lib/dsc-lib-registry/src/config.rs @@ -16,8 +16,8 @@ pub enum RegistryValueData { } #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -#[serde(rename = "Registry", deny_unknown_fields)] -pub struct Registry { +#[serde(rename = "RegistryKey", deny_unknown_fields)] +pub struct RegistryKey { /// The path to the registry key. #[serde(rename = "keyPath")] pub key_path: String, @@ -34,6 +34,30 @@ pub struct Registry { pub exist: Option, } +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[serde(rename = "RegistryList", deny_unknown_fields)] +pub struct RegistryList { + /// One or more registry keys/values to manage. + #[serde(rename = "registryKeys")] + pub registry_keys: Vec, + /// The information from a config set --what-if operation. + #[serde(rename = "_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum Registry { + List(RegistryList), + Single(RegistryKey), +} + +impl Default for Registry { + fn default() -> Self { + Registry::Single(RegistryKey::default()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Metadata { diff --git a/lib/dsc-lib-registry/src/lib.rs b/lib/dsc-lib-registry/src/lib.rs index 48e18affc..7e0713e7b 100644 --- a/lib/dsc-lib-registry/src/lib.rs +++ b/lib/dsc-lib-registry/src/lib.rs @@ -4,7 +4,7 @@ use registry::{Data, Hive, RegKey, Security, key, value}; use rust_i18n::t; use utfx::{U16CString, UCString}; -use crate::config::{Metadata, Registry, RegistryValueData}; +use crate::config::{Metadata, RegistryKey, RegistryValueData}; use crate::error::RegistryError; rust_i18n::i18n!("locales", fallback = "en-us"); @@ -13,27 +13,36 @@ pub mod error; pub mod config; pub struct RegistryHelper { - config: Registry, + config: RegistryKey, hive: Hive, subkey: String, what_if: bool, } impl RegistryHelper { - /// Create a new `RegistryHelper` from json. + /// Create a new `RegistryHelper` from json describing a single registry key. /// /// # Arguments /// - /// * `config` - The string with registry configuration information. + /// * `config` - JSON for a single `RegistryKey` instance. /// /// # Errors /// /// * `RegistryError` - The error that occurred. pub fn new_from_json(config: &str) -> Result { - let registry: Registry = match serde_json::from_str(config) { + let registry: RegistryKey = match serde_json::from_str(config) { Ok(config) => config, Err(e) => return Err(RegistryError::Json(e)), }; + Self::new_from_key(registry) + } + + /// Create a new `RegistryHelper` from an already-parsed `RegistryKey`. + /// + /// # Errors + /// + /// * `RegistryError` - The error that occurred. + pub fn new_from_key(registry: RegistryKey) -> Result { let key_path = registry.key_path.clone(); let (hive, subkey) = get_hive_from_path(&key_path)?; @@ -58,7 +67,7 @@ impl RegistryHelper { /// * `RegistryError` - The error that occurred. pub fn new(key_path: &str, value_name: Option, value_data: Option) -> Result { let (hive, subkey) = get_hive_from_path(key_path)?; - let config = Registry { + let config = RegistryKey { key_path: key_path.to_string(), value_name, value_data, @@ -83,12 +92,12 @@ impl RegistryHelper { /// /// # Returns /// - /// * `Registry` - The registry struct. + /// * `RegistryKey` - The registry key state. /// /// # Errors /// /// * `RegistryError` - The error that occurred. - pub fn get(&self) -> Result { + pub fn get(&self) -> Result { let exist: bool; let (reg_key, _subkey) = match self.open(Security::Read) { Ok((reg_key, subkey)) => { @@ -96,7 +105,7 @@ impl RegistryHelper { }, Err(RegistryError::RegistryKeyNotFound(_)) => { exist = false; - return Ok(Registry { + return Ok(RegistryKey { key_path: self.config.key_path.clone(), exist: Some(exist), ..Default::default() @@ -110,7 +119,7 @@ impl RegistryHelper { Ok(value) => value, Err(value::Error::NotFound(_,_)) => { exist = false; - return Ok(Registry { + return Ok(RegistryKey { key_path: self.config.key_path.clone(), value_name: Some(value_name.clone()), exist: Some(exist), @@ -120,14 +129,14 @@ impl RegistryHelper { Err(e) => return Err(RegistryError::RegistryValue(e)), }; - Ok(Registry { + Ok(RegistryKey { key_path: self.config.key_path.clone(), value_name: Some(value_name.clone()), value_data: convert_reg_value(&value)?, ..Default::default() }) } else { - Ok(Registry { + Ok(RegistryKey { key_path: self.config.key_path.clone(), ..Default::default() }) @@ -138,12 +147,12 @@ impl RegistryHelper { /// /// # Returns /// - /// * `Registry` - The registry struct. + /// * `RegistryKey` - The registry key state when applicable. /// /// # Errors /// /// * `RegistryError` - The error that occurred. - pub fn set(&self) -> Result, RegistryError> { + pub fn set(&self) -> Result, RegistryError> { let mut what_if_metadata: Vec = Vec::new(); let reg_key = match self.open(Security::Write) { Ok((reg_key, _subkey)) => Some(reg_key), @@ -224,7 +233,7 @@ impl RegistryHelper { }; if self.what_if { - return Ok(Some(Registry { + return Ok(Some(RegistryKey { key_path: self.config.key_path.clone(), value_data: convert_reg_value(&data)?, value_name: self.config.value_name.clone(), @@ -239,7 +248,7 @@ impl RegistryHelper { } if self.what_if { - return Ok(Some(Registry { + return Ok(Some(RegistryKey { key_path: self.config.key_path.clone(), metadata: if what_if_metadata.is_empty() { None } else { Some(Metadata { what_if: Some(what_if_metadata) })}, ..Default::default() @@ -258,7 +267,7 @@ impl RegistryHelper { /// # Errors /// /// * `RegistryError` - The error that occurred. - pub fn remove(&self) -> Result, RegistryError> { + pub fn remove(&self) -> Result, RegistryError> { // For deleting a value, we need SetValue permission (KEY_SET_VALUE). // Try to open with the minimal required permission. // If that fails due to permission, try with AllAccess as a fallback. @@ -284,7 +293,7 @@ impl RegistryHelper { if let Some(value_name) = &self.config.value_name { if self.what_if { what_if_metadata.push(t!("registry_helper.whatIfDeleteValue", value_name = value_name).to_string()); - return Ok(Some(Registry { + return Ok(Some(RegistryKey { key_path: self.config.key_path.clone(), value_name: Some(value_name.clone()), metadata: Some(Metadata { what_if: Some(what_if_metadata) }), @@ -312,7 +321,7 @@ impl RegistryHelper { if self.what_if { what_if_metadata.push(t!("registry_helper.whatIfDeleteSubkey", subkey_name = subkey_name).to_string()); - return Ok(Some(Registry { + return Ok(Some(RegistryKey { key_path: self.config.key_path.clone(), metadata: Some(Metadata { what_if: Some(what_if_metadata) }), ..Default::default() @@ -378,9 +387,9 @@ impl RegistryHelper { Ok((parent_key, subkeys)) } - fn handle_error_or_what_if(&self, error: RegistryError) -> Result, RegistryError> { + fn handle_error_or_what_if(&self, error: RegistryError) -> Result, RegistryError> { if self.what_if { - return Ok(Some(Registry { + return Ok(Some(RegistryKey { key_path: self.config.key_path.clone(), metadata: Some(Metadata { what_if: Some(vec![error.to_string()]) }), ..Default::default() diff --git a/resources/registry/src/main.rs b/resources/registry/src/main.rs index 8511dcdc5..71c11acf0 100644 --- a/resources/registry/src/main.rs +++ b/resources/registry/src/main.rs @@ -8,13 +8,49 @@ use std::env; use args::Arguments; use clap::Parser; -use dsc_lib_registry::{config::Registry, RegistryHelper}; +use dsc_lib_registry::{config::{Registry, RegistryKey, RegistryList}, RegistryHelper}; use rust_i18n::t; use schemars::schema_for; use std::process::exit; use tracing::{debug, error}; use tracing_subscriber::{filter::LevelFilter, prelude::__tracing_subscriber_SubscriberExt, EnvFilter, Layer}; +fn parse_input(input: &str) -> Result<(Vec, bool), String> { + match serde_json::from_str::(input) { + Ok(Registry::Single(rk)) => Ok((vec![rk], true)), + Ok(Registry::List(rl)) => Ok((rl.registry_keys, false)), + Err(e) => Err(e.to_string()), + } +} + +fn emit_results(results: Vec, was_single: bool) { + if was_single { + if let Some(rk) = results.into_iter().next() { + let json = serde_json::to_string(&rk).unwrap(); + println!("{json}"); + } + } else { + let list = RegistryList { + registry_keys: results, + metadata: None, + }; + let json = serde_json::to_string(&list).unwrap(); + println!("{json}"); + } +} + +fn make_helper(rk: RegistryKey, what_if: bool) -> RegistryHelper { + let mut helper = match RegistryHelper::new_from_key(rk) { + Ok(h) => h, + Err(err) => { + error!("{err}"); + exit(EXIT_INVALID_INPUT); + } + }; + if what_if { helper.enable_what_if(); } + helper +} + mod args; rust_i18n::i18n!("locales", fallback = "en-us"); @@ -48,86 +84,73 @@ fn main() { match subcommand { args::ConfigSubCommand::Get{input} => { debug!("Get input: {input}"); - let reg_helper = match RegistryHelper::new_from_json(&input) { - Ok(reg_helper) => reg_helper, - Err(err) => { - error!("{err}"); - exit(EXIT_INVALID_INPUT); - } + let (items, was_single) = match parse_input(&input) { + Ok(v) => v, + Err(err) => { error!("{err}"); exit(EXIT_INVALID_INPUT); } }; - match reg_helper.get() { - Ok(reg_config) => { - let json = serde_json::to_string(®_config).unwrap(); - println!("{json}"); - }, - Err(err) => { - error!("{err}"); - exit(EXIT_REGISTRY_ERROR); + let mut results: Vec = Vec::with_capacity(items.len()); + for rk in items { + let reg_helper = make_helper(rk, false); + match reg_helper.get() { + Ok(out) => results.push(out), + Err(err) => { error!("{err}"); exit(EXIT_REGISTRY_ERROR); } } } + emit_results(results, was_single); }, args::ConfigSubCommand::Set{input, what_if} => { debug!("Set input: {input}, what_if: {what_if}"); - let mut reg_helper = match RegistryHelper::new_from_json(&input) { - Ok(reg_helper) => reg_helper, - Err(err) => { - error!("{err}"); - exit(EXIT_INVALID_INPUT); - } + let (items, was_single) = match parse_input(&input) { + Ok(v) => v, + Err(err) => { error!("{err}"); exit(EXIT_INVALID_INPUT); } }; - if what_if { reg_helper.enable_what_if(); } - - // In what-if, if the desired state is _exist: false, route to delete - if what_if - && let Ok(desired) = serde_json::from_str::(&input) - && matches!(desired.exist, Some(false)) { - match reg_helper.remove() { - Ok(Some(reg_config)) => { - let json = serde_json::to_string(®_config).unwrap(); - println!("{json}"); - }, - Ok(None) => {}, - Err(err) => { - error!("{err}"); - exit(EXIT_REGISTRY_ERROR); - } - } - return; + let mut results: Vec = Vec::new(); + for rk in items { + // In what-if, if the desired state is _exist: false, route to delete + let route_to_delete = what_if && matches!(rk.exist, Some(false)); + let reg_helper = make_helper(rk, what_if); + let outcome = if route_to_delete { + reg_helper.remove() + } else { + reg_helper.set() + }; + match outcome { + Ok(Some(out)) => results.push(out), + Ok(None) => {}, + Err(err) => { error!("{err}"); exit(EXIT_REGISTRY_ERROR); } } - - match reg_helper.set() { - Ok(reg_config) => { - if let Some(config) = reg_config { - let json = serde_json::to_string(&config).unwrap(); - println!("{json}"); - } - }, - Err(err) => { - error!("{err}"); - exit(EXIT_REGISTRY_ERROR); + } + if was_single { + if let Some(rk) = results.into_iter().next() { + let json = serde_json::to_string(&rk).unwrap(); + println!("{json}"); } + } else { + emit_results(results, false); } }, args::ConfigSubCommand::Delete{input, what_if} => { debug!("Delete input: {input}, what_if: {what_if}"); - let mut reg_helper = match RegistryHelper::new_from_json(&input) { - Ok(reg_helper) => reg_helper, - Err(err) => { - error!("{err}"); - exit(EXIT_INVALID_INPUT); - } + let (items, was_single) = match parse_input(&input) { + Ok(v) => v, + Err(err) => { error!("{err}"); exit(EXIT_INVALID_INPUT); } }; - if what_if { reg_helper.enable_what_if(); } - match reg_helper.remove() { - Ok(Some(reg_config)) => { - let json = serde_json::to_string(®_config).unwrap(); + let mut results: Vec = Vec::new(); + for rk in items { + let reg_helper = make_helper(rk, what_if); + match reg_helper.remove() { + Ok(Some(out)) => results.push(out), + Ok(None) => {}, + Err(err) => { error!("{err}"); exit(EXIT_REGISTRY_ERROR); } + } + } + if was_single { + if let Some(rk) = results.into_iter().next() { + let json = serde_json::to_string(&rk).unwrap(); println!("{json}"); - }, - Ok(None) => {}, - Err(err) => { - error!("{err}"); - exit(EXIT_REGISTRY_ERROR); } + } else { + emit_results(results, false); } }, } diff --git a/resources/registry/tests/registry.config.get.tests.ps1 b/resources/registry/tests/registry.config.get.tests.ps1 index c012d673f..5622498da 100644 --- a/resources/registry/tests/registry.config.get.tests.ps1 +++ b/resources/registry/tests/registry.config.get.tests.ps1 @@ -44,4 +44,62 @@ Describe 'Registry config get tests' { $result[0].level | Should -BeExactly 'DEBUG' $result[0].fields.message | Should -BeLike 'Get Input:*' } + + It 'Can get multiple registry keys via the registryKeys array' -Skip:(!$IsWindows) { + $json = @' + { + "registryKeys": [ + { + "keyPath": "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion", + "valueName": "ProgramFilesPath" + }, + { + "keyPath": "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion" + } + ] + } +'@ + $out = registry config get --input $json 2>$null + $LASTEXITCODE | Should -Be 0 + $result = $out | ConvertFrom-Json + $result.registryKeys.Count | Should -Be 2 + $result.registryKeys[0].keyPath | Should -Be 'HKLM\Software\Microsoft\Windows\CurrentVersion' + $result.registryKeys[0].valueName | Should -Be 'ProgramFilesPath' + $result.registryKeys[0].valueData.ExpandString | Should -Be '%ProgramFiles%' + $result.registryKeys[1].keyPath | Should -Be 'HKLM\Software\Microsoft\Windows\CurrentVersion' + $result.registryKeys[1].valueName | Should -BeNullOrEmpty + } + + It 'Returns _exist=false per item when a key/value is missing in the registryKeys array' -Skip:(!$IsWindows) { + $json = @' + { + "registryKeys": [ + { "keyPath": "HKCU\\DSCArrayTestDoesNotExist" }, + { "keyPath": "HKCU\\Software", "valueName": "DSCArrayTestDoesNotExist" } + ] + } +'@ + $out = registry config get --input $json 2>$null + $LASTEXITCODE | Should -Be 0 + $result = $out | ConvertFrom-Json + $result.registryKeys.Count | Should -Be 2 + $result.registryKeys[0]._exist | Should -Be $false + $result.registryKeys[1]._exist | Should -Be $false + } + + It 'Returns the registryKeys wrapper shape when input used the wrapper, even with one item' -Skip:(!$IsWindows) { + $json = @' + { + "registryKeys": [ + { "keyPath": "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion", "valueName": "ProgramFilesPath" } + ] + } +'@ + $out = registry config get --input $json 2>$null + $LASTEXITCODE | Should -Be 0 + $result = $out | ConvertFrom-Json + $result.PSObject.Properties.Name | Should -Contain 'registryKeys' + $result.registryKeys.Count | Should -Be 1 + $result.registryKeys[0].valueName | Should -Be 'ProgramFilesPath' + } } diff --git a/resources/registry/tests/registry.config.set.tests.ps1 b/resources/registry/tests/registry.config.set.tests.ps1 index 7b6cb404c..d8307cb05 100644 --- a/resources/registry/tests/registry.config.set.tests.ps1 +++ b/resources/registry/tests/registry.config.set.tests.ps1 @@ -5,6 +5,7 @@ Describe 'registry config set tests' { AfterEach { if ($IsWindows) { Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore + Remove-Item -Path 'HKCU:\DSCArrayTest' -Recurse -ErrorAction Ignore } } @@ -196,4 +197,66 @@ Describe 'registry config set tests' { $result._exist | Should -Be $false $result.valueData | Should -BeNullOrEmpty } + + It 'Can set multiple values in one invocation via the registryKeys array' -Skip:(!$IsWindows) { + $json = @' + { + "registryKeys": [ + { + "keyPath": "HKCU\\DSCArrayTest\\A", + "valueName": "First", + "valueData": { "String": "alpha" } + }, + { + "keyPath": "HKCU\\DSCArrayTest\\B", + "valueName": "Second", + "valueData": { "DWord": 42 } + } + ] + } +'@ + $out = registry config set --input $json 2>$null + $LASTEXITCODE | Should -Be 0 + + $getOut = registry config get --input $json 2>$null | ConvertFrom-Json + $getOut.registryKeys.Count | Should -Be 2 + $getOut.registryKeys[0].valueData.String | Should -Be 'alpha' + $getOut.registryKeys[1].valueData.DWord | Should -Be 42 + } + + It 'Can delete multiple values in one invocation via the registryKeys array' -Skip:(!$IsWindows) { + $setJson = @' + { + "registryKeys": [ + { + "keyPath": "HKCU\\DSCArrayTest\\Del", + "valueName": "X", + "valueData": { "String": "x" } + }, + { + "keyPath": "HKCU\\DSCArrayTest\\Del", + "valueName": "Y", + "valueData": { "String": "y" } + } + ] + } +'@ + registry config set --input $setJson 2>$null | Out-Null + $LASTEXITCODE | Should -Be 0 + + $delJson = @' + { + "registryKeys": [ + { "keyPath": "HKCU\\DSCArrayTest\\Del", "valueName": "X" }, + { "keyPath": "HKCU\\DSCArrayTest\\Del", "valueName": "Y" } + ] + } +'@ + $out = registry config delete --input $delJson 2>$null + $LASTEXITCODE | Should -Be 0 + + $getOut = registry config get --input $delJson 2>$null | ConvertFrom-Json + $getOut.registryKeys[0]._exist | Should -Be $false + $getOut.registryKeys[1]._exist | Should -Be $false + } } diff --git a/resources/registry/tests/registry.config.whatif.tests.ps1 b/resources/registry/tests/registry.config.whatif.tests.ps1 index 4f0cf08b5..71cb519e0 100644 --- a/resources/registry/tests/registry.config.whatif.tests.ps1 +++ b/resources/registry/tests/registry.config.whatif.tests.ps1 @@ -200,4 +200,35 @@ Describe 'registry config whatif tests' { # For delete what-if, payload should only include keyPath (and optionally valueName when deleting a value) ($result.psobject.properties | Where-Object { $_.Name -ne '_metadata' } | Measure-Object).Count | Should -Be 1 } + + It 'Can whatif multiple keys in a registryKeys array' -Skip:(!$IsWindows) { + $json = @' + { + "registryKeys": [ + { + "keyPath": "HKCU\\1\\A", + "valueName": "First", + "valueData": { "String": "alpha" } + }, + { + "keyPath": "HKCU\\1\\B", + "valueName": "Second", + "valueData": { "DWord": 42 } + } + ] + } +'@ + $get_before = registry config get --input $json 2>$null + $result = registry config set -w --input $json 2>$null | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $result.registryKeys.Count | Should -Be 2 + $result.registryKeys[0].keyPath | Should -Be 'HKCU\1\A' + $result.registryKeys[0].valueData.String | Should -Be 'alpha' + $result.registryKeys[0]._metadata.whatIf | Should -Not -BeNullOrEmpty + $result.registryKeys[1].keyPath | Should -Be 'HKCU\1\B' + $result.registryKeys[1].valueData.DWord | Should -Be 42 + $result.registryKeys[1]._metadata.whatIf | Should -Not -BeNullOrEmpty + $get_after = registry config get --input $json 2>$null + $get_before | Should -EQ $get_after + } } diff --git a/resources/sshdconfig/src/get.rs b/resources/sshdconfig/src/get.rs index a162b57e2..045d1581b 100644 --- a/resources/sshdconfig/src/get.rs +++ b/resources/sshdconfig/src/get.rs @@ -3,7 +3,7 @@ #[cfg(windows)] use { - dsc_lib_registry::{config::{Registry, RegistryValueData}, RegistryHelper}, + dsc_lib_registry::{config::{RegistryKey, RegistryValueData}, RegistryHelper}, crate::args::DefaultShell, crate::metadata::windows::{DEFAULT_SHELL, DEFAULT_SHELL_CMD_OPTION, DEFAULT_SHELL_ESCAPE_ARGS, REGISTRY_PATH}, }; @@ -50,7 +50,7 @@ pub fn invoke_get(input: Option<&String>, setting: &Setting) -> Result Result<(), SshdConfigError> { let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(DEFAULT_SHELL.to_string()), None)?; - let default_shell: Registry = registry_helper.get()?; + let default_shell: RegistryKey = registry_helper.get()?; let mut shell = None; // default_shell is a single string consisting of the shell exe path if let Some(value) = default_shell.value_data { @@ -63,7 +63,7 @@ fn get_default_shell() -> Result<(), SshdConfigError> { } let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(DEFAULT_SHELL_CMD_OPTION.to_string()), None)?; - let option: Registry = registry_helper.get()?; + let option: RegistryKey = registry_helper.get()?; let mut cmd_option = None; if let Some(value) = option.value_data { match value { @@ -73,7 +73,7 @@ fn get_default_shell() -> Result<(), SshdConfigError> { } let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(DEFAULT_SHELL_ESCAPE_ARGS.to_string()), None)?; - let escape_args: Registry = registry_helper.get()?; + let escape_args: RegistryKey = registry_helper.get()?; let mut escape_arguments = None; if let Some(value) = escape_args.value_data { if let RegistryValueData::DWord(b) = value { From 2b60db08ec339fcbd5e5a81b7fd51964cb4ecfb3 Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 1 May 2026 11:23:52 +0200 Subject: [PATCH 2/2] Fix Copilot remarks --- resources/registry/locales/en-us.toml | 2 + resources/registry/src/main.rs | 38 ++++++++----- .../tests/registry.config.set.tests.ps1 | 53 +++++++++++++++++++ 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index 22cf3a564..c82962af3 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -32,3 +32,5 @@ tracingInitError = "Unable to set global default tracing subscriber. Tracing is debugAttach = "attach debugger to pid %{pid} and press any key to continue" debugEventReadError = "Error: Failed to read event: %{err}" debugEventUnexpectedError = "Unexpected event: %{e}" +jsonSerializationError = "Failed to serialize result to JSON: %{err}" +emptyRegistryKeysArray = "The 'registryKeys' array must contain at least one entry." diff --git a/resources/registry/src/main.rs b/resources/registry/src/main.rs index 71c11acf0..6bbd00a9c 100644 --- a/resources/registry/src/main.rs +++ b/resources/registry/src/main.rs @@ -18,24 +18,38 @@ use tracing_subscriber::{filter::LevelFilter, prelude::__tracing_subscriber_Subs fn parse_input(input: &str) -> Result<(Vec, bool), String> { match serde_json::from_str::(input) { Ok(Registry::Single(rk)) => Ok((vec![rk], true)), - Ok(Registry::List(rl)) => Ok((rl.registry_keys, false)), + Ok(Registry::List(rl)) => { + if rl.registry_keys.is_empty() { + Err(t!("main.emptyRegistryKeysArray").to_string()) + } else { + Ok((rl.registry_keys, false)) + } + }, Err(e) => Err(e.to_string()), } } +fn emit_json(value: &T) { + match serde_json::to_string(value) { + Ok(json) => println!("{json}"), + Err(err) => { + error!("{}", t!("main.jsonSerializationError", err = err)); + exit(EXIT_JSON_SERIALIZATION); + } + } +} + fn emit_results(results: Vec, was_single: bool) { if was_single { if let Some(rk) = results.into_iter().next() { - let json = serde_json::to_string(&rk).unwrap(); - println!("{json}"); + emit_json(&rk); } } else { let list = RegistryList { registry_keys: results, metadata: None, }; - let json = serde_json::to_string(&list).unwrap(); - println!("{json}"); + emit_json(&list); } } @@ -58,6 +72,7 @@ rust_i18n::i18n!("locales", fallback = "en-us"); const EXIT_SUCCESS: i32 = 0; const EXIT_INVALID_INPUT: i32 = 2; const EXIT_REGISTRY_ERROR: i32 = 3; +const EXIT_JSON_SERIALIZATION: i32 = 4; #[allow(clippy::too_many_lines)] fn main() { @@ -122,10 +137,9 @@ fn main() { } if was_single { if let Some(rk) = results.into_iter().next() { - let json = serde_json::to_string(&rk).unwrap(); - println!("{json}"); + emit_json(&rk); } - } else { + } else if !results.is_empty() { emit_results(results, false); } }, @@ -146,10 +160,9 @@ fn main() { } if was_single { if let Some(rk) = results.into_iter().next() { - let json = serde_json::to_string(&rk).unwrap(); - println!("{json}"); + emit_json(&rk); } - } else { + } else if !results.is_empty() { emit_results(results, false); } }, @@ -157,8 +170,7 @@ fn main() { }, args::SubCommand::Schema => { let schema = schema_for!(Registry); - let json =serde_json::to_string(&schema).unwrap(); - println!("{json}"); + emit_json(&schema); }, } diff --git a/resources/registry/tests/registry.config.set.tests.ps1 b/resources/registry/tests/registry.config.set.tests.ps1 index d8307cb05..6dd44371f 100644 --- a/resources/registry/tests/registry.config.set.tests.ps1 +++ b/resources/registry/tests/registry.config.set.tests.ps1 @@ -259,4 +259,57 @@ Describe 'registry config set tests' { $getOut.registryKeys[0]._exist | Should -Be $false $getOut.registryKeys[1]._exist | Should -Be $false } + + It 'set with registryKeys array produces no stdout when not what-if' -Skip:(!$IsWindows) { + $json = @' + { + "registryKeys": [ + { + "keyPath": "HKCU\\DSCArrayTest\\Quiet", + "valueName": "V", + "valueData": { "String": "v" } + } + ] + } +'@ + $out = registry config set --input $json 2>$null + $LASTEXITCODE | Should -Be 0 + $out | Should -BeNullOrEmpty + } + + It 'delete with registryKeys array produces no stdout when not what-if' -Skip:(!$IsWindows) { + $setJson = @' + { + "registryKeys": [ + { + "keyPath": "HKCU\\DSCArrayTest\\Quiet", + "valueName": "V", + "valueData": { "String": "v" } + } + ] + } +'@ + registry config set --input $setJson 2>$null | Out-Null + + $delJson = @' + { + "registryKeys": [ + { "keyPath": "HKCU\\DSCArrayTest\\Quiet", "valueName": "V" } + ] + } +'@ + $out = registry config delete --input $delJson 2>$null + $LASTEXITCODE | Should -Be 0 + $out | Should -BeNullOrEmpty + } + + It 'Empty registryKeys array is rejected as invalid input' -Skip:(!$IsWindows) { + $json = '{ "registryKeys": [] }' + registry config get --input $json 2>$null + $LASTEXITCODE | Should -Be 2 + registry config set --input $json 2>$null + $LASTEXITCODE | Should -Be 2 + registry config delete --input $json 2>$null + $LASTEXITCODE | Should -Be 2 + } }