use super::bindings as ffi;
use super::callbacks::{self, AcquireCompletion};
use super::frontend::Frontend;
use super::util;
use crate::error::{CpdbError, Result};
use crate::options::OptionsCollection;
use libc::c_char;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::os::fd::{FromRawFd, OwnedFd};
use std::ptr::NonNull;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Margin {
pub top: i32,
pub bottom: i32,
pub left: i32,
pub right: i32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Margins {
pub entries: Vec<Margin>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MediaSize {
pub width: i32,
pub length: i32,
}
#[derive(Debug)]
pub struct PrintFdHandle {
pub fd: OwnedFd,
pub job_id: String,
pub socket_path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrintSocketHandle {
pub socket_path: String,
pub job_id: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TranslationMap {
pub locale: Option<String>,
pub entries: HashMap<String, String>,
}
impl TranslationMap {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn get(&self, key: &str) -> Option<&str> {
self.entries.get(key).map(String::as_str)
}
}
#[derive(Debug)]
pub struct Printer<'frontend> {
raw: NonNull<ffi::cpdb_printer_obj_t>,
owned: bool,
_marker: PhantomData<&'frontend Frontend>,
}
impl<'frontend> Printer<'frontend> {
pub(crate) fn from_raw_borrowed(raw: *mut ffi::cpdb_printer_obj_t) -> Result<Self> {
let raw = NonNull::new(raw).ok_or(CpdbError::NullPointer)?;
Ok(Self {
raw,
owned: false,
_marker: PhantomData,
})
}
pub(crate) fn from_raw_owned(raw: *mut ffi::cpdb_printer_obj_t) -> Result<Self> {
let raw = NonNull::new(raw).ok_or(CpdbError::NullPointer)?;
Ok(Self {
raw,
owned: true,
_marker: PhantomData,
})
}
#[doc(hidden)]
pub fn as_raw(&self) -> *mut ffi::cpdb_printer_obj_t {
self.raw.as_ptr()
}
#[doc(hidden)]
pub(crate) fn is_owned(&self) -> bool {
self.owned
}
pub fn id(&self) -> Result<String> {
self.read_str_field(|p| unsafe { (*p).id })
}
pub fn name(&self) -> Result<String> {
self.read_str_field(|p| unsafe { (*p).name })
}
pub fn location(&self) -> Result<String> {
self.read_str_field(|p| unsafe { (*p).location })
}
pub fn description(&self) -> Result<String> {
self.read_str_field(|p| unsafe { (*p).info })
}
pub fn make_and_model(&self) -> Result<String> {
self.read_str_field(|p| unsafe { (*p).make_and_model })
}
pub fn backend_name(&self) -> Result<String> {
self.read_str_field(|p| unsafe { (*p).backend_name })
}
pub fn cached_state(&self) -> Result<String> {
self.read_str_field(|p| unsafe { (*p).state })
}
fn read_str_field<F>(&self, accessor: F) -> Result<String>
where
F: FnOnce(*mut ffi::cpdb_printer_obj_t) -> *const c_char,
{
let ptr = accessor(self.raw.as_ptr());
match unsafe { util::cstr_to_string(ptr) } {
Ok(s) => Ok(s),
Err(CpdbError::NullPointer) => Ok(String::new()),
Err(e) => Err(e),
}
}
pub fn get_updated_state(&self) -> Result<String> {
unsafe {
let raw = ffi::cpdbGetState(self.raw.as_ptr());
util::cstr_to_string(raw)
}
}
pub fn is_accepting_jobs(&self) -> Result<bool> {
Ok(unsafe { ffi::cpdbIsAcceptingJobs(self.raw.as_ptr()) } != 0)
}
pub fn set_user_default(&self) -> Result<bool> {
Ok(unsafe { ffi::cpdbSetUserDefaultPrinter(self.raw.as_ptr()) } != 0)
}
pub fn set_system_default(&self) -> Result<bool> {
Ok(unsafe { ffi::cpdbSetSystemDefaultPrinter(self.raw.as_ptr()) } != 0)
}
pub fn print_file(&self, file_path: &str) -> Result<String> {
let c_path = CString::new(file_path)?;
unsafe {
let id = ffi::cpdbPrintFile(self.raw.as_ptr(), c_path.as_ptr());
util::cstr_to_string_and_g_free(id)
.map_err(|_| CpdbError::JobFailed("cpdbPrintFile returned null".into()))
}
}
pub fn print_fd(&self, title: &str) -> Result<PrintFdHandle> {
let c_title = CString::new(title)?;
let mut jobid_ptr: *mut c_char = std::ptr::null_mut();
let mut socket_ptr: *mut c_char = std::ptr::null_mut();
let fd = unsafe {
ffi::cpdbPrintFD(
self.raw.as_ptr(),
&mut jobid_ptr,
c_title.as_ptr(),
&mut socket_ptr,
)
};
if fd < 0 {
unsafe {
if !jobid_ptr.is_null() {
glib_sys::g_free(jobid_ptr as glib_sys::gpointer);
}
if !socket_ptr.is_null() {
glib_sys::g_free(socket_ptr as glib_sys::gpointer);
}
}
return Err(CpdbError::JobFailed(
"cpdbPrintFD returned an invalid fd".into(),
));
}
let job_id = if jobid_ptr.is_null() {
String::new()
} else {
unsafe { util::cstr_to_string_and_g_free(jobid_ptr) }.unwrap_or_default()
};
let socket_path = if socket_ptr.is_null() {
None
} else {
unsafe { util::cstr_to_string_and_g_free(socket_ptr) }.ok()
};
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
Ok(PrintFdHandle {
fd,
job_id,
socket_path,
})
}
pub fn print_socket(&self, title: &str) -> Result<PrintSocketHandle> {
let c_title = CString::new(title)?;
let mut jobid_ptr: *mut c_char = std::ptr::null_mut();
let socket_ptr =
unsafe { ffi::cpdbPrintSocket(self.raw.as_ptr(), &mut jobid_ptr, c_title.as_ptr()) };
if socket_ptr.is_null() {
if !jobid_ptr.is_null() {
unsafe { glib_sys::g_free(jobid_ptr as glib_sys::gpointer) };
}
return Err(CpdbError::JobFailed(
"cpdbPrintSocket returned a null socket path".into(),
));
}
let socket_path = unsafe { util::cstr_to_string_and_g_free(socket_ptr) }?;
let job_id = if jobid_ptr.is_null() {
String::new()
} else {
unsafe { util::cstr_to_string_and_g_free(jobid_ptr) }.unwrap_or_default()
};
Ok(PrintSocketHandle {
socket_path,
job_id,
})
}
pub fn submit_job(
&self,
file_path: &str,
options: &[(&str, &str)],
title: &str,
) -> Result<String> {
let c_path = CString::new(file_path)?;
let c_title = CString::new(title)?;
for (key, value) in options {
let k = CString::new(*key)?;
let v = CString::new(*value)?;
unsafe { ffi::cpdbAddSettingToPrinter(self.raw.as_ptr(), k.as_ptr(), v.as_ptr()) };
}
unsafe {
let id = ffi::cpdbPrintFileWithJobTitle(
self.raw.as_ptr(),
c_path.as_ptr(),
c_title.as_ptr(),
);
util::cstr_to_string_and_g_free(id)
.map_err(|_| CpdbError::JobFailed("cpdbPrintFileWithJobTitle returned null".into()))
}
}
pub fn get_option(&self, option_name: &str) -> Result<Option<String>> {
let c_name = CString::new(option_name)?;
unsafe {
let opt = ffi::cpdbGetOption(self.raw.as_ptr(), c_name.as_ptr());
if opt.is_null() {
return Ok(None);
}
let dv = (*opt).default_value;
if dv.is_null() {
Ok(None)
} else {
util::cstr_to_string(dv).map(Some)
}
}
}
pub fn get_default(&self, option_name: &str) -> Result<String> {
let c_name = CString::new(option_name)?;
unsafe {
let v = ffi::cpdbGetDefault(self.raw.as_ptr(), c_name.as_ptr());
util::cstr_to_string_and_g_free(v)
}
}
pub fn get_current(&self, option_name: &str) -> Result<String> {
let c_name = CString::new(option_name)?;
unsafe {
let v = ffi::cpdbGetCurrent(self.raw.as_ptr(), c_name.as_ptr());
util::cstr_to_string_and_g_free(v)
}
}
pub fn acquire_details(&self) {
unsafe {
ffi::cpdbAcquireDetails(self.raw.as_ptr(), None, std::ptr::null_mut());
}
}
pub fn acquire_details_with<F>(&self, completion: F)
where
F: FnOnce(&Printer<'_>, bool) + Send + 'static,
{
let boxed: Box<AcquireCompletion> = Box::new(completion);
let user_data = callbacks::into_completion_user_data(boxed);
unsafe {
ffi::cpdbAcquireDetails(
self.raw.as_ptr(),
Some(callbacks::acquire_trampoline),
user_data,
);
}
}
pub fn get_options_collection(&self) -> Result<OptionsCollection> {
unsafe {
let opts = ffi::cpdbGetAllOptions(self.raw.as_ptr());
let opts = NonNull::new(opts).ok_or_else(|| {
CpdbError::BackendError(
"cpdbGetAllOptions returned null — call acquire_details() first".into(),
)
})?;
OptionsCollection::from_raw(opts.as_ptr())
}
}
pub fn get_setting(&self, name: &str) -> Result<Option<String>> {
let c_name = CString::new(name)?;
unsafe {
let v = ffi::cpdbGetSetting(self.raw.as_ptr(), c_name.as_ptr());
if v.is_null() {
Ok(None)
} else {
util::cstr_to_string(v).map(Some)
}
}
}
pub fn add_setting(&self, name: &str, value: &str) -> Result<()> {
let c_name = CString::new(name)?;
let c_val = CString::new(value)?;
unsafe { ffi::cpdbAddSettingToPrinter(self.raw.as_ptr(), c_name.as_ptr(), c_val.as_ptr()) };
Ok(())
}
pub fn clear_setting(&self, name: &str) -> Result<bool> {
let c_name = CString::new(name)?;
let existed =
unsafe { ffi::cpdbClearSettingFromPrinter(self.raw.as_ptr(), c_name.as_ptr()) };
Ok(existed != 0)
}
pub fn get_media(&self, media_name: &str) -> Result<Option<String>> {
let c_name = CString::new(media_name)?;
unsafe {
let m = ffi::cpdbGetMedia(self.raw.as_ptr(), c_name.as_ptr());
if m.is_null() {
return Ok(None);
}
let name_ptr = (*m).name;
if name_ptr.is_null() {
Ok(None)
} else {
util::cstr_to_string(name_ptr).map(Some)
}
}
}
pub fn get_media_size(&self, media_name: &str) -> Result<MediaSize> {
let c_name = CString::new(media_name)?;
let (mut width, mut length): (i32, i32) = (0, 0);
let rc = unsafe {
ffi::cpdbGetMediaSize(self.raw.as_ptr(), c_name.as_ptr(), &mut width, &mut length)
};
if rc == 0 {
Ok(MediaSize { width, length })
} else {
Err(CpdbError::NotFound(format!("media size '{media_name}'")))
}
}
pub fn get_media_margins(&self, media_name: &str) -> Result<Margins> {
let c_name = CString::new(media_name)?;
let mut raw_margins: *mut ffi::cpdb_margin_t = std::ptr::null_mut();
let count = unsafe {
ffi::cpdbGetMediaMargins(self.raw.as_ptr(), c_name.as_ptr(), &mut raw_margins)
};
if count <= 0 || raw_margins.is_null() {
return Err(CpdbError::NotFound(format!("media margins '{media_name}'")));
}
let mut out = Vec::with_capacity(count as usize);
for i in 0..(count as isize) {
let m = unsafe { &*raw_margins.offset(i) };
out.push(Margin {
top: m.top,
bottom: m.bottom,
left: m.left,
right: m.right,
});
}
Ok(Margins { entries: out })
}
pub fn acquire_translations(&self, locale: &str) -> Result<()> {
let c_locale = CString::new(locale)?;
unsafe {
ffi::cpdbAcquireTranslations(
self.raw.as_ptr(),
c_locale.as_ptr(),
None,
std::ptr::null_mut(),
);
}
Ok(())
}
pub fn acquire_translations_with<F>(&self, locale: &str, completion: F) -> Result<()>
where
F: FnOnce(&Printer<'_>, bool) + Send + 'static,
{
let c_locale = CString::new(locale)?;
let boxed: Box<AcquireCompletion> = Box::new(completion);
let user_data = callbacks::into_completion_user_data(boxed);
unsafe {
ffi::cpdbAcquireTranslations(
self.raw.as_ptr(),
c_locale.as_ptr(),
Some(callbacks::acquire_trampoline),
user_data,
);
}
Ok(())
}
pub fn get_option_translation(&self, option: &str, locale: &str) -> Result<Option<String>> {
let c_opt = CString::new(option)?;
let c_locale = CString::new(locale)?;
unsafe {
let t =
ffi::cpdbGetOptionTranslation(self.raw.as_ptr(), c_opt.as_ptr(), c_locale.as_ptr());
translation_to_option(t)
}
}
pub fn get_choice_translation(
&self,
option: &str,
choice: &str,
locale: &str,
) -> Result<Option<String>> {
let c_opt = CString::new(option)?;
let c_choice = CString::new(choice)?;
let c_locale = CString::new(locale)?;
unsafe {
let t = ffi::cpdbGetChoiceTranslation(
self.raw.as_ptr(),
c_opt.as_ptr(),
c_choice.as_ptr(),
c_locale.as_ptr(),
);
translation_to_option(t)
}
}
pub fn get_option_translation_from_table(
&self,
option: &str,
locale: &str,
) -> Result<Option<String>> {
let c_opt = CString::new(option)?;
let c_locale = CString::new(locale)?;
unsafe {
let t = ffi::cpdbGetOptionTranslationFromTable(
self.raw.as_ptr(),
c_opt.as_ptr(),
c_locale.as_ptr(),
);
translation_to_option(t)
}
}
pub fn get_choice_translation_from_table(
&self,
option: &str,
choice: &str,
locale: &str,
) -> Result<Option<String>> {
let c_opt = CString::new(option)?;
let c_choice = CString::new(choice)?;
let c_locale = CString::new(locale)?;
unsafe {
let t = ffi::cpdbGetChoiceTranslationFromTable(
self.raw.as_ptr(),
c_opt.as_ptr(),
c_choice.as_ptr(),
c_locale.as_ptr(),
);
translation_to_option(t)
}
}
pub fn get_group_translation(&self, group: &str, locale: &str) -> Result<Option<String>> {
let c_group = CString::new(group)?;
let c_locale = CString::new(locale)?;
unsafe {
let t = ffi::cpdbGetGroupTranslation(
self.raw.as_ptr(),
c_group.as_ptr(),
c_locale.as_ptr(),
);
translation_to_option(t)
}
}
pub fn get_all_translations(&self, locale: &str) -> Result<()> {
let c_locale = CString::new(locale)?;
unsafe { ffi::cpdbGetAllTranslations(self.raw.as_ptr(), c_locale.as_ptr()) };
Ok(())
}
pub fn translations(&self) -> TranslationMap {
let raw = self.raw.as_ptr();
let locale_ptr = unsafe { (*raw).locale };
let table = unsafe { (*raw).translations } as *mut glib_sys::GHashTable;
let locale = if locale_ptr.is_null() {
None
} else {
unsafe { util::cstr_to_string(locale_ptr) }.ok()
};
if table.is_null() {
return TranslationMap {
locale,
entries: HashMap::new(),
};
}
let mut entries: HashMap<String, String> = HashMap::new();
unsafe {
let mut iter = MaybeUninit::<glib_sys::GHashTableIter>::uninit();
glib_sys::g_hash_table_iter_init(iter.as_mut_ptr(), table);
let mut iter = iter.assume_init();
let mut key: glib_sys::gpointer = std::ptr::null_mut();
let mut value: glib_sys::gpointer = std::ptr::null_mut();
while glib_sys::g_hash_table_iter_next(&mut iter, &mut key, &mut value) != 0 {
if key.is_null() || value.is_null() {
continue;
}
let k = CStr::from_ptr(key as *const c_char)
.to_string_lossy()
.into_owned();
let v = CStr::from_ptr(value as *const c_char)
.to_string_lossy()
.into_owned();
entries.insert(k, v);
}
}
TranslationMap { locale, entries }
}
pub fn debug_dump(&self) {
unsafe { ffi::cpdbDebugPrinter(self.raw.as_ptr()) };
}
pub fn dump_basic_options(&self) {
unsafe { ffi::cpdbPrintBasicOptions(self.raw.as_ptr()) };
}
pub fn pickle_to_file(&self, path: &str, frontend: &Frontend) -> Result<()> {
let c_path = CString::new(path)?;
unsafe {
ffi::cpdbPicklePrinterToFile(self.raw.as_ptr(), c_path.as_ptr(), frontend.as_raw());
}
Ok(())
}
pub fn load_from_file(path: &str) -> Result<Printer<'static>> {
let c_path = CString::new(path)?;
let raw = unsafe { ffi::cpdbResurrectPrinterFromFile(c_path.as_ptr()) };
if raw.is_null() {
return Err(CpdbError::NotFound(format!("pickled printer at {path}")));
}
Printer::<'static>::from_raw_owned(raw)
}
}
unsafe fn translation_to_option(ptr: *mut c_char) -> Result<Option<String>> {
if ptr.is_null() {
Ok(None)
} else {
unsafe { util::cstr_to_string_and_g_free(ptr) }.map(Some)
}
}
impl Drop for Printer<'_> {
fn drop(&mut self) {
if self.owned {
unsafe { ffi::cpdbDeletePrinterObj(self.raw.as_ptr()) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_raw_borrowed_rejects_null() {
let r = Printer::from_raw_borrowed(std::ptr::null_mut());
assert!(matches!(r, Err(CpdbError::NullPointer)));
}
#[test]
fn from_raw_owned_rejects_null() {
let r = Printer::from_raw_owned(std::ptr::null_mut());
assert!(matches!(r, Err(CpdbError::NullPointer)));
}
#[test]
#[cfg_attr(miri, ignore)]
fn load_from_nonexistent_file_returns_error() {
let r = Printer::load_from_file("/tmp/cpdb-rs-nonexistent-pickle-file");
assert!(r.is_err());
}
}