cups_rs 0.3.0

Rust bindings for CUPS (Common UNIX Printing System)
Documentation
mod lifecycle;
mod management;
mod options;
mod status;

pub use management::{cancel_job, get_active_jobs, get_completed_jobs, get_job_info, get_jobs};
pub use options::{ColorMode, DuplexMode, Orientation, PrintOptions, PrintQuality};
pub use status::{JobInfo, JobStatus};

use crate::bindings;
use crate::destination::Destination;
use crate::error::{Error, Result};
use crate::error_helpers::{
    check_document_size, cups_error_to_our_error, validate_document_format,
};
use std::ffi::CString;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::ptr;

pub const FORMAT_PDF: &str = "application/pdf";
pub const FORMAT_POSTSCRIPT: &str = "application/postscript";
pub const FORMAT_TEXT: &str = "text/plain";
pub const FORMAT_JPEG: &str = "image/jpeg";

#[derive(Debug, Clone)]
pub struct Job {
    pub id: i32,
    pub dest_name: String,
    pub title: String,
}

impl Job {
    pub fn new(id: i32, dest_name: String, title: String) -> Self {
        Job {
            id,
            dest_name,
            title,
        }
    }

    pub fn submit_file<P: AsRef<Path>>(&self, file_path: P, format: &str) -> Result<()> {
        self.submit_file_with_options(file_path, format, &[], true)
    }

    pub fn submit_file_with_options<P: AsRef<Path>>(
        &self,
        file_path: P,
        format: &str,
        options: &[(String, String)],
        last_document: bool,
    ) -> Result<()> {
        let path = file_path.as_ref();

        if !path.exists() {
            return Err(Error::DocumentSubmissionFailed(format!(
                "File not found: {}",
                path.display()
            )));
        }

        validate_document_format(format, &self.dest_name)?;

        let metadata = path.metadata().map_err(|e| {
            Error::DocumentSubmissionFailed(format!("Cannot access file metadata: {}", e))
        })?;

        check_document_size(metadata.len() as usize, None)?;

        let mut file = File::open(path)
            .map_err(|e| Error::DocumentSubmissionFailed(format!("Failed to open file: {}", e)))?;

        let mut content = Vec::new();
        file.read_to_end(&mut content)
            .map_err(|e| Error::DocumentSubmissionFailed(format!("Failed to read file: {}", e)))?;

        self.submit_data_with_options(
            &content,
            format,
            path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("document"),
            options,
            last_document,
        )
    }

    pub fn submit_data(&self, data: &[u8], format: &str, doc_name: &str) -> Result<()> {
        self.submit_data_with_options(data, format, doc_name, &[], true)
    }

