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
142 changes: 132 additions & 10 deletions src/destination/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand Down Expand Up @@ -715,6 +713,125 @@ impl Destinations {
}
}

/// Get a destination's saved options via `cupsGetNamedDest`
pub fn named_destination(name: &str, instance: Option<&str>) -> Option<Destination> {
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<String> {
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
Expand Down Expand Up @@ -1170,6 +1287,11 @@ pub fn find_destinations(type_filter: u32, mask: u32) -> Result<Vec<Destination>
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();
Expand Down
4 changes: 4 additions & 0 deletions src/error_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
4 changes: 2 additions & 2 deletions src/job/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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() {
Expand Down
57 changes: 21 additions & 36 deletions src/job/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<P: AsRef<Path>>(&self, file_path: P, format: &str) -> Result<()> {
Expand All @@ -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))
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -177,7 +176,7 @@ impl Job {

return Err(cups_error_to_our_error(
"document start",
Some(&self.dest_name),
Some(self.dest_name()),
));
}

Expand Down Expand Up @@ -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<Job> {
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();
Expand Down Expand Up @@ -302,7 +294,7 @@ pub fn create_job(dest: &Destination, title: &str) -> Result<Job> {
}

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)))
}
Expand All @@ -313,13 +305,6 @@ pub fn create_job_with_options(
title: &str,
options: &PrintOptions,
) -> Result<Job> {
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();
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading