diff --git a/src/destination/mod.rs b/src/destination/mod.rs index 3869207..f71246f 100644 --- a/src/destination/mod.rs +++ b/src/destination/mod.rs @@ -219,8 +219,10 @@ impl Destination { let dinfo = unsafe { bindings::cupsCopyDestInfo(http, dest_ptr) }; unsafe { - if !options_ptr.is_null() { - bindings::cupsFreeOptions(num_options, options_ptr); + // cupsCopyDestInfo can reallocate the options array, so free the + // current one rather than the one passed in + if !dest.options.is_null() { + bindings::cupsFreeOptions(dest.num_options, dest.options); } if !dest.name.is_null() { @@ -290,7 +292,7 @@ impl Destination { } } - let dest = bindings::cups_dest_s { + let mut dest = bindings::cups_dest_s { name: name_c.into_raw(), instance: match instance_c { Some(s) => s.into_raw(), @@ -302,16 +304,12 @@ impl Destination { }; // Check if the option is supported - let result = info.is_option_supported( - http, - &dest as *const bindings::cups_dest_s as *mut bindings::cups_dest_s, - option, - ); + let result = info.is_option_supported(http, &mut dest, option); // Free the resources unsafe { - if !options_ptr.is_null() { - bindings::cupsFreeOptions(num_options, options_ptr); + if !dest.options.is_null() { + bindings::cupsFreeOptions(dest.num_options, dest.options); } // Need to free the raw strings we created @@ -715,6 +713,125 @@ impl Destinations { } } + /// Get a destination's saved options via `cupsGetNamedDest` + pub fn named_destination(name: &str, instance: Option<&str>) -> Option { + let name_c = CString::new(name).ok()?; + let instance_c = instance.and_then(|instance| CString::new(instance).ok()); + let instance_ptr = instance_c + .as_ref() + .map_or(ptr::null(), |instance| instance.as_ptr()); + + let dest = + unsafe { bindings::cupsGetNamedDest(ptr::null_mut(), name_c.as_ptr(), instance_ptr) }; + + if dest.is_null() { + return None; + } + + let destination = unsafe { Destination::from_raw(dest) }.ok(); + unsafe { bindings::cupsFreeDests(1, dest) }; + destination + } + + /// Get the name of the destination CUPS would print to by default + /// + /// Follows libcups' own precedence (`$LPDEST`/`$PRINTER`, user lpoptions, system + /// lpoptions, scheduler default) instead of just reading the user's file. + pub fn default_destination_name() -> Option { + let dest = unsafe { bindings::cupsGetNamedDest(ptr::null_mut(), ptr::null(), ptr::null()) }; + + if dest.is_null() { + return None; + } + + let named = unsafe { &*dest }; + let name = if named.name.is_null() { + None + } else { + let name = unsafe { CStr::from_ptr(named.name) } + .to_string_lossy() + .into_owned(); + + Some(if named.instance.is_null() { + name + } else { + let instance = unsafe { CStr::from_ptr(named.instance) }.to_string_lossy(); + format!("{name}/{instance}") + }) + }; + + unsafe { bindings::cupsFreeDests(1, dest) }; + + name.filter(|name| !name.is_empty()) + } + + /// Set one saved option on a destination + pub fn set_destination_option( + &mut self, + name: &str, + instance: Option<&str>, + option: &str, + value: &str, + ) -> Result<()> { + let option_c = CString::new(option)?; + let value_c = CString::new(value)?; + + // cupsAddDest only adds an entry to edit, not a queue + self.add_destination(name, instance)?; + + let found = self.with_destination(name, instance, |dest| unsafe { + dest.num_options = bindings::cupsAddOption( + option_c.as_ptr(), + value_c.as_ptr(), + dest.num_options, + &mut dest.options, + ); + }); + + if found { + Ok(()) + } else { + Err(Error::DestinationNotFound(name.to_string())) + } + } + + /// Clear the default destination, leaving none marked + pub fn clear_default_destination(&mut self) { + for index in 0..count_to_usize(self.num_dests) { + unsafe { + (*self.dests.add(index)).is_default = raw_is_default(false); + } + } + } + + /// Run `edit` against the named destination in this list + fn with_destination( + &mut self, + name: &str, + instance: Option<&str>, + edit: impl FnOnce(&mut bindings::cups_dest_s), + ) -> bool { + let Ok(name_c) = CString::new(name) else { + return false; + }; + let instance_c = instance.and_then(|instance| CString::new(instance).ok()); + let instance_ptr = instance_c + .as_ref() + .map(|value| value.as_ptr()) + .unwrap_or(ptr::null()); + + let dest = unsafe { + bindings::cupsGetDest(name_c.as_ptr(), instance_ptr, self.num_dests, self.dests) + }; + + if dest.is_null() { + return false; + } + + edit(unsafe { &mut *dest }); + true + } + /// Find a destination by name and instance /// /// # Arguments @@ -1170,6 +1287,11 @@ pub fn find_destinations(type_filter: u32, mask: u32) -> Result mod tests { use super::*; + #[test] + fn test_named_destination_not_found() { + assert!(Destinations::named_destination("no-such-printer-at-all", None).is_none()); + } + #[test] fn test_destination_creation() { let mut options = std::collections::HashMap::new(); diff --git a/src/error_helpers.rs b/src/error_helpers.rs index aea8f13..ed29601 100644 --- a/src/error_helpers.rs +++ b/src/error_helpers.rs @@ -42,6 +42,10 @@ pub fn cups_error_to_our_error(operation: &str, dest_name: Option<&str>) -> Erro Error::PrinterNotAccepting(dest_name.unwrap_or("unknown").to_string(), message.clone()) } + bindings::ipp_status_e_IPP_STATUS_ERROR_NOT_ACCEPTING_JOBS => { + Error::PrinterNotAccepting(dest_name.unwrap_or("unknown").to_string(), message.clone()) + } + _ => { if message.contains("offline") || message.contains("unreachable") { Error::PrinterOffline(dest_name.unwrap_or("unknown").to_string()) diff --git a/src/job/lifecycle.rs b/src/job/lifecycle.rs index 70f9644..ac0882d 100644 --- a/src/job/lifecycle.rs +++ b/src/job/lifecycle.rs @@ -6,7 +6,7 @@ use std::ptr; impl Job { pub fn close(&self) -> Result<()> { - let dest = crate::get_destination(&self.dest_name)?; + let dest = &self.dest; let dest_info = dest.get_detailed_info(ptr::null_mut())?; let dest_ptr = dest.as_ptr(); @@ -44,7 +44,7 @@ impl Job { } pub fn cancel(&self) -> Result<()> { - let dest = crate::get_destination(&self.dest_name)?; + let dest = &self.dest; let dest_ptr = dest.as_ptr(); if dest_ptr.is_null() { diff --git a/src/job/mod.rs b/src/job/mod.rs index 5e1f0ac..65a9a52 100644 --- a/src/job/mod.rs +++ b/src/job/mod.rs @@ -27,17 +27,24 @@ pub const FORMAT_JPEG: &str = "image/jpeg"; #[derive(Debug, Clone)] pub struct Job { pub id: i32, - pub dest_name: String, pub title: String, + /// Destination this job was created for, kept instead of looked up again by name + dest: Destination, } impl Job { - pub fn new(id: i32, dest_name: String, title: String) -> Self { - Job { - id, - dest_name, - title, - } + pub fn new(id: i32, dest: Destination, title: String) -> Self { + Job { id, title, dest } + } + + /// Returns the name of the destination this job was created for + pub fn dest_name(&self) -> &str { + &self.dest.name + } + + /// Returns the destination this job was created for + pub fn destination(&self) -> &Destination { + &self.dest } pub fn submit_file>(&self, file_path: P, format: &str) -> Result<()> { @@ -60,7 +67,7 @@ impl Job { ))); } - validate_document_format(format, &self.dest_name)?; + validate_document_format(format, self.dest_name())?; let metadata = path.metadata().map_err(|e| { Error::DocumentSubmissionFailed(format!("Cannot access file metadata: {}", e)) @@ -98,18 +105,10 @@ impl Job { options: &[(String, String)], last_document: bool, ) -> Result<()> { - validate_document_format(format, &self.dest_name)?; + validate_document_format(format, self.dest_name())?; check_document_size(data.len(), None)?; - let dest = crate::get_destination(&self.dest_name)?; - - if !dest.is_accepting_jobs() { - return Err(Error::PrinterNotAccepting( - self.dest_name.clone(), - "Printer is currently not accepting jobs".to_string(), - )); - } - + let dest = &self.dest; let dest_info = dest.get_detailed_info(ptr::null_mut())?; let dest_ptr = dest.as_ptr(); @@ -177,7 +176,7 @@ impl Job { return Err(cups_error_to_our_error( "document start", - Some(&self.dest_name), + Some(self.dest_name()), )); } @@ -250,20 +249,13 @@ impl Job { } else { Err(cups_error_to_our_error( "document finish", - Some(&self.dest_name), + Some(self.dest_name()), )) } } } pub fn create_job(dest: &Destination, title: &str) -> Result { - if !dest.is_accepting_jobs() { - return Err(Error::PrinterNotAccepting( - dest.name.clone(), - "Printer is not accepting new jobs".to_string(), - )); - } - let title_c = CString::new(title)?; let dest_info = dest.get_detailed_info(ptr::null_mut())?; let dest_ptr = dest.as_ptr(); @@ -302,7 +294,7 @@ pub fn create_job(dest: &Destination, title: &str) -> Result { } if status == bindings::ipp_status_e_IPP_STATUS_OK as bindings::ipp_status_t { - Ok(Job::new(job_id, dest.name.clone(), title.to_string())) + Ok(Job::new(job_id, dest.clone(), title.to_string())) } else { Err(cups_error_to_our_error("job creation", Some(&dest.name))) } @@ -313,13 +305,6 @@ pub fn create_job_with_options( title: &str, options: &PrintOptions, ) -> Result { - if !dest.is_accepting_jobs() { - return Err(Error::PrinterNotAccepting( - dest.name.clone(), - "Printer is not accepting new jobs".to_string(), - )); - } - let title_c = CString::new(title)?; let dest_info = dest.get_detailed_info(ptr::null_mut())?; let dest_ptr = dest.as_ptr(); @@ -380,7 +365,7 @@ pub fn create_job_with_options( } if status == bindings::ipp_status_e_IPP_STATUS_OK as bindings::ipp_status_t { - Ok(Job::new(job_id, dest.name.clone(), title.to_string())) + Ok(Job::new(job_id, dest.clone(), title.to_string())) } else { Err(cups_error_to_our_error( "job creation with options", diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 2c7e308..8288eb1 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -226,7 +226,7 @@ fn test_integration_job_lifecycle() { let job_result = create_job(&printer, "Integration Test Job"); let job = match job_result { Ok(j) => { - println!("Created job: {} on printer {}", j.id, j.dest_name); + println!("Created job: {} on printer {}", j.id, j.dest_name()); j } Err(e) => {