Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions resources/WindowsUpdate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ name = "wu_dsc"
path = "src/main.rs"

[dependencies]
rust-i18n = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

Expand Down
55 changes: 55 additions & 0 deletions resources/WindowsUpdate/locales/en-us.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
_version = 1

[main]
missingOperation = "Missing operation argument"
usage = "Usage: wu_dsc <get|set|export>"
windowsUpdateOnlySupported = "Windows Update resource is only supported on Windows"
unknownOperation = "Unknown operation '%{operation}'"
errorReadingInput = "Error reading input: %{err}"

[export]
failedParseInput = "Failed to parse input: %{err}"
noMatchingUpdateForFilter = "No matching update found for filter %{index}: %{criteria}"
failedSerializeOutput = "Failed to serialize output: %{err}"
criteriaTitle = "title '%{value}'"
criteriaId = "id '%{value}'"
criteriaIsInstalled = "is_installed %{value}"
criteriaDescription = "description '%{value}'"
criteriaIsUninstallable = "is_uninstallable %{value}"
criteriaKbArticleIds = "kb_article_ids %{value}"
criteriaRecommendedHardDiskSpace = "recommended_hard_disk_space %{value}"
criteriaMsrcSeverity = "msrc_severity %{value}"
criteriaSecurityBulletinIds = "security_bulletin_ids %{value}"
criteriaUpdateType = "update_type %{value}"

[get]
failedParseInput = "Failed to parse input: %{err}"
updatesArrayEmpty = "Updates array cannot be empty for get operation"
atLeastOneCriterionRequired = "At least one search criterion must be specified for get operation"
titleMatchedMultipleUpdates = "Title '%{title}' matched %{count} updates. Please use a more specific identifier such as 'id' or 'kb_article_ids' to uniquely identify the update."
criteriaMatchedMultipleUpdates = "Criteria matched %{count} updates. Please use more specific identifiers to uniquely identify the update."
noMatchingUpdateForCriteria = "No matching update found for criteria: %{criteria}"
failedSerializeOutput = "Failed to serialize output: %{err}"
criteriaTitle = "title '%{value}'"
criteriaId = "id '%{value}'"
criteriaIsInstalled = "is_installed %{value}"
criteriaKbArticleIds = "kb_article_ids %{value}"
criteriaUpdateType = "update_type %{value}"
criteriaMsrcSeverity = "msrc_severity %{value}"

[set]
failedParseInput = "Failed to parse input: %{err}"
updatesArrayEmpty = "Updates array cannot be empty for set operation"
atLeastOneCriterionRequired = "At least one search criterion must be specified for set operation"
titleMatchedMultipleUpdates = "Title '%{title}' matched %{count} updates. Please use a more specific identifier such as 'id' or 'kb_article_ids' to uniquely identify the update."
criteriaMatchedMultipleUpdates = "Criteria matched %{count} updates. Please use more specific identifiers to uniquely identify the update."
noMatchingUpdateForCriteria = "No matching update found for criteria: %{criteria}"
failedDownloadUpdate = "Failed to download update. Result code: %{code}"
failedInstallUpdate = "Failed to install update. Result code: %{code}"
failedSerializeOutput = "Failed to serialize output: %{err}"
criteriaTitle = "title '%{value}'"
criteriaId = "id '%{value}'"
criteriaIsInstalled = "is_installed %{value}"
criteriaKbArticleIds = "kb_article_ids %{value}"
criteriaUpdateType = "update_type %{value}"
criteriaMsrcSeverity = "msrc_severity %{value}"
21 changes: 12 additions & 9 deletions resources/WindowsUpdate/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
#[cfg(windows)]
mod windows_update;

use rust_i18n::t;
use std::io::{self, Read, IsTerminal};

rust_i18n::i18n!("locales", fallback = "en-us");

