From 91d0ccf3ea45b96e666e13d6076ef502ab55a478 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 19:18:08 +1000 Subject: [PATCH 01/18] Initial build --- resources/dism_dsc/locales/en-us.toml | 1 + .../optionalfeature.dsc.resource.json | 8 +++++- resources/dism_dsc/src/dism.rs | 14 +++++++--- .../dism_dsc/src/optional_feature/export.rs | 2 +- .../dism_dsc/src/optional_feature/get.rs | 2 +- .../dism_dsc/src/optional_feature/set.rs | 27 ++++++++++++------- .../dism_dsc/src/optional_feature/types.rs | 2 ++ 7 files changed, 40 insertions(+), 16 deletions(-) diff --git a/resources/dism_dsc/locales/en-us.toml b/resources/dism_dsc/locales/en-us.toml index 19a945a67..ebda4a505 100644 --- a/resources/dism_dsc/locales/en-us.toml +++ b/resources/dism_dsc/locales/en-us.toml @@ -40,6 +40,7 @@ failedSerializeOutput = "Failed to serialize output: %{err}" [set] failedParseInput = "Failed to parse input: %{err}" featuresArrayEmpty = "Features array cannot be empty for set operation" +sourcePathInvalid = "sourcePath '%{path}' is an invalid path" featureNameRequired = "featureName is required for set operation" stateRequired = "state is required for set operation" unsupportedDesiredState = "Unsupported desired state '%{state}'. Supported states for set are: Installed, NotPresent, Removed" diff --git a/resources/dism_dsc/optionalfeature.dsc.resource.json b/resources/dism_dsc/optionalfeature.dsc.resource.json index 77c4b82c9..5d7665d8c 100644 --- a/resources/dism_dsc/optionalfeature.dsc.resource.json +++ b/resources/dism_dsc/optionalfeature.dsc.resource.json @@ -91,7 +91,8 @@ "PartiallyInstalled" ], "title": "Feature state", - "description": "The current state of the optional feature. For set operations, only Installed, NotPresent, and Removed are accepted; other states are returned by get and export operations and are not valid inputs for set."}, + "description": "The current state of the optional feature. For set operations, only Installed, NotPresent, and Removed are accepted; other states are returned by get and export operations and are not valid inputs for set." + }, "displayName": { "type": "string", "title": "Display name", @@ -114,6 +115,11 @@ } } } + }, + "sourcePath": { + "type": "string", + "title": "Source path", + "description": "Specifies the path to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." } } } diff --git a/resources/dism_dsc/src/dism.rs b/resources/dism_dsc/src/dism.rs index aa693d8b0..dce685b49 100644 --- a/resources/dism_dsc/src/dism.rs +++ b/resources/dism_dsc/src/dism.rs @@ -319,8 +319,16 @@ impl DismSessionHandle { } /// Returns `Ok(true)` if DISM reports a reboot is required (HRESULT 3010). - pub fn enable_feature(&self, feature_name: &str) -> Result { + pub fn enable_feature(&self, feature_name: &str, source_paths: &Option>) -> Result { let wide_name = to_wide_null(feature_name); + + let source_count: u32 = source_paths.as_ref().map_or(0, |paths| paths.len() as u32); + let wide_source_paths: Option> = source_paths.as_ref().map(|paths| { + paths.into_iter().map(|p| to_wide_null(&p)).collect() + }); + let sources = wide_source_paths.map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); + let sources_ptr = sources.map_or(std::ptr::null(), |v| v.as_ptr()); + let hr = unsafe { (self.api.enable_feature)( self.handle, @@ -328,8 +336,8 @@ impl DismSessionHandle { std::ptr::null(), // Identifier DISM_PACKAGE_NONE, // PackageIdentifier 0, // LimitAccess = FALSE - std::ptr::null(), // SourcePaths - 0, // SourcePathCount + sources_ptr, // SourcePaths + source_count, // SourcePathCount 0, // EnableAll = FALSE std::ptr::null_mut(), // CancelEvent std::ptr::null_mut(), // Progress diff --git a/resources/dism_dsc/src/optional_feature/export.rs b/resources/dism_dsc/src/optional_feature/export.rs index 5b19f95e3..e41cc5120 100644 --- a/resources/dism_dsc/src/optional_feature/export.rs +++ b/resources/dism_dsc/src/optional_feature/export.rs @@ -88,7 +88,7 @@ pub fn handle_export(input: &str) -> Result { } } - let output = OptionalFeatureList { restart_required_meta: None, features: results }; + let output = OptionalFeatureList { restart_required_meta: None, source_path: None, features: results }; serde_json::to_string(&output) .map_err(|e| t!("export.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/get.rs b/resources/dism_dsc/src/optional_feature/get.rs index bb730cc4d..81232226b 100644 --- a/resources/dism_dsc/src/optional_feature/get.rs +++ b/resources/dism_dsc/src/optional_feature/get.rs @@ -27,7 +27,7 @@ pub fn handle_get(input: &str) -> Result { results.push(info); } - let output = OptionalFeatureList { restart_required_meta: None, features: results }; + let output = OptionalFeatureList { restart_required_meta: None, source_path: feature_list.source_path, features: results }; serde_json::to_string(&output) .map_err(|e| t!("get.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/set.rs b/resources/dism_dsc/src/optional_feature/set.rs index 46634b8f1..a4b655d71 100644 --- a/resources/dism_dsc/src/optional_feature/set.rs +++ b/resources/dism_dsc/src/optional_feature/set.rs @@ -16,6 +16,15 @@ pub fn handle_set(input: &str) -> Result { return Err(t!("set.featuresArrayEmpty").to_string()); } + // Validate source paths + if let Some(paths) = &feature_list.source_path { + for path in paths { + if !std::fs::exists(path).unwrap_or(false) { + return Err(t!("set.sourcePathInvalid", path = path).to_string()); + } + } + } + let session = DismSessionHandle::open()?; let mut results = Vec::new(); let mut reboot_required = false; @@ -32,15 +41,9 @@ pub fn handle_set(input: &str) -> Result { .ok_or_else(|| t!("set.stateRequired").to_string())?; let needs_reboot = match desired_state { - FeatureState::Installed => { - session.enable_feature(feature_name)? - } - FeatureState::NotPresent => { - session.disable_feature(feature_name, false)? - } - FeatureState::Removed => { - session.disable_feature(feature_name, true)? - } + FeatureState::Installed => session.enable_feature(feature_name, &feature_list.source_path)?, + FeatureState::NotPresent => session.disable_feature(feature_name, false)?, + FeatureState::Removed => session.disable_feature(feature_name, true)?, _ => { return Err(t!( "set.unsupportedDesiredState", @@ -64,7 +67,11 @@ pub fn handle_set(input: &str) -> Result { None }; - let output = OptionalFeatureList { restart_required_meta, features: results }; + let output = OptionalFeatureList { + restart_required_meta, + features: results, + source_path: feature_list.source_path, + }; serde_json::to_string(&output) .map_err(|e| t!("set.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/types.rs b/resources/dism_dsc/src/optional_feature/types.rs index 6415d6a48..0399e96ce 100644 --- a/resources/dism_dsc/src/optional_feature/types.rs +++ b/resources/dism_dsc/src/optional_feature/types.rs @@ -13,6 +13,8 @@ pub type FeatureState = DismState; pub struct OptionalFeatureList { #[serde(rename = "_restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required_meta: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_path: Option>, pub features: Vec, } From 885466189e8fb86e435808f0e322bd9559bdd067 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 19:25:25 +1000 Subject: [PATCH 02/18] Port to feature on demand --- .../dism_dsc/featureondemand.dsc.resource.json | 5 +++++ resources/dism_dsc/src/dism.rs | 14 +++++++++++--- .../dism_dsc/src/feature_on_demand/export.rs | 2 +- resources/dism_dsc/src/feature_on_demand/get.rs | 6 +++++- resources/dism_dsc/src/feature_on_demand/set.rs | 17 +++++++++++++++-- .../dism_dsc/src/feature_on_demand/types.rs | 2 ++ 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/resources/dism_dsc/featureondemand.dsc.resource.json b/resources/dism_dsc/featureondemand.dsc.resource.json index 0ab06bf78..0e63f58cf 100644 --- a/resources/dism_dsc/featureondemand.dsc.resource.json +++ b/resources/dism_dsc/featureondemand.dsc.resource.json @@ -116,6 +116,11 @@ } } } + }, + "sourcePath": { + "type": "string", + "title": "Source path", + "description": "Specifies the path to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." } } } diff --git a/resources/dism_dsc/src/dism.rs b/resources/dism_dsc/src/dism.rs index dce685b49..b36f305ac 100644 --- a/resources/dism_dsc/src/dism.rs +++ b/resources/dism_dsc/src/dism.rs @@ -465,18 +465,26 @@ impl DismSessionHandle { } /// Returns `Ok(true)` if DISM reports a reboot is required (HRESULT 3010). - pub fn add_capability(&self, name: &str) -> Result { + pub fn add_capability(&self, name: &str, source_paths: &Option>) -> Result { let add_cap = self.api.add_capability .ok_or_else(|| t!("dism.capabilitiesNotSupported").to_string())?; let wide_name = to_wide_null(name); + + let source_count: u32 = source_paths.as_ref().map_or(0, |paths| paths.len() as u32); + let wide_source_paths: Option> = source_paths.as_ref().map(|paths| { + paths.into_iter().map(|p| to_wide_null(&p)).collect() + }); + let sources = wide_source_paths.map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); + let sources_ptr = sources.map_or(std::ptr::null(), |v| v.as_ptr()); + let hr = unsafe { add_cap( self.handle, wide_name.as_ptr(), 0, // LimitAccess = FALSE - std::ptr::null(), // SourcePaths - 0, // SourcePathCount + sources_ptr, // SourcePaths + source_count, // SourcePathCount std::ptr::null_mut(), // CancelEvent std::ptr::null_mut(), // Progress std::ptr::null_mut(), // UserData diff --git a/resources/dism_dsc/src/feature_on_demand/export.rs b/resources/dism_dsc/src/feature_on_demand/export.rs index 6709a6a7e..b2b91eba0 100644 --- a/resources/dism_dsc/src/feature_on_demand/export.rs +++ b/resources/dism_dsc/src/feature_on_demand/export.rs @@ -91,7 +91,7 @@ pub fn handle_export(input: &str) -> Result { } } - let output = FeatureOnDemandList { restart_required_meta: None, capabilities: results }; + let output = FeatureOnDemandList { restart_required_meta: None, source_path: None, capabilities: results }; serde_json::to_string(&output) .map_err(|e| t!("fod_export.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/feature_on_demand/get.rs b/resources/dism_dsc/src/feature_on_demand/get.rs index db5087bd3..63f0d71df 100644 --- a/resources/dism_dsc/src/feature_on_demand/get.rs +++ b/resources/dism_dsc/src/feature_on_demand/get.rs @@ -44,7 +44,11 @@ pub fn handle_get(input: &str) -> Result { results.push(info); } - let output = FeatureOnDemandList { restart_required_meta: None, capabilities: results }; + let output = FeatureOnDemandList { + restart_required_meta: None, + source_path: capability_list.source_path, + capabilities: results + }; serde_json::to_string(&output) .map_err(|e| t!("fod_get.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/feature_on_demand/set.rs b/resources/dism_dsc/src/feature_on_demand/set.rs index afd055677..5f741845b 100644 --- a/resources/dism_dsc/src/feature_on_demand/set.rs +++ b/resources/dism_dsc/src/feature_on_demand/set.rs @@ -16,6 +16,15 @@ pub fn handle_set(input: &str) -> Result { return Err(t!("fod_set.capabilitiesArrayEmpty").to_string()); } + // Validate source paths + if let Some(paths) = &capability_list.source_path { + for path in paths { + if !std::fs::exists(path).unwrap_or(false) { + return Err(t!("set.sourcePathInvalid", path = path).to_string()); + } + } + } + let session = DismSessionHandle::open()?; let mut results = Vec::new(); let mut reboot_required = false; @@ -43,7 +52,7 @@ pub fn handle_set(input: &str) -> Result { CapabilityState::Installed => { match current_state { Some(CapabilityState::Installed) => false, - _ => session.add_capability(identity)?, + _ => session.add_capability(identity, &capability_list.source_path)?, } } CapabilityState::NotPresent => { @@ -84,7 +93,11 @@ pub fn handle_set(input: &str) -> Result { None }; - let output = FeatureOnDemandList { restart_required_meta, capabilities: results }; + let output = FeatureOnDemandList { + restart_required_meta, + source_path: capability_list.source_path, + capabilities: results + }; serde_json::to_string(&output) .map_err(|e| t!("fod_set.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/feature_on_demand/types.rs b/resources/dism_dsc/src/feature_on_demand/types.rs index 7b91ee57e..762fb8093 100644 --- a/resources/dism_dsc/src/feature_on_demand/types.rs +++ b/resources/dism_dsc/src/feature_on_demand/types.rs @@ -13,6 +13,8 @@ pub type CapabilityState = DismState; pub struct FeatureOnDemandList { #[serde(rename = "_restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required_meta: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_path: Option>, pub capabilities: Vec, } From ccd1e75b5d9555a3fad2d9ea94d16f49e7af8fb8 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 19:55:10 +1000 Subject: [PATCH 03/18] Change paths to plural, fix schema --- resources/dism_dsc/featureondemand.dsc.resource.json | 8 ++++---- resources/dism_dsc/optionalfeature.dsc.resource.json | 8 ++++---- resources/dism_dsc/src/feature_on_demand/export.rs | 2 +- resources/dism_dsc/src/feature_on_demand/get.rs | 2 +- resources/dism_dsc/src/feature_on_demand/set.rs | 6 +++--- resources/dism_dsc/src/feature_on_demand/types.rs | 2 +- resources/dism_dsc/src/optional_feature/export.rs | 2 +- resources/dism_dsc/src/optional_feature/get.rs | 2 +- resources/dism_dsc/src/optional_feature/set.rs | 6 +++--- resources/dism_dsc/src/optional_feature/types.rs | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/resources/dism_dsc/featureondemand.dsc.resource.json b/resources/dism_dsc/featureondemand.dsc.resource.json index 0e63f58cf..61185022c 100644 --- a/resources/dism_dsc/featureondemand.dsc.resource.json +++ b/resources/dism_dsc/featureondemand.dsc.resource.json @@ -117,10 +117,10 @@ } } }, - "sourcePath": { - "type": "string", - "title": "Source path", - "description": "Specifies the path to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." + "sourcePaths": { + "type": "array", + "title": "Source paths", + "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." } } } diff --git a/resources/dism_dsc/optionalfeature.dsc.resource.json b/resources/dism_dsc/optionalfeature.dsc.resource.json index 5d7665d8c..1c6dab468 100644 --- a/resources/dism_dsc/optionalfeature.dsc.resource.json +++ b/resources/dism_dsc/optionalfeature.dsc.resource.json @@ -116,10 +116,10 @@ } } }, - "sourcePath": { - "type": "string", - "title": "Source path", - "description": "Specifies the path to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." + "sourcePaths": { + "type": "array", + "title": "Source paths", + "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." } } } diff --git a/resources/dism_dsc/src/feature_on_demand/export.rs b/resources/dism_dsc/src/feature_on_demand/export.rs index b2b91eba0..ed1f8bf2d 100644 --- a/resources/dism_dsc/src/feature_on_demand/export.rs +++ b/resources/dism_dsc/src/feature_on_demand/export.rs @@ -91,7 +91,7 @@ pub fn handle_export(input: &str) -> Result { } } - let output = FeatureOnDemandList { restart_required_meta: None, source_path: None, capabilities: results }; + let output = FeatureOnDemandList { restart_required_meta: None, source_paths: None, capabilities: results }; serde_json::to_string(&output) .map_err(|e| t!("fod_export.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/feature_on_demand/get.rs b/resources/dism_dsc/src/feature_on_demand/get.rs index 63f0d71df..e853c7c11 100644 --- a/resources/dism_dsc/src/feature_on_demand/get.rs +++ b/resources/dism_dsc/src/feature_on_demand/get.rs @@ -46,7 +46,7 @@ pub fn handle_get(input: &str) -> Result { let output = FeatureOnDemandList { restart_required_meta: None, - source_path: capability_list.source_path, + source_paths: capability_list.source_paths, capabilities: results }; serde_json::to_string(&output) diff --git a/resources/dism_dsc/src/feature_on_demand/set.rs b/resources/dism_dsc/src/feature_on_demand/set.rs index 5f741845b..6193164bf 100644 --- a/resources/dism_dsc/src/feature_on_demand/set.rs +++ b/resources/dism_dsc/src/feature_on_demand/set.rs @@ -17,7 +17,7 @@ pub fn handle_set(input: &str) -> Result { } // Validate source paths - if let Some(paths) = &capability_list.source_path { + if let Some(paths) = &capability_list.source_paths { for path in paths { if !std::fs::exists(path).unwrap_or(false) { return Err(t!("set.sourcePathInvalid", path = path).to_string()); @@ -52,7 +52,7 @@ pub fn handle_set(input: &str) -> Result { CapabilityState::Installed => { match current_state { Some(CapabilityState::Installed) => false, - _ => session.add_capability(identity, &capability_list.source_path)?, + _ => session.add_capability(identity, &capability_list.source_paths)?, } } CapabilityState::NotPresent => { @@ -95,7 +95,7 @@ pub fn handle_set(input: &str) -> Result { let output = FeatureOnDemandList { restart_required_meta, - source_path: capability_list.source_path, + source_paths: capability_list.source_paths, capabilities: results }; serde_json::to_string(&output) diff --git a/resources/dism_dsc/src/feature_on_demand/types.rs b/resources/dism_dsc/src/feature_on_demand/types.rs index 762fb8093..f5d15a1e4 100644 --- a/resources/dism_dsc/src/feature_on_demand/types.rs +++ b/resources/dism_dsc/src/feature_on_demand/types.rs @@ -14,7 +14,7 @@ pub struct FeatureOnDemandList { #[serde(rename = "_restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required_meta: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - pub source_path: Option>, + pub source_paths: Option>, pub capabilities: Vec, } diff --git a/resources/dism_dsc/src/optional_feature/export.rs b/resources/dism_dsc/src/optional_feature/export.rs index e41cc5120..ccb40d2da 100644 --- a/resources/dism_dsc/src/optional_feature/export.rs +++ b/resources/dism_dsc/src/optional_feature/export.rs @@ -88,7 +88,7 @@ pub fn handle_export(input: &str) -> Result { } } - let output = OptionalFeatureList { restart_required_meta: None, source_path: None, features: results }; + let output = OptionalFeatureList { restart_required_meta: None, source_paths: None, features: results }; serde_json::to_string(&output) .map_err(|e| t!("export.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/get.rs b/resources/dism_dsc/src/optional_feature/get.rs index 81232226b..a9d1799c1 100644 --- a/resources/dism_dsc/src/optional_feature/get.rs +++ b/resources/dism_dsc/src/optional_feature/get.rs @@ -27,7 +27,7 @@ pub fn handle_get(input: &str) -> Result { results.push(info); } - let output = OptionalFeatureList { restart_required_meta: None, source_path: feature_list.source_path, features: results }; + let output = OptionalFeatureList { restart_required_meta: None, source_paths: feature_list.source_paths, features: results }; serde_json::to_string(&output) .map_err(|e| t!("get.failedSerializeOutput", err = e.to_string()).to_string()) } diff --git a/resources/dism_dsc/src/optional_feature/set.rs b/resources/dism_dsc/src/optional_feature/set.rs index a4b655d71..91191eab6 100644 --- a/resources/dism_dsc/src/optional_feature/set.rs +++ b/resources/dism_dsc/src/optional_feature/set.rs @@ -17,7 +17,7 @@ pub fn handle_set(input: &str) -> Result { } // Validate source paths - if let Some(paths) = &feature_list.source_path { + if let Some(paths) = &feature_list.source_paths { for path in paths { if !std::fs::exists(path).unwrap_or(false) { return Err(t!("set.sourcePathInvalid", path = path).to_string()); @@ -41,7 +41,7 @@ pub fn handle_set(input: &str) -> Result { .ok_or_else(|| t!("set.stateRequired").to_string())?; let needs_reboot = match desired_state { - FeatureState::Installed => session.enable_feature(feature_name, &feature_list.source_path)?, + FeatureState::Installed => session.enable_feature(feature_name, &feature_list.source_paths)?, FeatureState::NotPresent => session.disable_feature(feature_name, false)?, FeatureState::Removed => session.disable_feature(feature_name, true)?, _ => { @@ -70,7 +70,7 @@ pub fn handle_set(input: &str) -> Result { let output = OptionalFeatureList { restart_required_meta, features: results, - source_path: feature_list.source_path, + source_paths: feature_list.source_paths, }; serde_json::to_string(&output) .map_err(|e| t!("set.failedSerializeOutput", err = e.to_string()).to_string()) diff --git a/resources/dism_dsc/src/optional_feature/types.rs b/resources/dism_dsc/src/optional_feature/types.rs index 0399e96ce..b53426e68 100644 --- a/resources/dism_dsc/src/optional_feature/types.rs +++ b/resources/dism_dsc/src/optional_feature/types.rs @@ -14,7 +14,7 @@ pub struct OptionalFeatureList { #[serde(rename = "_restartRequired", skip_serializing_if = "Option::is_none")] pub restart_required_meta: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - pub source_path: Option>, + pub source_paths: Option>, pub features: Vec, } From 1ac5a522647de7998e3253ec99b395e9ed71680d Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 19:58:42 +1000 Subject: [PATCH 04/18] Add item type to schema --- resources/dism_dsc/featureondemand.dsc.resource.json | 5 ++++- resources/dism_dsc/optionalfeature.dsc.resource.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/dism_dsc/featureondemand.dsc.resource.json b/resources/dism_dsc/featureondemand.dsc.resource.json index 61185022c..9a5ae0bad 100644 --- a/resources/dism_dsc/featureondemand.dsc.resource.json +++ b/resources/dism_dsc/featureondemand.dsc.resource.json @@ -120,7 +120,10 @@ "sourcePaths": { "type": "array", "title": "Source paths", - "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." + "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations.", + "items": { + "type": "string" + } } } } diff --git a/resources/dism_dsc/optionalfeature.dsc.resource.json b/resources/dism_dsc/optionalfeature.dsc.resource.json index 1c6dab468..2bafa88d1 100644 --- a/resources/dism_dsc/optionalfeature.dsc.resource.json +++ b/resources/dism_dsc/optionalfeature.dsc.resource.json @@ -119,7 +119,10 @@ "sourcePaths": { "type": "array", "title": "Source paths", - "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations." + "description": "An array of paths to feature files if the files are not available in the local feature store. This is an optional property for set operations and is ignored for get operations.", + "items": { + "type": "string" + } } } } From 9a05ad23abeebc29a46d53e306c0bb25d5872702 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 20:31:46 +1000 Subject: [PATCH 05/18] Update docs --- .../install-remove-feature-on-demand.md | 46 +++++++++++++++++++ .../Windows/FeatureOnDemandList/index.md | 36 +++++++++++++++ .../enable-disable-optional-features.md | 44 ++++++++++++++++++ .../Windows/OptionalFeatureList/index.md | 38 ++++++++++++++- 4 files changed, 163 insertions(+), 1 deletion(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md index 6bd5ee93a..1b840fa7e 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md @@ -115,6 +115,52 @@ changedProperties: - capabilities ``` +## Install a feature on demand using an offline source + +To allow DISM to install capabilities that are not present on the machine in a offline environment, +add the offline Windows image file (WIM) path to the `sourcePaths` property. + +```powershell +$instance = @{ + sourcePaths = @('z:\sources\SxS') + capabilities = @( + @{ + identity = 'NetFX3~~~~' + state = 'Installed' + } + ) +} | ConvertTo-Json -Depth 3 + +dsc resource set --resource Microsoft.Windows/FeatureOnDemandList --input $instance +``` + +When the resource installs the capability, DSC returns the updated state: + +```yaml +beforeState: + sourcePath: + - z:\sources\SxS + capabilities: + - identitiy: NetFX3~~~~ + state: NotPresent + displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) + description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) + downloadSize: 0 + installSize: 487706170 +afterState: + sourcePath: + - z:\sources\SxS + capabilities: + - identitiy: NetFX3~~~~ + state: Installed + displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) + description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) + downloadSize: 487706170 + installSize: 487706170 +changedProperties: +- capabilities +``` + ## Manage multiple capabilities in a single operation You can install or remove multiple capabilities in a single **Set** call by specifying multiple diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index 1e5e0bbde..c1d71bb41 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -96,6 +96,12 @@ The following list describes the properties for the resource. - [capabilities](#capabilities) - An array of capability entries. +- **Instance properties:** The following properties are optional. + They define the desired state for an instance of the resource. + + - [sourcePaths](#sourcePaths) - The location of the source files to use for installation if + necessary. + - **Read-only properties:** The resource returns the following properties, but they aren't configurable. For more information about read-only properties, see the "Read-only resource properties" section in [DSC resource properties][05]. @@ -277,6 +283,28 @@ IsReadOnly : true The size in bytes that the capability occupies on disk after installation. This property is returned by **Get** and **Export** operations. +### sourcePaths + +
Expand for sourcePaths property metadata + +```yaml +Type : array +IsRequired : false +IsKey : false +IsReadOnly : false +``` + +
+ +Supplied at the top level of the **Set** operation, indicates the location of the source files to +use for installation if necessary. The DISM API will search these paths if the feature files are +not available in the local feature store. + +All paths supplied must be valid and existing local or network path to a Windows image file +(WIM). See the [feature on demand repository documentation][08] for more. + +This property is optional and will be omitted from the response if empty. + ### _restartRequired
Expand for _restartRequired property metadata @@ -314,6 +342,12 @@ The following snippet contains the JSON Schema that validates an instance of the "additionalProperties": true } }, + "sourcePaths": { + "type": "array", + "items": { + "type": "string" + } + }, "capabilities": { "type": "array", "items": { @@ -364,6 +398,7 @@ Common causes include: - The requested capability `identity` is not recognized by DISM. - The DISM API returned an error while querying or modifying capability state. - The process is not running with elevated privileges. +- An invalid path was supplied to `sourcePaths` in a **Set** operation. ## See also @@ -378,3 +413,4 @@ Common causes include: [05]: ../../../../../concepts/resources/properties.md#read-only-resource-properties [06]: ../OptionalFeatureList/index.md [07]: /windows-hardware/manufacture/desktop/features-on-demand-v2--capabilities +[08]: /windows-hardware/manufacture/desktop/features-on-demand-v2--capabilities?view=windows-11#fod-repositories \ No newline at end of file diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md index 465597108..5f2120a32 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md @@ -146,6 +146,50 @@ changedProperties: - features ``` +## Enable an optional feature using an offline source + +To allow DISM to install features that are not present on the machine in a offline environment, add +the offline Windows image file (WIM) path to the `sourcePaths` property. + +```powershell +$instance = @{ + sourcePaths = @('z:\sources\SxS') + features = @( + @{ + featureName = 'NetFx3' + state = 'Installed' + } + ) +} | ConvertTo-Json -Depth 3 + +dsc resource set --resource Microsoft.Windows/OptionalFeatureList --input $instance +``` + +When the resource enables the feature, DSC returns the updated state: + +```yaml +beforeState: + sourcePath: + - z:\sources\SxS + features: + - featureName: NetFx3 + state: NotPresent + displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) + description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) + restartRequired: No +afterState: + sourcePath: + - z:\sources\SxS + features: + - featureName: NetFx3 + state: Installed + displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) + description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) + restartRequired: Possible +changedProperties: +- features +``` + ## Manage multiple features in a single operation You can enable or disable multiple features in a single **Set** call by specifying multiple entries diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index f08d4bd47..9b6c47f0d 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -94,6 +94,12 @@ The following list describes the properties for the resource. - [features](#features) - An array of optional feature entries. +- **Instance properties:** The following properties are optional. + They define the desired state for an instance of the resource. + + - [sourcePaths](#sourcePaths) - The location of the source files to use for installation if + necessary. + - **Read-only properties:** The resource returns the following properties, but they aren't configurable. For more information about read-only properties, see the "Read-only resource properties" section in [DSC resource properties][05]. @@ -262,6 +268,28 @@ property is returned by **Get** and **Export** operations and cannot be set. | `Possible` | A restart may be required depending on system conditions. | | `Required` | A restart is required to complete the state change. | +### sourcePaths + +
Expand for sourcePaths property metadata + +```yaml +Type : array +IsRequired : false +IsKey : false +IsReadOnly : false +``` + +
+ +Supplied at the top level of the **Set** operation, indicates the location of the source files to +use for installation if necessary. The DISM API will search these paths if the feature files are +not available in the local feature store. + +All paths supplied must be valid and existing local or network path to a Windows image file +(WIM). See the [DISM enable feature documentation][08] for more. + +This property is optional and will be omitted from the response if empty. + ### _restartRequired
Expand for _restartRequired property metadata @@ -298,6 +326,12 @@ The following snippet contains the JSON Schema that validates an instance of the "type": "object", "additionalProperties": true } + }, + "sourcePaths": { + "type": "array", + "items": { + "type": "string" + } }, "features": { "type": "array", @@ -351,6 +385,7 @@ Common causes include: `Removed`). - The DISM API returned an error while querying or modifying feature state. - The process is not running with elevated privileges. +- An invalid path was supplied to `sourcePaths` in a **Set** operation. ## See also @@ -364,4 +399,5 @@ Common causes include: [04]: ./examples/export-optional-features.md [05]: ../../../../../concepts/resources/properties.md#read-only-resource-properties [06]: ../FeatureOnDemandList/index.md -[07]: /windows-server/administration/windows-commands/dism/dism-operating-system-package-servicing-command-line-options +[07]: /windows-hardware/manufacture/desktop/deployment-image-servicing-and-management--dism--command-line-options +[08]: /windows-hardware/manufacture/desktop/dism-operating-system-package-servicing-command-line-options#enable-feature \ No newline at end of file From 6f046366d947eb7b295f6f9ec81918a798dea9e6 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 20:34:21 +1000 Subject: [PATCH 06/18] Fix incorrect example output --- .../examples/install-remove-feature-on-demand.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md index 1b840fa7e..0fce1d834 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md @@ -143,8 +143,8 @@ beforeState: capabilities: - identitiy: NetFX3~~~~ state: NotPresent - displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) - description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) + displayName: '' + description: '' downloadSize: 0 installSize: 487706170 afterState: @@ -153,9 +153,9 @@ afterState: capabilities: - identitiy: NetFX3~~~~ state: Installed - displayName: NET Framework 3.5 (includes .NET 2.0 and 3.0) - description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) - downloadSize: 487706170 + displayName: '' + description: '' + downloadSize: 0 installSize: 487706170 changedProperties: - capabilities From 66e963ac9e56129be403a4cf88096fb9bd561d5d Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 20:43:01 +1000 Subject: [PATCH 07/18] Fix sourcepath type in examples --- .../examples/install-remove-feature-on-demand.md | 4 ++-- .../examples/enable-disable-optional-features.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md index 0fce1d834..de0d839a1 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md @@ -138,7 +138,7 @@ When the resource installs the capability, DSC returns the updated state: ```yaml beforeState: - sourcePath: + sourcePaths: - z:\sources\SxS capabilities: - identitiy: NetFX3~~~~ @@ -148,7 +148,7 @@ beforeState: downloadSize: 0 installSize: 487706170 afterState: - sourcePath: + sourcePaths: - z:\sources\SxS capabilities: - identitiy: NetFX3~~~~ diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md index 5f2120a32..aeeac7efa 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md @@ -169,7 +169,7 @@ When the resource enables the feature, DSC returns the updated state: ```yaml beforeState: - sourcePath: + sourcePaths: - z:\sources\SxS features: - featureName: NetFx3 @@ -178,7 +178,7 @@ beforeState: description: .NET Framework 3.5 (includes .NET 2.0 and 3.0) restartRequired: No afterState: - sourcePath: + sourcePaths: - z:\sources\SxS features: - featureName: NetFx3 From 6da2c305f0ea63f93c78a7be3f5f657ab2467782 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 20:45:38 +1000 Subject: [PATCH 08/18] Fix incorrect typos --- .../examples/install-remove-feature-on-demand.md | 4 ++-- resources/dism_dsc/src/feature_on_demand/set.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md index de0d839a1..cfe9ef2b7 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md @@ -141,7 +141,7 @@ beforeState: sourcePaths: - z:\sources\SxS capabilities: - - identitiy: NetFX3~~~~ + - identity: NetFX3~~~~ state: NotPresent displayName: '' description: '' @@ -151,7 +151,7 @@ afterState: sourcePaths: - z:\sources\SxS capabilities: - - identitiy: NetFX3~~~~ + - identity: NetFX3~~~~ state: Installed displayName: '' description: '' diff --git a/resources/dism_dsc/src/feature_on_demand/set.rs b/resources/dism_dsc/src/feature_on_demand/set.rs index 6193164bf..c4ddf58e6 100644 --- a/resources/dism_dsc/src/feature_on_demand/set.rs +++ b/resources/dism_dsc/src/feature_on_demand/set.rs @@ -20,7 +20,7 @@ pub fn handle_set(input: &str) -> Result { if let Some(paths) = &capability_list.source_paths { for path in paths { if !std::fs::exists(path).unwrap_or(false) { - return Err(t!("set.sourcePathInvalid", path = path).to_string()); + return Err(t!("fod_set.sourcePathInvalid", path = path).to_string()); } } } From 88e1e8edc631a08b13219abd62c571aa85b462d3 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 20:47:25 +1000 Subject: [PATCH 09/18] Fix localisations --- resources/dism_dsc/locales/en-us.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/dism_dsc/locales/en-us.toml b/resources/dism_dsc/locales/en-us.toml index ebda4a505..002603c60 100644 --- a/resources/dism_dsc/locales/en-us.toml +++ b/resources/dism_dsc/locales/en-us.toml @@ -31,6 +31,7 @@ failedSerializeOutput = "Failed to serialize output: %{err}" [fod_set] failedParseInput = "Failed to parse input: %{err}" capabilitiesArrayEmpty = "Capabilities array cannot be empty for set operation" +sourcePathInvalid = "Failed to parse value in sourcePaths: '%{path}' is an invalid path" identityRequired = "identity is required for set operation" stateRequired = "state is required for set operation" capabilityNotFound = "Capability '%{identity}' was not found on this system" @@ -40,7 +41,7 @@ failedSerializeOutput = "Failed to serialize output: %{err}" [set] failedParseInput = "Failed to parse input: %{err}" featuresArrayEmpty = "Features array cannot be empty for set operation" -sourcePathInvalid = "sourcePath '%{path}' is an invalid path" +sourcePathInvalid = "Failed to parse value in sourcePaths: '%{path}' is an invalid path" featureNameRequired = "featureName is required for set operation" stateRequired = "state is required for set operation" unsupportedDesiredState = "Unsupported desired state '%{state}'. Supported states for set are: Installed, NotPresent, Removed" From 1c25dcf2c59524071dd7fa5afcc6d2f0855d92c0 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 20:55:14 +1000 Subject: [PATCH 10/18] fix dangling pointers --- resources/dism_dsc/src/dism.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/dism_dsc/src/dism.rs b/resources/dism_dsc/src/dism.rs index b36f305ac..6b9c426fe 100644 --- a/resources/dism_dsc/src/dism.rs +++ b/resources/dism_dsc/src/dism.rs @@ -326,8 +326,8 @@ impl DismSessionHandle { let wide_source_paths: Option> = source_paths.as_ref().map(|paths| { paths.into_iter().map(|p| to_wide_null(&p)).collect() }); - let sources = wide_source_paths.map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); - let sources_ptr = sources.map_or(std::ptr::null(), |v| v.as_ptr()); + let sources = wide_source_paths.as_ref().map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); + let sources_ptr = sources.as_ref().map_or(std::ptr::null(), |v| v.as_ptr()); let hr = unsafe { (self.api.enable_feature)( @@ -475,8 +475,8 @@ impl DismSessionHandle { let wide_source_paths: Option> = source_paths.as_ref().map(|paths| { paths.into_iter().map(|p| to_wide_null(&p)).collect() }); - let sources = wide_source_paths.map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); - let sources_ptr = sources.map_or(std::ptr::null(), |v| v.as_ptr()); + let sources = wide_source_paths.as_ref().map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); + let sources_ptr = sources.as_ref().map_or(std::ptr::null(), |v| v.as_ptr()); let hr = unsafe { add_cap( From ec8c2da3916069ba98d74760d8332c05612e0071 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 21:09:15 +1000 Subject: [PATCH 11/18] Remove path validation and let dism handle it --- .../Microsoft/Windows/FeatureOnDemandList/index.md | 4 ++-- .../Microsoft/Windows/OptionalFeatureList/index.md | 4 ++-- resources/dism_dsc/src/feature_on_demand/set.rs | 9 --------- resources/dism_dsc/src/optional_feature/set.rs | 9 --------- 4 files changed, 4 insertions(+), 22 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index c1d71bb41..df5472e76 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -300,8 +300,8 @@ Supplied at the top level of the **Set** operation, indicates the location of th use for installation if necessary. The DISM API will search these paths if the feature files are not available in the local feature store. -All paths supplied must be valid and existing local or network path to a Windows image file -(WIM). See the [feature on demand repository documentation][08] for more. +All paths supplied should be to a Windows image file (WIM). See the +[feature on demand repository documentation][08] for more information on valid sources. This property is optional and will be omitted from the response if empty. diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index 9b6c47f0d..dbae902f4 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -285,8 +285,8 @@ Supplied at the top level of the **Set** operation, indicates the location of th use for installation if necessary. The DISM API will search these paths if the feature files are not available in the local feature store. -All paths supplied must be valid and existing local or network path to a Windows image file -(WIM). See the [DISM enable feature documentation][08] for more. +All paths supplied should be to a Windows image file (WIM). See the +[DISM enable feature documentation][08] for more information on valid sources. This property is optional and will be omitted from the response if empty. diff --git a/resources/dism_dsc/src/feature_on_demand/set.rs b/resources/dism_dsc/src/feature_on_demand/set.rs index c4ddf58e6..a74b3839c 100644 --- a/resources/dism_dsc/src/feature_on_demand/set.rs +++ b/resources/dism_dsc/src/feature_on_demand/set.rs @@ -16,15 +16,6 @@ pub fn handle_set(input: &str) -> Result { return Err(t!("fod_set.capabilitiesArrayEmpty").to_string()); } - // Validate source paths - if let Some(paths) = &capability_list.source_paths { - for path in paths { - if !std::fs::exists(path).unwrap_or(false) { - return Err(t!("fod_set.sourcePathInvalid", path = path).to_string()); - } - } - } - let session = DismSessionHandle::open()?; let mut results = Vec::new(); let mut reboot_required = false; diff --git a/resources/dism_dsc/src/optional_feature/set.rs b/resources/dism_dsc/src/optional_feature/set.rs index 91191eab6..5b25d6991 100644 --- a/resources/dism_dsc/src/optional_feature/set.rs +++ b/resources/dism_dsc/src/optional_feature/set.rs @@ -16,15 +16,6 @@ pub fn handle_set(input: &str) -> Result { return Err(t!("set.featuresArrayEmpty").to_string()); } - // Validate source paths - if let Some(paths) = &feature_list.source_paths { - for path in paths { - if !std::fs::exists(path).unwrap_or(false) { - return Err(t!("set.sourcePathInvalid", path = path).to_string()); - } - } - } - let session = DismSessionHandle::open()?; let mut results = Vec::new(); let mut reboot_required = false; From af6aa7ce2da61cc735cfd483876dc69f1c3dd3c9 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 21:16:08 +1000 Subject: [PATCH 12/18] clarified sourcePaths behaviour in docs --- .../resources/Microsoft/Windows/FeatureOnDemandList/index.md | 3 ++- .../resources/Microsoft/Windows/OptionalFeatureList/index.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index df5472e76..1aef24ea8 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -303,7 +303,8 @@ not available in the local feature store. All paths supplied should be to a Windows image file (WIM). See the [feature on demand repository documentation][08] for more information on valid sources. -This property is optional and will be omitted from the response if empty. +This property is optional and will be omitted from the response if empty. If a value is supplied +to the **Get** operation, the value will be echoed to the response. ### _restartRequired diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index dbae902f4..5ec3c5e0b 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -288,7 +288,8 @@ not available in the local feature store. All paths supplied should be to a Windows image file (WIM). See the [DISM enable feature documentation][08] for more information on valid sources. -This property is optional and will be omitted from the response if empty. +This property is optional and will be omitted from the response if empty. If a value is supplied +to the **Get** operation, the value will be echoed to the response. ### _restartRequired From b9a815456b5a6158cef382d03eeda71158ace92e Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 21:43:31 +1000 Subject: [PATCH 13/18] Change get function to return None for source pathand fix possible dangling-but-non-null pointer --- resources/dism_dsc/src/dism.rs | 30 ++++++++++++------- .../dism_dsc/src/feature_on_demand/get.rs | 2 +- .../dism_dsc/src/optional_feature/get.rs | 6 +++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/resources/dism_dsc/src/dism.rs b/resources/dism_dsc/src/dism.rs index 6b9c426fe..9e81f4df6 100644 --- a/resources/dism_dsc/src/dism.rs +++ b/resources/dism_dsc/src/dism.rs @@ -322,11 +322,16 @@ impl DismSessionHandle { pub fn enable_feature(&self, feature_name: &str, source_paths: &Option>) -> Result { let wide_name = to_wide_null(feature_name); - let source_count: u32 = source_paths.as_ref().map_or(0, |paths| paths.len() as u32); - let wide_source_paths: Option> = source_paths.as_ref().map(|paths| { - paths.into_iter().map(|p| to_wide_null(&p)).collect() - }); - let sources = wide_source_paths.as_ref().map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); + let wide_source_paths: Option>> = source_paths + .as_ref() + .filter(|paths| !paths.is_empty()) + .map(|paths| paths.iter().map(|p| to_wide_null(p)).collect()); + + let sources: Option> = wide_source_paths + .as_ref() + .map(|paths| paths.iter().map(|p| p.as_ptr()).collect()); + + let source_count = sources.as_ref().map_or(0, |paths| paths.len() as u32); let sources_ptr = sources.as_ref().map_or(std::ptr::null(), |v| v.as_ptr()); let hr = unsafe { @@ -471,11 +476,16 @@ impl DismSessionHandle { let wide_name = to_wide_null(name); - let source_count: u32 = source_paths.as_ref().map_or(0, |paths| paths.len() as u32); - let wide_source_paths: Option> = source_paths.as_ref().map(|paths| { - paths.into_iter().map(|p| to_wide_null(&p)).collect() - }); - let sources = wide_source_paths.as_ref().map(|paths| paths.iter().map(|p| p.as_ptr()).collect::>()); + let wide_source_paths: Option>> = source_paths + .as_ref() + .filter(|paths| !paths.is_empty()) + .map(|paths| paths.iter().map(|p| to_wide_null(p)).collect()); + + let sources: Option> = wide_source_paths + .as_ref() + .map(|paths| paths.iter().map(|p| p.as_ptr()).collect()); + + let source_count = sources.as_ref().map_or(0, |paths| paths.len() as u32); let sources_ptr = sources.as_ref().map_or(std::ptr::null(), |v| v.as_ptr()); let hr = unsafe { diff --git a/resources/dism_dsc/src/feature_on_demand/get.rs b/resources/dism_dsc/src/feature_on_demand/get.rs index e853c7c11..e84214cd7 100644 --- a/resources/dism_dsc/src/feature_on_demand/get.rs +++ b/resources/dism_dsc/src/feature_on_demand/get.rs @@ -46,7 +46,7 @@ pub fn handle_get(input: &str) -> Result { let output = FeatureOnDemandList { restart_required_meta: None, - source_paths: capability_list.source_paths, + source_paths: None, capabilities: results }; serde_json::to_string(&output) diff --git a/resources/dism_dsc/src/optional_feature/get.rs b/resources/dism_dsc/src/optional_feature/get.rs index a9d1799c1..11808a8ea 100644 --- a/resources/dism_dsc/src/optional_feature/get.rs +++ b/resources/dism_dsc/src/optional_feature/get.rs @@ -27,7 +27,11 @@ pub fn handle_get(input: &str) -> Result { results.push(info); } - let output = OptionalFeatureList { restart_required_meta: None, source_paths: feature_list.source_paths, features: results }; + let output = OptionalFeatureList { + restart_required_meta: None, + source_paths: None, + features: results + }; serde_json::to_string(&output) .map_err(|e| t!("get.failedSerializeOutput", err = e.to_string()).to_string()) } From 5daaea0d6ce14eb37ca2eaaeba10c2551b5831d2 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 21:44:24 +1000 Subject: [PATCH 14/18] Remove echo comments from docs --- .../resources/Microsoft/Windows/FeatureOnDemandList/index.md | 3 +-- .../resources/Microsoft/Windows/OptionalFeatureList/index.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index 1aef24ea8..df5472e76 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -303,8 +303,7 @@ not available in the local feature store. All paths supplied should be to a Windows image file (WIM). See the [feature on demand repository documentation][08] for more information on valid sources. -This property is optional and will be omitted from the response if empty. If a value is supplied -to the **Get** operation, the value will be echoed to the response. +This property is optional and will be omitted from the response if empty. ### _restartRequired diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index 5ec3c5e0b..dbae902f4 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -288,8 +288,7 @@ not available in the local feature store. All paths supplied should be to a Windows image file (WIM). See the [DISM enable feature documentation][08] for more information on valid sources. -This property is optional and will be omitted from the response if empty. If a value is supplied -to the **Get** operation, the value will be echoed to the response. +This property is optional and will be omitted from the response if empty. ### _restartRequired From 8374a060b3132b9817030db30936effbf0af99d1 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 21:46:01 +1000 Subject: [PATCH 15/18] Simplfy documentation wording --- .../Microsoft/Windows/FeatureOnDemandList/index.md | 8 +++----- .../Microsoft/Windows/OptionalFeatureList/index.md | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index df5472e76..46f4d4bc5 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -296,12 +296,10 @@ IsReadOnly : false
-Supplied at the top level of the **Set** operation, indicates the location of the source files to +Supplied at the top level of the **Set** operation, indicates the location of the source files to use for installation if necessary. The DISM API will search these paths if the feature files are -not available in the local feature store. - -All paths supplied should be to a Windows image file (WIM). See the -[feature on demand repository documentation][08] for more information on valid sources. +not available in the local feature store. See the [feature on demand repository documentation][08] +for more information on valid sources. This property is optional and will be omitted from the response if empty. diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index dbae902f4..ebe8fa0b8 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -283,10 +283,8 @@ IsReadOnly : false Supplied at the top level of the **Set** operation, indicates the location of the source files to use for installation if necessary. The DISM API will search these paths if the feature files are -not available in the local feature store. - -All paths supplied should be to a Windows image file (WIM). See the -[DISM enable feature documentation][08] for more information on valid sources. +not available in the local feature store. See the [DISM enable feature documentation][08] for more +information on valid sources. This property is optional and will be omitted from the response if empty. From a8a5cab723957447e601f0c4cd4789fe1aa4dba7 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 21:47:17 +1000 Subject: [PATCH 16/18] Fix wording in examples --- .../examples/install-remove-feature-on-demand.md | 4 ++-- .../examples/enable-disable-optional-features.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md index cfe9ef2b7..e478926df 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/examples/install-remove-feature-on-demand.md @@ -117,8 +117,8 @@ changedProperties: ## Install a feature on demand using an offline source -To allow DISM to install capabilities that are not present on the machine in a offline environment, -add the offline Windows image file (WIM) path to the `sourcePaths` property. +To allow DISM to install capabilities that are not present on the machine in an offline +environment, add the offline source path to the `sourcePaths` property. ```powershell $instance = @{ diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md index aeeac7efa..742ade55d 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/examples/enable-disable-optional-features.md @@ -148,8 +148,8 @@ changedProperties: ## Enable an optional feature using an offline source -To allow DISM to install features that are not present on the machine in a offline environment, add -the offline Windows image file (WIM) path to the `sourcePaths` property. +To allow DISM to install features that are not present on the machine in an offline environment, +add the offline source path to the `sourcePaths` property. ```powershell $instance = @{ From 7bc06dc8510a792182dd4f3f4b18aa86aef92772 Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Fri, 5 Jun 2026 22:08:08 +1000 Subject: [PATCH 17/18] Misc changes --- .../Microsoft/Windows/FeatureOnDemandList/index.md | 2 +- .../Microsoft/Windows/OptionalFeatureList/index.md | 2 +- resources/dism_dsc/src/feature_on_demand/get.rs | 6 +++--- resources/dism_dsc/src/optional_feature/get.rs | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index 46f4d4bc5..eb657a620 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -301,7 +301,7 @@ use for installation if necessary. The DISM API will search these paths if the f not available in the local feature store. See the [feature on demand repository documentation][08] for more information on valid sources. -This property is optional and will be omitted from the response if empty. +This property is optional and will be omitted from the response if no value is provided. ### _restartRequired diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index ebe8fa0b8..33b575017 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -286,7 +286,7 @@ use for installation if necessary. The DISM API will search these paths if the f not available in the local feature store. See the [DISM enable feature documentation][08] for more information on valid sources. -This property is optional and will be omitted from the response if empty. +This property is optional and will be omitted from the response if no value is provided. ### _restartRequired diff --git a/resources/dism_dsc/src/feature_on_demand/get.rs b/resources/dism_dsc/src/feature_on_demand/get.rs index e84214cd7..d6cddb9e1 100644 --- a/resources/dism_dsc/src/feature_on_demand/get.rs +++ b/resources/dism_dsc/src/feature_on_demand/get.rs @@ -45,9 +45,9 @@ pub fn handle_get(input: &str) -> Result { } let output = FeatureOnDemandList { - restart_required_meta: None, - source_paths: None, - capabilities: results + restart_required_meta: None, + source_paths: None, + capabilities: results }; serde_json::to_string(&output) .map_err(|e| t!("fod_get.failedSerializeOutput", err = e.to_string()).to_string()) diff --git a/resources/dism_dsc/src/optional_feature/get.rs b/resources/dism_dsc/src/optional_feature/get.rs index 11808a8ea..af4e2fe1f 100644 --- a/resources/dism_dsc/src/optional_feature/get.rs +++ b/resources/dism_dsc/src/optional_feature/get.rs @@ -28,9 +28,9 @@ pub fn handle_get(input: &str) -> Result { } let output = OptionalFeatureList { - restart_required_meta: None, - source_paths: None, - features: results + restart_required_meta: None, + source_paths: None, + features: results }; serde_json::to_string(&output) .map_err(|e| t!("get.failedSerializeOutput", err = e.to_string()).to_string()) From 3b5a1621fee6398032ab4f59577c6bb636ae36aa Mon Sep 17 00:00:00 2001 From: Alex-shearing Date: Sat, 6 Jun 2026 16:02:59 +1000 Subject: [PATCH 18/18] Remove unused translations --- .../resources/Microsoft/Windows/FeatureOnDemandList/index.md | 1 - .../resources/Microsoft/Windows/OptionalFeatureList/index.md | 1 - resources/dism_dsc/locales/en-us.toml | 2 -- 3 files changed, 4 deletions(-) diff --git a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md index eb657a620..bcd48709a 100644 --- a/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md +++ b/docs/reference/resources/Microsoft/Windows/FeatureOnDemandList/index.md @@ -396,7 +396,6 @@ Common causes include: - The requested capability `identity` is not recognized by DISM. - The DISM API returned an error while querying or modifying capability state. - The process is not running with elevated privileges. -- An invalid path was supplied to `sourcePaths` in a **Set** operation. ## See also diff --git a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md index 33b575017..8336b06d5 100644 --- a/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md +++ b/docs/reference/resources/Microsoft/Windows/OptionalFeatureList/index.md @@ -383,7 +383,6 @@ Common causes include: `Removed`). - The DISM API returned an error while querying or modifying feature state. - The process is not running with elevated privileges. -- An invalid path was supplied to `sourcePaths` in a **Set** operation. ## See also diff --git a/resources/dism_dsc/locales/en-us.toml b/resources/dism_dsc/locales/en-us.toml index 002603c60..19a945a67 100644 --- a/resources/dism_dsc/locales/en-us.toml +++ b/resources/dism_dsc/locales/en-us.toml @@ -31,7 +31,6 @@ failedSerializeOutput = "Failed to serialize output: %{err}" [fod_set] failedParseInput = "Failed to parse input: %{err}" capabilitiesArrayEmpty = "Capabilities array cannot be empty for set operation" -sourcePathInvalid = "Failed to parse value in sourcePaths: '%{path}' is an invalid path" identityRequired = "identity is required for set operation" stateRequired = "state is required for set operation" capabilityNotFound = "Capability '%{identity}' was not found on this system" @@ -41,7 +40,6 @@ failedSerializeOutput = "Failed to serialize output: %{err}" [set] failedParseInput = "Failed to parse input: %{err}" featuresArrayEmpty = "Features array cannot be empty for set operation" -sourcePathInvalid = "Failed to parse value in sourcePaths: '%{path}' is an invalid path" featureNameRequired = "featureName is required for set operation" stateRequired = "state is required for set operation" unsupportedDesiredState = "Unsupported desired state '%{state}'. Supported states for set are: Installed, NotPresent, Removed"