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
163 changes: 162 additions & 1 deletion src/destination/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +32,19 @@ pub struct Destination {
pub options: HashMap<String, String>,
}

#[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<Self> {
Expand Down Expand Up @@ -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<AttrSpec<'_>> = 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<String> = 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
Expand Down Expand Up @@ -1059,4 +1149,75 @@ mod tests {
assert!(reasons.contains(&"media-tray-empty-error".to_string()));
assert!(reasons.contains(&"toner-low-warning".to_string()));
}
}

#[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}"
);
}
}
57 changes: 57 additions & 0 deletions src/ipp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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());
}

Expand Down Expand Up @@ -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"
);
}
}
}