fn main() {
let args: Vec<String> = std::env::args().collect();

if args.len() < 2 {
eprintln!("Error: Missing operation argument");
eprintln!("Usage: wu_dsc <get|set|export>");
eprintln!("Error: {}", t!("main.missingOperation"));
eprintln!("{}", t!("main.usage"));
std::process::exit(1);
}

Expand Down Expand Up @@ -39,15 +42,15 @@ fn main() {

#[cfg(not(windows))]
{
eprintln!("Error: Windows Update resource is only supported on Windows");
eprintln!("Error: {}", t!("main.windowsUpdateOnlySupported"));
std::process::exit(1);
}
}
"get" => {
// Read input from stdin
let mut buffer = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buffer) {
eprintln!("Error reading input: {}", e);
eprintln!("{}", t!("main.errorReadingInput", err = e));
std::process::exit(1);
}

Expand All @@ -65,15 +68,15 @@ fn main() {

#[cfg(not(windows))]
{
eprintln!("Error: Windows Update resource is only supported on Windows");
eprintln!("Error: {}", t!("main.windowsUpdateOnlySupported"));
std::process::exit(1);
}
}
"set" => {
// Read input from stdin
let mut buffer = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buffer) {
eprintln!("Error reading input: {}", e);
eprintln!("{}", t!("main.errorReadingInput", err = e));
std::process::exit(1);
}

Expand All @@ -91,13 +94,13 @@ fn main() {

#[cfg(not(windows))]
{
eprintln!("Error: Windows Update resource is only supported on Windows");
eprintln!("Error: {}", t!("main.windowsUpdateOnlySupported"));
std::process::exit(1);
}
}
_ => {
eprintln!("Error: Unknown operation '{}'", operation);
eprintln!("Usage: wu_dsc <get|set|export>");
eprintln!("{}", t!("main.unknownOperation", operation = operation));
eprintln!("{}", t!("main.usage"));
std::process::exit(1);
}
}
Expand Down
36 changes: 20 additions & 16 deletions resources/WindowsUpdate/src/windows_update/export.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use rust_i18n::t;
use windows::{
core::*,
Win32::Foundation::*,
Expand All @@ -15,22 +16,24 @@ pub fn handle_export(input: &str) -> Result<String> {
// Parse optional filter input as UpdateList
let update_list: UpdateList = if input.trim().is_empty() {
UpdateList {
metadata: None,
updates: vec![UpdateInfo {
title: None,
description: None,
id: None,
installation_behavior: None,
is_installed: None,
description: None,
is_uninstallable: None,
kb_article_ids: None,
recommended_hard_disk_space: None,
msrc_severity: None,
security_bulletin_ids: None,
title: None,
update_type: None,
}]
}
} else {
serde_json::from_str(input)
.map_err(|e| Error::new(E_INVALIDARG, format!("Failed to parse input: {}", e)))?
.map_err(|e| Error::new(E_INVALIDARG.into(), t!("export.failedParseInput", err = e.to_string()).to_string()))?
};

let filters = &update_list.updates;
Expand Down Expand Up @@ -191,43 +194,43 @@ pub fn handle_export(input: &str) -> Result<String> {
// Construct error message with filter criteria
let mut criteria_parts = Vec::new();
if let Some(title) = &filter.title {
criteria_parts.push(format!("title '{}'", title));
criteria_parts.push(t!("export.criteriaTitle", value = title).to_string());
}
if let Some(id) = &filter.id {
criteria_parts.push(format!("id '{}'", id));
criteria_parts.push(t!("export.criteriaId", value = id).to_string());
}
if let Some(is_installed) = filter.is_installed {
criteria_parts.push(format!("is_installed {}", is_installed));
criteria_parts.push(t!("export.criteriaIsInstalled", value = is_installed).to_string());
}
if let Some(description) = &filter.description {
criteria_parts.push(format!("description '{}'", description));
criteria_parts.push(t!("export.criteriaDescription", value = description).to_string());
}
if let Some(is_uninstallable) = filter.is_uninstallable {
criteria_parts.push(format!("is_uninstallable {}", is_uninstallable));
criteria_parts.push(t!("export.criteriaIsUninstallable", value = is_uninstallable).to_string());
}
if let Some(kb_ids) = &filter.kb_article_ids {
criteria_parts.push(format!("kb_article_ids {:?}", kb_ids));
criteria_parts.push(t!("export.criteriaKbArticleIds", value = format!("{:?}", kb_ids)).to_string());
}
if let Some(space) = filter.recommended_hard_disk_space {
criteria_parts.push(format!("recommended_hard_disk_space {}", space));
criteria_parts.push(t!("export.criteriaRecommendedHardDiskSpace", value = space).to_string());
}
if let Some(severity) = &filter.msrc_severity {
criteria_parts.push(format!("msrc_severity {:?}", severity));
criteria_parts.push(t!("export.criteriaMsrcSeverity", value = format!("{:?}", severity)).to_string());
}
if let Some(bulletin_ids) = &filter.security_bulletin_ids {
criteria_parts.push(format!("security_bulletin_ids {:?}", bulletin_ids));
criteria_parts.push(t!("export.criteriaSecurityBulletinIds", value = format!("{:?}", bulletin_ids)).to_string());
}
if let Some(update_type) = &filter.update_type {
criteria_parts.push(format!("update_type {:?}", update_type));
criteria_parts.push(t!("export.criteriaUpdateType", value = format!("{:?}", update_type)).to_string());
}

let criteria_str = criteria_parts.join(", ");
let error_msg = format!("No matching update found for filter {}: {}", filter_index, criteria_str);
let error_msg = t!("export.noMatchingUpdateForFilter", index = filter_index, criteria = criteria_str).to_string();

// Emit JSON error to stderr
eprintln!("{{\"error\":\"{}\"}}", error_msg);

return Err(Error::new(E_FAIL, error_msg));
return Err(Error::new(E_FAIL.into(), error_msg));
}
}
}
Expand All @@ -245,10 +248,11 @@ pub fn handle_export(input: &str) -> Result<String> {
match result {
Ok(updates) => {
let result = UpdateList {
metadata: None,
updates
};
serde_json::to_string(&result)
.map_err(|e| Error::new(E_FAIL, format!("Failed to serialize output: {}", e)))
.map_err(|e| Error::new(E_FAIL.into(), t!("export.failedSerializeOutput", err = e.to_string()).to_string()))
}
Err(e) => Err(e),
}
Expand Down
58 changes: 43 additions & 15 deletions resources/WindowsUpdate/src/windows_update/get.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use rust_i18n::t;
use windows::{
core::*,
Win32::Foundation::*,
Expand All @@ -13,10 +14,10 @@ use crate::windows_update::types::{UpdateList, extract_update_info};
pub fn handle_get(input: &str) -> Result<String> {
// Parse input as UpdateList
let update_list: UpdateList = serde_json::from_str(input)
.map_err(|e| Error::new(E_INVALIDARG, format!("Failed to parse input: {}", e)))?;
.map_err(|e| Error::new(E_INVALIDARG.into(), t!("get.failedParseInput", err = e.to_string()).to_string()))?;

if update_list.updates.is_empty() {
return Err(Error::new(E_INVALIDARG, "Updates array cannot be empty for get operation"));
return Err(Error::new(E_INVALIDARG.into(), t!("get.updatesArrayEmpty").to_string()));
}

// Initialize COM
Expand Down Expand Up @@ -53,11 +54,12 @@ pub fn handle_get(input: &str) -> Result<String> {
&& update_input.is_installed.is_none()
&& update_input.update_type.is_none()
&& update_input.msrc_severity.is_none() {
return Err(Error::new(E_INVALIDARG, "At least one search criterion must be specified for get operation"));
return Err(Error::new(E_INVALIDARG.into(), t!("get.atLeastOneCriterionRequired").to_string()));
}

// Find the update matching ALL provided criteria (logical AND)
let mut found_update = None;
let mut matching_updates: Vec<IUpdate> = Vec::new();
for i in 0..count {
let update = all_updates.get_Item(i)?;

Expand Down Expand Up @@ -144,9 +146,34 @@ pub fn handle_get(input: &str) -> Result<String> {
}
}

// All criteria matched - extract and store the update
found_update = Some(extract_update_info(&update)?);
break;
// All criteria matched - collect this update
matching_updates.push(update.clone());
}

// Check if multiple updates matched the provided criteria
if matching_updates.len() > 1 {
// Determine if title was the only search criterion
let title_only = update_input.title.is_some()
&& update_input.id.is_none()
&& update_input.kb_article_ids.is_none()
&& update_input.is_installed.is_none()
&& update_input.update_type.is_none()
&& update_input.msrc_severity.is_none();

let error_msg = if title_only {
let search_title = update_input.title.as_ref().unwrap();
t!("get.titleMatchedMultipleUpdates", title = search_title, count = matching_updates.len()).to_string()
} else {
t!("get.criteriaMatchedMultipleUpdates", count = matching_updates.len()).to_string()
};

eprintln!("{{\"error\":\"{}\"}}", error_msg);
return Err(Error::new(E_INVALIDARG.into(), error_msg));
}

// Get the first (and should be only) match
if !matching_updates.is_empty() {
found_update = Some(extract_update_info(&matching_updates[0])?);
}

if let Some(update_info) = found_update {
Expand All @@ -155,31 +182,31 @@ pub fn handle_get(input: &str) -> Result<String> {
// No match found for this input - construct error message and return
let mut criteria_parts = Vec::new();
if let Some(title) = &update_input.title {
criteria_parts.push(format!("title '{}'", title));
criteria_parts.push(t!("get.criteriaTitle", value = title).to_string());
}
if let Some(id) = &update_input.id {
criteria_parts.push(format!("id '{}'", id));
criteria_parts.push(t!("get.criteriaId", value = id).to_string());
}
if let Some(is_installed) = update_input.is_installed {
criteria_parts.push(format!("is_installed {}", is_installed));
criteria_parts.push(t!("get.criteriaIsInstalled", value = is_installed).to_string());
}
if let Some(kb_ids) = &update_input.kb_article_ids {
criteria_parts.push(format!("kb_article_ids {:?}", kb_ids));
criteria_parts.push(t!("get.criteriaKbArticleIds", value = format!("{:?}", kb_ids)).to_string());
}
if let Some(update_type) = &update_input.update_type {
criteria_parts.push(format!("update_type {:?}", update_type));
criteria_parts.push(t!("get.criteriaUpdateType", value = format!("{:?}", update_type)).to_string());
}
if let Some(severity) = &update_input.msrc_severity {
criteria_parts.push(format!("msrc_severity {:?}", severity));
criteria_parts.push(t!("get.criteriaMsrcSeverity", value = format!("{:?}", severity)).to_string());
}

let criteria_str = criteria_parts.join(", ");
let error_msg = format!("No matching update found for criteria: {}", criteria_str);
let error_msg = t!("get.noMatchingUpdateForCriteria", criteria = criteria_str).to_string();

// Emit JSON error to stderr
eprintln!("{{\"error\":\"{}\"}}", error_msg);

return Err(Error::new(E_FAIL, error_msg));
return Err(Error::new(E_FAIL.into(), error_msg));
}
}

Expand All @@ -196,10 +223,11 @@ pub fn handle_get(input: &str) -> Result<String> {
match result {
Ok(updates) => {
let result = UpdateList {
metadata: None,
updates
};
serde_json::to_string(&result)
.map_err(|e| Error::new(E_FAIL, format!("Failed to serialize output: {}", e)))
.map_err(|e| Error::new(E_FAIL.into(), t!("get.failedSerializeOutput", err = e.to_string()).to_string()))
}
Err(e) => Err(e),
}
Expand Down
Loading