    pub fn submit_data_with_options(
        &self,
        data: &[u8],
        format: &str,
        doc_name: &str,
        options: &[(String, String)],
        last_document: bool,
    ) -> Result<()> {
        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_info = dest.get_detailed_info(ptr::null_mut())?;
        let dest_ptr = dest.as_ptr();

        if dest_ptr.is_null() {
            return Err(Error::NullPointer);
        }

        let doc_name_c = CString::new(doc_name)?;
        let format_c = CString::new(format)?;

        let mut cups_options_ptr: *mut bindings::cups_option_s = ptr::null_mut();
        let mut num_options = 0;

        for (name, value) in options {
            let name_c = CString::new(name.as_str())?;
            let value_c = CString::new(value.as_str())?;

            unsafe {
                num_options = bindings::cupsAddOption(
                    name_c.as_ptr(),
                    value_c.as_ptr(),
                    num_options,
                    &mut cups_options_ptr,
                );
            }
        }

        let status = unsafe {
            bindings::cupsStartDestDocument(
                ptr::null_mut(),
                dest_ptr,
                dest_info.as_ptr(),
                self.id,
                doc_name_c.as_ptr(),
                format_c.as_ptr(),
                num_options,
                cups_options_ptr,
                if last_document { 1 } else { 0 },
            )
        };

        if status != bindings::http_status_e_HTTP_STATUS_CONTINUE as bindings::http_status_t {
            unsafe {
                if !cups_options_ptr.is_null() {
                    bindings::cupsFreeOptions(num_options, cups_options_ptr);
                }

                let dest_box = Box::from_raw(dest_ptr);
                if !dest_box.name.is_null() {
                    let _ = CString::from_raw(dest_box.name);
                }
                if !dest_box.instance.is_null() {
                    let _ = CString::from_raw(dest_box.instance);
                }
                if !dest_box.options.is_null() {
                    bindings::cupsFreeOptions(dest_box.num_options, dest_box.options);
                }
            }

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

        let mut bytes_written = 0;
        let mut remaining = data.len();

        while remaining > 0 {
            let chunk_size = remaining.min(8192);
            let chunk = &data[bytes_written..bytes_written + chunk_size];

            let result = unsafe {
                bindings::cupsWriteRequestData(
                    ptr::null_mut(),
                    chunk.as_ptr() as *const ::std::os::raw::c_char,
                    chunk_size,
                )
            };

            if result != bindings::http_status_e_HTTP_STATUS_CONTINUE as bindings::http_status_t {
                unsafe {
                    if !cups_options_ptr.is_null() {
                        bindings::cupsFreeOptions(num_options, cups_options_ptr);
                    }

                    let dest_box = Box::from_raw(dest_ptr);
                    if !dest_box.name.is_null() {
                        let _ = CString::from_raw(dest_box.name);
                    }
                    if !dest_box.instance.is_null() {
                        let _ = CString::from_raw(dest_box.instance);
                    }
                    if !dest_box.options.is_null() {
                        bindings::cupsFreeOptions(dest_box.num_options, dest_box.options);
                    }
                }

                return Err(Error::DocumentSubmissionFailed(format!(
                    "Failed to write data at byte {} (network error or timeout)",
                    bytes_written
                )));
            }

            bytes_written += chunk_size;
            remaining -= chunk_size;
        }

        let finish_status = unsafe {
            bindings::cupsFinishDestDocument(ptr::null_mut(), dest_ptr, dest_info.as_ptr())
        };

        unsafe {
            if !cups_options_ptr.is_null() {
                bindings::cupsFreeOptions(num_options, cups_options_ptr);
            }

            let dest_box = Box::from_raw(dest_ptr);
            if !dest_box.name.is_null() {
                let _ = CString::from_raw(dest_box.name);
            }
            if !dest_box.instance.is_null() {
                let _ = CString::from_raw(dest_box.instance);
            }
            if !dest_box.options.is_null() {
                bindings::cupsFreeOptions(dest_box.num_options, dest_box.options);
            }
        }

        if finish_status == bindings::ipp_status_e_IPP_STATUS_OK as bindings::ipp_status_t {
            Ok(())
        } else {
            Err(cups_error_to_our_error(
                "document finish",
                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();

    if dest_ptr.is_null() {
        return Err(Error::NullPointer);
    }

    let mut job_id: i32 = 0;

    let status = unsafe {
        bindings::cupsCreateDestJob(
            ptr::null_mut(),
            dest_ptr,
            dest_info.as_ptr(),
            &mut job_id,
            title_c.as_ptr(),
            0,
            ptr::null_mut(),
        )
    };

    unsafe {
        let dest_box = Box::from_raw(dest_ptr);

        if !dest_box.name.is_null() {
            let _ = CString::from_raw(dest_box.name);
        }
        if !dest_box.instance.is_null() {
            let _ = CString::from_raw(dest_box.instance);
        }

        if !dest_box.options.is_null() {
            bindings::cupsFreeOptions(dest_box.num_options, dest_box.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()))
    } else {
        Err(cups_error_to_our_error("job creation", Some(&dest.name)))
    }
}

pub fn create_job_with_options(
    dest: &Destination,
    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();

    if dest_ptr.is_null() {
        return Err(Error::NullPointer);
    }

    let cups_options = options.as_cups_options();
    let mut cups_options_ptr: *mut bindings::cups_option_s = ptr::null_mut();
    let mut num_options = 0;

    for (name, value) in &cups_options {
        let name_c = CString::new(*name)?;
        let value_c = CString::new(*value)?;

        unsafe {
            num_options = bindings::cupsAddOption(
                name_c.as_ptr(),
                value_c.as_ptr(),
                num_options,
                &mut cups_options_ptr,
            );
        }
    }

    let mut job_id: i32 = 0;

    let status = unsafe {
        bindings::cupsCreateDestJob(
            ptr::null_mut(),
            dest_ptr,
            dest_info.as_ptr(),
            &mut job_id,
            title_c.as_ptr(),
            num_options,
            cups_options_ptr,
        )
    };

    unsafe {
        if !cups_options_ptr.is_null() {
            bindings::cupsFreeOptions(num_options, cups_options_ptr);
        }

        let dest_box = Box::from_raw(dest_ptr);

        if !dest_box.name.is_null() {
            let _ = CString::from_raw(dest_box.name);
        }
        if !dest_box.instance.is_null() {
            let _ = CString::from_raw(dest_box.instance);
        }

        if !dest_box.options.is_null() {
            bindings::cupsFreeOptions(dest_box.num_options, dest_box.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()))
    } else {
        Err(cups_error_to_our_error(
            "job creation with options",
            Some(&dest.name),
        ))
    }
}