diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f07364ab..237c7a221 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,9 +2,6 @@ "rust-analyzer.linkedProjects": [ "./dsc/Cargo.toml", "./dsc_lib/Cargo.toml", - "./ntreg/Cargo.toml", - "./ntstatuserror/Cargo.toml", - "./ntuserinfo/Cargo.toml", "./osinfo/Cargo.toml", "./registry/Cargo.toml", "./tools/test_group_resource/Cargo.toml", @@ -22,5 +19,6 @@ "yaml.schemas": { "schemas/2023/10/bundled/config/document.vscode.json": "**.dsc.{yaml,yml,config.yaml,config.yml}", "schemas/2023/10/bundled/resource/manifest.vscode.json": "**.dsc.resource.{yaml,yml}" - } + }, + "sarif-viewer.connectToGithubCodeScanning": "off" } \ No newline at end of file diff --git a/ntreg/Cargo.toml b/archive/ntreg/Cargo.toml similarity index 100% rename from ntreg/Cargo.toml rename to archive/ntreg/Cargo.toml diff --git a/ntreg/build.rs b/archive/ntreg/build.rs similarity index 100% rename from ntreg/build.rs rename to archive/ntreg/build.rs diff --git a/ntreg/src/lib.rs b/archive/ntreg/src/lib.rs similarity index 100% rename from ntreg/src/lib.rs rename to archive/ntreg/src/lib.rs diff --git a/ntreg/src/registry_key.rs b/archive/ntreg/src/registry_key.rs similarity index 100% rename from ntreg/src/registry_key.rs rename to archive/ntreg/src/registry_key.rs diff --git a/ntreg/src/registry_value.rs b/archive/ntreg/src/registry_value.rs similarity index 100% rename from ntreg/src/registry_value.rs rename to archive/ntreg/src/registry_value.rs diff --git a/ntreg/tests/registry_key_tests.rs b/archive/ntreg/tests/registry_key_tests.rs similarity index 100% rename from ntreg/tests/registry_key_tests.rs rename to archive/ntreg/tests/registry_key_tests.rs diff --git a/ntreg/tests/registry_value_tests.rs b/archive/ntreg/tests/registry_value_tests.rs similarity index 100% rename from ntreg/tests/registry_value_tests.rs rename to archive/ntreg/tests/registry_value_tests.rs diff --git a/ntstatuserror/Cargo.toml b/archive/ntstatuserror/Cargo.toml similarity index 100% rename from ntstatuserror/Cargo.toml rename to archive/ntstatuserror/Cargo.toml diff --git a/ntstatuserror/src/lib.rs b/archive/ntstatuserror/src/lib.rs similarity index 100% rename from ntstatuserror/src/lib.rs rename to archive/ntstatuserror/src/lib.rs diff --git a/ntstatuserror/tests/ntstatus_tests.rs b/archive/ntstatuserror/tests/ntstatus_tests.rs similarity index 100% rename from ntstatuserror/tests/ntstatus_tests.rs rename to archive/ntstatuserror/tests/ntstatus_tests.rs diff --git a/ntuserinfo/Cargo.toml b/archive/ntuserinfo/Cargo.toml similarity index 100% rename from ntuserinfo/Cargo.toml rename to archive/ntuserinfo/Cargo.toml diff --git a/ntuserinfo/src/lib.rs b/archive/ntuserinfo/src/lib.rs similarity index 100% rename from ntuserinfo/src/lib.rs rename to archive/ntuserinfo/src/lib.rs diff --git a/ntuserinfo/tests/ntcurrentuserinfo_tests.rs b/archive/ntuserinfo/tests/ntcurrentuserinfo_tests.rs similarity index 100% rename from ntuserinfo/tests/ntcurrentuserinfo_tests.rs rename to archive/ntuserinfo/tests/ntcurrentuserinfo_tests.rs diff --git a/registry/.cargo/config.toml b/archive/registry/.cargo/config.toml similarity index 100% rename from registry/.cargo/config.toml rename to archive/registry/.cargo/config.toml diff --git a/archive/registry/Cargo.toml b/archive/registry/Cargo.toml new file mode 100644 index 000000000..ea2d97174 --- /dev/null +++ b/archive/registry/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "registry" +version = "0.1.0" +edition = "2021" + +[profile.release] +strip = true +# optimize for size +opt-level = 2 +# enable link time optimization to remove dead code +lto = true + +[profile.dev] +lto = true + +[dependencies] +atty = { version = "0.2" } +clap = { version = "4.1", features = ["derive"] } +crossterm = { version = "0.26" } +ntreg = { path = "../ntreg" } +ntstatuserror = { path = "../ntstatuserror" } +schemars = { version = "0.8" } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } + +[target.'cfg(onecore)'.dependencies] +pal = { path = "../pal" } + +[build-dependencies] +static_vcruntime = "2.0" diff --git a/registry/README.md b/archive/registry/README.md similarity index 100% rename from registry/README.md rename to archive/registry/README.md diff --git a/archive/registry/build.rs b/archive/registry/build.rs new file mode 100644 index 000000000..ce4b7a084 --- /dev/null +++ b/archive/registry/build.rs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(onecore)] +fn main() { + // Prevent this build script from rerunning unnecessarily. + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rustc-link-lib=onecore_apiset"); + println!("cargo:rustc-link-lib=onecoreuap_apiset"); + static_vcruntime::metabuild(); +} + +#[cfg(not(onecore))] +fn main() { + // Prevent this build script from rerunning unnecessarily. + println!("cargo:rerun-if-changed=build.rs"); + static_vcruntime::metabuild(); +} diff --git a/archive/registry/registry.dsc.resource.json b/archive/registry/registry.dsc.resource.json new file mode 100644 index 000000000..98110f41c --- /dev/null +++ b/archive/registry/registry.dsc.resource.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", + "type": "Microsoft.Windows/Registry", + "description": "Manage Windows Registry keys and values", + "tags": [ + "Windows", + "NT" + ], + "version": "0.1.0", + "get": { + "executable": "registry", + "args": [ + "config", + "get" + ], + "input": "stdin" + }, + "set": { + "executable": "registry", + "args": [ + "config", + "set" + ], + "input": "stdin", + "implementsPretest": true, + "return": "state" + }, + "test": { + "executable": "registry", + "args": [ + "config", + "test" + ], + "input": "stdin", + "return": "state" + }, + "exitCodes": { + "0": "Success", + "1": "Invalid parameter", + "2": "Invalid input", + "3": "Registry error", + "4": "JSON serialization failed" + }, + "schema": { + "command": { + "executable": "registry", + "args": [ + "schema" + ] + } + } +} diff --git a/archive/registry/src/args.rs b/archive/registry/src/args.rs new file mode 100644 index 000000000..43282afcd --- /dev/null +++ b/archive/registry/src/args.rs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[clap(name = "registry", version = "0.0.1", about = "Manage state of Windows registry", long_about = None)] +pub struct Arguments { + + #[clap(subcommand)] + pub subcommand: SubCommand, +} + +#[derive(Debug, PartialEq, Eq, Subcommand)] +pub enum ConfigSubCommand { + #[clap(name = "get", about = "Retrieve registry configuration.")] + Get, + #[clap(name = "set", about = "Apply registry configuration.")] + Set, + #[clap(name = "test", about = "Validate registry configuration.")] + Test, +} + +#[derive(Debug, PartialEq, Eq, Subcommand)] +pub enum SubCommand { + #[clap(name = "query", about = "Query a registry key or value.", arg_required_else_help = true)] + Query { + #[clap(short, long, required = true, help = "The registry key path to query.")] + key_path: String, + #[clap(short, long, help = "The name of the value to query.")] + value_name: Option, + #[clap(short, long, help = "Recursively query subkeys.")] + recurse: bool, + }, + #[clap(name = "set", about = "Set a registry key or value.")] + Set { + #[clap(short, long, required = true, help = "The registry key path to set.")] + key_path: String, + #[clap(short, long, help = "The value to set.")] + value: String, + }, + #[clap(name = "test", about = "Validate registry matches input JSON.")] + Test, + #[clap(name = "remove", about = "Remove a registry key or value.", arg_required_else_help = true)] + Remove { + #[clap(short, long, required = true, help = "The registry key path to remove.")] + key_path: String, + #[clap(short, long, help = "The name of the value to remove.")] + value_name: Option, + #[clap(short, long, help = "Recursively remove subkeys.")] + recurse: bool, + }, + #[clap(name = "find", about = "Find a registry key or value.", arg_required_else_help = true)] + Find { + #[clap(short, long, required = true, help = "The registry key path to start find.")] + key_path: String, + #[clap(short, long, required = true, help = "The string to find.")] + find: String, + #[clap(short, long, help = "Recursively find.")] + recurse: bool, + #[clap(long, help = "Only find keys.")] + keys_only: bool, + #[clap(long, help = "Only find values.")] + values_only: bool, + }, + #[clap(name = "config", about = "Manage registry configuration.", arg_required_else_help = true)] + Config { + #[clap(subcommand)] + subcommand: ConfigSubCommand, + }, + #[clap(name = "schema", about = "Retrieve JSON schema.")] + Schema { + #[clap(short, long, help = "Pretty print JSON.")] + pretty: bool, + } +} diff --git a/registry/src/bcrypt.rs b/archive/registry/src/bcrypt.rs similarity index 100% rename from registry/src/bcrypt.rs rename to archive/registry/src/bcrypt.rs diff --git a/archive/registry/src/config.rs b/archive/registry/src/config.rs new file mode 100644 index 000000000..664494aeb --- /dev/null +++ b/archive/registry/src/config.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub enum RegistryValueData { + String(String), + ExpandString(String), + Binary(Vec), + DWord(u32), + MultiString(Vec), + QWord(u64), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[serde(rename = "Registry", deny_unknown_fields)] +pub struct Registry { + /// The ID of the resource. Value is ignored for input. + #[serde(rename = "$id", skip_serializing_if = "Option::is_none")] + pub id: Option, + /// The path to the registry key. + #[serde(rename = "keyPath")] + pub key_path: String, + /// The name of the registry value. + #[serde(rename = "valueName")] + #[serde(skip_serializing_if = "Option::is_none")] + pub value_name: Option, + /// The data of the registry value. + #[serde(rename = "valueData")] + #[serde(skip_serializing_if = "Option::is_none")] + pub value_data: Option, + /// Flag indicating whether the registry value should be present or absent. + #[serde(rename = "_exist")] + #[serde(skip_serializing_if = "Option::is_none")] + pub exist: Option, + /// Flag indicating whether the registry value should be overwritten if it already exists. + #[serde(rename = "_clobber")] + #[serde(skip_serializing_if = "Option::is_none")] + pub clobber: Option, + /// Flag indicating whether the resource is in the desired state. Value is ignored for input. + #[serde(rename = "_inDesiredState")] + #[serde(skip_serializing_if = "Option::is_none")] + pub in_desired_state: Option, +} + +impl Registry { + pub fn to_json(&self) -> String { + match serde_json::to_string(self) { + Ok(json) => json, + Err(e) => { + eprintln!("Failed to serialize to JSON: {e}"); + String::new() + } + } + } +} + +const ID: &str = "https://developer.microsoft.com/json-schemas/windows/registry/20230303/Microsoft.Windows.Registry.schema.json"; + +impl Default for Registry { + fn default() -> Self { + Self { + id: Some(ID.to_string()), + key_path: String::new(), + value_name: None, + value_data: None, + exist: None, + clobber: None, + in_desired_state: None, + } + } +} diff --git a/archive/registry/src/main.rs b/archive/registry/src/main.rs new file mode 100644 index 000000000..fbbc66816 --- /dev/null +++ b/archive/registry/src/main.rs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(debug_assertions)] +use crossterm::event; +#[cfg(debug_assertions)] +use std::env; + +use args::Arguments; +use atty::Stream; +use clap::Parser; +use schemars::schema_for; +use std::{io::{self, Read}, process::exit}; + +use crate::config::Registry; + +mod args; +#[cfg(onecore)] +mod bcrypt; +mod config; +mod regconfighelper; + +const EXIT_SUCCESS: i32 = 0; +const EXIT_INVALID_PARAMETER: i32 = 1; +const EXIT_INVALID_INPUT: i32 = 2; +const EXIT_REGISTRY_ERROR: i32 = 3; +const EXIT_JSON_SERIALIZATION_FAILED: i32 = 4; + +#[allow(clippy::too_many_lines)] +fn main() { + #[cfg(debug_assertions)] + check_debug(); + + let args = Arguments::parse(); + let input: Option = if atty::is(Stream::Stdin) { + None + } else { + let mut buffer: Vec = Vec::new(); + io::stdin().read_to_end(&mut buffer).unwrap(); + let input = match String::from_utf8(buffer) { + Ok(input) => input, + Err(e) => { + eprintln!("Invalid UTF-8 sequence: {e}"); + exit(EXIT_INVALID_INPUT); + } + }; + Some(input) + }; + + let mut config: Registry = Registry::default(); + // check if input is valid for subcommand + match args.subcommand { + args::SubCommand::Config { subcommand: _ } => { + if let Some(input) = input { + config = match serde_json::from_str(&input) { + Ok(config) => config, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + exit(EXIT_INVALID_INPUT); + } + }; + } else { + eprintln!("Error: Input JSON via STDIN is required for config subcommand."); + exit(EXIT_INVALID_PARAMETER); + } + } + _ => { + if input.is_some() && !input.as_ref().unwrap().is_empty() { + eprintln!("Error: Input JSON via STDIN is only valid for config subcommand: '{}'", input.unwrap()); + exit(EXIT_INVALID_INPUT); + } + } + } + + match args.subcommand { + args::SubCommand::Query { key_path, value_name, recurse } => { + eprintln!("Get key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); + }, + args::SubCommand::Set { key_path, value } => { + eprintln!("Set key_path: {key_path}, value: {value}"); + }, + args::SubCommand::Test => { + eprintln!("Test"); + }, + args::SubCommand::Remove { key_path, value_name, recurse } => { + eprintln!("Remove key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); + }, + args::SubCommand::Find { key_path, find, recurse, keys_only, values_only } => { + eprintln!("Find key_path: {key_path}, find: {find}, recurse: {recurse:?}, keys_only: {keys_only:?}, values_only: {values_only:?}"); + }, + args::SubCommand::Config { subcommand } => { + let json: String; + if let Err(err) = regconfighelper::validate_config(&config) { + eprintln!("Error validating config: {err}"); + exit(EXIT_INVALID_INPUT); + } + + if config.exist.is_none() { + config.exist = Some(true); + } + + match subcommand { + args::ConfigSubCommand::Get => { + match regconfighelper::config_get(&config) { + Ok(config) => { + json = config; + }, + Err(err) => { + eprintln!("Error getting config: {err}"); + exit(EXIT_REGISTRY_ERROR); + } + } + }, + args::ConfigSubCommand::Set => { + match regconfighelper::config_set(&config) { + Ok(result) => { + json = result; + }, + Err(err) => { + eprintln!("Error setting config: {err}"); + exit(EXIT_REGISTRY_ERROR); + } + } + }, + args::ConfigSubCommand::Test => { + match regconfighelper::config_test(&config) { + Ok(result) => { + json = result; + }, + Err(err) => { + eprintln!("Error testing config: {err}"); + exit(EXIT_REGISTRY_ERROR); + } + } + }, + } + + if json.is_empty() { + exit(EXIT_JSON_SERIALIZATION_FAILED); + } + + println!("{json}"); + }, + args::SubCommand::Schema { pretty } => { + let schema = schema_for!(Registry); + let json = if pretty { + serde_json::to_string_pretty(&schema).unwrap() + } + else { + serde_json::to_string(&schema).unwrap() + }; + println!("{json}"); + }, + } + + exit(EXIT_SUCCESS); +} + +#[cfg(debug_assertions)] +fn check_debug() { + if env::var("DEBUG_REGISTRY").is_ok() { + eprintln!("attach debugger to pid {} and press any key to continue", std::process::id()); + loop { + let event = event::read().unwrap(); + if let event::Event::Key(_key) = event { + break; + } + eprintln!("Unexpected event: {event:?}"); + } + } +} diff --git a/registry/src/regconfighelper.rs b/archive/registry/src/regconfighelper.rs similarity index 100% rename from registry/src/regconfighelper.rs rename to archive/registry/src/regconfighelper.rs diff --git a/build.ps1 b/build.ps1 index f893e2dc3..c35303a90 100644 --- a/build.ps1 +++ b/build.ps1 @@ -117,7 +117,7 @@ if (!$SkipBuild) { New-Item -ItemType Directory $target > $null # make sure dependencies are built first so clippy runs correctly -$windows_projects = @("pal", "ntreg", "ntstatuserror", "ntuserinfo", "registry", "reboot_pending", "wmi-adapter") +$windows_projects = @("pal", "registry", "reboot_pending", "wmi-adapter") # projects are in dependency order $projects = @( @@ -134,7 +134,7 @@ $projects = @( "resources/brew", "runcommandonset" ) -$pedantic_unclean_projects = @("ntreg") +$pedantic_unclean_projects = @() $clippy_unclean_projects = @("tree-sitter-dscexpression") $skip_test_projects_on_windows = @("tree-sitter-dscexpression") diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 7a6041687..83cc6771d 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -173,6 +173,15 @@ pub enum ResourceSubCommand { #[clap(short = 'f', long, help = "The output format to use")] format: Option, }, + #[clap(name = "delete", about = "Invoke the delete operation to a resource", arg_required_else_help = true)] + Delete { + #[clap(short, long, help = "The name or DscResource JSON of the resource to invoke `delete` on")] + resource: String, + #[clap(short, long, help = "The input to pass to the resource as JSON or YAML", conflicts_with = "path")] + input: Option, + #[clap(short = 'p', long, help = "The path to a JSON or YAML file used as input to the configuration or resource", conflicts_with = "input")] + path: Option, + }, #[clap(name = "schema", about = "Get the JSON schema for a resource", arg_required_else_help = true)] Schema { #[clap(short, long, help = "The name of the resource to get the JSON schema")] diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index 7123d7d35..ba40fcac0 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -94,12 +94,6 @@ pub fn get_all(dsc: &DscManager, resource_type: &str, format: &Option) { if input.is_empty() { error!("Error: Input is empty"); @@ -142,12 +136,6 @@ pub fn set(dsc: &DscManager, resource_type: &str, mut input: String, format: &Op } } -/// Test operation. -/// -/// # Panics -/// -/// Will panic if adapter-based resource is not found. -/// pub fn test(dsc: &DscManager, resource_type: &str, mut input: String, format: &Option) { let Some(mut resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); @@ -185,6 +173,33 @@ pub fn test(dsc: &DscManager, resource_type: &str, mut input: String, format: &O } } +pub fn delete(dsc: &DscManager, resource_type: &str, mut input: String) { + let Some(mut resource) = get_resource(dsc, resource_type) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); + return + }; + + debug!("resource.type_name - {} implemented_as - {:?}", resource.type_name, resource.implemented_as); + + if let Some(requires) = &resource.require_adapter { + input = add_type_name_to_json(input, resource.type_name.clone()); + if let Some(pr) = get_resource(dsc, requires) { + resource = pr; + } else { + error!("Adapter {} not found", requires); + return; + }; + } + + match resource.delete(input.as_str()) { + Ok(()) => {} + Err(err) => { + error!("Error: {err}"); + exit(EXIT_DSC_ERROR); + } + } +} + pub fn schema(dsc: &DscManager, resource_type: &str, format: &Option) { let Some(resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 8de87c6d0..2d153c39a 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -399,85 +399,7 @@ pub fn resource(subcommand: &ResourceSubCommand, stdin: &Option) { match subcommand { ResourceSubCommand::List { resource_name, adapter_name, description, tags, format } => { - - let mut write_table = false; - let mut table = Table::new(&["Type", "Kind", "Version", "Caps", "RequireAdapter", "Description"]); - if format.is_none() && atty::is(Stream::Stdout) { - // write as table if format is not specified and interactive - write_table = true; - } - for resource in dsc.list_available_resources( - &resource_name.clone().unwrap_or("*".to_string()), - &adapter_name.clone().unwrap_or_default()) { - let mut capabilities = "g---".to_string(); - if resource.capabilities.contains(&Capability::Set) { capabilities.replace_range(1..2, "s"); } - if resource.capabilities.contains(&Capability::Test) { capabilities.replace_range(2..3, "t"); } - if resource.capabilities.contains(&Capability::Export) { capabilities.replace_range(3..4, "e"); } - - // if description, tags, or write_table is specified, pull resource manifest if it exists - if let Some(ref resource_manifest) = resource.manifest { - let manifest = match import_manifest(resource_manifest.clone()) { - Ok(resource_manifest) => resource_manifest, - Err(err) => { - error!("Error in manifest for {0}: {err}", resource.type_name); - continue; - } - }; - - // if description is specified, skip if resource description does not contain it - if description.is_some() && - (manifest.description.is_none() | !manifest.description.unwrap_or_default().to_lowercase().contains(&description.as_ref().unwrap_or(&String::new()).to_lowercase())) { - continue; - } - - // if tags is specified, skip if resource tags do not contain the tags - if let Some(tags) = tags { - let Some(manifest_tags) = manifest.tags else { continue; }; - - let mut found = false; - for tag_to_find in tags { - for tag in &manifest_tags { - if tag.to_lowercase() == tag_to_find.to_lowercase() { - found = true; - break; - } - } - } - if !found { continue; } - } - } else { - // resource does not have a manifest but filtering on description or tags was requested - skip such resource - if description.is_some() || tags.is_some() { - continue; - } - } - - if write_table { - table.add_row(vec![ - resource.type_name, - format!("{:?}", resource.kind), - resource.version, - capabilities, - resource.require_adapter.unwrap_or_default(), - resource.description.unwrap_or_default() - ]); - } - else { - // convert to json - let json = match serde_json::to_string(&resource) { - Ok(json) => json, - Err(err) => { - error!("JSON Error: {err}"); - exit(EXIT_JSON_ERROR); - } - }; - write_output(&json, format); - // insert newline separating instances if writing to console - if atty::is(Stream::Stdout) { println!(); } - } - } - - if write_table { table.print(); } + list_resources(&mut dsc, resource_name, adapter_name, description, tags, format); }, ResourceSubCommand::Schema { resource , format } => { dsc.discover_resources(&[resource.to_lowercase().to_string()]); @@ -505,5 +427,102 @@ pub fn resource(subcommand: &ResourceSubCommand, stdin: &Option) { let parsed_input = get_input(input, stdin, path); resource_command::test(&dsc, resource, parsed_input, format); }, + ResourceSubCommand::Delete { resource, input, path } => { + dsc.discover_resources(&[resource.to_lowercase().to_string()]); + let parsed_input = get_input(input, stdin, path); + resource_command::delete(&dsc, resource, parsed_input); + }, + } +} + +fn list_resources(dsc: &mut DscManager, resource_name: &Option, adapter_name: &Option, description: &Option, tags: &Option>, format: &Option) { + let mut write_table = false; + let mut table = Table::new(&["Type", "Kind", "Version", "Caps", "RequireAdapter", "Description"]); + if format.is_none() && atty::is(Stream::Stdout) { + // write as table if format is not specified and interactive + write_table = true; + } + for resource in dsc.list_available_resources(&resource_name.clone().unwrap_or("*".to_string()), &adapter_name.clone().unwrap_or_default()) { + let mut capabilities = "------".to_string(); + let capability_types = [ + (Capability::Get, "g"), + (Capability::Set, "s"), + (Capability::SetHandlesExist, "x"), + (Capability::Test, "t"), + (Capability::Delete, "d"), + (Capability::Export, "e"), + ]; + + for (i, (capability, letter)) in capability_types.iter().enumerate() { + if resource.capabilities.contains(capability) { + capabilities.replace_range(i..=i, letter); + } + } + + // if description, tags, or write_table is specified, pull resource manifest if it exists + if let Some(ref resource_manifest) = resource.manifest { + let manifest = match import_manifest(resource_manifest.clone()) { + Ok(resource_manifest) => resource_manifest, + Err(err) => { + error!("Error in manifest for {0}: {err}", resource.type_name); + continue; + } + }; + + // if description is specified, skip if resource description does not contain it + if description.is_some() && + (manifest.description.is_none() | !manifest.description.unwrap_or_default().to_lowercase().contains(&description.as_ref().unwrap_or(&String::new()).to_lowercase())) { + continue; + } + + // if tags is specified, skip if resource tags do not contain the tags + if let Some(tags) = tags { + let Some(manifest_tags) = manifest.tags else { continue; }; + + let mut found = false; + for tag_to_find in tags { + for tag in &manifest_tags { + if tag.to_lowercase() == tag_to_find.to_lowercase() { + found = true; + break; + } + } + } + if !found { continue; } + } + } else { + // resource does not have a manifest but filtering on description or tags was requested - skip such resource + if description.is_some() || tags.is_some() { + continue; + } + } + + if write_table { + table.add_row(vec![ + resource.type_name, + format!("{:?}", resource.kind), + resource.version, + capabilities, + resource.require_adapter.unwrap_or_default(), + resource.description.unwrap_or_default() + ]); + } + else { + // convert to json + let json = match serde_json::to_string(&resource) { + Ok(json) => json, + Err(err) => { + error!("JSON Error: {err}"); + exit(EXIT_JSON_ERROR); + } + }; + write_output(&json, format); + // insert newline separating instances if writing to console + if atty::is(Stream::Stdout) { println!(); } + } + } + + if write_table { + table.print(); } } diff --git a/dsc/tests/dsc.exist.tests.ps1 b/dsc/tests/dsc.exist.tests.ps1 new file mode 100644 index 000000000..58c0711be --- /dev/null +++ b/dsc/tests/dsc.exist.tests.ps1 @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe '_exist tests' { + It 'Resource supporting exist on set should receive _exist for: ' -TestCases @( + @{ exist = $true } + @{ exist = $false } + ) { + param($exist) + + $json = @" + { + "_exist": $exist + } +"@ + $out = dsc resource set -r Test/Exist --input $json | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + + $out.afterState._exist | Should -Be $exist + if ($exist) { + $out.afterState.state | Should -Be 'Present' + } + else { + $out.afterState.state | Should -Be 'Absent' + } + } +} diff --git a/dsc/tests/dsc_args.tests.ps1 b/dsc/tests/dsc_args.tests.ps1 index 2917a0e67..83b2ad5be 100644 --- a/dsc/tests/dsc_args.tests.ps1 +++ b/dsc/tests/dsc_args.tests.ps1 @@ -69,7 +69,6 @@ Describe 'config argument tests' { param($text) $output = $text | dsc resource get -r Microsoft.Windows/Registry $output = $output | ConvertFrom-Json - $output.actualState.'$id' | Should -BeExactly 'https://developer.microsoft.com/json-schemas/windows/registry/20230303/Microsoft.Windows.Registry.schema.json' $output.actualState.keyPath | Should -BeExactly 'HKLM\Software\Microsoft\Windows NT\CurrentVersion' $output.actualState.valueName | Should -BeExactly 'ProductName' $output.actualState.valueData.String | Should -Match 'Windows .*' diff --git a/dsc/tests/dsc_get.tests.ps1 b/dsc/tests/dsc_get.tests.ps1 index bc2f87429..96500bb79 100644 --- a/dsc/tests/dsc_get.tests.ps1 +++ b/dsc/tests/dsc_get.tests.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe 'config get tests' { +Describe 'resource get tests' { It 'should get from registry using resource' -Skip:(!$IsWindows) -TestCases @( @{ type = 'string' } ) { @@ -32,7 +32,6 @@ Describe 'config get tests' { $output = $json | dsc resource get -r $resource $LASTEXITCODE | Should -Be 0 $output = $output | ConvertFrom-Json - $output.actualState.'$id' | Should -BeExactly 'https://developer.microsoft.com/json-schemas/windows/registry/20230303/Microsoft.Windows.Registry.schema.json' $output.actualState.keyPath | Should -BeExactly 'HKLM\Software\Microsoft\Windows NT\CurrentVersion' $output.actualState.valueName | Should -BeExactly 'ProductName' $output.actualState.valueData.String | Should -Match 'Windows .*' diff --git a/dsc/tests/dsc_set.tests.ps1 b/dsc/tests/dsc_set.tests.ps1 index 0bda542bb..bffdf1950 100644 --- a/dsc/tests/dsc_set.tests.ps1 +++ b/dsc/tests/dsc_set.tests.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe 'config set tests' { +Describe 'resource set tests' { BeforeAll { $manifest = @' { @@ -61,7 +61,7 @@ Describe 'config set tests' { "_exist": false } '@ - $json | registry config set + $null = registry config set --input $json } } @@ -73,11 +73,12 @@ Describe 'config set tests' { "_exist": false } '@ - $json | registry config set + $null = registry config set --input $json } } - It 'can set and remove a registry value' -Skip:(!$IsWindows) { + # test pending changes in engine to call delete if _exist is not handled directly + It 'can set and remove a registry value' -Pending { $json = @' { "keyPath": "HKCU\\1\\2\\3", diff --git a/dsc/tests/dsc_test.tests.ps1 b/dsc/tests/dsc_test.tests.ps1 index 09fdb72ce..a108e60fd 100644 --- a/dsc/tests/dsc_test.tests.ps1 +++ b/dsc/tests/dsc_test.tests.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe 'config test tests' { +Describe 'resource test tests' { It 'should confirm matching state' -Skip:(!$IsWindows) { $json = @' { @@ -9,7 +9,7 @@ Describe 'config test tests' { "valueName": "ProductName" } '@ - $current = $json | registry config get + $current = registry config get --input $json $out = $current | dsc resource test -r Microsoft.Windows/registry $LASTEXITCODE | Should -Be 0 $out = $out | ConvertFrom-Json @@ -49,10 +49,9 @@ Describe 'config test tests' { $LASTEXITCODE | Should -Be 0 $out = $out | ConvertFrom-Json $out.inDesiredState | Should -BeFalse - $out.differingProperties.Count | Should -Be 3 - $out.differingProperties[0] | Should -BeExactly 'valueName' - $out.differingProperties[1] | Should -BeExactly 'valueData' - $out.differingProperties[2] | Should -BeExactly '_exist' + $out.differingProperties.Count | Should -Be 2 + $out.differingProperties[0] | Should -BeExactly 'valueData' + $out.differingProperties[1] | Should -BeExactly '_exist' } It 'can accept the use of --format as a subcommand' { diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index 27ba7b36e..2535fe07b 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -390,12 +390,18 @@ fn load_manifest(path: &Path) -> Result { // all command based resources are required to support `get` let mut capabilities = vec![Capability::Get]; - if manifest.set.is_some() { + if let Some(set) = &manifest.set { capabilities.push(Capability::Set); + if set.handles_exist == Some(true) { + capabilities.push(Capability::SetHandlesExist); + } } if manifest.test.is_some() { capabilities.push(Capability::Test); } + if manifest.delete.is_some() { + capabilities.push(Capability::Delete); + } if manifest.export.is_some() { capabilities.push(Capability::Export); } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index ab0cf7569..278e5a0f3 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -71,7 +71,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul } } - info!("Invoking get {} using {}", &resource.resource_type, &resource.get.executable); + info!("Invoking get '{}' using '{}'", &resource.resource_type, &resource.get.executable); let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, get_args, input_filter, Some(cwd), env)?; log_resource_traces(&stderr); if exit_code != 0 { @@ -119,21 +119,6 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te }; verify_json(resource, cwd, desired)?; - let mut env: Option> = None; - let mut input_desired: Option<&str> = None; - let mut args = set.args.clone(); - match &set.input { - InputKind::Env => { - env = Some(json_to_hashmap(desired)?); - }, - InputKind::Stdin => { - input_desired = Some(desired); - }, - InputKind::Arg(arg_token) => { - replace_token(&mut args, arg_token, desired)?; - }, - } - // 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.unwrap_or_default() { info!("No pretest, invoking test {}", &resource.resource_type); @@ -196,8 +181,23 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); }; - info!("Invoking set {} using {}", &resource.resource_type, &set.executable); - let (exit_code, stdout, stderr) = invoke_command(&set.executable, set.args.clone(), input_desired, Some(cwd), env)?; + let mut env: Option> = None; + let mut input_desired: Option<&str> = None; + let mut args = set.args.clone(); + match &set.input { + InputKind::Env => { + env = Some(json_to_hashmap(desired)?); + }, + InputKind::Stdin => { + input_desired = Some(desired); + }, + InputKind::Arg(arg_token) => { + replace_token(&mut args, arg_token, desired)?; + }, + } + + info!("Invoking set '{}' using '{}'", &resource.resource_type, &set.executable); + let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env)?; log_resource_traces(&stderr); if exit_code != 0 { return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); @@ -281,7 +281,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te /// /// Error is returned if the underlying command returns a non-zero exit code. pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Result { - let Some(test) = resource.test.as_ref() else { + let Some(test) = &resource.test else { return Err(DscError::NotImplemented("test".to_string())); }; @@ -302,7 +302,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re }, } - info!("Invoking test {} using {}", &resource.resource_type, &test.executable); + info!("Invoking test '{}' using '{}'", &resource.resource_type, &test.executable); let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, input_expected, Some(cwd), env)?; log_resource_traces(&stderr); if exit_code != 0 { @@ -375,6 +375,48 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re } } +/// Invoke the delete operation against a command resource. +/// +/// # Arguments +/// +/// * `resource` - The resource manifest for the command resource. +/// * `cwd` - The current working directory. +/// * `filter` - The filter to apply to the resource in JSON. +/// +/// # Errors +/// +/// Error is returned if the underlying command returns a non-zero exit code. +pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Result<(), DscError> { + let Some(delete) = &resource.delete else { + return Err(DscError::NotImplemented("delete".to_string())); + }; + + let mut env: Option> = None; + let mut input_filter: Option<&str> = None; + let mut get_args = resource.get.args.clone(); + verify_json(resource, cwd, filter)?; + match &delete.input { + InputKind::Env => { + env = Some(json_to_hashmap(filter)?); + }, + InputKind::Stdin => { + input_filter = Some(filter); + }, + InputKind::Arg(arg_name) => { + replace_token(&mut get_args, arg_name, filter)?; + }, + } + + info!("Invoking delete '{}' using '{}'", &resource.resource_type, &delete.executable); + let (exit_code, _stdout, stderr) = invoke_command(&delete.executable, get_args, input_filter, Some(cwd), env)?; + log_resource_traces(&stderr); + if exit_code != 0 { + return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); + } + + Ok(()) +} + /// Invoke the validate operation against a command resource. /// /// # Arguments diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index ad7e36299..f217524b1 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -44,9 +44,17 @@ pub struct DscResource { #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] pub enum Capability { + /// The resource supports retriving configuration. Get, + /// The resource supports applying configuration. Set, + /// The resource supports the `_exist` property directly. + SetHandlesExist, + /// The resource supports validating configuration. Test, + /// The resource supports deleting configuration. + Delete, + /// The resource supports exporting configuration. Export, } @@ -121,6 +129,17 @@ pub trait Invoke { /// This function will return an error if the underlying resource fails. fn test(&self, expected: &str) -> Result; + /// Invoke the delete operation on the resource. + /// + /// # Arguments + /// + /// * `filter` - The filter as JSON to apply to the resource. + /// + /// # Errors + /// + /// This function will return an error if the underlying resource fails. + fn delete(&self, filter: &str) -> Result<(), DscError>; + /// Invoke the validate operation on the resource. /// /// # Arguments @@ -225,6 +244,21 @@ impl Invoke for DscResource { } } + fn delete(&self, filter: &str) -> Result<(), DscError> { + match &self.implemented_as { + ImplementedAs::Custom(_custom) => { + Err(DscError::NotImplemented("set custom resources".to_string())) + }, + ImplementedAs::Command => { + let Some(manifest) = &self.manifest else { + return Err(DscError::MissingManifest(self.type_name.clone())); + }; + let resource_manifest = import_manifest(manifest.clone())?; + command_resource::invoke_delete(&resource_manifest, &self.directory, filter) + }, + } + } + fn validate(&self, config: &str) -> Result { match &self.implemented_as { ImplementedAs::Custom(_custom) => { diff --git a/dsc_lib/src/dscresources/resource_manifest.rs b/dsc_lib/src/dscresources/resource_manifest.rs index c372f9242..3b00a5c13 100644 --- a/dsc_lib/src/dscresources/resource_manifest.rs +++ b/dsc_lib/src/dscresources/resource_manifest.rs @@ -42,6 +42,9 @@ pub struct ResourceManifest { /// Details how to call the Test method of the resource. #[serde(skip_serializing_if = "Option::is_none")] pub test: Option, + /// Details how to call the Delete method of the resource. + #[serde(skip_serializing_if = "Option::is_none")] + pub delete: Option, /// Details how to call the Export method of the resource. #[serde(skip_serializing_if = "Option::is_none")] pub export: Option, @@ -143,6 +146,9 @@ pub struct SetMethod { /// Whether to run the Test method before the Set method. True means the resource will perform its own test before running the Set method. #[serde(rename = "implementsPretest", skip_serializing_if = "Option::is_none")] pub pre_test: Option, + /// Indicates that the resource directly handles `_exist` as a property. + #[serde(rename = "handlesExist", skip_serializing_if = "Option::is_none")] + pub handles_exist: Option, /// The type of return value expected from the Set method. #[serde(rename = "return", skip_serializing_if = "Option::is_none")] pub returns: Option, @@ -161,6 +167,16 @@ pub struct TestMethod { pub returns: Option, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct DeleteMethod { + /// The command to run to test the state of the resource. + pub executable: String, + /// The arguments to pass to the command to perform a Test. + pub args: Option>, + /// How to pass required input for a Test. + pub input: InputKind, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct ValidateMethod { // TODO: enable validation via schema or command /// The command to run to validate the state of the resource. diff --git a/registry/Cargo.toml b/registry/Cargo.toml index ea2d97174..cb1517fa7 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -14,17 +14,14 @@ lto = true lto = true [dependencies] -atty = { version = "0.2" } -clap = { version = "4.1", features = ["derive"] } -crossterm = { version = "0.26" } -ntreg = { path = "../ntreg" } -ntstatuserror = { path = "../ntstatuserror" } -schemars = { version = "0.8" } -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", features = ["preserve_order"] } - -[target.'cfg(onecore)'.dependencies] -pal = { path = "../pal" } +clap = { version = "4.4", features = ["derive"] } +crossterm = "0.27.0" +registry = "1.2.3" +schemars = "0.8.0" +serde = "1.0.130" +serde_json = "1.0.68" +thiserror = "1.0.30" +utfx = "0.1.0" [build-dependencies] static_vcruntime = "2.0" diff --git a/registry/build.rs b/registry/build.rs index ce4b7a084..495c64a43 100644 --- a/registry/build.rs +++ b/registry/build.rs @@ -1,16 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#[cfg(onecore)] -fn main() { - // Prevent this build script from rerunning unnecessarily. - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rustc-link-lib=onecore_apiset"); - println!("cargo:rustc-link-lib=onecoreuap_apiset"); - static_vcruntime::metabuild(); -} - -#[cfg(not(onecore))] fn main() { // Prevent this build script from rerunning unnecessarily. println!("cargo:rerun-if-changed=build.rs"); diff --git a/registry/registry.dsc.resource.json b/registry/registry.dsc.resource.json index 98110f41c..5e46ad736 100644 --- a/registry/registry.dsc.resource.json +++ b/registry/registry.dsc.resource.json @@ -1,52 +1,60 @@ { - "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", - "type": "Microsoft.Windows/Registry", - "description": "Manage Windows Registry keys and values", - "tags": [ - "Windows", - "NT" - ], - "version": "0.1.0", - "get": { - "executable": "registry", - "args": [ - "config", - "get" + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", + "type": "Microsoft.Windows/Registry", + "description": "Manage Windows Registry keys and values", + "tags": [ + "Windows" ], - "input": "stdin" - }, - "set": { - "executable": "registry", - "args": [ - "config", - "set" - ], - "input": "stdin", - "implementsPretest": true, - "return": "state" - }, - "test": { - "executable": "registry", - "args": [ - "config", - "test" - ], - "input": "stdin", - "return": "state" - }, - "exitCodes": { - "0": "Success", - "1": "Invalid parameter", - "2": "Invalid input", - "3": "Registry error", - "4": "JSON serialization failed" - }, - "schema": { - "command": { - "executable": "registry", - "args": [ - "schema" - ] + "version": "0.1.0", + "get": { + "executable": "registry", + "args": [ + "config", + "get", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + } + }, + "set": { + "executable": "registry", + "args": [ + "config", + "set", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + } + }, + "delete": { + "executable": "registry", + "args": [ + "config", + "remove", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + } + }, + "exitCodes": { + "0": "Success", + "1": "Invalid parameter", + "2": "Invalid input", + "3": "Registry error", + "4": "JSON serialization failed" + }, + "schema": { + "command": { + "executable": "registry", + "args": [ + "schema" + ] + } } - } } diff --git a/registry/src/args.rs b/registry/src/args.rs index 43282afcd..889fdaa7d 100644 --- a/registry/src/args.rs +++ b/registry/src/args.rs @@ -14,11 +14,20 @@ pub struct Arguments { #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum ConfigSubCommand { #[clap(name = "get", about = "Retrieve registry configuration.")] - Get, + Get { + #[clap(short, long, required = true, help = "The registry JSON input.")] + input: String, + }, #[clap(name = "set", about = "Apply registry configuration.")] - Set, - #[clap(name = "test", about = "Validate registry configuration.")] - Test, + Set { + #[clap(short, long, required = true, help = "The registry JSON input.")] + input: String, + }, + #[clap(name = "delete", about = "Delete registry configuration.")] + Remove { + #[clap(short, long, required = true, help = "The registry JSON input.")] + input: String, + }, } #[derive(Debug, PartialEq, Eq, Subcommand)] @@ -39,8 +48,6 @@ pub enum SubCommand { #[clap(short, long, help = "The value to set.")] value: String, }, - #[clap(name = "test", about = "Validate registry matches input JSON.")] - Test, #[clap(name = "remove", about = "Remove a registry key or value.", arg_required_else_help = true)] Remove { #[clap(short, long, required = true, help = "The registry key path to remove.")] @@ -69,8 +76,5 @@ pub enum SubCommand { subcommand: ConfigSubCommand, }, #[clap(name = "schema", about = "Retrieve JSON schema.")] - Schema { - #[clap(short, long, help = "Pretty print JSON.")] - pretty: bool, - } + Schema, } diff --git a/registry/src/config.rs b/registry/src/config.rs index 664494aeb..ee1833c7f 100644 --- a/registry/src/config.rs +++ b/registry/src/config.rs @@ -17,58 +17,15 @@ pub enum RegistryValueData { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[serde(rename = "Registry", deny_unknown_fields)] pub struct Registry { - /// The ID of the resource. Value is ignored for input. - #[serde(rename = "$id", skip_serializing_if = "Option::is_none")] - pub id: Option, /// The path to the registry key. #[serde(rename = "keyPath")] pub key_path: String, /// The name of the registry value. - #[serde(rename = "valueName")] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "valueName", skip_serializing_if = "Option::is_none")] pub value_name: Option, /// The data of the registry value. - #[serde(rename = "valueData")] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "valueData", skip_serializing_if = "Option::is_none")] pub value_data: Option, - /// Flag indicating whether the registry value should be present or absent. - #[serde(rename = "_exist")] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "_exist", skip_serializing_if = "Option::is_none")] pub exist: Option, - /// Flag indicating whether the registry value should be overwritten if it already exists. - #[serde(rename = "_clobber")] - #[serde(skip_serializing_if = "Option::is_none")] - pub clobber: Option, - /// Flag indicating whether the resource is in the desired state. Value is ignored for input. - #[serde(rename = "_inDesiredState")] - #[serde(skip_serializing_if = "Option::is_none")] - pub in_desired_state: Option, -} - -impl Registry { - pub fn to_json(&self) -> String { - match serde_json::to_string(self) { - Ok(json) => json, - Err(e) => { - eprintln!("Failed to serialize to JSON: {e}"); - String::new() - } - } - } -} - -const ID: &str = "https://developer.microsoft.com/json-schemas/windows/registry/20230303/Microsoft.Windows.Registry.schema.json"; - -impl Default for Registry { - fn default() -> Self { - Self { - id: Some(ID.to_string()), - key_path: String::new(), - value_name: None, - value_data: None, - exist: None, - clobber: None, - in_desired_state: None, - } - } } diff --git a/registry/src/error.rs b/registry/src/error.rs new file mode 100644 index 000000000..0c768fbf4 --- /dev/null +++ b/registry/src/error.rs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use thiserror::Error; + +#[derive(Error, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum RegistryError { + #[error("Invalid hive: {0}.")] + InvalidHive(String), + + #[error("JSON: {0}")] + Json(#[from] serde_json::Error), + + #[error("Registry: {0}")] + Registry(#[from] registry::Error), + + #[error("Registry key: {0}")] + RegistryKey(#[from] registry::key::Error), + + #[error("Registry key not found: {0}")] + RegistryKeyNotFound(String), + + #[error("Registry value: {0}")] + RegistryValue(#[from] registry::value::Error), + + #[error("UTF-16 conversion of {0} failed due to interior NULL values")] + Utf16Conversion(String), + + #[error("Unsupported registry value data type")] + UnsupportedValueDataType, +} diff --git a/registry/src/main.rs b/registry/src/main.rs index fbbc66816..1581740bf 100644 --- a/registry/src/main.rs +++ b/registry/src/main.rs @@ -7,24 +7,21 @@ use crossterm::event; use std::env; use args::Arguments; -use atty::Stream; use clap::Parser; +use registry_helper::RegistryHelper; use schemars::schema_for; -use std::{io::{self, Read}, process::exit}; +use std::process::exit; use crate::config::Registry; mod args; -#[cfg(onecore)] -mod bcrypt; -mod config; -mod regconfighelper; +pub mod config; +mod error; +mod registry_helper; const EXIT_SUCCESS: i32 = 0; -const EXIT_INVALID_PARAMETER: i32 = 1; const EXIT_INVALID_INPUT: i32 = 2; const EXIT_REGISTRY_ERROR: i32 = 3; -const EXIT_JSON_SERIALIZATION_FAILED: i32 = 4; #[allow(clippy::too_many_lines)] fn main() { @@ -32,46 +29,6 @@ fn main() { check_debug(); let args = Arguments::parse(); - let input: Option = if atty::is(Stream::Stdin) { - None - } else { - let mut buffer: Vec = Vec::new(); - io::stdin().read_to_end(&mut buffer).unwrap(); - let input = match String::from_utf8(buffer) { - Ok(input) => input, - Err(e) => { - eprintln!("Invalid UTF-8 sequence: {e}"); - exit(EXIT_INVALID_INPUT); - } - }; - Some(input) - }; - - let mut config: Registry = Registry::default(); - // check if input is valid for subcommand - match args.subcommand { - args::SubCommand::Config { subcommand: _ } => { - if let Some(input) = input { - config = match serde_json::from_str(&input) { - Ok(config) => config, - Err(err) => { - eprintln!("Error JSON does not match schema: {err}"); - exit(EXIT_INVALID_INPUT); - } - }; - } else { - eprintln!("Error: Input JSON via STDIN is required for config subcommand."); - exit(EXIT_INVALID_PARAMETER); - } - } - _ => { - if input.is_some() && !input.as_ref().unwrap().is_empty() { - eprintln!("Error: Input JSON via STDIN is only valid for config subcommand: '{}'", input.unwrap()); - exit(EXIT_INVALID_INPUT); - } - } - } - match args.subcommand { args::SubCommand::Query { key_path, value_name, recurse } => { eprintln!("Get key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); @@ -79,9 +36,6 @@ fn main() { args::SubCommand::Set { key_path, value } => { eprintln!("Set key_path: {key_path}, value: {value}"); }, - args::SubCommand::Test => { - eprintln!("Test"); - }, args::SubCommand::Remove { key_path, value_name, recurse } => { eprintln!("Remove key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); }, @@ -89,66 +43,63 @@ fn main() { eprintln!("Find key_path: {key_path}, find: {find}, recurse: {recurse:?}, keys_only: {keys_only:?}, values_only: {values_only:?}"); }, args::SubCommand::Config { subcommand } => { - let json: String; - if let Err(err) = regconfighelper::validate_config(&config) { - eprintln!("Error validating config: {err}"); - exit(EXIT_INVALID_INPUT); - } - - if config.exist.is_none() { - config.exist = Some(true); - } - match subcommand { - args::ConfigSubCommand::Get => { - match regconfighelper::config_get(&config) { - Ok(config) => { - json = config; + args::ConfigSubCommand::Get{input} => { + let reg_helper = match RegistryHelper::new(&input) { + Ok(reg_helper) => reg_helper, + Err(err) => { + eprintln!("Error: {err}"); + exit(EXIT_INVALID_INPUT); + } + }; + match reg_helper.get() { + Ok(reg_config) => { + let json = serde_json::to_string(®_config).unwrap(); + println!("{json}"); }, Err(err) => { - eprintln!("Error getting config: {err}"); + eprintln!("Error: {err}"); exit(EXIT_REGISTRY_ERROR); } } }, - args::ConfigSubCommand::Set => { - match regconfighelper::config_set(&config) { - Ok(result) => { - json = result; - }, + args::ConfigSubCommand::Set{input} => { + let reg_helper = match RegistryHelper::new(&input) { + Ok(reg_helper) => reg_helper, + Err(err) => { + eprintln!("Error: {err}"); + exit(EXIT_INVALID_INPUT); + } + }; + match reg_helper.set() { + Ok(()) => {}, Err(err) => { - eprintln!("Error setting config: {err}"); + eprintln!("Error: {err}"); exit(EXIT_REGISTRY_ERROR); } } }, - args::ConfigSubCommand::Test => { - match regconfighelper::config_test(&config) { - Ok(result) => { - json = result; - }, + args::ConfigSubCommand::Remove{input} => { + let reg_helper = match RegistryHelper::new(&input) { + Ok(reg_helper) => reg_helper, + Err(err) => { + eprintln!("Error: {err}"); + exit(EXIT_INVALID_INPUT); + } + }; + match reg_helper.remove() { + Ok(()) => {}, Err(err) => { - eprintln!("Error testing config: {err}"); + eprintln!("Error: {err}"); exit(EXIT_REGISTRY_ERROR); } } }, } - - if json.is_empty() { - exit(EXIT_JSON_SERIALIZATION_FAILED); - } - - println!("{json}"); }, - args::SubCommand::Schema { pretty } => { + args::SubCommand::Schema => { let schema = schema_for!(Registry); - let json = if pretty { - serde_json::to_string_pretty(&schema).unwrap() - } - else { - serde_json::to_string(&schema).unwrap() - }; + let json =serde_json::to_string(&schema).unwrap(); println!("{json}"); }, } @@ -161,11 +112,22 @@ fn check_debug() { if env::var("DEBUG_REGISTRY").is_ok() { eprintln!("attach debugger to pid {} and press any key to continue", std::process::id()); loop { - let event = event::read().unwrap(); - if let event::Event::Key(_key) = event { - break; + let event = match event::read() { + Ok(event) => event, + Err(err) => { + eprintln!("Error: Failed to read event: {err}"); + break; + } + }; + if let event::Event::Key(key) = event { + // workaround bug in 0.26+ https://github.com/crossterm-rs/crossterm/issues/752#issuecomment-1414909095 + if key.kind == event::KeyEventKind::Press { + break; + } + } else { + eprintln!("Unexpected event: {event:?}"); + continue; } - eprintln!("Unexpected event: {event:?}"); } } } diff --git a/registry/src/registry_helper.rs b/registry/src/registry_helper.rs new file mode 100644 index 000000000..66b73b478 --- /dev/null +++ b/registry/src/registry_helper.rs @@ -0,0 +1,355 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use registry::{Data, Hive, RegKey, Security, key, value}; +use utfx::{U16CString, UCString}; +use crate::config::{Registry, RegistryValueData}; +use crate::error::RegistryError; + +pub struct RegistryHelper { + config: Registry, + hive: Hive, + subkey: String, +} + +impl RegistryHelper { + pub fn new(config: &str) -> Result { + let registry: Registry = match serde_json::from_str(config) { + Ok(config) => config, + Err(e) => return Err(RegistryError::Json(e)), + }; + let key_path = registry.key_path.clone(); + let (hive, subkey) = get_hive_from_path(&key_path)?; + + Ok( + Self { + config: registry, + hive, + subkey: subkey.to_string(), + } + ) + } + + pub fn get(&self) -> Result { + let exist: bool; + let (reg_key, _subkey) = match self.open(Security::Read) { + Ok((reg_key, subkey)) => { + (reg_key, subkey) + }, + Err(RegistryError::RegistryKeyNotFound(_)) => { + exist = false; + return Ok(Registry { + key_path: self.config.key_path.clone(), + value_name: None, + value_data: None, + exist: Some(exist), + }); + }, + Err(e) => return Err(e), + }; + + if let Some(value_name) = &self.config.value_name { + let value = match reg_key.value(value_name) { + Ok(value) => value, + Err(value::Error::NotFound(_,_)) => { + exist = false; + return Ok(Registry { + key_path: self.config.key_path.clone(), + value_name: Some(value_name.clone()), + value_data: None, + exist: Some(exist), + }); + }, + Err(e) => return Err(RegistryError::RegistryValue(e)), + }; + + Ok(Registry { + key_path: self.config.key_path.clone(), + value_name: Some(value_name.clone()), + value_data: Some(convert_reg_value(&value)?), + exist: None, + }) + } else { + Ok(Registry { + key_path: self.config.key_path.clone(), + value_name: None, + value_data: None, + exist: None, + }) + } + } + + pub fn set(&self) -> Result<(), RegistryError> { + let reg_key = match self.open(Security::Write) { + Ok((reg_key, _subkey)) => reg_key, + // handle NotFound error + Err(RegistryError::RegistryKeyNotFound(_)) => { + // if the key doesn't exist, some of the parent keys may + // not exist either, so we need to find the valid parent key + // and then create the subkeys that don't exist + let (parent_key, subkeys) = self.get_valid_parent_key_and_subkeys()?; + let mut reg_key = parent_key; + for subkey in subkeys { + let Ok(path) = UCString::::from_str(subkey) else { + return Err(RegistryError::Utf16Conversion("subkey".to_string())); + }; + + reg_key = reg_key.create(path, Security::CreateSubKey)?; + } + + self.open(Security::Write)?.0 + }, + Err(e) => return Err(e), + }; + + if let Some(value_data) = &self.config.value_data { + let Ok(value_name) = U16CString::from_str(self.config.value_name.as_ref().unwrap()) else { + return Err(RegistryError::Utf16Conversion("valueName".to_string())); + }; + + match value_data { + RegistryValueData::String(s) => { + let Ok(utf16) = U16CString::from_str(s) else { + return Err(RegistryError::Utf16Conversion("valueData".to_string())); + }; + reg_key.set_value(&value_name, &Data::String(utf16))?; + }, + RegistryValueData::ExpandString(s) => { + let Ok(utf16) = U16CString::from_str(s) else { + return Err(RegistryError::Utf16Conversion("valueData".to_string())); + }; + reg_key.set_value(&value_name, &Data::ExpandString(utf16))?; + }, + RegistryValueData::Binary(b) => { + reg_key.set_value(&value_name, &Data::Binary(b.clone()))?; + }, + RegistryValueData::DWord(d) => { + reg_key.set_value(&value_name, &Data::U32(*d))?; + }, + RegistryValueData::MultiString(m) => { + let mut m16: Vec> = Vec::>::new(); + for s in m { + let Ok(utf16) = U16CString::from_str(s) else { + return Err(RegistryError::Utf16Conversion("valueData".to_string())); + }; + m16.push(utf16); + } + reg_key.set_value(&value_name, &Data::MultiString(m16))?; + }, + RegistryValueData::QWord(q) => { + reg_key.set_value(&value_name, &Data::U64(*q))?; + }, + } + } + + Ok(()) + } + + pub fn remove(&self) -> Result<(), RegistryError> { + let (reg_key, _subkey) = match self.open(Security::AllAccess) { + Ok(reg_key) => reg_key, + // handle NotFound error + Err(RegistryError::RegistryKeyNotFound(_)) => { + eprintln!("Key already does not exist"); + return Ok(()); + }, + Err(e) => return Err(e), + }; + if let Some(value_name) = &self.config.value_name { + reg_key.delete_value(value_name)?; + } else { + // to delete the key, we need to open the parent key first + let parent_path = get_parent_key_path(&self.config.key_path); + let (hive, parent_subkey) = get_hive_from_path(parent_path)?; + let parent_reg_key = hive.open(parent_subkey, Security::AllAccess)?; + + // get the subkey name + let subkey_name = &self.config.key_path[parent_path.len() + 1..]; + eprintln!("Deleting subkey '{subkey_name}' using {parent_reg_key}"); + let Ok(subkey_name) = UCString::::from_str(subkey_name) else { + return Err(RegistryError::Utf16Conversion("subkey_name".to_string())); + }; + + parent_reg_key.delete(subkey_name, true)?; + } + Ok(()) + } + + fn open(&self, permission: Security) -> Result<(RegKey, &str), RegistryError> { + open_regkey(&self.config.key_path, permission) + } + + // Find the valid parent key that exists and the subkeys that don't exist + // the subkeys are returned in reverse order (the closest subkey is the last one in the vector) + fn get_valid_parent_key_and_subkeys(&self) -> Result<(RegKey, Vec<&str>), RegistryError> { + let parent_key: RegKey; + let mut subkeys: Vec<&str> = Vec::new(); + let parent_key_path = get_parent_key_path(&self.subkey); + let subkey_name = &self.subkey[parent_key_path.len() + 1..]; + subkeys.push(subkey_name); + let mut current_key_path = parent_key_path; + + loop { + // we try to open with CreateSubKey permission to know if we can create the key + match self.hive.open(current_key_path, Security::CreateSubKey) { + Ok(regkey) => { + parent_key = regkey; + break; + }, + Err(key::Error::NotFound(_,_)) => { + let parent_key_path = get_parent_key_path(current_key_path); + if parent_key_path.is_empty() { + subkeys.insert(0, current_key_path); + current_key_path = ""; + } else { + let subkey_name = ¤t_key_path[parent_key_path.len() + 1..]; + subkeys.insert(0, subkey_name); + current_key_path = parent_key_path; + } + }, + Err(e) => { + return Err(RegistryError::RegistryKey(e)); + }, + } + } + + Ok((parent_key, subkeys)) + } +} + +fn get_hive_from_path(path: &str) -> Result<(Hive, &str), RegistryError> { + // split the key path to hive and subkey otherwise it's just a hive + let (hive, subkey)= match path.find('\\') { + Some(index) => { + // split at index, but don't include the character at index + let (hive, subkey) = path.split_at(index); + (hive, &subkey[1..]) + }, + None => (path, ""), + }; + + match hive { + "HKCC" | "HKEY_CURRENT_CONFIG" => Ok((Hive::CurrentConfig, subkey)), + "HKCU" | "HKEY_CURRENT_USER" => Ok((Hive::CurrentUser, subkey)), + "HKCR" | "HKEY_CLASSES_ROOT" => Ok((Hive::ClassesRoot, subkey)), + "HKLM" | "HKEY_LOCAL_MACHINE" => Ok((Hive::LocalMachine, subkey)), + "HKU" | "HKEY_USERS" => Ok((Hive::Users, subkey)), + _ => Err(RegistryError::InvalidHive(hive.to_string())) + } +} + +fn open_regkey(path: &str, permission: Security) -> Result<(RegKey, &str), RegistryError> { + let (hive, subkey) = get_hive_from_path(path)?; + match hive.open(subkey, permission) { + Ok(regkey) => Ok((regkey, subkey)), + // handle NotFound error + Err(key::Error::NotFound(_, _)) => { + Err(RegistryError::RegistryKeyNotFound(path.to_string())) + }, + Err(e) => Err(RegistryError::RegistryKey(e)), + } +} + +fn get_parent_key_path(key_path: &str) -> &str { + match key_path.rfind('\\') { + Some(index) => &key_path[..index], + None => "", + } +} + +fn convert_reg_value(value: &Data) -> Result { + match value { + Data::String(s) => Ok(RegistryValueData::String(s.to_string_lossy())), + Data::ExpandString(s) => Ok(RegistryValueData::ExpandString(s.to_string_lossy())), + Data::Binary(b) => Ok(RegistryValueData::Binary(b.clone())), + Data::U32(d) => Ok(RegistryValueData::DWord(*d)), + Data::MultiString(m) => { + let m: Vec = m.iter().map(|s| s.to_string_lossy()).collect(); + Ok(RegistryValueData::MultiString(m)) + }, + Data::U64(q) => Ok(RegistryValueData::QWord(*q)), + _ => Err(RegistryError::UnsupportedValueDataType) + } +} + +#[test] +fn get_hklm_key() { + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKEY_LOCAL_MACHINE"}"#).unwrap(); + let reg_config = reg_helper.get().unwrap(); + assert_eq!(reg_config.key_path, r#"HKEY_LOCAL_MACHINE"#); + assert_eq!(reg_config.value_name, None); + assert_eq!(reg_config.value_data, None); +} + +#[test] +fn get_product_name() { + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","valueName":"ProductName"}"#).unwrap(); + let reg_config = reg_helper.get().unwrap(); + assert_eq!(reg_config.key_path, r#"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion"#); + assert_eq!(reg_config.value_name, Some("ProductName".to_string())); + assert!(matches!(reg_config.value_data, Some(RegistryValueData::String(s)) if s.starts_with("Windows "))); +} + +#[test] +fn get_nonexisting_key() { + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\DoesNotExist"}"#).unwrap(); + let reg_config = reg_helper.get().unwrap(); + assert_eq!(reg_config.key_path, r#"HKCU\DoesNotExist"#); + assert_eq!(reg_config.value_name, None); + assert_eq!(reg_config.value_data, None); + assert_eq!(reg_config.exist, Some(false)); +} + +#[test] +fn get_nonexisting_value() { + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\Software","valueName":"DoesNotExist"}"#).unwrap(); + let reg_config = reg_helper.get().unwrap(); + assert_eq!(reg_config.key_path, r#"HKCU\Software"#); + assert_eq!(reg_config.value_name, Some("DoesNotExist".to_string())); + assert_eq!(reg_config.value_data, None); + assert_eq!(reg_config.exist, Some(false)); +} + +#[test] +fn set_and_remove_test_value() { + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\DSCTest\\DSCSubKey","valueName":"TestValue","valueData": { "String": "Hello"} }"#).unwrap(); + reg_helper.set().unwrap(); + let result = reg_helper.get().unwrap(); + assert_eq!(result.key_path, r#"HKCU\DSCTest\DSCSubKey"#); + assert_eq!(result.value_name, Some("TestValue".to_string())); + assert_eq!(result.value_data, Some(RegistryValueData::String("Hello".to_string()))); + reg_helper.remove().unwrap(); + let result = reg_helper.get().unwrap(); + assert_eq!(result.key_path, r#"HKCU\DSCTest\DSCSubKey"#); + assert_eq!(result.value_name, Some("TestValue".to_string())); + assert_eq!(result.value_data, None); + assert_eq!(result.exist, Some(false)); + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\DSCTest"}"#).unwrap(); + let result = reg_helper.get().unwrap(); + assert_eq!(result.key_path, r#"HKCU\DSCTest"#); + assert_eq!(result.value_name, None); + assert_eq!(result.value_data, None); + reg_helper.remove().unwrap(); + let result = reg_helper.get().unwrap(); + assert_eq!(result.key_path, r#"HKCU\DSCTest"#); + assert_eq!(result.value_name, None); + assert_eq!(result.value_data, None); + assert_eq!(result.exist, Some(false)); +} + +#[test] +fn delete_tree() { + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\DSCTest2\\DSCSubKey","valueName":"TestValue","valueData": { "String": "Hello"} }"#).unwrap(); + reg_helper.set().unwrap(); + let result = reg_helper.get().unwrap(); + assert_eq!(result.key_path, r#"HKCU\DSCTest2\DSCSubKey"#); + assert_eq!(result.value_name, Some("TestValue".to_string())); + assert_eq!(result.value_data, Some(RegistryValueData::String("Hello".to_string()))); + let reg_helper = RegistryHelper::new(r#"{"keyPath":"HKCU\\DSCTest2"}"#).unwrap(); + reg_helper.remove().unwrap(); + let result = reg_helper.get().unwrap(); + assert_eq!(result.key_path, r#"HKCU\DSCTest2"#); + assert_eq!(result.value_name, None); + assert_eq!(result.value_data, None); + assert_eq!(result.exist, Some(false)); +} diff --git a/registry/tests/registry.config.get.tests.ps1 b/registry/tests/registry.config.get.tests.ps1 index 33d8ed344..64f8fe3cf 100644 --- a/registry/tests/registry.config.get.tests.ps1 +++ b/registry/tests/registry.config.get.tests.ps1 @@ -8,11 +8,11 @@ Describe 'Registry config get tests' { "keyPath": "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion" } '@ - $out = $json | registry config get + $out = registry config get --input $json $LASTEXITCODE | Should -Be 0 $result = $out | ConvertFrom-Json $result.keyPath | Should -Be 'HKLM\Software\Microsoft\Windows\CurrentVersion' - ($result.psobject.properties | Measure-Object).Count | Should -Be 2 + ($result.psobject.properties | Measure-Object).Count | Should -Be 1 } It 'Can get a registry value' -Skip:(!$IsWindows) { @@ -22,12 +22,12 @@ Describe 'Registry config get tests' { "valueName": "ProgramFilesPath" } '@ - $out = $json | registry config get + $out = registry config get --input $json $LASTEXITCODE | Should -Be 0 $result = $out | ConvertFrom-Json $result.keyPath | Should -Be 'HKLM\Software\Microsoft\Windows\CurrentVersion' $result.valueName | Should -Be 'ProgramFilesPath' $result.valueData.ExpandString | Should -Be '%ProgramFiles%' - ($result.psobject.properties | Measure-Object).Count | Should -Be 4 + ($result.psobject.properties | Measure-Object).Count | Should -Be 3 } } diff --git a/registry/tests/registry.config.set.tests.ps1 b/registry/tests/registry.config.set.tests.ps1 index 4f9a327a0..815c3b3ee 100644 --- a/registry/tests/registry.config.set.tests.ps1 +++ b/registry/tests/registry.config.set.tests.ps1 @@ -12,35 +12,21 @@ Describe 'registry config set tests' { } } '@ - $out = $json | registry config set + $out = registry config set --input $json $LASTEXITCODE | Should -Be 0 - $result = $out | ConvertFrom-Json + $out | Should -BeNullOrEmpty + $result = registry config get --input $json | ConvertFrom-Json $result.keyPath | Should -Be 'HKCU\1\2\3' $result.valueName | Should -Be 'Hello' $result.valueData.String | Should -Be 'World' - ($result.psobject.properties | Measure-Object).Count | Should -Be 4 + ($result.psobject.properties | Measure-Object).Count | Should -Be 3 - $out = $json | registry config get + $out = registry config get --input $json $LASTEXITCODE | Should -Be 0 $result = $out | ConvertFrom-Json $result.keyPath | Should -Be 'HKCU\1\2\3' $result.valueName | Should -Be 'Hello' $result.valueData.String | Should -Be 'World' - ($result.psobject.properties | Measure-Object).Count | Should -Be 4 - } - - It 'Can set a key to be absent' -Skip:(!$IsWindows) { - $json = @' - { - "keyPath": "HKCU\\1", - "_exist": false - } -'@ - $out = $json | registry config set - $LASTEXITCODE | Should -Be 0 - $result = $out | ConvertFrom-Json - $result.keyPath | Should -BeExactly 'HKCU\1' - $result._exist | Should -Be $false ($result.psobject.properties | Measure-Object).Count | Should -Be 3 } } diff --git a/registry/tests/registry.config.test.tests.ps1 b/registry/tests/registry.config.test.tests.ps1 deleted file mode 100644 index 7bfe0f43d..000000000 --- a/registry/tests/registry.config.test.tests.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -Describe 'registry config test tests' { - It 'Can test a registry key ' -Skip:(!$IsWindows) -TestCases @( - @{ test = 'exists and present'; exist = 'true'; key = 'CurrentVersion' } - @{ test = 'does not exist and absent'; exist = 'false'; key = 'DoesNotExist' } - ){ - param($exist, $key) - $json = @" - { - "keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\$key", - "_exist": $exist - } -"@ - $out = $json | registry config test - $LASTEXITCODE | Should -Be 0 - $result = $out | ConvertFrom-Json - $result.keyPath | Should -BeExactly "HKLM\Software\Microsoft\Windows NT\$key" - $result._exist | Should -Be $exist - ($result.psobject.properties | Measure-Object).Count | Should -Be 4 - } - - It 'Can report failure if a registry key ' -Skip:(!$IsWindows) -TestCases @( - @{ test = 'exists'; exist = 'false'; expectedExist = $true; key = 'CurrentVersion' } - @{ test = 'does not exist'; exist = 'true'; expectedExist = $false; key = 'DoesNotExist' } - ){ - param($exist, $expectedExist, $key) - $json = @" - { - "keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\$key", - "_exist": $exist - } -"@ - $out = $json | registry config test - $LASTEXITCODE | Should -Be 0 - $result = $out | ConvertFrom-Json - $result.keyPath | Should -BeExactly "HKLM\Software\Microsoft\Windows NT\$key" - $result._inDesiredState | Should -Be $false - $result._exist | Should -BeExactly $expectedExist - ($result.psobject.properties | Measure-Object).Count | Should -Be 4 - } - - It 'Can test a registry value exists' -Skip:(!$IsWindows) { - $json = @" - { - "keyPath": "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion", - "valueName": "ProgramFilesPath", - "_exist": true - } -"@ - $out = $json | registry config test - $LASTEXITCODE | Should -Be 0 - $result = $out | ConvertFrom-Json - $result.keyPath | Should -BeExactly 'HKLM\Software\Microsoft\Windows\CurrentVersion' - $result.valueName | Should -BeExactly 'ProgramFilesPath' - $result.valueData.ExpandString | Should -BeExactly '%ProgramFiles%' - $result._inDesiredState | Should -Be $true - $result._exist | Should -Be $true - ($result.psobject.properties | Measure-Object).Count | Should -Be 6 - } -} diff --git a/tools/dsctest/dscexist.dsc.resource.json b/tools/dsctest/dscexist.dsc.resource.json new file mode 100644 index 000000000..0f909fd15 --- /dev/null +++ b/tools/dsctest/dscexist.dsc.resource.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", + "type": "Test/Exist", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "exist", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + } + }, + "set": { + "executable": "dsctest", + "args": [ + "exist", + "--input", + "{json}" + ], + "input": { + "arg": "{json}" + }, + "handlesExist": true, + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "exist" + ] + } + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 6bf2ab0f9..3f9b62dc3 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -6,6 +6,7 @@ use clap::{Parser, Subcommand, ValueEnum}; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] pub enum Schemas { Echo, + Exist, Sleep, } @@ -36,4 +37,10 @@ pub enum SubCommand { #[clap(name = "input", short, long, help = "The input to the sleep command as JSON")] input: String, }, + + #[clap(name = "exist", about = "Check if a resource exists")] + Exist { + #[clap(name = "input", short, long, help = "The input to the exist command as JSON")] + input: String, + }, } diff --git a/tools/dsctest/src/exist.rs b/tools/dsctest/src/exist.rs new file mode 100644 index 000000000..d8b882a68 --- /dev/null +++ b/tools/dsctest/src/exist.rs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub enum State { + Present, + Absent, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Exist { + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + #[serde(rename = "_exist")] + pub exist: bool, +} diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 1862ea7b8..67f890a9e 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -3,12 +3,14 @@ mod args; mod echo; +mod exist; mod sleep; use args::{Args, Schemas, SubCommand}; use clap::Parser; use schemars::schema_for; use crate::echo::Echo; +use crate::exist::{Exist, State}; use crate::sleep::Sleep; use std::{thread, time::Duration}; @@ -25,11 +27,30 @@ fn main() { }; serde_json::to_string(&echo).unwrap() }, + SubCommand::Exist { input } => { + let mut exist = match serde_json::from_str::(&input) { + Ok(exist) => exist, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + if exist.exist { + exist.state = Some(State::Present); + } else { + exist.state = Some(State::Absent); + } + + serde_json::to_string(&exist).unwrap() + }, SubCommand::Schema { subcommand } => { let schema = match subcommand { Schemas::Echo => { schema_for!(Echo) }, + Schemas::Exist => { + schema_for!(Exist) + }, Schemas::Sleep => { schema_for!(Sleep) }, diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index b7e2b3ea8..cf2e742a4 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -38,6 +38,7 @@ fn main() { }, set: None, test: None, + delete: None, export: None, validate: None, adapter: None, @@ -71,6 +72,7 @@ fn main() { }, set: None, test: None, + delete: None, export: None, validate: None, adapter: None,