From cb52d35d5b9362e41ea559860f9e63e26df9ccbf Mon Sep 17 00:00:00 2001 From: abd002 Date: Fri, 30 Jan 2026 19:44:16 +0200 Subject: [PATCH] Fix IppRequest::send() request copy; add Destination::get_attrs() helper --- src/destination/mod.rs | 163 ++++++++++++++++++++++++++++++++++++++++- src/ipp.rs | 57 ++++++++++++++ 2 files changed, 219 insertions(+), 1 deletion(-) diff --git a/src/destination/mod.rs b/src/destination/mod.rs index 2ca216d..eb2d567 100644 --- a/src/destination/mod.rs +++ b/src/destination/mod.rs @@ -10,6 +10,7 @@ use crate::bindings; use crate::constants; use crate::error::{Error, Result}; use crate::error_helpers::cups_error_to_our_error; +use crate::{HttpConnection, IppRequest, IppOperation, IppTag, IppValueTag}; use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::marker::PhantomData; @@ -31,6 +32,19 @@ pub struct Destination { pub options: HashMap, } +#[derive(Clone, Copy)] +pub enum AttrKind { + StringLike, + IntegerLike, + Boolean, +} + +#[derive(Clone, Copy)] +pub struct AttrSpec<'a> { + pub name: &'a str, + pub kind: AttrKind, +} + impl Destination { /// Create a new Destination instance from raw cups_dest_t pointer pub(crate) unsafe fn from_raw(dest_ptr: *const bindings::cups_dest_s) -> Result { @@ -375,6 +389,82 @@ impl Destination { // Leak the box to keep the memory alive Box::into_raw(dest) } + + /// Fetch and populate missing attributes from the printer via IPP + /// + /// Note: The caller is responsible for passing an `HttpConnection` connected + /// to the correct CUPS server for this destination. + pub fn get_attrs(&mut self, conn: &HttpConnection, attrs: &[AttrSpec<'_>]) -> Result<()> { + let missing: Vec> = attrs + .iter() + .copied() + .filter(|a| !self.options.contains_key(a.name)) + .collect(); + if missing.is_empty() { + return Ok(()); + } + + let uri = match self.options.get("printer-uri-supported") { + Some(u) => u.as_str(), + None => return Err(Error::UnsupportedFeature("printer-uri-supported missing".to_string())), + }; + + // Build GetPrinterAttributes + let mut req = IppRequest::new(IppOperation::GetPrinterAttributes)?; + req.add_string(IppTag::Operation, IppValueTag::Uri, "printer-uri", uri)?; + + // requested-attributes by names + let names: Vec<&str> = missing.iter().map(|a| a.name).collect(); + req.add_strings( + IppTag::Operation, + IppValueTag::Keyword, + "requested-attributes", + &names, + )?; + + // Post to the specific printer resource path, not the scheduler root + let resource = uri + .strip_prefix("ipp://") + .or_else(|| uri.strip_prefix("ipps://")) + .and_then(|rest| rest.split_once('/').map(|(_, path)| format!("/{}", path))) + .ok_or_else(|| { + Error::UnsupportedFeature(format!("invalid printer-uri-supported: {uri}")) + })?; + + let resp = req.send(conn, &resource)?; + + for spec in missing { + let Some(attr) = resp.find_attribute(spec.name, None) else { + continue; + }; + + let mut vals: Vec = Vec::new(); + for i in 0..attr.count() { + match spec.kind { + AttrKind::StringLike => { + if let Some(s) = attr.get_string(i) { + let s = s.trim().to_string(); + if !s.is_empty() { + vals.push(s); + } + } + } + AttrKind::IntegerLike => { + vals.push(attr.get_integer(i).to_string()); + } + AttrKind::Boolean => { + vals.push(if attr.get_boolean(i) { "true" } else { "false" }.to_string()); + } + } + } + + if !vals.is_empty() { + self.options.insert(spec.name.to_string(), vals.join(",")); + } + } + + Ok(()) + } } /// A collection of CUPS destinations with automatic cleanup @@ -1059,4 +1149,75 @@ mod tests { assert!(reasons.contains(&"media-tray-empty-error".to_string())); assert!(reasons.contains(&"toner-low-warning".to_string())); } -} \ No newline at end of file + + #[test] + fn test_get_attrs_error_path() { + use crate::ConnectionFlags; + let mut dest = Destination { + name: "TestPrinter".to_string(), + instance: None, + is_default: false, + options: std::collections::HashMap::new(), + }; + + let printer = match get_default_destination() { + Ok(p) => p, + Err(_) => return, + }; + let conn = match printer.connect(ConnectionFlags::Scheduler, Some(5000), None) { + Ok(c) => c, + Err(_) => return, + }; + + let attrs = vec![ + AttrSpec { + name: "marker-levels", + kind: AttrKind::IntegerLike, + }, + ]; + + let result = dest.get_attrs(&conn, &attrs); + assert!(result.is_err(), "Expected error due to missing printer-uri-supported"); + + if let Err(crate::error::Error::UnsupportedFeature(msg)) = result { + assert_eq!(msg, "printer-uri-supported missing"); + } else { + panic!("Expected UnsupportedFeature error, got {:?}", result); + } + } + + #[test] + fn test_get_attrs_success_path() { + use crate::ConnectionFlags; + let mut printer = match get_default_destination() { + Ok(p) => p, + Err(_) => return, + }; + let conn = match printer.connect(ConnectionFlags::Scheduler, Some(5000), None) { + Ok(c) => c, + Err(_) => return, + }; + + printer.options.remove("printer-is-accepting-jobs"); + + let attrs = vec![ + AttrSpec { + name: "printer-is-accepting-jobs", + kind: AttrKind::Boolean, + }, + ]; + + let result = printer.get_attrs(&conn, &attrs); + assert!(result.is_ok(), "get_attrs failed: {:?}", result); + + let v = printer + .options + .get("printer-is-accepting-jobs") + .expect("attr was not populated"); + + assert!( + v == "true" || v == "false", + "expected boolean string, got {v}" + ); + } +} diff --git a/src/ipp.rs b/src/ipp.rs index 031aae0..3430ef7 100644 --- a/src/ipp.rs +++ b/src/ipp.rs @@ -376,6 +376,7 @@ impl IppRequest { let resource_c = CString::new(resource)?; // Note: cupsDoRequest frees the request, so we need to create a copy + // create an empty IPP message for the outgoing copy let request_copy = unsafe { bindings::ippNew() }; if request_copy.is_null() { return Err(Error::UnsupportedFeature( @@ -384,6 +385,11 @@ impl IppRequest { } unsafe { + // Copy request header fields + bindings::ippSetOperation(request_copy, bindings::ippGetOperation(self.ipp)); + bindings::ippSetRequestId(request_copy, bindings::ippGetRequestId(self.ipp)); + + // Copy all attributes bindings::ippCopyAttributes(request_copy, self.ipp, 0, None, ptr::null_mut()); } @@ -595,4 +601,55 @@ mod tests { assert!(!IppStatus::ErrorBadRequest.is_successful()); assert!(!IppStatus::ErrorNotFound.is_successful()); } + + #[test] + fn test_ipp_request_send_preserves_operation() { + use crate::{get_default_destination, ConnectionFlags}; + + // Skip test if no CUPS server + let printer = match get_default_destination() { + Ok(p) => p, + Err(_) => return, + }; + + // Skip test if connection fails + let connection = match printer.connect(ConnectionFlags::Scheduler, Some(5000), None) { + Ok(c) => c, + Err(_) => return, + }; + + // Create a GetPrinterAttributes request + let mut request = IppRequest::new(IppOperation::GetPrinterAttributes).unwrap(); + + // Use the actual printer URI if available, otherwise fallback to a plausible one + let uri = printer.uri().cloned().unwrap_or_else(|| "ipp://localhost/printers/default".to_string()); + + // Add minimal required attributes + request.add_string( + IppTag::Operation, + IppValueTag::Uri, + "printer-uri", + &uri, + ).unwrap(); + + // Post to the specific printer resource path, not the scheduler root + let resource = uri + .strip_prefix("ipp://") + .or_else(|| uri.strip_prefix("ipps://")) + .and_then(|rest| rest.split_once('/').map(|(_, path)| format!("/{}", path))) + .unwrap_or_else(|| "/".to_string()); + + // Send the request + let response = request.send(&connection, &resource); + + // If the operation code was LOST (became 0), CUPS returns ErrorBadRequest (0x0400). + // Since we preserved it, this should return a successful response or another error. + if let Ok(resp) = response { + assert_ne!( + resp.status(), + IppStatus::ErrorBadRequest, + "Operation code was lost in send() copy" + ); + } + } }