use std::ffi::{ c_char, c_int, c_uint, c_void, CStr, CString };
use std::ptr;
use std::sync::OnceLock;
use super::{ Job, JobState, Printer, PrinterState, PrintOptions };
type Http = c_void;
type DestInfo = c_void;
#[ repr( C ) ]
struct CupsOption
{
name: *mut c_char,
value: *mut c_char,
}
#[ repr( C ) ]
struct CupsDest
{
name: *mut c_char,
instance: *mut c_char,
is_default: c_int,
num_options: c_int,
options: *mut CupsOption,
}
#[ repr( C ) ]
struct CupsJob
{
id: c_int,
dest: *mut c_char,
title: *mut c_char,
user: *mut c_char,
format: *mut c_char,
state: c_int,
size: c_int,
priority: c_int,
completed_time: libc::time_t,
creation_time: libc::time_t,
processing_time: libc::time_t,
}
struct Lib
{
get_dests: unsafe extern "C" fn( *mut Http, *mut *mut CupsDest ) -> c_int,
set_dests: unsafe extern "C" fn( *mut Http, c_int, *mut CupsDest ) -> c_int,
free_dests: unsafe extern "C" fn( c_int, *mut CupsDest ),
get_named_dest: unsafe extern "C" fn( *mut Http, *const c_char, *const c_char ) -> *mut CupsDest,
get_option: unsafe extern "C" fn( *const c_char, c_int, *mut CupsOption ) -> *const c_char,
add_option: unsafe extern "C" fn( *const c_char, *const c_char, c_int, *mut *mut CupsOption ) -> c_int,
free_options: unsafe extern "C" fn( c_int, *mut CupsOption ),
connect_dest: unsafe extern "C" fn( *mut CupsDest, c_uint, c_int, *mut c_int, *mut c_char, usize, *mut c_void, *mut c_void ) -> *mut Http,
copy_dest_info: unsafe extern "C" fn( *mut Http, *mut CupsDest ) -> *mut DestInfo,
free_dest_info: unsafe extern "C" fn( *mut DestInfo ),
create_dest_job: unsafe extern "C" fn( *mut Http, *mut CupsDest, *mut DestInfo, *mut c_int, *const c_char, c_int, *mut CupsOption ) -> c_int,
start_dest_document: unsafe extern "C" fn( *mut Http, *mut CupsDest, *mut DestInfo, c_int, *const c_char, *const c_char, c_int, *mut CupsOption, c_int ) -> c_int,
write_request_data: unsafe extern "C" fn( *mut Http, *const c_char, usize ) -> c_int,
finish_dest_document: unsafe extern "C" fn( *mut Http, *mut CupsDest, *mut DestInfo ) -> c_int,
cancel_dest_job: unsafe extern "C" fn( *mut Http, *mut CupsDest, c_int ) -> c_int,
get_jobs: unsafe extern "C" fn( *mut Http, *mut *mut CupsJob, *const c_char, c_int, c_int ) -> c_int,
free_jobs: unsafe extern "C" fn( c_int, *mut CupsJob ),
cancel_job: unsafe extern "C" fn( *mut Http, *const c_char, c_int, c_int ) -> c_int,
http_close: unsafe extern "C" fn( *mut Http ),
last_error_string: unsafe extern "C" fn() -> *const c_char,
}
unsafe impl Send for Lib {}
unsafe impl Sync for Lib {}
const HTTP_STATUS_CONTINUE: c_int = 100;
const CHUNK: usize = 64 * 1024;
const CONNECT_TIMEOUT_MS: c_int = 30_000;
fn lib() -> Result<&'static Lib, String>
{
static LIB: OnceLock<Option<Lib>> = OnceLock::new();
LIB.get_or_init( || unsafe { open() } ).as_ref().ok_or_else( || rust_i18n::t!( "print.no_cups" ).to_string() )
}
unsafe fn open() -> Option<Lib>
{
unsafe
{
let handle = libc::dlopen( c"libcups.so.2".as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL );
if handle.is_null() { return None; }
macro_rules! sym
{
( $name:literal ) =>
{{
let symbol = libc::dlsym( handle, $name.as_ptr() );
if symbol.is_null() { return None; }
std::mem::transmute::<*mut c_void, _>( symbol )
}};
}
Some( Lib
{
get_dests: sym!( c"cupsGetDests2" ),
set_dests: sym!( c"cupsSetDests2" ),
free_dests: sym!( c"cupsFreeDests" ),
get_named_dest: sym!( c"cupsGetNamedDest" ),
get_option: sym!( c"cupsGetOption" ),
add_option: sym!( c"cupsAddOption" ),
free_options: sym!( c"cupsFreeOptions" ),
connect_dest: sym!( c"cupsConnectDest" ),
copy_dest_info: sym!( c"cupsCopyDestInfo" ),
free_dest_info: sym!( c"cupsFreeDestInfo" ),
create_dest_job: sym!( c"cupsCreateDestJob" ),
start_dest_document: sym!( c"cupsStartDestDocument" ),
write_request_data: sym!( c"cupsWriteRequestData" ),
finish_dest_document: sym!( c"cupsFinishDestDocument" ),
cancel_dest_job: sym!( c"cupsCancelDestJob" ),
get_jobs: sym!( c"cupsGetJobs2" ),
free_jobs: sym!( c"cupsFreeJobs" ),
cancel_job: sym!( c"cupsCancelJob2" ),
http_close: sym!( c"httpClose" ),
last_error_string: sym!( c"cupsLastErrorString" ),
} )
}
}
fn ipp_ok( status: c_int ) -> bool
{
( 0..0x100 ).contains( &status )
}
unsafe fn string( s: *const c_char ) -> String
{
unsafe
{
if s.is_null() { String::new() } else { CStr::from_ptr( s ).to_string_lossy().into_owned() }
}
}
unsafe fn last_error( lib: &Lib ) -> String
{
unsafe
{
let message = string( ( lib.last_error_string )() );
if message.is_empty() { rust_i18n::t!( "print.failed" ).to_string() } else { message }
}
}
fn c( s: &str ) -> CString
{
CString::new( s.replace( '\0', "" ) ).unwrap_or_default()
}
unsafe fn option( lib: &Lib, dest: &CupsDest, name: &CStr ) -> String
{
unsafe
{
string( ( lib.get_option )( name.as_ptr(), dest.num_options, dest.options ) )
}
}
unsafe fn printer( lib: &Lib, dest: &CupsDest ) -> Printer
{
unsafe
{
let name = string( dest.name );
let instance = ( !dest.instance.is_null() ).then( || string( dest.instance ) );
let description = option( lib, dest, c"printer-info" );
let state = match option( lib, dest, c"printer-state" ).as_str()
{
"3" => PrinterState::Idle,
"4" => PrinterState::Printing,
"5" => PrinterState::Stopped,
_ => PrinterState::Unknown,
};
let reasons = option( lib, dest, c"printer-state-reasons" );
Printer
{
description: if description.is_empty() { name.clone() } else { description },
location: option( lib, dest, c"printer-location" ),
model: option( lib, dest, c"printer-make-and-model" ),
accepting: option( lib, dest, c"printer-is-accepting-jobs" ) != "false",
reasons: if reasons == "none" { String::new() } else { reasons },
default: dest.is_default != 0,
name,
instance,
state,
}
}
}
pub fn printers() -> Result<Vec<Printer>, String>
{
let lib = lib()?;
unsafe
{
let mut dests = ptr::null_mut();
let count = ( lib.get_dests )( ptr::null_mut(), &mut dests );
let mut list: Vec<Printer> = ( 0..count.max( 0 ) as usize ).map( |i| printer( lib, &*dests.add( i ) ) ).collect();
( lib.free_dests )( count, dests );
list.sort_by( |a, b| b.default.cmp( &a.default ).then_with( || a.description.to_lowercase().cmp( &b.description.to_lowercase() ) ) );
Ok( list )
}
}
pub fn set_default( name: &str, instance: Option<&str> ) -> Result<(), String>
{
let lib = lib()?;
unsafe
{
let mut dests = ptr::null_mut();
let count = ( lib.get_dests )( ptr::null_mut(), &mut dests );
let mut found = false;
for i in 0..count.max( 0 ) as usize
{
let dest = &mut *dests.add( i );
let matches = string( dest.name ) == name && ( !dest.instance.is_null() ).then( || string( dest.instance ) ).as_deref() == instance;
dest.is_default = matches as c_int;
found |= matches;
}
let written = found && ( lib.set_dests )( ptr::null_mut(), count, dests ) == 0;
let error = if written { None } else { Some( last_error( lib ) ) };
( lib.free_dests )( count, dests );
error.map_or( Ok( () ), Err )
}
}
pub fn print( printer: &Printer, title: &str, options: &PrintOptions, pdf: &[ u8 ] ) -> Result<i32, String>
{
let lib = lib()?;
let name = c( &printer.name );
let instance = printer.instance.as_deref().map( c );
let title = c( title );
unsafe
{
let dest = ( lib.get_named_dest )( ptr::null_mut(), name.as_ptr(), instance.as_ref().map_or( ptr::null(), |i| i.as_ptr() ) );
if dest.is_null() { return Err( last_error( lib ) ); }
let mut resource = [ 0 as c_char; 1024 ];
let http = ( lib.connect_dest )( dest, 0, CONNECT_TIMEOUT_MS, ptr::null_mut(), resource.as_mut_ptr(), resource.len(), ptr::null_mut(), ptr::null_mut() );
if http.is_null()
{
let error = last_error( lib );
( lib.free_dests )( 1, dest );
return Err( error );
}
let info = ( lib.copy_dest_info )( http, dest );
let result = send( lib, http, dest, info, &title, options, pdf );
if !info.is_null() { ( lib.free_dest_info )( info ); }
( lib.http_close )( http );
( lib.free_dests )( 1, dest );
result
}
}
unsafe fn send( lib: &Lib, http: *mut Http, dest: *mut CupsDest, info: *mut DestInfo, title: &CStr, options: &PrintOptions, pdf: &[ u8 ] ) -> Result<i32, String>
{
unsafe
{
let mut count = 0;
let mut list = ptr::null_mut();
for ( key, value ) in ipp_options( options )
{
let ( key, value ) = ( c( key ), c( &value ) );
count = ( lib.add_option )( key.as_ptr(), value.as_ptr(), count, &mut list );
}
let mut job = 0;
let result = if !ipp_ok( ( lib.create_dest_job )( http, dest, info, &mut job, title.as_ptr(), count, list ) )
{
Err( last_error( lib ) )
} else if ( lib.start_dest_document )( http, dest, info, job, title.as_ptr(), c"application/pdf".as_ptr(), 0, ptr::null_mut(), 1 ) != HTTP_STATUS_CONTINUE {
let error = last_error( lib );
( lib.cancel_dest_job )( http, dest, job );
Err( error )
} else {
let written = pdf.chunks( CHUNK ).all( |chunk| ( lib.write_request_data )( http, chunk.as_ptr().cast(), chunk.len() ) == HTTP_STATUS_CONTINUE );
let status = ( lib.finish_dest_document )( http, dest, info );
if written && ipp_ok( status ) { Ok( job ) } else { Err( last_error( lib ) ) }
};
( lib.free_options )( count, list );
result
}
}
fn ipp_options( options: &PrintOptions ) -> Vec<( &'static str, String )>
{
vec![
( "copies", options.copies.clamp( 1, PrintOptions::MAX_COPIES ).to_string() ),
( "media", options.paper.media.to_string() ),
( "sides", options.sides.ipp().to_string() ),
( "print-color-mode", if options.color { "color" } else { "monochrome" }.to_string() ),
]
}
pub fn jobs( printer: Option<&str> ) -> Result<Vec<Job>, String>
{
let lib = lib()?;
let name = printer.map( c );
unsafe
{
let mut list = ptr::null_mut();
let count = ( lib.get_jobs )( ptr::null_mut(), &mut list, name.as_ref().map_or( ptr::null(), |n| n.as_ptr() ), 1, 0 );
if count < 0 { return Err( last_error( lib ) ); }
let jobs = ( 0..count as usize ).map( |i|
{
let job = &*list.add( i );
Job
{
id: job.id,
printer: string( job.dest ),
title: string( job.title ),
state: JobState::from_ipp( job.state ),
size_kb: job.size.max( 0 ) as u32,
created: job.creation_time as i64,
}
} ).collect();
( lib.free_jobs )( count, list );
Ok( jobs )
}
}
pub fn cancel( printer: &str, job: i32 ) -> Result<(), String>
{
let lib = lib()?;
let name = c( printer );
unsafe
{
if ipp_ok( ( lib.cancel_job )( ptr::null_mut(), name.as_ptr(), job, 0 ) ) { Ok( () ) } else { Err( last_error( lib ) ) }
}
}