From c8febcc2ed9de7a07beb3560c82e0d580381f742 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 4 May 2026 11:50:38 -0700 Subject: [PATCH 01/16] Initial work for registry adapter --- Cargo.lock | 2 +- lib/dsc-lib/locales/en-us.toml | 1 + .../src/discovery/command_discovery.rs | 20 +- .../dscresources/adapted_resource_manifest.rs | 12 +- .../src/dscresources/command_resource.rs | 89 ++++----- lib/dsc-lib/src/dscresources/dscresource.rs | 3 + .../src/dscresources/resource_manifest.rs | 12 +- resources/registry/Cargo.toml | 2 +- resources/registry/locales/en-us.toml | 4 + .../registry/registry.dsc.manifests.json | 39 ++++ resources/registry/src/adapter.rs | 30 +++ resources/registry/src/args.rs | 25 ++- resources/registry/src/error.rs | 12 ++ resources/registry/src/main.rs | 32 +++- ...s_personalization.dsc.adaptedResource.yaml | 171 ++++++++++++++++++ 15 files changed, 381 insertions(+), 73 deletions(-) create mode 100644 resources/registry/src/adapter.rs create mode 100644 resources/registry/src/error.rs create mode 100644 resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml diff --git a/Cargo.lock b/Cargo.lock index e64487eee..9fb54391a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "dsc-resource-registry" -version = "1.0.0" +version = "1.1.0" dependencies = [ "clap", "crossterm", diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 81083031c..e00a545c3 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -192,6 +192,7 @@ exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' runProcessError = "Failed to run process '%{executable}': %{error}" whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. See https://github.com/PowerShell/DSC/issues/1361 for migration information." securityContextRequired = "Operation '%{operation}' for resource '%{resource}' requires security context '%{context}'" +noAdaptedContent = "No adapted content available for resource '%{resource}'" [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 4da974bc7..9439f2cd3 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -4,6 +4,7 @@ use crate::{discovery::{DiscoveryExtensionCache, DiscoveryManifestCache, DiscoveryResourceCache, discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, matches_adapter_requirement}, dscresources::{adapted_resource_manifest::AdaptedDscResourceManifest, resource_manifest::SetDeleteArgKind}, parser::Statement, types::{FullyQualifiedTypeName, TypeNameFilter}}; use crate::{locked_clear, locked_is_empty, locked_extend, locked_clone, locked_get}; use crate::configure::{config_doc::ResourceDiscoveryMode, context::Context}; +use crate::dscresources::adapted_resource_manifest::AdaptedPathOrContent; use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; use crate::dscresources::resource_manifest::{Kind, ResourceManifest, SchemaKind}; use crate::dscresources::command_resource::invoke_command; @@ -31,7 +32,6 @@ const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extens const DSC_MANIFEST_LIST_EXTENSIONS: [&str; 3] = [".dsc.manifests.json", ".dsc.manifests.yaml", ".dsc.manifests.yml"]; const DSC_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.resource.json", ".dsc.resource.yaml", ".dsc.resource.yml"]; -// use BTreeMap so that the results are sorted by the typename, the Vec is sorted by version static ADAPTERS: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryResourceCache::new())); static RESOURCES: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryResourceCache::new())); static EXTENSIONS: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryExtensionCache::new())); @@ -748,13 +748,22 @@ fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceMani )); } + let mut resource = DscResource::new(); let directory = path.parent().unwrap(); - let resource_path = directory.join(&manifest.path); - if !resource_path.exists() { - return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = manifest.type_name).to_string())); + match &manifest.path_or_content { + AdaptedPathOrContent::Path(resource_path) => { + let resource_path = directory.join(resource_path); + if !resource_path.exists() { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = manifest.type_name).to_string())); + } + resource.path = resource_path; + }, + AdaptedPathOrContent::Content(content) => { + resource.path = path.to_path_buf(); + resource.adapted_content = Some(content.clone()); + } } - let mut resource = DscResource::new(); resource.type_name = manifest.type_name.clone(); resource.kind = Kind::Resource; resource.implemented_as = None; @@ -763,7 +772,6 @@ fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceMani resource.version = manifest.version.clone(); resource.capabilities = manifest.capabilities.clone(); resource.require_adapter = Some(manifest.require_adapter.clone()); - resource.path = resource_path; resource.directory = directory.to_path_buf(); resource.manifest = None; resource.schema = Some(manifest.schema.clone()); diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index 906c3af86..0c581f338 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -14,6 +14,13 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::path::PathBuf; +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum AdaptedPathOrContent { + Path(PathBuf), + Content(Map), +} + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( @@ -45,8 +52,9 @@ pub struct AdaptedDscResourceManifest { /// An optional message indicating the resource is deprecated. If provided, the message will be shown when the resource is used. #[serde(skip_serializing_if = "Option::is_none")] pub deprecation_message: Option, - /// The file path to the resource. - pub path: PathBuf, + /// The file path to the resource or the content of the resource itself. + #[schemars(flatten)] + pub path_or_content: AdaptedPathOrContent, /// The description of the resource. pub description: Option, /// The author of the resource. diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index c03525ab6..e09e83795 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -7,8 +7,8 @@ use jsonschema::Validator; use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; -use std::{collections::HashMap, env, path::{Path, PathBuf}, process::Stdio}; -use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; +use std::{collections::HashMap, env, path::Path, process::Stdio}; +use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::{command_resource, resource_manifest::SchemaArgKind}, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; use crate::dscerror::DscError; use super::{ dscresource::{get_diff, redact, DscResource}, @@ -26,11 +26,6 @@ use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command}; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; -pub struct CommandResourceInfo { - pub type_name: FullyQualifiedTypeName, - pub path: Option, -} - /// Invoke the get operation on a resource /// /// # Arguments @@ -50,17 +45,12 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option< let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), + let command_resource = match target_resource { + Some(target) => target, + None => resource }; - validate_security_context(&get.require_security_context, &resource_type, "get")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, - }; - let args = process_get_args(get.args.as_ref(), filter, &command_resource_info); + validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?; + let args = process_get_args(get.args.as_ref(), filter, &command_resource); if !filter.is_empty() { verify_json_from_manifest(resource, filter, target_resource)?; command_input = get_command_input(get.input.as_ref(), filter)?; @@ -110,16 +100,10 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }; let operation_type: String; let mut is_synthetic_what_if = false; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(target) => target, + None => resource }; - let set_method = match execution_type { ExecutionKind::Actual => { operation_type = "set".to_string(); @@ -132,7 +116,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let (_, supports_whatif) = process_set_delete_args( set.args.as_ref(), "", - &command_resource_info, + &command_resource, execution_type ); supports_whatif @@ -564,20 +548,15 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op return Err(DscError::NotImplemented("validate".to_string())); }; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(target) => target, + None => resource }; - let args = process_get_args(validate.args.as_ref(), config, &command_resource_info); + let args = process_get_args(validate.args.as_ref(), config, &command_resource); let command_input = get_command_input(validate.input.as_ref(), config)?; - info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = &command_resource.type_name, executable = &validate.executable)); + let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result: ValidateResult = serde_json::from_str(&stdout)?; Ok(result) } @@ -939,7 +918,7 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_get_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo) -> Option> { +pub fn process_get_args(args: Option<&Vec>, input: &str, resource: &DscResource) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -951,6 +930,15 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, command_res GetArgKind::String(s) => { processed_args.push(s.clone()); }, + GetArgKind::AdaptedContent { adapted_content_arg } => { + // adapted content is the JSON content with secrets redacted and additional properties added by the adapter; it is only used for get operations and is meant to be used when the command needs to call other commands as part of its execution and wants to pass along the adapted content to avoid multiple rounds of redaction + processed_args.push(adapted_content_arg.clone()); + if let Some(adapted_content) = &resource.adapted_content { + processed_args.push(serde_json::to_string(&adapted_content).unwrap()); + } else { + debug!("{}", t!("dscresources.commandResource.noAdaptedContent", resource = &resource.type_name)); + } + }, GetArgKind::Json { json_input_arg, mandatory } => { if input.is_empty() && *mandatory != Some(true) { continue; @@ -961,7 +949,7 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, command_res }, GetArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); - processed_args.push(command_resource_info.type_name.to_string()); + processed_args.push(resource.type_name.to_string()); }, GetArgKind::ResourcePath { resource_path_arg, include_quotes} => { if let Some(path) = &command_resource_info.path { @@ -1011,7 +999,7 @@ fn process_schema_args(args: Option<&Vec>, command_resource_info: /// # Returns /// /// A vector of strings representing the processed arguments -fn process_set_delete_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo, execution_type: &ExecutionKind) -> (Option>, bool) { +fn process_set_delete_args(args: Option<&Vec>, input: &str, resource: &DscResource, execution_type: &ExecutionKind) -> (Option>, bool) { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return (None, false); @@ -1024,6 +1012,15 @@ fn process_set_delete_args(args: Option<&Vec>, input: &str, co SetDeleteArgKind::String(s) => { processed_args.push(s.clone()); }, + SetDeleteArgKind::AdaptedContent { adapted_content_arg } => { + // adapted content is the JSON content with secrets redacted and additional properties added by the adapter; it is only used for get operations and is meant to be used when the command needs to call other commands as part of its execution and wants to pass along the adapted content to avoid multiple rounds of redaction + processed_args.push(adapted_content_arg.clone()); + if let Some(adapted_content) = &resource.adapted_content { + processed_args.push(serde_json::to_string(&adapted_content).unwrap()); + } else { + debug!("{}", t!("dscresources.commandResource.noAdaptedContent", resource = &resource.type_name)); + } + }, SetDeleteArgKind::Json { json_input_arg, mandatory } => { if input.is_empty() && *mandatory != Some(true) { continue; @@ -1042,16 +1039,6 @@ fn process_set_delete_args(args: Option<&Vec>, input: &str, co } } }, - SetDeleteArgKind::ResourceType { resource_type_arg } => { - processed_args.push(resource_type_arg.clone()); - processed_args.push(command_resource_info.type_name.to_string()); - }, - SetDeleteArgKind::WhatIf { what_if_arg } => { - supports_whatif = true; - if execution_type == &ExecutionKind::WhatIf { - processed_args.push(what_if_arg.clone()); - } - } } } diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index 6b5d544e3..f3f57c0b9 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -61,6 +61,8 @@ pub struct DscResource { pub target_resource: Option>, /// The manifest of the resource. pub manifest: Option, + /// The content of the adapted resource, if available. + pub adapted_content: Option>, } #[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema, Ord, PartialOrd)] @@ -116,6 +118,7 @@ impl DscResource { schema: None, target_resource: None, manifest: None, + adapted_content: None, } } diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index 568e44f9b..6d371434d 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -104,7 +104,11 @@ pub struct ResourceManifest { pub enum GetArgKind { /// The argument is a string. String(String), - /// The argument accepts the JSON input object. + AdaptedContent { + /// The argument that accepts the JSON content from the manifest. + #[serde(rename = "adaptedContentArg")] + adapted_content_arg: String, + }, Json { /// The argument that accepts the JSON input object. #[serde(rename = "jsonInputArg")] @@ -133,7 +137,11 @@ pub enum GetArgKind { pub enum SetDeleteArgKind { /// The argument is a string. String(String), - /// The argument accepts the JSON input object. + AdaptedContent { + /// The argument that accepts the JSON content from the manifest. + #[serde(rename = "adaptedContentArg")] + adapted_content_arg: String, + }, Json { /// The argument that accepts the JSON input object. #[serde(rename = "jsonInputArg")] diff --git a/resources/registry/Cargo.toml b/resources/registry/Cargo.toml index 3d89a7b7a..fb06a0ef2 100644 --- a/resources/registry/Cargo.toml +++ b/resources/registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dsc-resource-registry" -version = "1.0.0" +version = "1.1.0" edition = "2024" [[bin]] diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index eb2f897af..1b534d031 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -2,6 +2,10 @@ _version = 1 [args] about = "Manage state of Windows registry" +adapterAbout = "Use adapted registry resources." +adapterGetAbout = "Use 'get' operation with adapted registry resources." +adapterSetAbout = "Use 'set' operation with adapted registry resources." +adapterArgsAdaptedResourceHelp = "The content of the adapted resource to use." configAbout = "Manage registry configuration." configArgsInputHelp = "The registry JSON input." configArgsWhatIfHelp = "Run as a what-if operation instead of applying the registry configuration." diff --git a/resources/registry/registry.dsc.manifests.json b/resources/registry/registry.dsc.manifests.json index 260b860d8..ae0dc76ab 100644 --- a/resources/registry/registry.dsc.manifests.json +++ b/resources/registry/registry.dsc.manifests.json @@ -117,6 +117,45 @@ ] } } + }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Windows.Adapter/Registry", + "kind": "adapter", + "description": "Adapter for registry based resources", + "tags": [ + "Windows" + ], + "version": "0.1.0", + "get": { + "executable": "registry", + "args": [ + "adapter", + "get", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "set": { + "executable": "registry", + "args": [ + "adapter", + "set", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "exitCodes": { + "0": "Success", + "1": "Invalid parameter", + "2": "Invalid input", + "3": "Registry error", + "4": "JSON serialization failed" + } } ] } diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs new file mode 100644 index 000000000..16b5a1ad5 --- /dev/null +++ b/resources/registry/src/adapter.rs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::error::RegistryResourceError; +use serde::Deserialize; +use serde_json::{Map, Value}; +use tracing::debug; + +#[derive(Deserialize)] +struct AdaptedRegistryResource { + #[serde(flatten)] + properties: Map, +} + +pub fn adapter_get(input: &str) -> Result { + debug!("Adapter Get with input: {input}"); + let adapted_resource: AdaptedRegistryResource = serde_json::from_str(input) + .map_err(|e| RegistryResourceError::AdapterInputParseError(e.to_string()))?; + + for (key, value) in adapted_resource.properties.iter() { + debug!("Property: {key} = {value}"); + } + Ok("{}".to_string()) +} + +pub fn adapter_set(input: &str) -> Result { + debug!("Adapter Set with input: {input}"); + // adapter set is not implemented, return empty result for now + Ok("{}".to_string()) +} diff --git a/resources/registry/src/args.rs b/resources/registry/src/args.rs index 08e36f349..dfd26ab62 100644 --- a/resources/registry/src/args.rs +++ b/resources/registry/src/args.rs @@ -5,13 +5,31 @@ use clap::{Parser, Subcommand}; use rust_i18n::t; #[derive(Parser)] -#[clap(name = "registry", version = "0.0.1", about = t!("args.about").to_string(), long_about = None)] +#[clap(name = "registry", version = env!("CARGO_PKG_VERSION"), about = t!("args.about").to_string(), long_about = None)] pub struct Arguments { #[clap(subcommand)] pub subcommand: SubCommand, } +#[derive(Debug, PartialEq, Eq, Subcommand)] +pub enum AdapterSubCommand { + #[clap(name = "get", about = t!("args.adapterGetAbout").to_string())] + Get { + #[clap(short, long, required = true, help = t!("args.adapterArgsInputHelp").to_string())] + input: String, + #[clap(short, long, required = true, help = t!("args.adapterArgsAdaptedResourceHelp").to_string())] + adapted_resource: String, + }, + #[clap(name = "set", about = t!("args.adapterSetAbout").to_string())] + Set { + #[clap(short, long, required = true, help = t!("args.adapterArgsInputHelp").to_string())] + input: String, + #[clap(short, long, required = true, help = t!("args.adapterArgsAdaptedResourceHelp").to_string())] + adapted_resource: String, + }, +} + #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum ConfigSubCommand { #[clap(name = "get", about = t!("args.configGetAbout").to_string())] @@ -79,6 +97,11 @@ pub enum SubCommand { #[clap(long, help = t!("args.findArgsValuesOnlyHelp").to_string())] values_only: bool, }, + #[clap(name = "adapter", about = t!("args.adapterAbout").to_string(), arg_required_else_help = true)] + Adapter { + #[clap(subcommand)] + subcommand: AdapterSubCommand, + }, #[clap(name = "config", about = t!("args.configAbout").to_string(), arg_required_else_help = true)] Config { #[clap(subcommand)] diff --git a/resources/registry/src/error.rs b/resources/registry/src/error.rs new file mode 100644 index 000000000..63dbe3231 --- /dev/null +++ b/resources/registry/src/error.rs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RegistryResourceError { + #[error("Failed to parse adapter input: {0}")] + AdapterInputParseError(String), + #[error("Registry error: {0}")] + RegistryError(#[from] dsc_lib_registry::error::RegistryError), +} diff --git a/resources/registry/src/main.rs b/resources/registry/src/main.rs index 885188fc1..d1afbfca6 100644 --- a/resources/registry/src/main.rs +++ b/resources/registry/src/main.rs @@ -6,7 +6,8 @@ use crossterm::event; #[cfg(debug_assertions)] use std::env; -use args::Arguments; +use adapter::{adapter_get, adapter_set}; +use args::{AdapterSubCommand, Arguments, ConfigSubCommand, SubCommand}; use clap::Parser; use dsc_lib_registry::{config::Registry, RegistryHelper}; use rust_i18n::t; @@ -16,7 +17,9 @@ use tracing::{error, trace}; use tracing_subscriber::{filter::LevelFilter, prelude::__tracing_subscriber_SubscriberExt, EnvFilter, Layer}; use types::RegistryList; +mod adapter; mod args; +mod error; mod types; rust_i18n::i18n!("locales", fallback = "en-us"); @@ -34,21 +37,32 @@ fn main() { let args = Arguments::parse(); match args.subcommand { - args::SubCommand::Query { key_path, value_name, recurse } => { + SubCommand::Adapter { subcommand } => { + let json = match subcommand { + AdapterSubCommand::Get { input } => { + adapter_get(&input) + }, + AdapterSubCommand::Set { input } => { + adapter_set(&input) + }, + }; + println!("{json}"); + }, + SubCommand::Query { key_path, value_name, recurse } => { trace!("Get key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); }, - args::SubCommand::Set { key_path, value } => { + SubCommand::Set { key_path, value } => { trace!("Set key_path: {key_path}, value: {value}"); }, - args::SubCommand::Remove { key_path, value_name, recurse } => { + SubCommand::Remove { key_path, value_name, recurse } => { trace!("Remove key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); }, - args::SubCommand::Find { key_path, find, recurse, keys_only, values_only } => { + SubCommand::Find { key_path, find, recurse, keys_only, values_only } => { trace!("Find key_path: {key_path}, find: {find}, recurse: {recurse:?}, keys_only: {keys_only:?}, values_only: {values_only:?}"); }, - args::SubCommand::Config { subcommand } => { + SubCommand::Config { subcommand } => { match subcommand { - args::ConfigSubCommand::Get{input, list} => { + ConfigSubCommand::Get{input, list} => { trace!("Get input: {input}"); let mut output = RegistryList { registry_entries: vec![] }; let reg_list = import_input(&input, list); @@ -80,7 +94,7 @@ fn main() { println!("{json}"); exit(EXIT_SUCCESS); }, - args::ConfigSubCommand::Set{input, list, what_if} => { + ConfigSubCommand::Set{input, list, what_if} => { trace!("Set input: {input}, what_if: {what_if}"); let mut output = RegistryList { registry_entries: vec![] }; let reg_list = import_input(&input, list); @@ -141,7 +155,7 @@ fn main() { } exit(EXIT_SUCCESS); }, - args::ConfigSubCommand::Delete{input, what_if} => { + ConfigSubCommand::Delete{input, what_if} => { trace!("Delete input: {input}, what_if: {what_if}"); let mut reg_helper = match RegistryHelper::new_from_json(&input) { Ok(reg_helper) => reg_helper, diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml new file mode 100644 index 000000000..0428467af --- /dev/null +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -0,0 +1,171 @@ +# Adapted Registry resource for personalization settings defined in: +# https://learn.microsoft.com/en-us/windows/apps/develop/settings/settings-common#personalization---colors + +$schema: https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json +type: Microsoft.Windows/Personalization +kind: resource +version: 1.0.0 +capabilities: +- get +- set +description: Controls Windows personalization settings such as accent color, light/dark mode, and transparency effects. +author: Microsoft Corporation +requireAdapter: Microsoft.Windows.Adapter/Registry +content: + appUsesLightTheme: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: AppUseLightTheme + mapJsonToRegistry: + false: 0 + true: 1 + systemUsesLightTheme: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: SystemUsesLightTheme + mapJsonToRegistry: + false: 0 + true: 1 + colorPrevalence: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: ColorPrevalence + mapJsonToRegistry: + false: 0 + true: 1 + transparencyEffects: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: EnableTransparency + mapJsonToRegistry: + false: 0 + true: 1 + startMenuVisiblePlaces: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Start + valueType: REG_BINARY + valueName: VisiblePlaces + mapJsonToRegistry: + # Documents: {2D34D5CE-FA5A-4543-82F2-22E6EAF7773C} + Documents: [0xCE, 0xD5, 0x34, 0x2D, 0x5A, 0xFA, 0x43, 0x45, 0x82, 0xF2, 0x22, 0xE6, 0xEA, 0xF7, 0x77, 0x3C] + # Downloads: {E367B32F-89DE-4355-BFCE-61F37B18A937} + Downloads: [0x2F, 0xB3, 0x67, 0xE3, 0xDE, 0x89, 0x55, 0x43, 0xBF, 0xCE, 0x61, 0xF3, 0x7B, 0x18, 0xA9, 0x37] + # Music: {B00B0620-7F51-4C32-AA1E-34CC547F7315} + Music: [0x20, 0x06, 0x0B, 0xB0, 0x51, 0x7F, 0x32, 0x4C, 0xAA, 0x1E, 0x34, 0xCC, 0x54, 0x7F, 0x73, 0x15] + # Pictures: {383F07A0-E80A-4C80-B05A-86DB845DBC4D} + Pictures: [0xA0, 0x07, 0x3F, 0x38, 0x0A, 0xE8, 0x80, 0x4C, 0xB0, 0x5A, 0x86, 0xDB, 0x84, 0x5D, 0xBC, 0x4D] + # Videos: {42B3A5C5-7D86-42F4-80A4-93FACA7A88B5} + Videos: [0xC5, 0xA5, 0xB3, 0x42, 0x86, 0x7D, 0xF4, 0x42, 0x80, 0xA4, 0x93, 0xFA, 0xCA, 0x7A, 0x88, 0xB5] + # Network: {FE758144-080D-42AE-8BDA-34ED97B66394} + Network: [0x44, 0x81, 0x75, 0xFE, 0x0D, 0x08, 0xAE, 0x42, 0x8B, 0xDA, 0x34, 0xED, 0x97, 0xB6, 0x63, 0x94] + # UserProfile: {74BDB04A-F94A-4F68-8BD6-4398071DA8BC} + UserProfile: [0x4A, 0xB0, 0xBD, 0x74, 0x4A, 0xF9, 0x68, 0x4F, 0x8B, 0xD6, 0x43, 0x98, 0x07, 0x1D, 0xA8, 0xBC] + # Explorer: {148A24BC-D60C-4289-A080-6ED9BBA24882} + Explorer: [0xBC, 0x24, 0x8A, 0x14, 0x0C, 0xD6, 0x89, 0x42, 0xA0, 0x80, 0x6E, 0xD9, 0xBB, 0xA2, 0x48, 0x82] + # Settings: {52730886-51AA-4243-9F7B-2776584659D4} + Settings: [0x86, 0x88, 0x73, 0x52, 0xAA, 0x51, 0x43, 0x42, 0x9F, 0x7B, 0x27, 0x76, 0x58, 0x46, 0x59, 0xD4] + startMenuShowRecentList: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Start + valueType: REG_BOOL + valueName: ShowRecentList + mapJsonToRegistry: + false: false + true: true + showRecommendedApps: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced + valueType: REG_BOOL + valueName: Start_TrackDocs + mapJsonToRegistry: + false: false + true: true + taskbarShowBadges: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + valueType: REG_SZ + valueName: SystemSettings_Taskbar_Badging + mapJsonToRegistry: + false: '0' + true: '1' + desktopTaskbarShowBadges: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_Badging + mapJsonToRegistry: + false: '0' + true: '1' + multimonitorTaskbarGroupingMode: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarGlomLevel + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_GroupingMode + mapJsonToRegistry: + # Always combine, hide labels: 0 + AlwaysCombineHideLabels: 0 + # Combine when taskbar is full: 1 + CombineWhenTaskbarIsFull: 1 + # Never combine: 2 + NeverCombine: 2 +schema: + embedded: + $schema: http://json-schema.org/draft-07/schema# + $id: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Adapted/Three/v0.1.0/schema.json + title: Microsoft.Windows/Personalization + description: Windows personalization settings such as accent color, light/dark mode, and transparency effects. + type: object + required: [] + additionalProperties: false + properties: + appUsesLightTheme: + type: boolean + title: App Uses Light Theme + description: Indicates whether the app uses the light theme. + systemUsesLightTheme: + type: boolean + title: System Uses Light Theme + description: Indicates whether the system uses the light theme. + colorPrevalence: + type: boolean + title: Color Prevalence + description: Indicates whether the accent color is applied to Start, taskbar, and action center + transparencyEffects: + type: boolean + title: Transparency Effects + description: Indicates whether transparency effects are enabled. + startMenuVisiblePlaces: + type: array + title: Start Menu Visible Places + description: A list of places that are visible in the Start menu. + items: + type: string + enum: + - Documents + - Downloads + - Music + - Pictures + - Videos + - Network + - UserProfile + - Explorer + - Settings + startMenuShowRecentList: + type: boolean + title: Start Menu Show Recent List + description: Specifies whether apps that were recently installed in Start in various surfaces are shown on Start. + showRecommendedApps: + type: boolean + title: Show Recommended Apps + description: Specifies whether recommended files in Start, recent files in File Explorer, and items in Jump Lists are shown. + taskbarShowBadges: + type: boolean + title: Taskbar Show Badges + description: Specifies whether badges on taskbar buttons are shown. + desktopTaskbarShowBadges: + type: boolean + title: Desktop Taskbar Show Badges + description: Specifies whether badges on taskbar buttons are shown on the desktop. + multimonitorTaskbarGroupingMode: + type: string + title: Multimonitor Taskbar Grouping Mode + description: Specifies how taskbar buttons are grouped on multiple displays. + enum: + - AlwaysCombineHideLabels + - CombineWhenTaskbarIsFull + - NeverCombine + \ No newline at end of file From db7f91674dfe5d0b472a728608ded888271f4a52 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 12 May 2026 21:43:12 -0700 Subject: [PATCH 02/16] Enable adaptedContent and registry adapter --- .../src/discovery/command_discovery.rs | 57 ++++---- .../dscresources/adapted_resource_manifest.rs | 2 +- .../src/dscresources/command_resource.rs | 138 ++++++++---------- .../src/dscresources/resource_manifest.rs | 2 +- lib/dsc-lib/src/extensions/discover.rs | 11 +- .../registry/registry.dsc.manifests.json | 9 ++ resources/registry/src/adapter.rs | 13 +- resources/registry/src/main.rs | 20 ++- .../tests/adaptedRegistry.get.tests.ps1 | 0 .../.project.data.json | 10 ++ .../personalization_get.tests.ps1 | 14 ++ ...s_personalization.dsc.adaptedResource.yaml | 32 ++-- tools/test_group_resource/src/main.rs | 3 + 13 files changed, 172 insertions(+), 139 deletions(-) create mode 100644 resources/registry/tests/adaptedRegistry.get.tests.ps1 create mode 100644 resources/windows_personalization/.project.data.json create mode 100644 resources/windows_personalization/personalization_get.tests.ps1 diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 9439f2cd3..afbc9084e 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -391,38 +391,39 @@ impl ResourceDiscovery for CommandDiscovery { let mut adapter_resources_count = 0; // invoke the list command - let list_command = &manifest.adapter.clone().unwrap().list; - let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args.clone(), None, Some(&adapter.directory), None, manifest.exit_codes.as_ref()) - { - Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), - Err(e) => { - // In case of error, log and continue - warn!("{e}"); - continue; - }, - }; + if let Some(list_command) = &manifest.adapter.clone().unwrap().list { + let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args.clone(), None, Some(&adapter.directory), None, manifest.exit_codes.as_ref()) + { + Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), + Err(e) => { + // In case of error, log and continue + warn!("{e}"); + continue; + }, + }; - if exit_code != 0 { - // in case of failure, log and continue - warn!("Adapter failed to list resources with exit code {exit_code}: {stderr}"); - continue; - } + if exit_code != 0 { + // in case of failure, log and continue + warn!("Adapter failed to list resources with exit code {exit_code}: {stderr}"); + continue; + } - for line in stdout.lines() { - match serde_json::from_str::(line){ - Result::Ok(resource) => { - if resource.require_adapter.is_none() { - warn!("{}", DscError::MissingRequires(adapter_name.to_string(), resource.type_name.to_string()).to_string()); - continue; - } + for line in stdout.lines() { + match serde_json::from_str::(line){ + Result::Ok(resource) => { + if resource.require_adapter.is_none() { + warn!("{}", DscError::MissingRequires(adapter_name.to_string(), resource.type_name.to_string()).to_string()); + continue; + } - if name_filter.is_match(&resource.type_name) { - insert_resource(&mut adapted_resources, &resource); - adapter_resources_count += 1; + if name_filter.is_match(&resource.type_name) { + insert_resource(&mut adapted_resources, &resource); + adapter_resources_count += 1; + } + }, + Result::Err(err) => { + warn!("Failed to parse resource: {line} -> {err}"); } - }, - Result::Err(err) => { - warn!("Failed to parse resource: {line} -> {err}"); } } } diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index 0c581f338..e13d38db7 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -53,7 +53,7 @@ pub struct AdaptedDscResourceManifest { #[serde(skip_serializing_if = "Option::is_none")] pub deprecation_message: Option, /// The file path to the resource or the content of the resource itself. - #[schemars(flatten)] + #[serde(flatten)] pub path_or_content: AdaptedPathOrContent, /// The description of the resource. pub description: Option, diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index e09e83795..84df35270 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -8,7 +8,7 @@ use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; use std::{collections::HashMap, env, path::Path, process::Stdio}; -use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::{command_resource, resource_manifest::SchemaArgKind}, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; +use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::{resource_manifest::SchemaArgKind}, types::ExitCodesMap, util::canonicalize_which}; use crate::dscerror::DscError; use super::{ dscresource::{get_diff, redact, DscResource}, @@ -125,7 +125,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut if has_native_whatif { &manifest.set } else if manifest.what_if.is_some() { - warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource_type)); + warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &command_resource.type_name)); &manifest.what_if } else { is_synthetic_what_if = true; @@ -136,12 +136,12 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let Some(set) = set_method.as_ref() else { return Err(DscError::NotImplemented("set".to_string())); }; - validate_security_context(&set.require_security_context, &resource_type, "set")?; + validate_security_context(&set.require_security_context, &command_resource.type_name, "set")?; verify_json_from_manifest(resource, desired, target_resource)?; // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !skip_test && set.pre_test != Some(true) { - info!("{}", t!("dscresources.commandResource.noPretest", resource = &resource.type_name)); + info!("{}", t!("dscresources.commandResource.noPretest", resource = &command_resource.type_name)); let test_result = invoke_test(resource, desired, target_resource)?; if is_synthetic_what_if { return Ok(test_result.into()); @@ -177,21 +177,16 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - validate_security_context(&get.require_security_context, &resource_type, "get")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; - let args = process_get_args(get.args.as_ref(), desired, &command_resource_info); + validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?; + let args = process_get_args(get.args.as_ref(), desired, &command_resource); let command_input = get_command_input(get.input.as_ref(), desired)?; - info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.type_name, executable = &get.executable)); - let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &command_resource.type_name, executable = &get.executable)); + let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.setVerifyGet", resource = &resource.type_name, executable = &get.executable)); @@ -219,7 +214,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &command_resource_info, execution_type); + let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &command_resource, execution_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -232,7 +227,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }, } - let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&resource.directory), env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&command_resource.directory), env, manifest.exit_codes.as_ref())?; let return_kind = if execution_type == &ExecutionKind::WhatIf { set.what_if_returns.as_ref().or(set.returns.as_ref()) @@ -244,7 +239,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut Some(ReturnKind::State) => { if resource.kind == Kind::Resource { - debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &resource.type_name, executable = &set.executable)); + debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &command_resource.type_name, executable = &set.executable)); verify_json_from_manifest(resource, &stdout, target_resource)?; } @@ -272,14 +267,14 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }; if resource.kind == Kind::Resource { - debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &resource.type_name, executable = &set.executable)); + debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &command_resource.type_name, executable = &set.executable)); verify_json_from_manifest(resource, actual_line, target_resource)?; } let actual_value: Value = serde_json::from_str(actual_line)?; // TODO: need schema for diff_properties to validate against let Some(diff_line) = lines.next() else { - return Err(DscError::Command(resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); + return Err(DscError::Command(command_resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_line)?; Ok(SetResult::Resource(ResourceSetResponse { @@ -337,23 +332,18 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti verify_json_from_manifest(resource, expected, target_resource)?; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - validate_security_context(&test.require_security_context, &resource_type, "test")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; - let args = process_get_args(test.args.as_ref(), expected, &command_resource_info); + validate_security_context(&test.require_security_context, &command_resource.type_name, "test")?; + let args = process_get_args(test.args.as_ref(), expected, &command_resource); let command_input = get_command_input(test.input.as_ref(), expected)?; - info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.type_name, executable = &test.executable)); - let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &command_resource.type_name, executable = &test.executable)); + let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; - if resource.kind == Kind::Importer { + if command_resource.kind == Kind::Importer { debug!("{}", t!("dscresources.commandResource.testGroupTestResponse")); let group_test_response: Vec = serde_json::from_str(&stdout)?; return Ok(TestResult::Group(group_test_response)); @@ -362,8 +352,8 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti let mut expected_value: Value = serde_json::from_str(expected)?; match test.returns { Some(ReturnKind::State) => { - if resource.kind == Kind::Resource { - debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &resource.type_name, executable = &test.executable)); + if command_resource.kind == Kind::Resource { + debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &command_resource.type_name, executable = &test.executable)); verify_json_from_manifest(resource, &stdout, target_resource)?; } @@ -494,17 +484,12 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti verify_json_from_manifest(resource, filter, target_resource)?; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - validate_security_context(&delete.require_security_context, &resource_type, "delete")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; - let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, &command_resource_info, execution_type); + validate_security_context(&delete.require_security_context, &command_resource.type_name, "delete")?; + let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, &command_resource, execution_type); if execution_type == &ExecutionKind::WhatIf && !supports_whatif { // perform a synthetic what-if by calling test and wrapping the TestResult in DeleteResultKind::SyntheticWhatIf let test_result = invoke_test(resource, filter, target_resource)?; @@ -512,8 +497,8 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti } let command_input = get_command_input(delete.input.as_ref(), filter)?; - info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = &command_resource.type_name, executable = &delete.executable)); + let (_exit_code, stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result = if execution_type == &ExecutionKind::WhatIf { let delete_result: DeleteResult = serde_json::from_str(&stdout)?; DeleteResultKind::ResourceWhatIf(delete_result) @@ -571,25 +556,31 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op /// /// Error if schema is not available or if there is an error getting the schema pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>) -> Result { - let Some(manifest) = &resource.manifest else { - return Err(DscError::MissingManifest(resource.type_name.to_string())); + let target_resource = match target_resource { + Some(r) => r, + None => resource, }; + + if let Some(schema) = &target_resource.schema { + return Ok(serde_json::to_string(schema)?); + } + + let Some(manifest) = &target_resource.manifest else { + return Err(DscError::MissingManifest(target_resource.type_name.to_string())); + }; + let Some(schema_kind) = manifest.schema.as_ref() else { - return Err(DscError::SchemaNotAvailable(resource.type_name.to_string())); + return Err(DscError::SchemaNotAvailable(target_resource.type_name.to_string())); }; match schema_kind { SchemaKind::Command(command) => { - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - let args = process_schema_args(command.args.as_ref(), &CommandResourceInfo { type_name: resource_type, path: None }); - let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, args, None, Some(&resource.directory), None, manifest.exit_codes.as_ref())?; + let args = process_schema_args(command.args.as_ref(), target_resource); + let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, args, None, Some(&target_resource.directory), None, manifest.exit_codes.as_ref())?; Ok(stdout) }, SchemaKind::Embedded(schema) => { - let json = serde_json::to_string(schema)?; + let json = serde_json::to_string(&schema)?; Ok(json) }, } @@ -640,16 +631,11 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc let mut command_input: CommandInput = CommandInput { env: None, stdin: None }; let args: Option>; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - validate_security_context(&export.require_security_context, &resource_type, "export")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; + validate_security_context(&export.require_security_context, &command_resource.type_name, "export")?; if let Some(input) = input { if !input.is_empty() { verify_json_from_manifest(resource, input, target_resource)?; @@ -657,9 +643,9 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_get_args(export.args.as_ref(), input, &command_resource_info); + args = process_get_args(export.args.as_ref(), input, &command_resource); } else { - args = process_get_args(export.args.as_ref(), "", &command_resource_info); + args = process_get_args(export.args.as_ref(), "", &command_resource); } let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -707,12 +693,10 @@ pub fn invoke_resolve(resource: &DscResource, input: &str) -> Result>, input: &str, resource: & Some(processed_args) } -fn process_schema_args(args: Option<&Vec>, command_resource_info: &CommandResourceInfo) -> Option> { +fn process_schema_args(args: Option<&Vec>, command_resource: &DscResource) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -981,7 +965,7 @@ fn process_schema_args(args: Option<&Vec>, command_resource_info: }, SchemaArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); - processed_args.push(command_resource_info.type_name.to_string()); + processed_args.push(command_resource.type_name.to_string()); }, } } diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index 6d371434d..93d558712 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -338,7 +338,7 @@ pub struct ResolveMethod { #[dsc_repo_schema(base_name = "manifest.adapter", folder_path = "resource")] pub struct Adapter { /// The way to list adapter supported resources. - pub list: ListMethod, + pub list: Option, /// Defines how the adapter supports accepting configuration. #[serde(alias = "config", rename = "inputKind")] pub input_kind: AdapterInputKind, diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 4b017ae3f..488703590 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -8,7 +8,7 @@ use crate::{ dscerror::DscError, dscresources::{ command_resource::{ - invoke_command, process_get_args, CommandResourceInfo + invoke_command, process_get_args, }, dscresource::DscResource, resource_manifest::GetArgKind, @@ -68,11 +68,10 @@ impl DscExtension { let Some(discover) = extension.discover else { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Discover.to_string())); }; - let command_resource_info = CommandResourceInfo { - type_name: self.type_name.clone(), - path: None, - }; - let args = process_get_args(discover.args.as_ref(), "", &command_resource_info); + let mut extension_resource = DscResource::new(); + extension_resource.type_name = self.type_name.clone(); + extension_resource.path = self.path.clone(); + let args = process_get_args(discover.args.as_ref(), "", &extension_resource); if let Some(deprecation_message) = extension.deprecation_message.as_ref() { warn!("{}", t!("extensions.dscextension.deprecationMessage", extension = self.type_name, message = deprecation_message)); } diff --git a/resources/registry/registry.dsc.manifests.json b/resources/registry/registry.dsc.manifests.json index ae0dc76ab..d23aebd90 100644 --- a/resources/registry/registry.dsc.manifests.json +++ b/resources/registry/registry.dsc.manifests.json @@ -127,6 +127,9 @@ "Windows" ], "version": "0.1.0", + "adapter": { + "inputKind": "single" + }, "get": { "executable": "registry", "args": [ @@ -135,6 +138,9 @@ { "jsonInputArg": "--input", "mandatory": true + }, + { + "adaptedContentArg": "--adapted-resource" } ] }, @@ -146,6 +152,9 @@ { "jsonInputArg": "--input", "mandatory": true + }, + { + "adaptedContentArg": "--adapted-resource" } ] }, diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index 16b5a1ad5..23f32c493 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -12,9 +12,9 @@ struct AdaptedRegistryResource { properties: Map, } -pub fn adapter_get(input: &str) -> Result { +pub fn adapter_get(input: &str, adapted_resource: &str) -> Result { debug!("Adapter Get with input: {input}"); - let adapted_resource: AdaptedRegistryResource = serde_json::from_str(input) + let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) .map_err(|e| RegistryResourceError::AdapterInputParseError(e.to_string()))?; for (key, value) in adapted_resource.properties.iter() { @@ -23,8 +23,13 @@ pub fn adapter_get(input: &str) -> Result { Ok("{}".to_string()) } -pub fn adapter_set(input: &str) -> Result { +pub fn adapter_set(input: &str, adapted_resource: &str) -> Result { debug!("Adapter Set with input: {input}"); - // adapter set is not implemented, return empty result for now + let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) + .map_err(|e| RegistryResourceError::AdapterInputParseError(e.to_string()))?; + + for (key, value) in adapted_resource.properties.iter() { + debug!("Property: {key} = {value}"); + } Ok("{}".to_string()) } diff --git a/resources/registry/src/main.rs b/resources/registry/src/main.rs index d1afbfca6..515d8f695 100644 --- a/resources/registry/src/main.rs +++ b/resources/registry/src/main.rs @@ -38,15 +38,23 @@ fn main() { let args = Arguments::parse(); match args.subcommand { SubCommand::Adapter { subcommand } => { - let json = match subcommand { - AdapterSubCommand::Get { input } => { - adapter_get(&input) + let result = match subcommand { + AdapterSubCommand::Get { input, adapted_resource } => { + adapter_get(&input, &adapted_resource) }, - AdapterSubCommand::Set { input } => { - adapter_set(&input) + AdapterSubCommand::Set { input, adapted_resource } => { + adapter_set(&input, &adapted_resource) }, }; - println!("{json}"); + match result { + Ok(output) => { + println!("{output}"); + }, + Err(err) => { + error!("{err}"); + exit(EXIT_INVALID_INPUT); + } + } }, SubCommand::Query { key_path, value_name, recurse } => { trace!("Get key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); diff --git a/resources/registry/tests/adaptedRegistry.get.tests.ps1 b/resources/registry/tests/adaptedRegistry.get.tests.ps1 new file mode 100644 index 000000000..e69de29bb diff --git a/resources/windows_personalization/.project.data.json b/resources/windows_personalization/.project.data.json new file mode 100644 index 000000000..776bee547 --- /dev/null +++ b/resources/windows_personalization/.project.data.json @@ -0,0 +1,10 @@ +{ + "Name": "windows_personalization", + "Kind": "Resource", + "SupportedPlatformOS": "Windows", + "CopyFiles": { + "Windows": [ + "windows_personalization.dsc.adaptedresource.yaml" + ] + } +} diff --git a/resources/windows_personalization/personalization_get.tests.ps1 b/resources/windows_personalization/personalization_get.tests.ps1 new file mode 100644 index 000000000..4c67831f1 --- /dev/null +++ b/resources/windows_personalization/personalization_get.tests.ps1 @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Windows Personalization get tests' { + It 'Can get a personalization setting' -Skip:(!$IsWindows) { + $json = @{ + "appUseLightTheme" = $true + } | ConvertTo-Json -Compress + $out = dsc resource get -r Microsoft.Windows/Personalization -i $json 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because "dsc resource get failed with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + Write-Verbose -Verbose "Output from dsc resource get: $($out | ConvertTo-Json -Compress)" + $out.appUseLightTheme | Should -Be $true + } +} diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml index 0428467af..a343c467f 100644 --- a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -17,29 +17,29 @@ content: valueType: REG_DWORD valueName: AppUseLightTheme mapJsonToRegistry: - false: 0 - true: 1 + 'false': 0 + 'true': 1 systemUsesLightTheme: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize valueType: REG_DWORD valueName: SystemUsesLightTheme mapJsonToRegistry: - false: 0 - true: 1 + 'false': 0 + 'true': 1 colorPrevalence: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize valueType: REG_DWORD valueName: ColorPrevalence mapJsonToRegistry: - false: 0 - true: 1 + 'false': 0 + 'true': 1 transparencyEffects: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize valueType: REG_DWORD valueName: EnableTransparency mapJsonToRegistry: - false: 0 - true: 1 + 'false': 0 + 'true': 1 startMenuVisiblePlaces: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Start valueType: REG_BINARY @@ -68,29 +68,29 @@ content: valueType: REG_BOOL valueName: ShowRecentList mapJsonToRegistry: - false: false - true: true + 'false': false + 'true': true showRecommendedApps: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced valueType: REG_BOOL valueName: Start_TrackDocs mapJsonToRegistry: - false: false - true: true + 'false': false + 'true': true taskbarShowBadges: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges valueType: REG_SZ valueName: SystemSettings_Taskbar_Badging mapJsonToRegistry: - false: '0' - true: '1' + 'false': '0' + 'true': '1' desktopTaskbarShowBadges: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges valueType: REG_SZ valueName: SystemSettings_DesktopTaskbar_Badging mapJsonToRegistry: - false: '0' - true: '1' + 'false': '0' + 'true': '1' multimonitorTaskbarGroupingMode: keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarGlomLevel valueType: REG_SZ diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index 31b8520c9..60176a3c9 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -27,6 +27,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), require_adapter: Some("Test/TestGroup".parse().unwrap()), + adapted_content: None, target_resource: None, schema: None, manifest: Some(ResourceManifest { @@ -55,6 +56,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), require_adapter: Some("Test/TestGroup".parse().unwrap()), + adapted_content: None, target_resource: None, schema: None, manifest: Some(ResourceManifest { @@ -87,6 +89,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), require_adapter: None, + adapted_content: None, target_resource: None, manifest: None, schema: None, From d1be78cf557899eb5e1f03638957a8730a2ef067 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 13 May 2026 12:05:11 -0700 Subject: [PATCH 03/16] updating adapter --- resources/registry/src/adapter.rs | 21 ++++- resources/registry/src/error.rs | 2 + ...s_personalization.dsc.adaptedResource.yaml | 78 +++++++++---------- 3 files changed, 58 insertions(+), 43 deletions(-) diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index 23f32c493..8589bf29d 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::error::RegistryResourceError; +use dsc_lib_registry::RegistryHelper; use serde::Deserialize; use serde_json::{Map, Value}; use tracing::debug; @@ -12,13 +13,25 @@ struct AdaptedRegistryResource { properties: Map, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AdaptedRegistryValue { + key_path: String, + value_name: String, + value_type: String, + map_json_to_registry: Value, +} + pub fn adapter_get(input: &str, adapted_resource: &str) -> Result { debug!("Adapter Get with input: {input}"); let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) - .map_err(|e| RegistryResourceError::AdapterInputParseError(e.to_string()))?; - + .map_err(|e| RegistryResourceError::AdaptedResourceDeserializationError(e.to_string()))?; + let mut result = Map::new(); + for (key, value) in adapted_resource.properties.iter() { - debug!("Property: {key} = {value}"); + let adapted_registry_value: AdaptedRegistryValue = serde_json::from_value(value.clone()) + .map_err(|e| RegistryResourceError::AdaptedResourceDeserializationError(e.to_string()))?; + let reg_helper = RegistryHelper::new() } Ok("{}".to_string()) } @@ -26,7 +39,7 @@ pub fn adapter_get(input: &str, adapted_resource: &str) -> Result Result { debug!("Adapter Set with input: {input}"); let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) - .map_err(|e| RegistryResourceError::AdapterInputParseError(e.to_string()))?; + .map_err(|e| RegistryResourceError::AdaptedResourceDeserializationError(e.to_string()))?; for (key, value) in adapted_resource.properties.iter() { debug!("Property: {key} = {value}"); diff --git a/resources/registry/src/error.rs b/resources/registry/src/error.rs index 63dbe3231..6b84e4976 100644 --- a/resources/registry/src/error.rs +++ b/resources/registry/src/error.rs @@ -7,6 +7,8 @@ use thiserror::Error; pub enum RegistryResourceError { #[error("Failed to parse adapter input: {0}")] AdapterInputParseError(String), + #[error("Adapted resource deserialization error: {0}")] + AdaptedResourceDeserializationError(String), #[error("Registry error: {0}")] RegistryError(#[from] dsc_lib_registry::error::RegistryError), } diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml index a343c467f..a82d7d483 100644 --- a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -63,45 +63,45 @@ content: Explorer: [0xBC, 0x24, 0x8A, 0x14, 0x0C, 0xD6, 0x89, 0x42, 0xA0, 0x80, 0x6E, 0xD9, 0xBB, 0xA2, 0x48, 0x82] # Settings: {52730886-51AA-4243-9F7B-2776584659D4} Settings: [0x86, 0x88, 0x73, 0x52, 0xAA, 0x51, 0x43, 0x42, 0x9F, 0x7B, 0x27, 0x76, 0x58, 0x46, 0x59, 0xD4] - startMenuShowRecentList: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Start - valueType: REG_BOOL - valueName: ShowRecentList - mapJsonToRegistry: - 'false': false - 'true': true - showRecommendedApps: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced - valueType: REG_BOOL - valueName: Start_TrackDocs - mapJsonToRegistry: - 'false': false - 'true': true - taskbarShowBadges: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges - valueType: REG_SZ - valueName: SystemSettings_Taskbar_Badging - mapJsonToRegistry: - 'false': '0' - 'true': '1' - desktopTaskbarShowBadges: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges - valueType: REG_SZ - valueName: SystemSettings_DesktopTaskbar_Badging - mapJsonToRegistry: - 'false': '0' - 'true': '1' - multimonitorTaskbarGroupingMode: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarGlomLevel - valueType: REG_SZ - valueName: SystemSettings_DesktopTaskbar_GroupingMode - mapJsonToRegistry: - # Always combine, hide labels: 0 - AlwaysCombineHideLabels: 0 - # Combine when taskbar is full: 1 - CombineWhenTaskbarIsFull: 1 - # Never combine: 2 - NeverCombine: 2 + startMenuShowRecentList: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Start + valueType: REG_BOOL + valueName: ShowRecentList + mapJsonToRegistry: + 'false': false + 'true': true + showRecommendedApps: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced + valueType: REG_BOOL + valueName: Start_TrackDocs + mapJsonToRegistry: + 'false': false + 'true': true + taskbarShowBadges: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + valueType: REG_SZ + valueName: SystemSettings_Taskbar_Badging + mapJsonToRegistry: + 'false': '0' + 'true': '1' + desktopTaskbarShowBadges: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_Badging + mapJsonToRegistry: + 'false': '0' + 'true': '1' + multimonitorTaskbarGroupingMode: + keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarGlomLevel + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_GroupingMode + mapJsonToRegistry: + # Always combine, hide labels: 0 + AlwaysCombineHideLabels: 0 + # Combine when taskbar is full: 1 + CombineWhenTaskbarIsFull: 1 + # Never combine: 2 + NeverCombine: 2 schema: embedded: $schema: http://json-schema.org/draft-07/schema# From bee59d075e97186fe268828d5fc7ef5fabf451f6 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 13 May 2026 14:20:28 -0700 Subject: [PATCH 04/16] fix values as strings --- .../windows_personalization.dsc.adaptedResource.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml index a82d7d483..86f053f7a 100644 --- a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -97,11 +97,11 @@ content: valueName: SystemSettings_DesktopTaskbar_GroupingMode mapJsonToRegistry: # Always combine, hide labels: 0 - AlwaysCombineHideLabels: 0 + AlwaysCombineHideLabels: '0' # Combine when taskbar is full: 1 - CombineWhenTaskbarIsFull: 1 + CombineWhenTaskbarIsFull: '1' # Never combine: 2 - NeverCombine: 2 + NeverCombine: '2' schema: embedded: $schema: http://json-schema.org/draft-07/schema# From 1833ad5506e38576f858880e3dcd9a3484383fca Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Wed, 13 May 2026 21:22:45 -0700 Subject: [PATCH 05/16] rebase --- resources/registry/src/adapter.rs | 8 +++++--- resources/registry/src/error.rs | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index 8589bf29d..217bef8da 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::error::RegistryResourceError; -use dsc_lib_registry::RegistryHelper; +//use dsc_lib_registry::RegistryHelper; use serde::Deserialize; use serde_json::{Map, Value}; use tracing::debug; @@ -26,12 +26,14 @@ pub fn adapter_get(input: &str, adapted_resource: &str) -> Result Date: Thu, 14 May 2026 21:54:20 -0700 Subject: [PATCH 06/16] fix merge conflict --- .../src/dscresources/command_resource.rs | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 84df35270..a9a9c598a 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -936,13 +936,11 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, resource: & processed_args.push(resource.type_name.to_string()); }, GetArgKind::ResourcePath { resource_path_arg, include_quotes} => { - if let Some(path) = &command_resource_info.path { - processed_args.push(resource_path_arg.clone()); - if *include_quotes { - processed_args.push(format!("\"{}\"", path.to_string_lossy())); - } else { - processed_args.push(path.to_string_lossy().to_string()); - } + processed_args.push(resource_path_arg.clone()); + if *include_quotes { + processed_args.push(format!("\"{}\"", resource.path.to_string_lossy())); + } else { + processed_args.push(resource.path.to_string_lossy().to_string()); } }, } @@ -1014,15 +1012,23 @@ fn process_set_delete_args(args: Option<&Vec>, input: &str, re processed_args.push(input.to_string()); }, SetDeleteArgKind::ResourcePath { resource_path_arg, include_quotes} => { - if let Some(path) = &command_resource_info.path { - processed_args.push(resource_path_arg.clone()); - if *include_quotes { - processed_args.push(format!("\"{}\"", path.to_string_lossy())); - } else { - processed_args.push(path.to_string_lossy().to_string()); - } + processed_args.push(resource_path_arg.clone()); + if *include_quotes { + processed_args.push(format!("\"{}\"", resource.path.to_string_lossy())); + } else { + processed_args.push(resource.path.to_string_lossy().to_string()); } }, + SetDeleteArgKind::ResourceType { resource_type_arg } => { + processed_args.push(resource_type_arg.clone()); + processed_args.push(resource.type_name.to_string()); + }, + SetDeleteArgKind::WhatIf { what_if_arg } => { + supports_whatif = true; + if execution_type == &ExecutionKind::WhatIf { + processed_args.push(what_if_arg.clone()); + } + } } } From 8e59d7a8e4ec24824f9dd6ff3cf9fdca6ea9c2fb Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 26 May 2026 17:15:15 -0700 Subject: [PATCH 07/16] fix mapping from json to registry and vice versa --- lib/dsc-lib-jsonschema/.versions.json | 3 +- resources/registry/locales/en-us.toml | 10 + .../registry/registry.dsc.manifests.json | 14 + resources/registry/src/adapter.rs | 286 +++++++++++++++++- resources/registry/src/args.rs | 7 + resources/registry/src/error.rs | 2 +- resources/registry/src/main.rs | 5 +- .../personalization_get.tests.ps1 | 38 ++- ...s_personalization.dsc.adaptedResource.yaml | 141 +++++++-- 9 files changed, 460 insertions(+), 46 deletions(-) diff --git a/lib/dsc-lib-jsonschema/.versions.json b/lib/dsc-lib-jsonschema/.versions.json index 6ffb355c8..ccb9af183 100644 --- a/lib/dsc-lib-jsonschema/.versions.json +++ b/lib/dsc-lib-jsonschema/.versions.json @@ -1,10 +1,11 @@ { "latestMajor": "V3", "latestMinor": "V3_2", - "latestPatch": "V3_2_0", + "latestPatch": "V3_2_1", "all": [ "V3", "V3_2", + "V3_2_1", "V3_2_0", "V3_1", "V3_1_3", diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index 1b534d031..5bf3e7dc7 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -1,5 +1,15 @@ _version = 1 +[adapter] +getProcessingKey = "Processing key: %{key}" +getNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key}" +registryValueNotFound = "Registry value not found for key path: %{key_path} and value name: %{value_name}" +couldNotConvertRegistryValue = "Could not convert registry value for key path: %{key_path} and value name: %{value_name} to JSON type: %{json_type}" +unsupportedConversionToJsonType = "Unsupported conversion from %{registry_value_data} to JSON type: %{json_type}" +mappingNotFound = "No mapping found for registry key path: %{key_path} and value name: %{value_name}" +unsupportedValueType = "For value name %{value_name}, unsupported value type: %{value}" +couldNotConvertDefaultValue = "Could not convert default value: %{default_value} to registry data type: %{reg_type}" + [args] about = "Manage state of Windows registry" adapterAbout = "Use adapted registry resources." diff --git a/resources/registry/registry.dsc.manifests.json b/resources/registry/registry.dsc.manifests.json index d23aebd90..a2b86b719 100644 --- a/resources/registry/registry.dsc.manifests.json +++ b/resources/registry/registry.dsc.manifests.json @@ -158,6 +158,20 @@ } ] }, + "export": { + "executable": "registry", + "args": [ + "adapter", + "export", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "adaptedContentArg": "--adapted-resource" + } + ] + }, "exitCodes": { "0": "Success", "1": "Invalid parameter", diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index 217bef8da..a9145aa0f 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::collections::HashMap; + use crate::error::RegistryResourceError; -//use dsc_lib_registry::RegistryHelper; +use dsc_lib_registry::{RegistryHelper, config::RegistryValueData}; +use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; -use tracing::debug; +use tracing::{debug, trace, warn}; #[derive(Deserialize)] struct AdaptedRegistryResource { @@ -13,38 +16,293 @@ struct AdaptedRegistryResource { properties: Map, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum RegistryDataType { + RegBinary, + RegDword, + #[serde(rename = "REG_SZ")] + RegString, + #[serde(rename = "REG_EXPAND_SZ")] + RegExpandString, + #[serde(rename = "REG_MULTI_SZ")] + RegMultiString, + RegQword, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +enum JsonType { + Boolean, + BooleanArray, + Number, + NumberArray, + String, + StringArray, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct AdaptedRegistryValue { key_path: String, value_name: String, - value_type: String, + value_type: RegistryDataType, + json_type: JsonType, map_json_to_registry: Value, + default_value_if_not_found: Value, } pub fn adapter_get(input: &str, adapted_resource: &str) -> Result { - debug!("Adapter Get with input: {input}"); let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) - .map_err(|e| RegistryResourceError::AdaptedResourceDeserializationError(e.to_string()))?; -// let mut result = Map::new(); + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + let mut result = Map::new(); + let mut resource_map = HashMap::new(); for (key, value) in adapted_resource.properties.iter() { let adapted_registry_value: AdaptedRegistryValue = serde_json::from_value(value.clone()) - .map_err(|e| RegistryResourceError::AdaptedResourceDeserializationError(e.to_string()))?; - debug!("Processing property: {key}"); - debug!("Key path: {}, Value name: {}, Value type: {}, Map JSON to Registry: {}", adapted_registry_value.key_path, adapted_registry_value.value_name, adapted_registry_value.value_type, adapted_registry_value.map_json_to_registry); - // let reg_helper = RegistryHelper::new(); + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + resource_map.insert(key.clone(), adapted_registry_value); } - Ok("{}".to_string()) + + let input_map: Map = serde_json::from_str(input) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + for (key, value) in input_map.iter() { + if let Some(adapted_registry_value) = resource_map.get(key) { + debug!("{}", t!("adapter.getProcessingKey", key = key)); + if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { + let registry_data = get_registry_value_data(&adapted_registry_value.value_name, value, json_map, &adapted_registry_value.value_type)?; + let registry_helper = RegistryHelper::new(&adapted_registry_value.key_path, Some(adapted_registry_value.value_name.clone()), Some(registry_data))?; + match registry_helper.get() { + Ok(registry) => { + if let Some(exist) = registry.exist { + if !exist { + let default_registry_data = convert_default_value_to_registry_data(&adapted_registry_value.default_value_if_not_found, &adapted_registry_value.value_type)?; + let json_value = convert_registry_value_data_to_mapped_json(&default_registry_data, &adapted_registry_value.json_type, json_map)?; + result.insert(key.clone(), json_value); + continue; + } + } + if let Some(registry_value) = registry.value_data { + let json_value = convert_registry_value_data_to_mapped_json(®istry_value, &adapted_registry_value.json_type, json_map)?; + result.insert(key.clone(), json_value); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.registryValueNotFound", key_path = adapted_registry_value.key_path, value_name = adapted_registry_value.value_name).to_string())); + } + }, + Err(e) => { + return Err(RegistryResourceError::RegistryError(e)); + } + } + } else { + warn!("No mapping found for key {}", key); + } + } else { + debug!("{}", t!("adapter.getNoAdaptedRegistryValueFound", key = key)); + } + } + + Ok(serde_json::to_string(&result).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?) +} + +fn convert_default_value_to_registry_data(default_value: &Value, reg_type: &RegistryDataType) -> Result { + match (default_value, reg_type) { + (Value::Bool(b), RegistryDataType::RegDword) => Ok(RegistryValueData::DWord(if *b { 1 } else { 0 })), + (Value::String(s), RegistryDataType::RegString) => Ok(RegistryValueData::String(s.clone())), + (Value::String(s), RegistryDataType::RegExpandString) => Ok(RegistryValueData::ExpandString(s.clone())), + (Value::Array(a), RegistryDataType::RegMultiString) => { + let mut result = Vec::new(); + for v in a { + if let Value::String(s) = v { + result.push(s.clone()); + } + } + Ok(RegistryValueData::MultiString(result)) + }, + (Value::Array(a), RegistryDataType::RegBinary) => { + let mut result = Vec::new(); + for v in a { + if let Value::Number(s) = v { + if let Some(u) = s.as_u64() { + result.push(u as u8); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())); + } + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())); + } + } + Ok(RegistryValueData::Binary(result)) + }, + _ => Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())), + } +} + +fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, json_type: &JsonType, map: &Map) -> Result { + // use the `map` to reverse convert the `RegistryValueData` back to the original JSON value based on the registry data type + // convert the registry value to the appropriate JSON type based on `json_type` + // for a boolean, a 0 is false and a 1 is true + // a reg_binary for json_type that is an array will be null delimited + let json_value = match (value_data, json_type) { + (RegistryValueData::Binary(byte_vec), JsonType::StringArray) => { + // use first value in map to get length of bytes to compare + let first_value = map.values().next(); + let first_value_length = first_value.and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); + let mut result = Vec::new(); + for slice in byte_vec.chunks(first_value_length) { + let matched_key = map.iter().find_map(|(k, v)| { + if let Ok(mapped_bytes) = serde_json::from_value::>(v.clone()) { + if mapped_bytes == slice { + return Some(k.clone()); + } + } else { + warn!("Failed to convert value to Vec: {:?}", v); + } + None + }); + if let Some(key) = matched_key { + result.push(Value::String(key)); + } else { + // convert slice to string as hex bytes + let hex_string = slice.iter().map(|b| format!("{:02X}", b)).collect::>().join(" "); + warn!("No mapping found for byte slice {hex_string}, skipping"); + } + } + Value::Array(result) + }, + (RegistryValueData::DWord(dword), JsonType::Boolean) => { + match dword { + 0 => Value::Bool(false), + 1 => Value::Bool(true), + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertRegistryValue", key_path = "unknown", value_name = "unknown", json_type = "boolean").to_string())), + } + }, + (RegistryValueData::DWord(dword), JsonType::String) => { + let mapped_value = map.iter().find_map(|(k, v)| { + if v.as_u64().map_or(false, |num| num == *dword as u64) { + Some(k.clone()) + } else { + None + } + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappingNotFound", key_path = "unknown", value_name = "unknown").to_string()))?; + Value::String(mapped_value) + }, + (RegistryValueData::String(s), JsonType::String) => { + let mapped_key = map.iter().find_map(|(k, v)| { + if v.as_str().map_or(false, |v_str| v_str == s) { + Some(k.clone()) + } else { + None + } + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappingNotFound", key_path = "unknown", value_name = "unknown").to_string()))?; + Value::String(mapped_key.to_string()) + }, + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedConversionToJsonType", registry_value_data = format!("{:?}", value_data), json_type = format!("{:?}", json_type)).to_string())), + }; + Ok(json_value) +} + +fn get_registry_value_data(value_name: &str, value: &Value, map: &Map, data_type: &RegistryDataType) -> Result { + let registry_value_data = if value.is_array() { + let value_array = value.as_array().unwrap(); + match data_type { + RegistryDataType::RegBinary => { + let mut byte_vec = Vec::new(); + for item in value_array.iter() { + if let Some(s) = item.as_str() { + let mapped_value = map.get(s).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in binary type", s)))?; + let byte_vec_item = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + byte_vec.extend(byte_vec_item); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", item)).to_string())); + } + } + RegistryValueData::Binary(byte_vec) + }, + RegistryDataType::RegMultiString => { + let mut string_vec = Vec::new(); + for item in value_array.iter() { + if let Some(s) = item.as_str() { + let mapped_value = map.get(s).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in multi string type", s)))?; + let string_vec_item = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a string", s)))?.to_string(); + string_vec.push(string_vec_item); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", item)).to_string())); + } + } + RegistryValueData::MultiString(string_vec) + }, + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", value)).to_string())), + } + } else { + let value_str = if value.is_string() { + value.as_str().unwrap().to_string() + } else if value.is_number() || value.is_boolean() { + value.to_string() + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", value)).to_string())); + }; + match data_type { + RegistryDataType::RegBinary => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in binary type", value_str)))?; + let byte_vec = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + RegistryValueData::Binary(byte_vec) + }, + RegistryDataType::RegDword => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in dword type", value_str)))?; + let dword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a u64", value_str)))? as u32; + RegistryValueData::DWord(dword) + }, + RegistryDataType::RegExpandString => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in expand string type", value_str)))?; + let expand_string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a string", value_str)))?.to_string(); + RegistryValueData::ExpandString(expand_string) + }, + RegistryDataType::RegMultiString => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in multi string type", value_str)))?; + let multi_string = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + RegistryValueData::MultiString(multi_string) + }, + RegistryDataType::RegQword => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in qword type", value_str)))?; + let qword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a u64", value_str)))?; + RegistryValueData::QWord(qword) + }, + RegistryDataType::RegString => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in string type", value_str)))?; + let string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a string", value_str)))?.to_string(); + RegistryValueData::String(string) + }, + } + }; + Ok(registry_value_data) } pub fn adapter_set(input: &str, adapted_resource: &str) -> Result { - debug!("Adapter Set with input: {input}"); + trace!("Adapter Set with input: {input}"); let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) - .map_err(|e| RegistryResourceError::AdaptedResourceDeserializationError(e.to_string()))?; + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; for (key, value) in adapted_resource.properties.iter() { - debug!("Property: {key} = {value}"); + trace!("Property: {key} = {value}"); } Ok("{}".to_string()) } + +pub fn adapter_export(input: &str, adapted_resource: &str) -> Result { + trace!("Adapter Export with input: {input}"); + + // if input is provided, use that to perform a `get` and return that result + // if no input is provided, then create an input that contains all keys in the adapted resource with empty values and perform a `get` to return the default values for all keys + let input: String = if input.is_empty() { + let adapted_resource_map: AdaptedRegistryResource = serde_json::from_str(adapted_resource) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + let mut map = Map::new(); + for key in adapted_resource_map.properties.keys() { + map.insert(key.clone(), Value::String(String::new())); + } + serde_json::to_string(&map).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))? + } else { + input.to_string() + }; + adapter_get(&input, adapted_resource) +} diff --git a/resources/registry/src/args.rs b/resources/registry/src/args.rs index dfd26ab62..7fef7be80 100644 --- a/resources/registry/src/args.rs +++ b/resources/registry/src/args.rs @@ -28,6 +28,13 @@ pub enum AdapterSubCommand { #[clap(short, long, required = true, help = t!("args.adapterArgsAdaptedResourceHelp").to_string())] adapted_resource: String, }, + #[clap(name = "export", about = t!("args.adapterExportAbout").to_string())] + Export { + #[clap(short, long, required = true, help = t!("args.adapterArgsInputHelp").to_string())] + input: String, + #[clap(short, long, required = true, help = t!("args.adapterArgsAdaptedResourceHelp").to_string())] + adapted_resource: String, + }, } #[derive(Debug, PartialEq, Eq, Subcommand)] diff --git a/resources/registry/src/error.rs b/resources/registry/src/error.rs index a3b7f3427..b2802453b 100644 --- a/resources/registry/src/error.rs +++ b/resources/registry/src/error.rs @@ -6,7 +6,7 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum RegistryResourceError { #[error("Adapted resource deserialization error: {0}")] - AdaptedResourceDeserializationError(String), + AdaptedResource(String), #[error("Registry error: {0}")] RegistryError(#[from] dsc_lib_registry::error::RegistryError), } diff --git a/resources/registry/src/main.rs b/resources/registry/src/main.rs index 515d8f695..9a756f50e 100644 --- a/resources/registry/src/main.rs +++ b/resources/registry/src/main.rs @@ -6,7 +6,7 @@ use crossterm::event; #[cfg(debug_assertions)] use std::env; -use adapter::{adapter_get, adapter_set}; +use adapter::{adapter_export, adapter_get, adapter_set}; use args::{AdapterSubCommand, Arguments, ConfigSubCommand, SubCommand}; use clap::Parser; use dsc_lib_registry::{config::Registry, RegistryHelper}; @@ -45,6 +45,9 @@ fn main() { AdapterSubCommand::Set { input, adapted_resource } => { adapter_set(&input, &adapted_resource) }, + AdapterSubCommand::Export { input, adapted_resource } => { + adapter_export(&input, &adapted_resource) + }, }; match result { Ok(output) => { diff --git a/resources/windows_personalization/personalization_get.tests.ps1 b/resources/windows_personalization/personalization_get.tests.ps1 index 4c67831f1..e211b432e 100644 --- a/resources/windows_personalization/personalization_get.tests.ps1 +++ b/resources/windows_personalization/personalization_get.tests.ps1 @@ -2,13 +2,43 @@ # Licensed under the MIT License. Describe 'Windows Personalization get tests' { - It 'Can get a personalization setting' -Skip:(!$IsWindows) { + It 'Convert dword to boolean' -Skip:(!$IsWindows) { $json = @{ - "appUseLightTheme" = $true + "appsUseLightTheme" = $true } | ConvertTo-Json -Compress $out = dsc resource get -r Microsoft.Windows/Personalization -i $json 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 -Because "dsc resource get failed with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" - Write-Verbose -Verbose "Output from dsc resource get: $($out | ConvertTo-Json -Compress)" - $out.appUseLightTheme | Should -Be $true + $out.actualState.appsUseLightTheme | Should -BeIn @($true, $false) + } + + It 'Convert binary to string array' -Skip:(!$IsWindows) { + $json = @{ + "startMenuVisiblePlaces" = @() + } | ConvertTo-Json -Compress + + $existingVisiblePlaces = (Get-ItemProperty -path HKCU:\Software\Microsoft\Windows\CurrentVersion\Start -Name VisiblePlaces).VisiblePlaces + + try { + $allVisiblePlaces = @(134, 8, 115, 82, 170, 81, 67, 66, 159, 123, 39, 118, 88, 70, 89, 212, 206, 213, 52, 45, 90, 250, 67, 69, 130, 242, + 34, 230, 234, 247, 119, 60, 47, 179, 103, 227, 222, 137, 85, 67, 191, 206, 97, 243, 123, 24, 169, 55, 32, 6, 11, 176, 81, 127, 50, 76, + 170, 30, 52, 204, 84, 127, 115, 21, 160, 7, 63, 56, 10, 232, 128, 76, 176, 90, 134, 219, 132, 93, 188, 77, 197, 165, 179, 66, 134, 125, + 244, 66, 128, 164, 147, 250, 202, 122, 136, 181, 68, 129, 117, 254, 13, 8, 174, 66, 139, 218, 52, 237, 151, 182, 99, 148, 74, 176, 189, + 116, 74, 249, 104, 79, 139, 214, 67, 152, 7, 29, 168, 188, 188, 36, 138, 20, 12, 214, 137, 66, 160, 128, 110, 217, 187, 162, 72, 130) + Set-ItemProperty -path HKCU:\Software\Microsoft\Windows\CurrentVersion\Start -Name VisiblePlaces -Value $allVisiblePlaces + $out = dsc resource get -r Microsoft.Windows/Personalization -i $json 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because "dsc resource get failed with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + $out.actualState.startMenuVisiblePlaces | Should -BeExactly @("Settings","Documents","Downloads","Music","Pictures","Videos","Network","UserProfile","Explorer") + } finally { + Set-ItemProperty -path HKCU:\Software\Microsoft\Windows\CurrentVersion\Start -Name VisiblePlaces -Value $existingVisiblePlaces + } + } + + It 'Convert dword to string enum' -Skip:(!$IsWindows) { + $json = @{ + "multimonitorTaskbarGroupingMode" = 'NeverCombine' + } | ConvertTo-Json -Compress + $out = dsc resource get -r Microsoft.Windows/Personalization -i $json 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because "dsc resource get failed with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + $out.actualState.multimonitorTaskbarGroupingMode | Should -BeIn @('NeverCombine', 'AlwaysCombineHideLabels', 'CombineWhenTaskbarIsFull') } } diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml index 86f053f7a..215584b95 100644 --- a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -12,38 +12,57 @@ description: Controls Windows personalization settings such as accent color, lig author: Microsoft Corporation requireAdapter: Microsoft.Windows.Adapter/Registry content: - appUsesLightTheme: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + appsUseLightTheme: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize valueType: REG_DWORD - valueName: AppUseLightTheme + valueName: AppsUseLightTheme + jsonType: boolean + defaultValueIfNotFound: 0 mapJsonToRegistry: 'false': 0 'true': 1 systemUsesLightTheme: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize valueType: REG_DWORD valueName: SystemUsesLightTheme + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + autoColorization: + keyPath: HKCU\Control Panel\Desktop + valueType: REG_DWORD + valueName: AutoColorization + jsonType: boolean + defaultValueIfNotFound: 0 mapJsonToRegistry: 'false': 0 'true': 1 colorPrevalence: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize valueType: REG_DWORD valueName: ColorPrevalence + jsonType: boolean + defaultValueIfNotFound: 0 mapJsonToRegistry: 'false': 0 'true': 1 transparencyEffects: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize valueType: REG_DWORD valueName: EnableTransparency + jsonType: boolean + defaultValueIfNotFound: 0 mapJsonToRegistry: 'false': 0 'true': 1 startMenuVisiblePlaces: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Start + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Start valueType: REG_BINARY valueName: VisiblePlaces + jsonType: stringArray + defaultValueIfNotFound: [] mapJsonToRegistry: # Documents: {2D34D5CE-FA5A-4543-82F2-22E6EAF7773C} Documents: [0xCE, 0xD5, 0x34, 0x2D, 0x5A, 0xFA, 0x43, 0x45, 0x82, 0xF2, 0x22, 0xE6, 0xEA, 0xF7, 0x77, 0x3C] @@ -62,57 +81,102 @@ content: # Explorer: {148A24BC-D60C-4289-A080-6ED9BBA24882} Explorer: [0xBC, 0x24, 0x8A, 0x14, 0x0C, 0xD6, 0x89, 0x42, 0xA0, 0x80, 0x6E, 0xD9, 0xBB, 0xA2, 0x48, 0x82] # Settings: {52730886-51AA-4243-9F7B-2776584659D4} - Settings: [0x86, 0x88, 0x73, 0x52, 0xAA, 0x51, 0x43, 0x42, 0x9F, 0x7B, 0x27, 0x76, 0x58, 0x46, 0x59, 0xD4] + Settings: [0x86, 0x08, 0x73, 0x52, 0xAA, 0x51, 0x43, 0x42, 0x9F, 0x7B, 0x27, 0x76, 0x58, 0x46, 0x59, 0xD4] startMenuShowRecentList: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Start - valueType: REG_BOOL + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Start + valueType: REG_DWORD valueName: ShowRecentList + jsonType: boolean + defaultValueIfNotFound: 0 mapJsonToRegistry: - 'false': false - 'true': true + 'false': 0 + 'true': 1 showRecommendedApps: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced - valueType: REG_BOOL + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced + valueType: REG_DWORD valueName: Start_TrackDocs + jsonType: boolean + defaultValueIfNotFound: 0 mapJsonToRegistry: - 'false': false - 'true': true + 'false': 0 + 'true': 1 taskbarShowBadges: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges valueType: REG_SZ valueName: SystemSettings_Taskbar_Badging + jsonType: boolean + defaultValueIfNotFound: '0' mapJsonToRegistry: 'false': '0' 'true': '1' desktopTaskbarShowBadges: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges valueType: REG_SZ valueName: SystemSettings_DesktopTaskbar_Badging + jsonType: boolean + defaultValueIfNotFound: '0' mapJsonToRegistry: 'false': '0' 'true': '1' multimonitorTaskbarGroupingMode: - keyPath: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarGlomLevel + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarGlomLevel valueType: REG_SZ valueName: SystemSettings_DesktopTaskbar_GroupingMode + jsonType: string + defaultValueIfNotFound: '0' mapJsonToRegistry: - # Always combine, hide labels: 0 AlwaysCombineHideLabels: '0' - # Combine when taskbar is full: 1 CombineWhenTaskbarIsFull: '1' - # Never combine: 2 NeverCombine: '2' + multimonitorTarkbar: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarEnabled + valueType: REG_SZ + valueName: SystemSettings_Taskbar_MultiMon + jsonType: boolean + defaultValueIfNotFound: '0' + mapJsonToRegistry: + 'false': '0' + 'true': '1' + multimonitorDesktopTaskbar: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarEnabled + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_MultiMon + jsonType: boolean + defaultValueIfNotFound: '0' + mapJsonToRegistry: + 'false': '0' + 'true': '1' + multimonitorTaskbarMode: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarMode + valueType: REG_SZ + valueName: SystemSettings_Taskbar_MultiMonTaskbarMode + jsonType: string + defaultValueIfNotFound: '0' + mapJsonToRegistry: + Duplicate: '0' + PrimaryAndMonitorWindowIsOn: '1' + MonitorWindowIsOn: '2' + multimonitorDesktopTaskbarMode: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarMode + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_MultiMonTaskbarMode + jsonType: string + defaultValueIfNotFound: '0' + mapJsonToRegistry: + Duplicate: '0' + PrimaryAndMonitorWindowIsOn: '1' + MonitorWindowIsOn: '2' schema: embedded: $schema: http://json-schema.org/draft-07/schema# $id: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Adapted/Three/v0.1.0/schema.json title: Microsoft.Windows/Personalization - description: Windows personalization settings such as accent color, light/dark mode, and transparency effects. + description: Windows personalization settings such as accent color, light/dark mode, and transparency effects as described in https://learn.microsoft.com/windows/apps/develop/settings/settings-common type: object required: [] additionalProperties: false properties: - appUsesLightTheme: + appsUseLightTheme: type: boolean title: App Uses Light Theme description: Indicates whether the app uses the light theme. @@ -120,6 +184,10 @@ schema: type: boolean title: System Uses Light Theme description: Indicates whether the system uses the light theme. + autoColorization: + type: boolean + title: Auto Colorization + description: Signifies auto-apply accent color based on background or manually. colorPrevalence: type: boolean title: Color Prevalence @@ -168,4 +236,27 @@ schema: - AlwaysCombineHideLabels - CombineWhenTaskbarIsFull - NeverCombine - \ No newline at end of file + multimonitorTarkbar: + type: boolean + title: Multimonitor Taskbar + description: Enables showing the taskbar on multiple displays. + multimonitorDesktopTaskbar: + type: boolean + title: Multimonitor Desktop Taskbar + description: Enables showing the desktop taskbar on multiple displays. + multimonitorTaskbarMode: + type: string + title: Multimonitor Taskbar Mode + description: Specifies the mode of the taskbar on multiple displays. + enum: + - Duplicate + - PrimaryAndMonitorWindowIsOn + - MonitorWindowIsOn + multimonitorDesktopTaskbarMode: + type: string + title: Multimonitor Desktop Taskbar Mode + description: Specifies the mode of the desktop taskbar on multiple displays. + enum: + - Duplicate + - PrimaryAndMonitorWindowIsOn + - MonitorWindowIsOn From bad2fa762a743f8b70fb31d1a8067cb3de9148d7 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 14:53:02 -0700 Subject: [PATCH 08/16] Fix export to work add tests --- resources/registry/locales/en-us.toml | 5 +- resources/registry/src/adapter.rs | 60 ++++++++++++++----- .../tests/adaptedRegistry.get.tests.ps1 | 0 .../personalization_export.tests.ps1 | 31 ++++++++++ ...s_personalization.dsc.adaptedResource.yaml | 2 +- 5 files changed, 79 insertions(+), 19 deletions(-) delete mode 100644 resources/registry/tests/adaptedRegistry.get.tests.ps1 create mode 100644 resources/windows_personalization/personalization_export.tests.ps1 diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index 5bf3e7dc7..7cd377852 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -4,11 +4,12 @@ _version = 1 getProcessingKey = "Processing key: %{key}" getNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key}" registryValueNotFound = "Registry value not found for key path: %{key_path} and value name: %{value_name}" -couldNotConvertRegistryValue = "Could not convert registry value for key path: %{key_path} and value name: %{value_name} to JSON type: %{json_type}" +couldNotConvertRegistryValue = "Could not convert registry value for key path '%{key_path}' and value name '%{value_name}' to JSON type: %{json_type}" unsupportedConversionToJsonType = "Unsupported conversion from %{registry_value_data} to JSON type: %{json_type}" mappingNotFound = "No mapping found for registry key path: %{key_path} and value name: %{value_name}" unsupportedValueType = "For value name %{value_name}, unsupported value type: %{value}" -couldNotConvertDefaultValue = "Could not convert default value: %{default_value} to registry data type: %{reg_type}" +couldNotConvertDefaultValue = "Could not convert default value '%{default_value}' to registry data type: %{reg_type}" +valueMappingNotFound = "No mapping found for value '%{value}' of type %{reg_type}" [args] about = "Manage state of Windows registry" diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index a9145aa0f..f4261220d 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -21,12 +21,12 @@ struct AdaptedRegistryResource { enum RegistryDataType { RegBinary, RegDword, - #[serde(rename = "REG_SZ")] - RegString, #[serde(rename = "REG_EXPAND_SZ")] RegExpandString, #[serde(rename = "REG_MULTI_SZ")] RegMultiString, + #[serde(rename = "REG_SZ")] + RegString, RegQword, } @@ -107,6 +107,13 @@ pub fn adapter_get(input: &str, adapted_resource: &str) -> Result Result { match (default_value, reg_type) { (Value::Bool(b), RegistryDataType::RegDword) => Ok(RegistryValueData::DWord(if *b { 1 } else { 0 })), + (Value::Number(n), RegistryDataType::RegDword) => { + if let Some(u) = n.as_u64() { + Ok(RegistryValueData::DWord(u as u32)) + } else { + Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())) + } + }, (Value::String(s), RegistryDataType::RegString) => Ok(RegistryValueData::String(s.clone())), (Value::String(s), RegistryDataType::RegExpandString) => Ok(RegistryValueData::ExpandString(s.clone())), (Value::Array(a), RegistryDataType::RegMultiString) => { @@ -173,7 +180,7 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js match dword { 0 => Value::Bool(false), 1 => Value::Bool(true), - _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertRegistryValue", key_path = "unknown", value_name = "unknown", json_type = "boolean").to_string())), + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = dword.to_string(), reg_type = "RegDword").to_string())), } }, (RegistryValueData::DWord(dword), JsonType::String) => { @@ -183,7 +190,7 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js } else { None } - }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappingNotFound", key_path = "unknown", value_name = "unknown").to_string()))?; + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = dword.to_string(), reg_type = "RegDword").to_string()))?; Value::String(mapped_value) }, (RegistryValueData::String(s), JsonType::String) => { @@ -193,9 +200,23 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js } else { None } - }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappingNotFound", key_path = "unknown", value_name = "unknown").to_string()))?; + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegString").to_string()))?; Value::String(mapped_key.to_string()) }, + (RegistryValueData::String(s), JsonType::Boolean) => { + let mapped_value = map.iter().find_map(|(k, v)| { + if v.as_str().map_or(false, |v_str| v_str == s) { + Some(k.clone()) + } else { + None + } + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegString").to_string()))?; + match mapped_value.as_str() { + "true" => Value::Bool(true), + "false" => Value::Bool(false), + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegString").to_string())), + } + }, _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedConversionToJsonType", registry_value_data = format!("{:?}", value_data), json_type = format!("{:?}", json_type)).to_string())), }; Ok(json_value) @@ -209,7 +230,7 @@ fn get_registry_value_data(value_name: &str, value: &Value, map: &Map>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; byte_vec.extend(byte_vec_item); } else { @@ -222,8 +243,8 @@ fn get_registry_value_data(value_name: &str, value: &Value, map: &Map { - let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in binary type", value_str)))?; + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegBinary").to_string()))?; let byte_vec = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; RegistryValueData::Binary(byte_vec) }, RegistryDataType::RegDword => { - let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in dword type", value_str)))?; + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegDword").to_string()))?; let dword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a u64", value_str)))? as u32; RegistryValueData::DWord(dword) }, RegistryDataType::RegExpandString => { - let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in expand string type", value_str)))?; + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegExpandString").to_string()))?; let expand_string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a string", value_str)))?.to_string(); RegistryValueData::ExpandString(expand_string) }, RegistryDataType::RegMultiString => { - let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in multi string type", value_str)))?; + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegMultiString").to_string()))?; let multi_string = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; RegistryValueData::MultiString(multi_string) }, RegistryDataType::RegQword => { - let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in qword type", value_str)))?; + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegQword").to_string()))?; let qword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a u64", value_str)))?; RegistryValueData::QWord(qword) }, RegistryDataType::RegString => { - let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(format!("No mapping found for value {} in string type", value_str)))?; + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegString").to_string()))?; let string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a string", value_str)))?.to_string(); RegistryValueData::String(string) }, @@ -292,13 +313,20 @@ pub fn adapter_export(input: &str, adapted_resource: &str) -> Result $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $resultProperties = $out.resources[0].properties.psobject.properties + ($resultProperties | Measure-Object).Count | Should -Be $properties.Count + foreach ($key in $properties.Keys) { + $resultProperties[$key].Value | Should -BeIn $properties[$key] -Because "Property $key has value $($resultProperties[$key].Value) which is not in expected values $($properties[$key])" + } + } +} diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml index 215584b95..425ab6de2 100644 --- a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -128,7 +128,7 @@ content: AlwaysCombineHideLabels: '0' CombineWhenTaskbarIsFull: '1' NeverCombine: '2' - multimonitorTarkbar: + multimonitorTaskbar: keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarEnabled valueType: REG_SZ valueName: SystemSettings_Taskbar_MultiMon From a3282e049620f42e826ce445f15c19e85a595747 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 16:26:06 -0700 Subject: [PATCH 09/16] add support for `set` --- resources/registry/locales/en-us.toml | 2 + resources/registry/src/adapter.rs | 32 ++++++++++--- resources/registry/src/main.rs | 6 ++- .../personalization_set.tests.ps1 | 45 +++++++++++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 resources/windows_personalization/personalization_set.tests.ps1 diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index 7cd377852..3c7dcd285 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -3,6 +3,8 @@ _version = 1 [adapter] getProcessingKey = "Processing key: %{key}" getNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key}" +setProcessingKey = "Setting key: %{key}" +setNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key}" registryValueNotFound = "Registry value not found for key path: %{key_path} and value name: %{value_name}" couldNotConvertRegistryValue = "Could not convert registry value for key path '%{key_path}' and value name '%{value_name}' to JSON type: %{json_type}" unsupportedConversionToJsonType = "Unsupported conversion from %{registry_value_data} to JSON type: %{json_type}" diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index f4261220d..23001ccb5 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -298,15 +298,37 @@ fn get_registry_value_data(value_name: &str, value: &Value, map: &Map Result { - trace!("Adapter Set with input: {input}"); +pub fn adapter_set(input: &str, adapted_resource: &str) -> Result<(), RegistryResourceError> { let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; - + let mut resource_map = HashMap::new(); + for (key, value) in adapted_resource.properties.iter() { - trace!("Property: {key} = {value}"); + let adapted_registry_value: AdaptedRegistryValue = serde_json::from_value(value.clone()) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + resource_map.insert(key.clone(), adapted_registry_value); } - Ok("{}".to_string()) + + let input_map: Map = serde_json::from_str(input) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + for (key, value) in input_map.iter() { + if let Some(adapted_registry_value) = resource_map.get(key) { + debug!("{}", t!("adapter.setProcessingKey", key = key)); + if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { + let registry_data = get_registry_value_data(&adapted_registry_value.value_name, value, json_map, &adapted_registry_value.value_type)?; + let registry_helper = RegistryHelper::new(&adapted_registry_value.key_path, Some(adapted_registry_value.value_name.clone()), Some(registry_data))?; + if let Err(e) = registry_helper.set() { + return Err(RegistryResourceError::RegistryError(e)); + } + } else { + warn!("No mapping found for key {}", key); + } + } else { + debug!("{}", t!("adapter.setNoAdaptedRegistryValueFound", key = key)); + } + } + + Ok(()) } pub fn adapter_export(input: &str, adapted_resource: &str) -> Result { diff --git a/resources/registry/src/main.rs b/resources/registry/src/main.rs index 9a756f50e..2f9ebff17 100644 --- a/resources/registry/src/main.rs +++ b/resources/registry/src/main.rs @@ -43,7 +43,11 @@ fn main() { adapter_get(&input, &adapted_resource) }, AdapterSubCommand::Set { input, adapted_resource } => { - adapter_set(&input, &adapted_resource) + if let Err(e) = adapter_set(&input, &adapted_resource) { + error!("{e}"); + exit(EXIT_REGISTRY_ERROR); + } + exit(EXIT_SUCCESS); }, AdapterSubCommand::Export { input, adapted_resource } => { adapter_export(&input, &adapted_resource) diff --git a/resources/windows_personalization/personalization_set.tests.ps1 b/resources/windows_personalization/personalization_set.tests.ps1 new file mode 100644 index 000000000..643cba6ff --- /dev/null +++ b/resources/windows_personalization/personalization_set.tests.ps1 @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Personalization resource set tests' -Skip:(!$IsWindows) { + BeforeAll { + $currentSettings = dsc resource export -r Microsoft.Windows/Personalization 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because "Failed to export current personalization settings with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + } + + AfterAll { + dsc resource set -r Microsoft.Windows/Personalization -i $currentSettings 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + } + + It 'Can set personalization settings' { + $newSettings = @{ + appsUseLightTheme = $true + systemUsesLightTheme = $true + autoColorization = $true + colorPrevalence = $true + transparencyEffects = $true + startMenuVisiblePlaces = @('Documents', 'Downloads','Music', 'Pictures', 'Videos', 'Network', 'UserProfile', 'Explorer', 'Settings') + startMenuShowRecentList = $true + showRecommendedApps = $true + taskbarShowBadges = $true + desktopTaskbarShowBadges = $true + multimonitorTaskbarGroupingMode = 'CombineWhenTaskbarIsFull' + multimonitorTaskbar = $true + multimonitorDesktopTaskbar = $true + multimonitorTaskbarMode = 'PrimaryAndMonitorWindowIsOn' + multimonitorDesktopTaskbarMode = 'MonitorWindowIsOn' + } + + dsc resource set -r Microsoft.Windows/Personalization -i ($newSettings | ConvertTo-Json) 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + + $out = dsc resource export -r Microsoft.Windows/Personalization 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + foreach ($key in $newSettings.Keys) { + $keyValue = $out.resources[0].properties.$key + $expectedValue = $newSettings.$key + $keyValue | Should -Be $expectedValue -Because "Property $key has value $keyValue which is not equal to expected value $expectedValue" + } + } +} From e604361f8f6fc20319ba355a0764dcabd792c3c0 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 16:51:52 -0700 Subject: [PATCH 10/16] clippy and loc fixes --- .../src/dscresources/command_resource.rs | 18 +-- resources/registry/locales/en-us.toml | 4 + resources/registry/src/adapter.rs | 148 +++++++++--------- 3 files changed, 86 insertions(+), 84 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index a9a9c598a..41e2d158b 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -50,7 +50,7 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option< None => resource }; validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?; - let args = process_get_args(get.args.as_ref(), filter, &command_resource); + let args = process_get_args(get.args.as_ref(), filter, command_resource); if !filter.is_empty() { verify_json_from_manifest(resource, filter, target_resource)?; command_input = get_command_input(get.input.as_ref(), filter)?; @@ -116,7 +116,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let (_, supports_whatif) = process_set_delete_args( set.args.as_ref(), "", - &command_resource, + command_resource, execution_type ); supports_whatif @@ -182,7 +182,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut None => resource, }; validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?; - let args = process_get_args(get.args.as_ref(), desired, &command_resource); + let args = process_get_args(get.args.as_ref(), desired, command_resource); let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &command_resource.type_name, executable = &get.executable)); @@ -214,7 +214,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &command_resource, execution_type); + let (args, _) = process_set_delete_args(set.args.as_ref(), desired, command_resource, execution_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -337,7 +337,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti None => resource, }; validate_security_context(&test.require_security_context, &command_resource.type_name, "test")?; - let args = process_get_args(test.args.as_ref(), expected, &command_resource); + let args = process_get_args(test.args.as_ref(), expected, command_resource); let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &command_resource.type_name, executable = &test.executable)); @@ -489,7 +489,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti None => resource, }; validate_security_context(&delete.require_security_context, &command_resource.type_name, "delete")?; - let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, &command_resource, execution_type); + let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, command_resource, execution_type); if execution_type == &ExecutionKind::WhatIf && !supports_whatif { // perform a synthetic what-if by calling test and wrapping the TestResult in DeleteResultKind::SyntheticWhatIf let test_result = invoke_test(resource, filter, target_resource)?; @@ -537,7 +537,7 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op Some(target) => target, None => resource }; - let args = process_get_args(validate.args.as_ref(), config, &command_resource); + let args = process_get_args(validate.args.as_ref(), config, command_resource); let command_input = get_command_input(validate.input.as_ref(), config)?; info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = &command_resource.type_name, executable = &validate.executable)); @@ -643,9 +643,9 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_get_args(export.args.as_ref(), input, &command_resource); + args = process_get_args(export.args.as_ref(), input, command_resource); } else { - args = process_get_args(export.args.as_ref(), "", &command_resource); + args = process_get_args(export.args.as_ref(), "", command_resource); } let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index 3c7dcd285..02b60fed3 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -12,6 +12,10 @@ mappingNotFound = "No mapping found for registry key path: %{key_path} and value unsupportedValueType = "For value name %{value_name}, unsupported value type: %{value}" couldNotConvertDefaultValue = "Could not convert default value '%{default_value}' to registry data type: %{reg_type}" valueMappingNotFound = "No mapping found for value '%{value}' of type %{reg_type}" +mappedValueNotU64 = "Mapped value for '%{value}' is not a u64" +mappedValueNotString = "Mapped value for '%{value}' is not a string" +unmappedByteSlice = "No mapping found for byte slice: %{hex_string}" +nonStringInMultiStringDefault = "Non-string value found in default value array for REG_MULTI_SZ: %{value}" [args] about = "Manage state of Windows registry" diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index 23001ccb5..3ceb40c2a 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -17,17 +17,19 @@ struct AdaptedRegistryResource { } #[derive(Debug, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] enum RegistryDataType { - RegBinary, - RegDword, + #[serde(rename = "REG_BINARY")] + Binary, + #[serde(rename = "REG_DWORD")] + Dword, #[serde(rename = "REG_EXPAND_SZ")] - RegExpandString, + ExpandString, #[serde(rename = "REG_MULTI_SZ")] - RegMultiString, + MultiString, #[serde(rename = "REG_SZ")] - RegString, - RegQword, + String, + #[serde(rename = "REG_QWORD")] + Qword, } #[derive(Deserialize, Debug, Clone)] @@ -52,17 +54,21 @@ struct AdaptedRegistryValue { default_value_if_not_found: Value, } -pub fn adapter_get(input: &str, adapted_resource: &str) -> Result { - let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) +fn build_resource_map(adapted_resource: &str) -> Result, RegistryResourceError> { + let adapted: AdaptedRegistryResource = serde_json::from_str(adapted_resource) .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; - let mut result = Map::new(); - let mut resource_map = HashMap::new(); + adapted.properties.into_iter() + .map(|(k, v)| { + let arv: AdaptedRegistryValue = serde_json::from_value(v) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + Ok((k, arv)) + }) + .collect() +} - for (key, value) in adapted_resource.properties.iter() { - let adapted_registry_value: AdaptedRegistryValue = serde_json::from_value(value.clone()) - .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; - resource_map.insert(key.clone(), adapted_registry_value); - } +pub fn adapter_get(input: &str, adapted_resource: &str) -> Result { + let resource_map = build_resource_map(adapted_resource)?; + let mut result = Map::new(); let input_map: Map = serde_json::from_str(input) .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; @@ -74,13 +80,11 @@ pub fn adapter_get(input: &str, adapted_resource: &str) -> Result { - if let Some(exist) = registry.exist { - if !exist { - let default_registry_data = convert_default_value_to_registry_data(&adapted_registry_value.default_value_if_not_found, &adapted_registry_value.value_type)?; - let json_value = convert_registry_value_data_to_mapped_json(&default_registry_data, &adapted_registry_value.json_type, json_map)?; - result.insert(key.clone(), json_value); - continue; - } + if let Some(exist) = registry.exist && !exist { + let default_registry_data = convert_default_value_to_registry_data(&adapted_registry_value.default_value_if_not_found, &adapted_registry_value.value_type)?; + let json_value = convert_registry_value_data_to_mapped_json(&default_registry_data, &adapted_registry_value.json_type, json_map)?; + result.insert(key.clone(), json_value); + continue; } if let Some(registry_value) = registry.value_data { let json_value = convert_registry_value_data_to_mapped_json(®istry_value, &adapted_registry_value.json_type, json_map)?; @@ -101,31 +105,33 @@ pub fn adapter_get(input: &str, adapted_resource: &str) -> Result Result { match (default_value, reg_type) { - (Value::Bool(b), RegistryDataType::RegDword) => Ok(RegistryValueData::DWord(if *b { 1 } else { 0 })), - (Value::Number(n), RegistryDataType::RegDword) => { + (Value::Bool(b), RegistryDataType::Dword) => Ok(RegistryValueData::DWord(if *b { 1 } else { 0 })), + (Value::Number(n), RegistryDataType::Dword) => { if let Some(u) = n.as_u64() { Ok(RegistryValueData::DWord(u as u32)) } else { Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())) } }, - (Value::String(s), RegistryDataType::RegString) => Ok(RegistryValueData::String(s.clone())), - (Value::String(s), RegistryDataType::RegExpandString) => Ok(RegistryValueData::ExpandString(s.clone())), - (Value::Array(a), RegistryDataType::RegMultiString) => { + (Value::String(s), RegistryDataType::String) => Ok(RegistryValueData::String(s.clone())), + (Value::String(s), RegistryDataType::ExpandString) => Ok(RegistryValueData::ExpandString(s.clone())), + (Value::Array(a), RegistryDataType::MultiString) => { let mut result = Vec::new(); for v in a { if let Value::String(s) = v { result.push(s.clone()); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.nonStringInMultiStringDefault", value = v.to_string()).to_string())); } } Ok(RegistryValueData::MultiString(result)) }, - (Value::Array(a), RegistryDataType::RegBinary) => { + (Value::Array(a), RegistryDataType::Binary) => { let mut result = Vec::new(); for v in a { if let Value::Number(s) = v { @@ -151,27 +157,31 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js // a reg_binary for json_type that is an array will be null delimited let json_value = match (value_data, json_type) { (RegistryValueData::Binary(byte_vec), JsonType::StringArray) => { - // use first value in map to get length of bytes to compare - let first_value = map.values().next(); - let first_value_length = first_value.and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); + // Pre-build reverse lookup: deserialized bytes -> key name + let reverse_map: Vec<(String, Vec)> = map.iter().filter_map(|(k, v)| { + match serde_json::from_value::>(v.clone()) { + Ok(bytes) => Some((k.clone(), bytes)), + Err(_) => { + warn!("Failed to convert value to Vec: {:?}", v); + None + } + } + }).collect(); + let first_value_length = reverse_map.first().map(|(_, b)| b.len()).unwrap_or(0); let mut result = Vec::new(); for slice in byte_vec.chunks(first_value_length) { - let matched_key = map.iter().find_map(|(k, v)| { - if let Ok(mapped_bytes) = serde_json::from_value::>(v.clone()) { - if mapped_bytes == slice { - return Some(k.clone()); - } + let matched_key = reverse_map.iter().find_map(|(k, bytes)| { + if bytes == slice { + Some(k.clone()) } else { - warn!("Failed to convert value to Vec: {:?}", v); + None } - None }); if let Some(key) = matched_key { result.push(Value::String(key)); } else { - // convert slice to string as hex bytes let hex_string = slice.iter().map(|b| format!("{:02X}", b)).collect::>().join(" "); - warn!("No mapping found for byte slice {hex_string}, skipping"); + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unmappedByteSlice", hex_string = hex_string).to_string())); } } Value::Array(result) @@ -185,7 +195,7 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js }, (RegistryValueData::DWord(dword), JsonType::String) => { let mapped_value = map.iter().find_map(|(k, v)| { - if v.as_u64().map_or(false, |num| num == *dword as u64) { + if v.as_u64() == Some(*dword as u64) { Some(k.clone()) } else { None @@ -195,7 +205,7 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js }, (RegistryValueData::String(s), JsonType::String) => { let mapped_key = map.iter().find_map(|(k, v)| { - if v.as_str().map_or(false, |v_str| v_str == s) { + if v.as_str().is_some_and(|v_str| v_str == s) { Some(k.clone()) } else { None @@ -205,7 +215,7 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js }, (RegistryValueData::String(s), JsonType::Boolean) => { let mapped_value = map.iter().find_map(|(k, v)| { - if v.as_str().map_or(false, |v_str| v_str == s) { + if v.as_str().is_some_and(|v_str| v_str == s) { Some(k.clone()) } else { None @@ -223,10 +233,9 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js } fn get_registry_value_data(value_name: &str, value: &Value, map: &Map, data_type: &RegistryDataType) -> Result { - let registry_value_data = if value.is_array() { - let value_array = value.as_array().unwrap(); + let registry_value_data = if let Some(value_array) = value.as_array() { match data_type { - RegistryDataType::RegBinary => { + RegistryDataType::Binary => { let mut byte_vec = Vec::new(); for item in value_array.iter() { if let Some(s) = item.as_str() { @@ -239,7 +248,7 @@ fn get_registry_value_data(value_name: &str, value: &Value, map: &Map { + RegistryDataType::MultiString => { let mut string_vec = Vec::new(); for item in value_array.iter() { if let Some(s) = item.as_str() { @@ -255,42 +264,42 @@ fn get_registry_value_data(value_name: &str, value: &Value, map: &Map return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", value)).to_string())), } } else { - let value_str = if value.is_string() { - value.as_str().unwrap().to_string() + let value_str = if let Some(s) = value.as_str() { + s.to_string() } else if value.is_number() || value.is_boolean() { value.to_string() } else { return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", value)).to_string())); }; match data_type { - RegistryDataType::RegBinary => { + RegistryDataType::Binary => { let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegBinary").to_string()))?; let byte_vec = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; RegistryValueData::Binary(byte_vec) }, - RegistryDataType::RegDword => { + RegistryDataType::Dword => { let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegDword").to_string()))?; - let dword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a u64", value_str)))? as u32; + let dword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotU64", value = value_str).to_string()))? as u32; RegistryValueData::DWord(dword) }, - RegistryDataType::RegExpandString => { + RegistryDataType::ExpandString => { let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegExpandString").to_string()))?; - let expand_string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a string", value_str)))?.to_string(); + let expand_string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotString", value = value_str).to_string()))?.to_string(); RegistryValueData::ExpandString(expand_string) }, - RegistryDataType::RegMultiString => { + RegistryDataType::MultiString => { let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegMultiString").to_string()))?; let multi_string = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; RegistryValueData::MultiString(multi_string) }, - RegistryDataType::RegQword => { + RegistryDataType::Qword => { let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegQword").to_string()))?; - let qword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a u64", value_str)))?; + let qword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotU64", value = value_str).to_string()))?; RegistryValueData::QWord(qword) }, - RegistryDataType::RegString => { + RegistryDataType::String => { let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegString").to_string()))?; - let string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(format!("Mapped value for {} is not a string", value_str)))?.to_string(); + let string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotString", value = value_str).to_string()))?.to_string(); RegistryValueData::String(string) }, } @@ -299,15 +308,7 @@ fn get_registry_value_data(value_name: &str, value: &Value, map: &Map Result<(), RegistryResourceError> { - let adapted_resource: AdaptedRegistryResource = serde_json::from_str(adapted_resource) - .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; - let mut resource_map = HashMap::new(); - - for (key, value) in adapted_resource.properties.iter() { - let adapted_registry_value: AdaptedRegistryValue = serde_json::from_value(value.clone()) - .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; - resource_map.insert(key.clone(), adapted_registry_value); - } + let resource_map = build_resource_map(adapted_resource)?; let input_map: Map = serde_json::from_str(input) .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; @@ -337,12 +338,9 @@ pub fn adapter_export(input: &str, adapted_resource: &str) -> Result Date: Fri, 29 May 2026 17:47:20 -0700 Subject: [PATCH 11/16] fix copilot feedback --- lib/dsc-lib/locales/en-us.toml | 1 + .../src/dscresources/command_resource.rs | 11 ++- resources/registry/locales/en-us.toml | 2 + resources/registry/src/adapter.rs | 99 ++++++++++--------- .../.project.data.json | 2 +- .../personalization_set.tests.ps1 | 2 +- ...s_personalization.dsc.adaptedResource.yaml | 2 +- 7 files changed, 65 insertions(+), 54 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index e00a545c3..90ed80f09 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -193,6 +193,7 @@ runProcessError = "Failed to run process '%{executable}': %{error}" whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. See https://github.com/PowerShell/DSC/issues/1361 for migration information." securityContextRequired = "Operation '%{operation}' for resource '%{resource}' requires security context '%{context}'" noAdaptedContent = "No adapted content available for resource '%{resource}'" +invalidAdaptedContent = "Invalid adapted content for resource '%{resource}': %{error}" [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 41e2d158b..109595432 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -915,10 +915,16 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, resource: & processed_args.push(s.clone()); }, GetArgKind::AdaptedContent { adapted_content_arg } => { - // adapted content is the JSON content with secrets redacted and additional properties added by the adapter; it is only used for get operations and is meant to be used when the command needs to call other commands as part of its execution and wants to pass along the adapted content to avoid multiple rounds of redaction processed_args.push(adapted_content_arg.clone()); if let Some(adapted_content) = &resource.adapted_content { - processed_args.push(serde_json::to_string(&adapted_content).unwrap()); + match serde_json::to_string(&adapted_content) { + Err(e) => { + warn!("{}", t!("dscresources.commandResource.invalidAdaptedContent", resource = &resource.type_name, error = e)); + } + Ok(s) => { + processed_args.push(s); + } + } } else { debug!("{}", t!("dscresources.commandResource.noAdaptedContent", resource = &resource.type_name)); } @@ -995,7 +1001,6 @@ fn process_set_delete_args(args: Option<&Vec>, input: &str, re processed_args.push(s.clone()); }, SetDeleteArgKind::AdaptedContent { adapted_content_arg } => { - // adapted content is the JSON content with secrets redacted and additional properties added by the adapter; it is only used for get operations and is meant to be used when the command needs to call other commands as part of its execution and wants to pass along the adapted content to avoid multiple rounds of redaction processed_args.push(adapted_content_arg.clone()); if let Some(adapted_content) = &resource.adapted_content { processed_args.push(serde_json::to_string(&adapted_content).unwrap()); diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index 02b60fed3..ae72bb5fc 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -16,6 +16,8 @@ mappedValueNotU64 = "Mapped value for '%{value}' is not a u64" mappedValueNotString = "Mapped value for '%{value}' is not a string" unmappedByteSlice = "No mapping found for byte slice: %{hex_string}" nonStringInMultiStringDefault = "Non-string value found in default value array for REG_MULTI_SZ: %{value}" +emptyReverseMap = "The reverse map is empty" +mapJsonToRegistryNotFound = "No mapping found for key: %{key}" [args] about = "Manage state of Windows registry" diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs index 3ceb40c2a..cbef5c582 100644 --- a/resources/registry/src/adapter.rs +++ b/resources/registry/src/adapter.rs @@ -108,6 +108,54 @@ pub fn adapter_get(input: &str, adapted_resource: &str) -> Result Result<(), RegistryResourceError> { + let resource_map = build_resource_map(adapted_resource)?; + + let input_map: Map = serde_json::from_str(input) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + for (key, value) in input_map.iter() { + if let Some(adapted_registry_value) = resource_map.get(key) { + debug!("{}", t!("adapter.setProcessingKey", key = key)); + if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { + let registry_data = get_registry_value_data(&adapted_registry_value.value_name, value, json_map, &adapted_registry_value.value_type)?; + let registry_helper = RegistryHelper::new(&adapted_registry_value.key_path, Some(adapted_registry_value.value_name.clone()), Some(registry_data))?; + if let Err(e) = registry_helper.set() { + return Err(RegistryResourceError::RegistryError(e)); + } + } else { + warn!("No mapping found for key {}", key); + } + } else { + debug!("{}", t!("adapter.setNoAdaptedRegistryValueFound", key = key)); + } + } + + Ok(()) +} + +pub fn adapter_export(input: &str, adapted_resource: &str) -> Result { + trace!("Adapter Export with input: {input}"); + + // if input is provided, use that to perform a `get` and return that result + // if no input is provided, then create an input that contains all keys in the adapted resource with first values and perform a `get` to return the default values for all keys + let input: String = if input.is_empty() { + let resource_map = build_resource_map(adapted_resource)?; + let mut map = Map::new(); + for (key, adapted_registry_value) in &resource_map { + if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { + let first_key = json_map.keys().next().cloned().unwrap_or_default(); + map.insert(key.clone(), Value::String(first_key)); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.mapJsonToRegistryNotFound", key = key).to_string())); + } + } + serde_json::to_string(&map).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))? + } else { + input.to_string() + }; + adapter_get(&input, adapted_resource) +} + fn convert_default_value_to_registry_data(default_value: &Value, reg_type: &RegistryDataType) -> Result { match (default_value, reg_type) { (Value::Bool(b), RegistryDataType::Dword) => Ok(RegistryValueData::DWord(if *b { 1 } else { 0 })), @@ -167,6 +215,9 @@ fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, js } } }).collect(); + if reverse_map.is_empty() { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.emptyReverseMap").to_string())); + } let first_value_length = reverse_map.first().map(|(_, b)| b.len()).unwrap_or(0); let mut result = Vec::new(); for slice in byte_vec.chunks(first_value_length) { @@ -306,51 +357,3 @@ fn get_registry_value_data(value_name: &str, value: &Value, map: &Map Result<(), RegistryResourceError> { - let resource_map = build_resource_map(adapted_resource)?; - - let input_map: Map = serde_json::from_str(input) - .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; - for (key, value) in input_map.iter() { - if let Some(adapted_registry_value) = resource_map.get(key) { - debug!("{}", t!("adapter.setProcessingKey", key = key)); - if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { - let registry_data = get_registry_value_data(&adapted_registry_value.value_name, value, json_map, &adapted_registry_value.value_type)?; - let registry_helper = RegistryHelper::new(&adapted_registry_value.key_path, Some(adapted_registry_value.value_name.clone()), Some(registry_data))?; - if let Err(e) = registry_helper.set() { - return Err(RegistryResourceError::RegistryError(e)); - } - } else { - warn!("No mapping found for key {}", key); - } - } else { - debug!("{}", t!("adapter.setNoAdaptedRegistryValueFound", key = key)); - } - } - - Ok(()) -} - -pub fn adapter_export(input: &str, adapted_resource: &str) -> Result { - trace!("Adapter Export with input: {input}"); - - // if input is provided, use that to perform a `get` and return that result - // if no input is provided, then create an input that contains all keys in the adapted resource with first values and perform a `get` to return the default values for all keys - let input: String = if input.is_empty() { - let resource_map = build_resource_map(adapted_resource)?; - let mut map = Map::new(); - for (key, adapted_registry_value) in &resource_map { - if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { - let first_key = json_map.keys().next().cloned().unwrap_or_default(); - map.insert(key.clone(), Value::String(first_key)); - } else { - return Err(RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = key, reg_type = "unknown").to_string())); - } - } - serde_json::to_string(&map).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))? - } else { - input.to_string() - }; - adapter_get(&input, adapted_resource) -} diff --git a/resources/windows_personalization/.project.data.json b/resources/windows_personalization/.project.data.json index 776bee547..b11d0636b 100644 --- a/resources/windows_personalization/.project.data.json +++ b/resources/windows_personalization/.project.data.json @@ -4,7 +4,7 @@ "SupportedPlatformOS": "Windows", "CopyFiles": { "Windows": [ - "windows_personalization.dsc.adaptedresource.yaml" + "windows_personalization.dsc.adaptedResource.yaml" ] } } diff --git a/resources/windows_personalization/personalization_set.tests.ps1 b/resources/windows_personalization/personalization_set.tests.ps1 index 643cba6ff..2478e9601 100644 --- a/resources/windows_personalization/personalization_set.tests.ps1 +++ b/resources/windows_personalization/personalization_set.tests.ps1 @@ -8,7 +8,7 @@ Describe 'Personalization resource set tests' -Skip:(!$IsWindows) { } AfterAll { - dsc resource set -r Microsoft.Windows/Personalization -i $currentSettings 2>$TestDrive/error.log + dsc config set -r Microsoft.Windows/Personalization -i $currentSettings 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) } diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml index 425ab6de2..1628ab6ba 100644 --- a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -236,7 +236,7 @@ schema: - AlwaysCombineHideLabels - CombineWhenTaskbarIsFull - NeverCombine - multimonitorTarkbar: + multimonitorTaskbar: type: boolean title: Multimonitor Taskbar description: Enables showing the taskbar on multiple displays. From 7cd1aa7200b7ab79b7c6bbb4219d32b456d4e64e Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 20:04:06 -0700 Subject: [PATCH 12/16] fix schema retrieval for adapted resources --- lib/dsc-lib/src/dscresources/command_resource.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 109595432..51495d3b1 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -556,6 +556,10 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op /// /// Error if schema is not available or if there is an error getting the schema pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>) -> Result { + let Some(manifest) = &resource.manifest else { + return Err(DscError::MissingManifest(resource.type_name.to_string())); + }; + let target_resource = match target_resource { Some(r) => r, None => resource, @@ -565,10 +569,6 @@ pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>) return Ok(serde_json::to_string(schema)?); } - let Some(manifest) = &target_resource.manifest else { - return Err(DscError::MissingManifest(target_resource.type_name.to_string())); - }; - let Some(schema_kind) = manifest.schema.as_ref() else { return Err(DscError::SchemaNotAvailable(target_resource.type_name.to_string())); }; @@ -576,7 +576,7 @@ pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>) match schema_kind { SchemaKind::Command(command) => { let args = process_schema_args(command.args.as_ref(), target_resource); - let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, args, None, Some(&target_resource.directory), None, manifest.exit_codes.as_ref())?; + let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, args, None, Some(&resource.directory), None, manifest.exit_codes.as_ref())?; Ok(stdout) }, SchemaKind::Embedded(schema) => { From 7dd1cd7313d477c39f1123d92fbba1dc78ef517b Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 21:29:37 -0700 Subject: [PATCH 13/16] fix validation --- lib/dsc-lib/src/dscresources/command_resource.rs | 8 ++++---- resources/registry/locales/en-us.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 51495d3b1..87ede4376 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -533,15 +533,15 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op return Err(DscError::NotImplemented("validate".to_string())); }; - let command_resource = match target_resource { + let target_resource = match target_resource { Some(target) => target, None => resource }; - let args = process_get_args(validate.args.as_ref(), config, command_resource); + let args = process_get_args(validate.args.as_ref(), config, target_resource); let command_input = get_command_input(validate.input.as_ref(), config)?; - info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = &command_resource.type_name, executable = &validate.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = &resource.type_name, executable = &validate.executable)); + let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result: ValidateResult = serde_json::from_str(&stdout)?; Ok(result) } diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index ae72bb5fc..02573d9e4 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -8,7 +8,6 @@ setNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key registryValueNotFound = "Registry value not found for key path: %{key_path} and value name: %{value_name}" couldNotConvertRegistryValue = "Could not convert registry value for key path '%{key_path}' and value name '%{value_name}' to JSON type: %{json_type}" unsupportedConversionToJsonType = "Unsupported conversion from %{registry_value_data} to JSON type: %{json_type}" -mappingNotFound = "No mapping found for registry key path: %{key_path} and value name: %{value_name}" unsupportedValueType = "For value name %{value_name}, unsupported value type: %{value}" couldNotConvertDefaultValue = "Could not convert default value '%{default_value}' to registry data type: %{reg_type}" valueMappingNotFound = "No mapping found for value '%{value}' of type %{reg_type}" @@ -22,6 +21,7 @@ mapJsonToRegistryNotFound = "No mapping found for key: %{key}" [args] about = "Manage state of Windows registry" adapterAbout = "Use adapted registry resources." +adapterExportAbout = "Use 'export' operation with adapted registry resources." adapterGetAbout = "Use 'get' operation with adapted registry resources." adapterSetAbout = "Use 'set' operation with adapted registry resources." adapterArgsAdaptedResourceHelp = "The content of the adapted resource to use." From 594971fc1a8d1f63b253c234796568768ebb2882 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 21:45:22 -0700 Subject: [PATCH 14/16] fix working directory for command invocation --- lib/dsc-lib/src/dscresources/command_resource.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 87ede4376..59f1e6f43 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -186,7 +186,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &command_resource.type_name, executable = &get.executable)); - let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; if resource.kind == Kind::Resource { debug!("{}", t!("dscresources.commandResource.setVerifyGet", resource = &resource.type_name, executable = &get.executable)); @@ -227,7 +227,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }, } - let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&command_resource.directory), env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(&resource.directory), env, manifest.exit_codes.as_ref())?; let return_kind = if execution_type == &ExecutionKind::WhatIf { set.what_if_returns.as_ref().or(set.returns.as_ref()) @@ -341,7 +341,7 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &command_resource.type_name, executable = &test.executable)); - let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; if command_resource.kind == Kind::Importer { debug!("{}", t!("dscresources.commandResource.testGroupTestResponse")); @@ -498,7 +498,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti let command_input = get_command_input(delete.input.as_ref(), filter)?; info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = &command_resource.type_name, executable = &delete.executable)); - let (_exit_code, stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&command_resource.directory), command_input.env, manifest.exit_codes.as_ref())?; + let (_exit_code, stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result = if execution_type == &ExecutionKind::WhatIf { let delete_result: DeleteResult = serde_json::from_str(&stdout)?; DeleteResultKind::ResourceWhatIf(delete_result) From ca4cb07ab4a27e6ce264811a6c70b13e2c347366 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 22:09:18 -0700 Subject: [PATCH 15/16] fix loc strings --- resources/registry/locales/en-us.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index 02573d9e4..42c77e96a 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -6,7 +6,6 @@ getNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key setProcessingKey = "Setting key: %{key}" setNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key}" registryValueNotFound = "Registry value not found for key path: %{key_path} and value name: %{value_name}" -couldNotConvertRegistryValue = "Could not convert registry value for key path '%{key_path}' and value name '%{value_name}' to JSON type: %{json_type}" unsupportedConversionToJsonType = "Unsupported conversion from %{registry_value_data} to JSON type: %{json_type}" unsupportedValueType = "For value name %{value_name}, unsupported value type: %{value}" couldNotConvertDefaultValue = "Could not convert default value '%{default_value}' to registry data type: %{reg_type}" @@ -21,6 +20,7 @@ mapJsonToRegistryNotFound = "No mapping found for key: %{key}" [args] about = "Manage state of Windows registry" adapterAbout = "Use adapted registry resources." +adapterArgsInputHelp = "The registry JSON input for the adapted resource." adapterExportAbout = "Use 'export' operation with adapted registry resources." adapterGetAbout = "Use 'get' operation with adapted registry resources." adapterSetAbout = "Use 'set' operation with adapted registry resources." From 0f05b2affd5ddb0734a43d88102f8a0b08e305ef Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 29 May 2026 23:19:31 -0700 Subject: [PATCH 16/16] fix tests --- .../windows_personalization/personalization_export.tests.ps1 | 3 ++- .../windows_personalization/personalization_set.tests.ps1 | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/windows_personalization/personalization_export.tests.ps1 b/resources/windows_personalization/personalization_export.tests.ps1 index a3b103bef..3d93c4690 100644 --- a/resources/windows_personalization/personalization_export.tests.ps1 +++ b/resources/windows_personalization/personalization_export.tests.ps1 @@ -9,7 +9,8 @@ Describe 'Personalization resource export tests' -Skip:(!$IsWindows) { autoColorization = @($true, $false) colorPrevalence = @($true, $false) transparencyEffects = @($true, $false) - startMenuVisiblePlaces = @('Documents', 'Downloads','Music', 'Pictures', 'Videos', 'Network', 'UserProfile', 'Explorer', 'Settings') + # $null included as it's possible to have no visible places in start menu + startMenuVisiblePlaces = @($null, 'Documents', 'Downloads','Music', 'Pictures', 'Videos', 'Network', 'UserProfile', 'Explorer', 'Settings') startMenuShowRecentList = @($true, $false) showRecommendedApps = @($true, $false) taskbarShowBadges = @($true, $false) diff --git a/resources/windows_personalization/personalization_set.tests.ps1 b/resources/windows_personalization/personalization_set.tests.ps1 index 2478e9601..73a1245a0 100644 --- a/resources/windows_personalization/personalization_set.tests.ps1 +++ b/resources/windows_personalization/personalization_set.tests.ps1 @@ -8,7 +8,7 @@ Describe 'Personalization resource set tests' -Skip:(!$IsWindows) { } AfterAll { - dsc config set -r Microsoft.Windows/Personalization -i $currentSettings 2>$TestDrive/error.log + dsc config set -i $currentSettings 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) }