diff --git a/examples/authentication.rs b/examples/authentication.rs index aa4d9e7..20319bf 100644 --- a/examples/authentication.rs +++ b/examples/authentication.rs @@ -1,6 +1,7 @@ use cups_rs::{ - auth::{set_password_callback, get_password, do_authentication}, - get_destination, create_job, Result, + Result, + auth::{do_authentication, get_password, set_password_callback}, + create_job, get_destination, }; use std::io::{self, Write}; @@ -14,14 +15,14 @@ fn main() -> Result<()> { println!("Prompt: {}", prompt); println!("Method: {}", method); println!("Resource: {}", resource); - + print!("Enter password (or 'q' to quit): "); io::stdout().flush().unwrap(); - + let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); let input = input.trim(); - + if input == "q" || input.is_empty() { println!("Authentication cancelled"); None @@ -38,16 +39,14 @@ fn main() -> Result<()> { } // Try to access a printer that might require authentication - let printer_name = std::env::args() - .nth(1) - .unwrap_or_else(|| "PDF".to_string()); + let printer_name = std::env::args().nth(1).unwrap_or_else(|| "PDF".to_string()); println!("\nTrying to access printer: {}", printer_name); - + match get_destination(&printer_name) { Ok(destination) => { println!("Successfully connected to: {}", destination.full_name()); - + // Try to create a job (this might trigger authentication) println!("Attempting to create a job..."); match create_job(&destination, "Authentication test job") { @@ -56,7 +55,7 @@ fn main() -> Result<()> { } Err(e) => { println!("Job creation failed: {}", e); - + // Try manual authentication println!("Attempting manual authentication..."); match do_authentication(None, "POST", "/") { @@ -74,7 +73,7 @@ fn main() -> Result<()> { // Test removing the callback println!("\nRemoving password callback..."); set_password_callback(None)?; - + match get_password("Test prompt after removal:", None, "GET", "/test") { Some(password) => println!("Unexpected password: {}", password), None => println!("Callback correctly removed - no password provided"), @@ -82,4 +81,4 @@ fn main() -> Result<()> { println!("\nAuthentication example completed!"); Ok(()) -} \ No newline at end of file +} diff --git a/examples/discover_printers.rs b/examples/discover_printers.rs index 1933717..85f3625 100644 --- a/examples/discover_printers.rs +++ b/examples/discover_printers.rs @@ -1,6 +1,6 @@ use cups_rs::{ - get_all_destinations, get_default_destination, find_destinations, - Destinations, PRINTER_LOCAL, PRINTER_REMOTE, Result, + Destinations, PRINTER_LOCAL, PRINTER_REMOTE, Result, find_destinations, get_all_destinations, + get_default_destination, }; fn main() -> Result<()> { @@ -44,7 +44,10 @@ fn main() -> Result<()> { // Method 2: Advanced management (new API) println!("\n--- Advanced Destination Management ---"); let mut managed_destinations = Destinations::get_all()?; - println!("Managing {} destinations with new API", managed_destinations.len()); + println!( + "Managing {} destinations with new API", + managed_destinations.len() + ); // Add a test destination to demonstrate management println!("Adding test destination 'MyTestPrinter'..."); @@ -58,13 +61,17 @@ fn main() -> Result<()> { // Set a new default if we have printers if let Some(first_dest) = destinations.first() { println!("Setting '{}' as default...", first_dest.name); - managed_destinations.set_default_destination(&first_dest.name, first_dest.instance.as_deref())?; + managed_destinations + .set_default_destination(&first_dest.name, first_dest.instance.as_deref())?; println!(" ✓ Default updated"); } // Clean up test destination let removed = managed_destinations.remove_destination("MyTestPrinter", None)?; - println!("Test destination removed: {}", if removed { "✓" } else { "✗" }); + println!( + "Test destination removed: {}", + if removed { "✓" } else { "✗" } + ); // Show current default (existing API) match get_default_destination() { diff --git a/examples/multi_document.rs b/examples/multi_document.rs index d289282..8f1110a 100644 --- a/examples/multi_document.rs +++ b/examples/multi_document.rs @@ -1,13 +1,9 @@ -use cups_rs::{ - create_job, get_destination, get_job_info, Result, FORMAT_TEXT, -}; +use cups_rs::{FORMAT_TEXT, Result, create_job, get_destination, get_job_info}; fn main() -> Result<()> { println!("CUPS Multi-Document Job Example"); - let printer_name = std::env::args() - .nth(1) - .unwrap_or_else(|| "PDF".to_string()); + let printer_name = std::env::args().nth(1).unwrap_or_else(|| "PDF".to_string()); let destination = get_destination(&printer_name)?; println!("Using printer: {}", destination.full_name()); @@ -15,17 +11,24 @@ fn main() -> Result<()> { let job = create_job(&destination, "Multi-document job")?; println!("Created job ID: {}", job.id); - std::fs::write("doc1.txt", "Document 1: First page of the multi-document job\n")?; - std::fs::write("doc2.txt", "Document 2: Second page of the multi-document job\n")?; - std::fs::write("doc3.txt", "Document 3: Final page of the multi-document job\n")?; + std::fs::write( + "doc1.txt", + "Document 1: First page of the multi-document job\n", + )?; + std::fs::write( + "doc2.txt", + "Document 2: Second page of the multi-document job\n", + )?; + std::fs::write( + "doc3.txt", + "Document 3: Final page of the multi-document job\n", + )?; println!("Submitting document 1 (not last)..."); job.submit_file_with_options("doc1.txt", FORMAT_TEXT, &[], false)?; println!("Submitting document 2 (not last)..."); - let doc2_options = vec![ - ("print-quality".to_string(), "high".to_string()), - ]; + let doc2_options = vec![("print-quality".to_string(), "high".to_string())]; job.submit_file_with_options("doc2.txt", FORMAT_TEXT, &doc2_options, false)?; println!("Submitting document 3 (last document)..."); @@ -47,4 +50,4 @@ fn main() -> Result<()> { println!("Multi-document job completed! Check: lpstat -o"); Ok(()) -} \ No newline at end of file +} diff --git a/examples/server_config.rs b/examples/server_config.rs index 405918f..bd825f5 100644 --- a/examples/server_config.rs +++ b/examples/server_config.rs @@ -1,9 +1,10 @@ use cups_rs::{ + Result, config::{ - get_server, set_server, get_user, set_user, get_encryption, set_encryption, - get_user_agent, set_user_agent, CupsConfig, EncryptionMode, + CupsConfig, EncryptionMode, get_encryption, get_server, get_user, get_user_agent, + set_encryption, set_server, set_user, set_user_agent, }, - get_all_destinations, Result, + get_all_destinations, }; fn main() -> Result<()> { @@ -17,7 +18,7 @@ fn main() -> Result<()> { // Test individual configuration functions println!("\n--- Testing Individual Configuration ---"); - + // Test server configuration println!("Setting server to 'print.example.com:8631'..."); set_server(Some("print.example.com:8631"))?; @@ -44,7 +45,7 @@ fn main() -> Result<()> { set_user(None)?; set_encryption(EncryptionMode::IfRequested); set_user_agent(None)?; - + println!("Server restored to: {}", get_server()); println!("User restored to: {}", get_user()); println!("Encryption restored to: {:?}", get_encryption()); @@ -69,7 +70,10 @@ fn main() -> Result<()> { // Note: This will likely fail since scoped.example.com doesn't exist match get_all_destinations() { Ok(destinations) => { - println!(" Successfully connected! Found {} printers", destinations.len()); + println!( + " Successfully connected! Found {} printers", + destinations.len() + ); } Err(e) => { println!(" Expected connection failure: {}", e); @@ -95,13 +99,13 @@ fn main() -> Result<()> { .with_user("developer")? .with_encryption(EncryptionMode::Never) .with_user_agent("DevApp/1.0-debug")?; - + let dev_summary = _dev_config.current_config(); println!(" {}", dev_summary); // The configuration will be automatically restored when _dev_config is dropped println!("\nServer configuration demo completed!"); println!("Note: All configurations are thread-local in CUPS"); - + Ok(()) -} \ No newline at end of file +} diff --git a/src/auth.rs b/src/auth.rs index e6dda0a..066458c 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -6,40 +6,40 @@ use std::ptr; use std::sync::Arc; /// Password callback function type -/// +/// /// This callback is called when CUPS needs authentication credentials. -/// +/// /// # Parameters /// - `prompt`: The authentication prompt string /// - `http_connection`: Optional HTTP connection (None for CUPS_HTTP_DEFAULT) /// - `method`: HTTP method ("GET", "POST", "PUT", etc.) /// - `resource`: The resource path being accessed -/// +/// /// # Returns /// - `Some(String)`: The password to use for authentication /// - `None`: Cancel authentication pub type PasswordCallback = dyn Fn(&str, Option<&str>, &str, &str) -> Option + Send + Sync; /// Client certificate callback function type -/// +/// /// This callback is called when CUPS needs a client certificate for authentication. -/// +/// /// # Parameters /// - `server_name`: The server name requiring the certificate -/// +/// /// # Returns /// - `Some(Vec)`: The certificate data in DER format /// - `None`: No certificate available pub type ClientCertCallback = dyn Fn(&str) -> Option> + Send + Sync; /// Server certificate validation callback function type -/// +/// /// This callback is called to validate server certificates. -/// +/// /// # Parameters /// - `server_name`: The server name /// - `certificate`: The server certificate data in DER format -/// +/// /// # Returns /// - `true`: Accept the certificate /// - `false`: Reject the certificate @@ -47,29 +47,29 @@ pub type ServerCertCallback = dyn Fn(&str, &[u8]) -> bool + Send + Sync; // Thread-local storage for authentication callbacks thread_local! { - static PASSWORD_CALLBACK: std::cell::RefCell>> = + static PASSWORD_CALLBACK: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; - static CLIENT_CERT_CALLBACK: std::cell::RefCell>> = + static CLIENT_CERT_CALLBACK: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; - static SERVER_CERT_CALLBACK: std::cell::RefCell>> = + static SERVER_CERT_CALLBACK: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; } /// Set a password callback for GUI applications -/// +/// /// This function sets a password callback that will be called whenever /// CUPS needs authentication credentials. The callback should prompt /// the user for a password and return it. -/// +/// /// Pass `None` to restore the default console-based authentication. -/// +/// /// # Arguments /// - `callback`: The password callback function, or None to restore default -/// +/// /// # Example /// ```rust /// use cups_rs::auth::set_password_callback; -/// +/// /// let result = set_password_callback(Some(Box::new(|prompt, _http, _method, _resource| { /// println!("Authentication required: {}", prompt); /// // In a real GUI app, show a password dialog here @@ -79,7 +79,7 @@ thread_local! { /// ``` pub fn set_password_callback(callback: Option>) -> Result<()> { let has_callback = callback.is_some(); - + PASSWORD_CALLBACK.with(|cb| { *cb.borrow_mut() = callback.map(|c| Arc::from(c)); }); @@ -97,19 +97,19 @@ pub fn set_password_callback(callback: Option>) -> Result< } /// Set a client certificate callback for SSL/TLS authentication -/// +/// /// This function sets a callback that will be called when CUPS needs /// a client certificate for SSL/TLS authentication. -/// +/// /// Pass `None` to remove the current callback. -/// +/// /// # Arguments /// - `callback`: The client certificate callback function, or None to remove -/// +/// /// # Example /// ```rust /// use cups_rs::auth::set_client_cert_callback; -/// +/// /// let result = set_client_cert_callback(Some(Box::new(|server_name| { /// println!("Certificate required for: {}", server_name); /// // In a real app, load certificate from file or keystore @@ -124,24 +124,24 @@ pub fn set_client_cert_callback(callback: Option>) -> Re // Note: cupsSetClientCertCB might not be available in all CUPS versions // This is a placeholder for when the binding is available - + Ok(()) } /// Set a server certificate validation callback -/// +/// /// This function sets a callback that will be called to validate /// server certificates during SSL/TLS connections. -/// +/// /// Pass `None` to use default certificate validation. -/// +/// /// # Arguments /// - `callback`: The server certificate validation callback, or None for default -/// +/// /// # Example /// ```rust /// use cups_rs::auth::set_server_cert_callback; -/// +/// /// let result = set_server_cert_callback(Some(Box::new(|server_name, cert_data| { /// println!("Validating certificate for: {}", server_name); /// println!("Certificate size: {} bytes", cert_data.len()); @@ -157,21 +157,21 @@ pub fn set_server_cert_callback(callback: Option>) -> Re // Note: cupsSetServerCertCB might not be available in all CUPS versions // This is a placeholder for when the binding is available - + Ok(()) } /// Get a password using the current password callback -/// +/// /// This function calls the current password callback to get a password /// for authentication. It's typically used internally by CUPS. -/// +/// /// # Arguments /// - `prompt`: The authentication prompt /// - `http`: Optional HTTP connection /// - `method`: HTTP method being used /// - `resource`: The resource being accessed -/// +/// /// # Returns /// - `Some(String)`: The password provided by the callback /// - `None`: No password callback set or user cancelled @@ -192,13 +192,13 @@ pub fn get_password( } /// Get a client certificate using the current callback -/// +/// /// This function calls the current client certificate callback to get /// a certificate for SSL/TLS authentication. -/// +/// /// # Arguments /// - `server_name`: The server name requiring the certificate -/// +/// /// # Returns /// - `Some(Vec)`: The certificate data in DER format /// - `None`: No certificate callback set or no certificate available @@ -214,14 +214,14 @@ pub fn get_client_certificate(server_name: &str) -> Option> { } /// Validate a server certificate using the current callback -/// +/// /// This function calls the current server certificate validation callback /// to validate a server certificate. -/// +/// /// # Arguments /// - `server_name`: The server name /// - `certificate`: The certificate data in DER format -/// +/// /// # Returns /// - `true`: Certificate is valid/accepted /// - `false`: Certificate is invalid/rejected or no callback set @@ -237,16 +237,16 @@ pub fn validate_server_certificate(server_name: &str, certificate: &[u8]) -> boo } /// Perform authentication for an HTTP request -/// +/// /// This function handles authentication for a specific HTTP request. /// It will call the password callback if needed and set up the /// appropriate authentication headers. -/// +/// /// # Arguments /// - `http_connection`: HTTP connection (use None for CUPS_HTTP_DEFAULT) /// - `method`: HTTP method ("GET", "POST", "PUT", etc.) /// - `resource`: The resource path -/// +/// /// # Returns /// - `Ok(())`: Authentication successful or not required /// - `Err(Error)`: Authentication failed @@ -270,7 +270,8 @@ pub fn do_authentication( Ok(()) } else { Err(Error::AuthenticationFailed(format!( - "Authentication failed for {} {}", method, resource + "Authentication failed for {} {}", + method, resource ))) } } @@ -353,7 +354,7 @@ mod tests { // Test client certificate callback let cert_data = vec![1, 2, 3, 4, 5]; let cert_data_clone = cert_data.clone(); - + let result = set_client_cert_callback(Some(Box::new(move |server_name| { if server_name == "test.example.com" { Some(cert_data_clone.clone()) @@ -387,14 +388,14 @@ mod tests { // Test removing callbacks let result = set_client_cert_callback(None); assert!(result.is_ok()); - + let no_cert = get_client_certificate("test.example.com"); assert_eq!(no_cert, None); let result = set_server_cert_callback(None); assert!(result.is_ok()); - + let no_validation = validate_server_certificate("trusted.example.com", &[1, 2, 3]); assert!(!no_validation); } -} \ No newline at end of file +} diff --git a/src/config.rs b/src/config.rs index 97a4828..278faf2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,13 +40,13 @@ impl Into for EncryptionMode { } /// Get the current CUPS server hostname/address -/// +/// /// Returns the hostname/address of the current server. This can be a /// fully-qualified hostname, a numeric IPv4 or IPv6 address, or a domain /// socket pathname. -/// +/// /// Note: The current server is tracked separately for each thread. -/// +/// /// # Returns /// - Server hostname/address string /// - Default server if none has been set @@ -56,37 +56,35 @@ pub fn get_server() -> String { if server_ptr.is_null() { "localhost".to_string() // Default fallback } else { - CStr::from_ptr(server_ptr) - .to_string_lossy() - .into_owned() + CStr::from_ptr(server_ptr).to_string_lossy().into_owned() } } } /// Set the default CUPS server name and port -/// +/// /// The server string can be a fully-qualified hostname, a numeric IPv4 or IPv6 /// address, or a domain socket pathname. Hostnames and numeric IP addresses can /// be optionally followed by a colon and port number to override the default /// port 631, e.g. "hostname:8631". -/// +/// /// Note: The current server is tracked separately for each thread. -/// +/// /// # Arguments /// - `server`: Server name/address, or None to restore default -/// +/// /// # Examples /// ```rust /// use cups_rs::config::set_server; -/// +/// /// // Set specific server /// let result = set_server(Some("print-server.company.com")); /// assert!(result.is_ok()); -/// +/// /// // Set server with custom port /// let result = set_server(Some("192.168.1.100:8631")); /// assert!(result.is_ok()); -/// +/// /// // Restore default server /// let result = set_server(None); /// assert!(result.is_ok()); @@ -107,12 +105,12 @@ pub fn set_server(server: Option<&str>) -> Result<()> { } /// Get the current user name -/// +/// /// Returns the current user's name as used by CUPS for authentication /// and job ownership. -/// +/// /// Note: The current user name is tracked separately for each thread. -/// +/// /// # Returns /// - Current user name /// - System username if none has been set @@ -124,30 +122,28 @@ pub fn get_user() -> String { .or_else(|_| std::env::var("USERNAME")) .unwrap_or_else(|_| "anonymous".to_string()) } else { - CStr::from_ptr(user_ptr) - .to_string_lossy() - .into_owned() + CStr::from_ptr(user_ptr).to_string_lossy().into_owned() } } } /// Set the default user name -/// +/// /// Sets the user name to use for authentication and job ownership. -/// +/// /// Note: The current user name is tracked separately for each thread. -/// +/// /// # Arguments /// - `user`: Username, or None to restore default -/// +/// /// # Examples /// ```rust /// use cups_rs::config::set_user; -/// +/// /// // Set specific user /// let result = set_user(Some("john.doe")); /// assert!(result.is_ok()); -/// +/// /// // Restore default user /// let result = set_user(None); /// assert!(result.is_ok()); @@ -168,15 +164,15 @@ pub fn set_user(user: Option<&str>) -> Result<()> { } /// Get the current encryption settings -/// +/// /// Returns the current encryption preference for CUPS connections. -/// +/// /// The default encryption setting comes from the CUPS_ENCRYPTION environment /// variable, then the ~/.cups/client.conf file, and finally the /// /etc/cups/client.conf file. If not set, the default is IfRequested. -/// +/// /// Note: The current encryption setting is tracked separately for each thread. -/// +/// /// # Returns /// - Current encryption mode pub fn get_encryption() -> EncryptionMode { @@ -187,21 +183,21 @@ pub fn get_encryption() -> EncryptionMode { } /// Set the encryption preference -/// +/// /// Sets the encryption preference for CUPS connections. -/// +/// /// Note: The current encryption setting is tracked separately for each thread. -/// +/// /// # Arguments /// - `mode`: Encryption mode to use -/// +/// /// # Examples /// ```rust /// use cups_rs::config::{set_encryption, EncryptionMode}; -/// +/// /// // Require encryption for all connections /// set_encryption(EncryptionMode::Required); -/// +/// /// // Use encryption only if requested /// set_encryption(EncryptionMode::IfRequested); /// ``` @@ -212,9 +208,9 @@ pub fn set_encryption(mode: EncryptionMode) { } /// Get the current HTTP User-Agent string -/// +/// /// Returns the User-Agent string used in HTTP requests to CUPS servers. -/// +/// /// # Returns /// - Current User-Agent string /// - Default CUPS User-Agent if none has been set @@ -224,29 +220,27 @@ pub fn get_user_agent() -> String { if agent_ptr.is_null() { format!("CUPS/2.4 (cups-rs/{})", env!("CARGO_PKG_VERSION")) } else { - CStr::from_ptr(agent_ptr) - .to_string_lossy() - .into_owned() + CStr::from_ptr(agent_ptr).to_string_lossy().into_owned() } } } /// Set the default HTTP User-Agent string -/// +/// /// Sets the User-Agent string used in HTTP requests to CUPS servers. /// This is useful for identifying your application in server logs. -/// +/// /// # Arguments /// - `user_agent`: User-Agent string, or None to restore default -/// +/// /// # Examples /// ```rust /// use cups_rs::config::set_user_agent; -/// +/// /// // Set custom User-Agent /// let result = set_user_agent(Some("MyPrintApp/1.0")); /// assert!(result.is_ok()); -/// +/// /// // Restore default User-Agent /// let result = set_user_agent(None); /// assert!(result.is_ok()); @@ -267,7 +261,7 @@ pub fn set_user_agent(user_agent: Option<&str>) -> Result<()> { } /// Configuration manager for CUPS settings -/// +/// /// This struct provides a convenient way to manage CUPS configuration /// settings with automatic cleanup when dropped. #[derive(Debug)] @@ -280,7 +274,7 @@ pub struct CupsConfig { impl CupsConfig { /// Create a new configuration manager - /// + /// /// This captures the current configuration state so it can be restored /// when the CupsConfig is dropped. pub fn new() -> Self { @@ -393,20 +387,23 @@ mod tests { #[test] fn test_server_configuration() { let original_server = get_server(); - + // Test setting custom server let test_server = "test.example.com:8631"; set_server(Some(test_server)).unwrap(); - + // CUPS might normalize the server name, so check if it contains our test server let current_server = get_server(); - assert!(current_server.contains("test.example.com"), - "Expected server to contain 'test.example.com', got: {}", current_server); - + assert!( + current_server.contains("test.example.com"), + "Expected server to contain 'test.example.com', got: {}", + current_server + ); + // Test restoring default set_server(None).unwrap(); // Note: The exact default may vary by system - + // Restore original for cleanliness set_server(Some(&original_server)).unwrap(); } @@ -414,17 +411,18 @@ mod tests { #[test] fn test_config_manager() { let original_server = get_server(); - + { let _config = CupsConfig::new() - .with_server("managed.example.com").unwrap() + .with_server("managed.example.com") + .unwrap() .with_encryption(EncryptionMode::Required); - + assert_eq!(get_server(), "managed.example.com"); assert_eq!(get_encryption(), EncryptionMode::Required); } - + // Settings should be restored after config is dropped assert_eq!(get_server(), original_server); } -} \ No newline at end of file +} diff --git a/src/connection.rs b/src/connection.rs index 4d24318..9d94255 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -96,15 +96,15 @@ impl Drop for HttpConnection { impl Destination { /// Connect to this destination - /// + /// /// Opens a direct connection to the destination, which can be used for /// sending IPP requests directly to the printer or CUPS scheduler. - /// + /// /// # Arguments /// * `flags` - Whether to connect to scheduler or device directly /// * `timeout_ms` - Connection timeout in milliseconds, None for indefinite /// * `cancel` - Optional cancellation flag - /// + /// /// # Returns /// * `Ok((HttpConnection, String))` - Connection and resource path /// * `Err(Error)` - Connection failed @@ -140,7 +140,7 @@ impl Destination { cancel_ptr, resource_buf.as_mut_ptr() as *mut ::std::os::raw::c_char, RESOURCE_SIZE, - None, // No callback for now + None, // No callback for now ptr::null_mut(), // No user data ) }; @@ -167,17 +167,17 @@ impl Destination { } /// Connect to this destination with a callback - /// + /// /// Opens a connection with a callback function that can monitor the /// connection process and potentially cancel it. - /// + /// /// # Arguments /// * `flags` - Whether to connect to scheduler or device directly /// * `timeout_ms` - Connection timeout in milliseconds, None for indefinite /// * `cancel` - Optional cancellation flag /// * `callback` - Callback function for connection monitoring /// * `user_data` - User data passed to callback - /// + /// /// # Returns /// * `Ok(HttpConnection)` - Established connection /// * `Err(Error)` - Connection failed or was cancelled @@ -283,15 +283,15 @@ unsafe extern "C" fn connect_dest_callback( } /// Connect to a destination -/// +/// /// This is a convenience function that creates a connection to a destination. -/// +/// /// # Arguments /// * `dest` - Destination to connect to /// * `flags` - Connection flags /// * `timeout_ms` - Connection timeout in milliseconds, None for indefinite /// * `cancel` - Optional cancellation flag -/// +/// /// # Returns /// * `Ok(HttpConnection)` - Established connection /// * `Err(Error)` - Connection failed @@ -325,8 +325,11 @@ mod tests { Ok(conn) => { assert!(conn.is_connected()); assert!(!conn.resource_path().is_empty()); - println!("Connected to '{}' with resource path: '{}'", - dest.name, conn.resource_path()); + println!( + "Connected to '{}' with resource path: '{}'", + dest.name, + conn.resource_path() + ); } Err(e) => { // Connection might fail in test environment, that's OK diff --git a/src/destination/dest_info.rs b/src/destination/dest_info.rs index 8890eaf..286e643 100644 --- a/src/destination/dest_info.rs +++ b/src/destination/dest_info.rs @@ -342,7 +342,7 @@ impl DestinationInfo { } /// Get ready (loaded) media - /// + /// /// Returns the media sizes that are currently loaded/ready in the printer. /// This is different from supported media - ready media are the ones actually /// available for immediate use. @@ -356,9 +356,8 @@ impl DestinationInfo { Err(_) => return Ok(Vec::new()), }; - let ready_attr = unsafe { - bindings::cupsFindDestReady(http, dest, self.dinfo, option_c.as_ptr()) - }; + let ready_attr = + unsafe { bindings::cupsFindDestReady(http, dest, self.dinfo, option_c.as_ptr()) }; if ready_attr.is_null() { return Ok(Vec::new()); @@ -366,13 +365,13 @@ impl DestinationInfo { let mut ready_media = Vec::new(); let count = unsafe { bindings::ippGetCount(ready_attr) }; - + for i in 0..count { unsafe { let media_name_ptr = bindings::ippGetString(ready_attr, i, ptr::null_mut()); if !media_name_ptr.is_null() { let media_name = CStr::from_ptr(media_name_ptr).to_string_lossy(); - + // Try to get the full media size info for this ready media match self.get_media_by_name(http, dest, &media_name, 0) { Ok(size) => ready_media.push(size), @@ -397,7 +396,7 @@ impl DestinationInfo { } /// Get ready (loaded) finishings - /// + /// /// Returns the finishing processes that are currently ready/available. /// For example, if a printer has staple and punch finishers but is out of staples, /// this will only return punch options. @@ -411,9 +410,8 @@ impl DestinationInfo { Err(_) => return Ok(Vec::new()), }; - let ready_attr = unsafe { - bindings::cupsFindDestReady(http, dest, self.dinfo, option_c.as_ptr()) - }; + let ready_attr = + unsafe { bindings::cupsFindDestReady(http, dest, self.dinfo, option_c.as_ptr()) }; if ready_attr.is_null() { return Ok(Vec::new()); @@ -421,7 +419,7 @@ impl DestinationInfo { let mut ready_finishings = Vec::new(); let count = unsafe { bindings::ippGetCount(ready_attr) }; - + for i in 0..count { let finishing = unsafe { bindings::ippGetInteger(ready_attr, i) }; ready_finishings.push(finishing); @@ -431,7 +429,7 @@ impl DestinationInfo { } /// Get default value for an option - /// + /// /// Returns the default value for a given option as a string. /// This is the printer's default, not the user's saved preference. pub fn get_default_value( @@ -442,9 +440,8 @@ impl DestinationInfo { ) -> Result> { let option_c = CString::new(option)?; - let default_attr = unsafe { - bindings::cupsFindDestDefault(http, dest, self.dinfo, option_c.as_ptr()) - }; + let default_attr = + unsafe { bindings::cupsFindDestDefault(http, dest, self.dinfo, option_c.as_ptr()) }; if default_attr.is_null() { return Ok(None); @@ -466,12 +463,16 @@ impl DestinationInfo { // If not an integer, try as boolean let bool_value = bindings::ippGetBoolean(default_attr, 0); - Ok(Some(if bool_value != 0 { "true".to_string() } else { "false".to_string() })) + Ok(Some(if bool_value != 0 { + "true".to_string() + } else { + "false".to_string() + })) } } /// Get supported values for an option - /// + /// /// Returns a list of all values supported for the given option. /// The returned values are formatted as strings. pub fn get_supported_values( @@ -482,9 +483,8 @@ impl DestinationInfo { ) -> Result> { let option_c = CString::new(option)?; - let supported_attr = unsafe { - bindings::cupsFindDestSupported(http, dest, self.dinfo, option_c.as_ptr()) - }; + let supported_attr = + unsafe { bindings::cupsFindDestSupported(http, dest, self.dinfo, option_c.as_ptr()) }; if supported_attr.is_null() { return Ok(Vec::new()); @@ -492,7 +492,7 @@ impl DestinationInfo { let mut supported_values = Vec::new(); let count = unsafe { bindings::ippGetCount(supported_attr) }; - + for i in 0..count { unsafe { // Try to get as string first @@ -505,14 +505,19 @@ impl DestinationInfo { // If not a string, try as integer let int_value = bindings::ippGetInteger(supported_attr, i); - if int_value != 0 || i == 0 { // Include 0 if it's the first value + if int_value != 0 || i == 0 { + // Include 0 if it's the first value supported_values.push(int_value.to_string()); continue; } // If not an integer, try as boolean let bool_value = bindings::ippGetBoolean(supported_attr, i); - supported_values.push(if bool_value != 0 { "true".to_string() } else { "false".to_string() }); + supported_values.push(if bool_value != 0 { + "true".to_string() + } else { + "false".to_string() + }); } } @@ -520,7 +525,7 @@ impl DestinationInfo { } /// Get supported options for job creation - /// + /// /// Returns a list of all options that can be used when creating jobs /// for this destination. pub fn get_supported_options( diff --git a/src/destination/media_size.rs b/src/destination/media_size.rs index aa10245..cda12c8 100644 --- a/src/destination/media_size.rs +++ b/src/destination/media_size.rs @@ -155,4 +155,4 @@ mod tests { assert_eq!(media.printable_width(), 21590 - 635 - 635); assert_eq!(media.printable_length(), 27940 - 635 - 635); } -} \ No newline at end of file +} diff --git a/src/destination/mod.rs b/src/destination/mod.rs index 2ca216d..390b329 100644 --- a/src/destination/mod.rs +++ b/src/destination/mod.rs @@ -490,21 +490,24 @@ impl Destinations { } /// Add a destination to the list of destinations - /// + /// /// If the named destination already exists, the destination list is returned unchanged. /// Adding a new instance of a destination creates a copy of that destination's options. - /// + /// /// # Arguments /// - `name`: Destination name /// - `instance`: Instance name or None for none/primary - /// + /// /// # Returns /// - `Ok(())`: Destination added successfully /// - `Err(Error)`: Failed to add destination pub fn add_destination(&mut self, name: &str, instance: Option<&str>) -> Result<()> { let name_c = CString::new(name)?; let instance_c = instance.map(|i| CString::new(i)).transpose()?; - let instance_ptr = instance_c.as_ref().map(|c| c.as_ptr()).unwrap_or(ptr::null()); + let instance_ptr = instance_c + .as_ref() + .map(|c| c.as_ptr()) + .unwrap_or(ptr::null()); let new_num_dests = unsafe { bindings::cupsAddDest( @@ -525,14 +528,14 @@ impl Destinations { } /// Remove a destination from the destination list - /// + /// /// Removing a destination/instance does not delete the class or printer queue, /// merely the lpoptions for that destination/instance. - /// + /// /// # Arguments /// - `name`: Destination name /// - `instance`: Instance name or None - /// + /// /// # Returns /// - `Ok(true)`: Destination was found and removed /// - `Ok(false)`: Destination was not found @@ -540,7 +543,10 @@ impl Destinations { pub fn remove_destination(&mut self, name: &str, instance: Option<&str>) -> Result { let name_c = CString::new(name)?; let instance_c = instance.map(|i| CString::new(i)).transpose()?; - let instance_ptr = instance_c.as_ref().map(|c| c.as_ptr()).unwrap_or(ptr::null()); + let instance_ptr = instance_c + .as_ref() + .map(|c| c.as_ptr()) + .unwrap_or(ptr::null()); let old_count = self.num_dests; let new_num_dests = unsafe { @@ -557,38 +563,36 @@ impl Destinations { } /// Set the default destination - /// + /// /// This marks one of the destinations in the list as the default destination. - /// + /// /// # Arguments /// - `name`: Destination name /// - `instance`: Instance name or None - /// + /// /// # Returns /// - `Ok(())`: Default destination set successfully /// - `Err(Error)`: Failed to set default destination pub fn set_default_destination(&mut self, name: &str, instance: Option<&str>) -> Result<()> { let name_c = CString::new(name)?; let instance_c = instance.map(|i| CString::new(i)).transpose()?; - let instance_ptr = instance_c.as_ref().map(|c| c.as_ptr()).unwrap_or(ptr::null()); + let instance_ptr = instance_c + .as_ref() + .map(|c| c.as_ptr()) + .unwrap_or(ptr::null()); unsafe { - bindings::cupsSetDefaultDest( - name_c.as_ptr(), - instance_ptr, - self.num_dests, - self.dests, - ); + bindings::cupsSetDefaultDest(name_c.as_ptr(), instance_ptr, self.num_dests, self.dests); } Ok(()) } /// Save the list of destinations to the user's lpoptions file - /// + /// /// This saves the current destination list and their options to the user's /// lpoptions file for persistence across sessions. - /// + /// /// # Returns /// - `Ok(())`: Destinations saved successfully /// - `Err(Error)`: Failed to save destinations @@ -611,11 +615,11 @@ impl Destinations { } /// Find a destination by name and instance - /// + /// /// # Arguments /// - `name`: Destination name to search for /// - `instance`: Instance name or None - /// + /// /// # Returns /// - `Some(Destination)`: Found destination /// - `None`: Destination not found @@ -625,15 +629,13 @@ impl Destinations { Err(_) => return None, }; let instance_c = instance.and_then(|i| CString::new(i).ok()); - let instance_ptr = instance_c.as_ref().map(|c| c.as_ptr()).unwrap_or(ptr::null()); + let instance_ptr = instance_c + .as_ref() + .map(|c| c.as_ptr()) + .unwrap_or(ptr::null()); let dest_ptr = unsafe { - bindings::cupsGetDest( - name_c.as_ptr(), - instance_ptr, - self.num_dests, - self.dests, - ) + bindings::cupsGetDest(name_c.as_ptr(), instance_ptr, self.num_dests, self.dests) }; if dest_ptr.is_null() { @@ -655,15 +657,15 @@ pub struct OptionConflict { impl DestinationInfo { /// Check for option conflicts and get resolutions for a new option/value pair - /// + /// /// This function checks if adding a new option/value pair would conflict /// with existing options and provides resolutions if conflicts are found. - /// + /// /// # Arguments /// - `current_options`: Current option/value pairs /// - `new_option`: The new option name to check /// - `new_value`: The new option value to check - /// + /// /// # Returns /// - `Ok(None)`: No conflicts found /// - `Ok(Some(OptionConflict))`: Conflicts found with resolution @@ -698,14 +700,21 @@ impl DestinationInfo { // Get destination pointer (we need to create one temporarily) let dest_name_c = CString::new(dest.name.as_str())?; - let dest_instance_c = dest.instance.as_ref().map(|i| CString::new(i.as_str())).transpose()?; - let dest_instance_ptr = dest_instance_c.as_ref().map(|c| c.as_ptr()).unwrap_or(ptr::null()); + let dest_instance_c = dest + .instance + .as_ref() + .map(|i| CString::new(i.as_str())) + .transpose()?; + let dest_instance_ptr = dest_instance_c + .as_ref() + .map(|c| c.as_ptr()) + .unwrap_or(ptr::null()); let dest_ptr = unsafe { bindings::cupsGetDest( dest_name_c.as_ptr(), dest_instance_ptr, - 1, // We just need a temporary dest + 1, // We just need a temporary dest ptr::null_mut(), // Let CUPS find it ) }; @@ -759,8 +768,10 @@ impl DestinationInfo { unsafe { let option = &*conflicts.offset(i as isize); if !option.name.is_null() && !option.value.is_null() { - let name = CStr::from_ptr(option.name).to_string_lossy().into_owned(); - let value = CStr::from_ptr(option.value).to_string_lossy().into_owned(); + let name = + CStr::from_ptr(option.name).to_string_lossy().into_owned(); + let value = + CStr::from_ptr(option.value).to_string_lossy().into_owned(); conflicting_options.push((name, value)); } } @@ -773,8 +784,10 @@ impl DestinationInfo { unsafe { let option = &*resolved.offset(i as isize); if !option.name.is_null() && !option.value.is_null() { - let name = CStr::from_ptr(option.name).to_string_lossy().into_owned(); - let value = CStr::from_ptr(option.value).to_string_lossy().into_owned(); + let name = + CStr::from_ptr(option.name).to_string_lossy().into_owned(); + let value = + CStr::from_ptr(option.value).to_string_lossy().into_owned(); resolved_options.push((name, value)); } } @@ -982,7 +995,7 @@ pub fn find_destinations(type_filter: u32, mask: u32) -> Result #[cfg(test)] mod tests { use super::*; - + #[test] fn test_destination_creation() { let mut options = std::collections::HashMap::new(); @@ -1020,7 +1033,7 @@ mod tests { #[test] fn test_destination_state_parsing() { let mut options = std::collections::HashMap::new(); - + // Test different printer states options.insert("printer-state".to_string(), "4".to_string()); let dest = Destination { @@ -1044,9 +1057,11 @@ mod tests { #[test] fn test_destination_state_reasons() { let mut options = std::collections::HashMap::new(); - options.insert("printer-state-reasons".to_string(), - "media-tray-empty-error,toner-low-warning".to_string()); - + options.insert( + "printer-state-reasons".to_string(), + "media-tray-empty-error,toner-low-warning".to_string(), + ); + let dest = Destination { name: "Test".to_string(), instance: None, @@ -1059,4 +1074,4 @@ mod tests { assert!(reasons.contains(&"media-tray-empty-error".to_string())); assert!(reasons.contains(&"toner-low-warning".to_string())); } -} \ No newline at end of file +} diff --git a/src/destination/printer_state.rs b/src/destination/printer_state.rs index 0a88ae2..e44c61b 100644 --- a/src/destination/printer_state.rs +++ b/src/destination/printer_state.rs @@ -58,7 +58,10 @@ mod tests { assert_eq!(PrinterState::from_cups_state("3"), PrinterState::Idle); assert_eq!(PrinterState::from_cups_state("4"), PrinterState::Processing); assert_eq!(PrinterState::from_cups_state("5"), PrinterState::Stopped); - assert_eq!(PrinterState::from_cups_state("unknown"), PrinterState::Unknown); + assert_eq!( + PrinterState::from_cups_state("unknown"), + PrinterState::Unknown + ); } #[test] @@ -84,4 +87,4 @@ mod tests { assert_eq!(PrinterState::Stopped.to_cups_value(), "5"); assert_eq!(PrinterState::Unknown.to_cups_value(), "0"); } -} \ No newline at end of file +} diff --git a/src/error.rs b/src/error.rs index ccc0888..4a5448d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -113,12 +113,13 @@ impl Error { pub fn error_category(&self) -> ErrorCategory { match self { - Error::ServerUnavailable | Error::NetworkError(_) | Error::Timeout | Error::ConnectionFailed(_) => { - ErrorCategory::Network - } - Error::AuthenticationRequired(_) | Error::AuthenticationFailed(_) | Error::PermissionDenied(_) => { - ErrorCategory::Authentication - } + Error::ServerUnavailable + | Error::NetworkError(_) + | Error::Timeout + | Error::ConnectionFailed(_) => ErrorCategory::Network, + Error::AuthenticationRequired(_) + | Error::AuthenticationFailed(_) + | Error::PermissionDenied(_) => ErrorCategory::Authentication, Error::PrinterOffline(_) | Error::PrinterNotAccepting(_, _) => ErrorCategory::Printer, Error::InvalidFormat(_, _) | Error::DocumentTooLarge(_, _) => ErrorCategory::Document, Error::JobCreationFailed(_) | Error::JobManagementFailed(_) => ErrorCategory::Job, @@ -141,7 +142,9 @@ impl Error { } Error::DocumentTooLarge(_, _) => "Reduce document size or split into smaller files", Error::NetworkError(_) => "Check network connectivity to CUPS server", - Error::ConnectionFailed(_) => "Check if destination is reachable and CUPS service is running", + Error::ConnectionFailed(_) => { + "Check if destination is reachable and CUPS service is running" + } Error::Timeout => "Retry the operation or increase timeout value", Error::ConfigurationError(_) => "Check CUPS configuration files", _ => "Check CUPS logs for more details: sudo tail /var/log/cups/error_log", diff --git a/src/ipp.rs b/src/ipp.rs index 910bbcf..a5187e7 100644 --- a/src/ipp.rs +++ b/src/ipp.rs @@ -169,24 +169,18 @@ impl IppStatus { bindings::ipp_status_e_IPP_STATUS_OK_IGNORED_OR_SUBSTITUTED => { IppStatus::OkIgnoredOrSubstituted } - bindings::ipp_status_e_IPP_STATUS_OK_CONFLICTING => { - IppStatus::OkConflicting - } + bindings::ipp_status_e_IPP_STATUS_OK_CONFLICTING => IppStatus::OkConflicting, bindings::ipp_status_e_IPP_STATUS_ERROR_BAD_REQUEST => IppStatus::ErrorBadRequest, bindings::ipp_status_e_IPP_STATUS_ERROR_FORBIDDEN => IppStatus::ErrorForbidden, bindings::ipp_status_e_IPP_STATUS_ERROR_NOT_AUTHENTICATED => { IppStatus::ErrorNotAuthenticated } - bindings::ipp_status_e_IPP_STATUS_ERROR_NOT_AUTHORIZED => { - IppStatus::ErrorNotAuthorized - } + bindings::ipp_status_e_IPP_STATUS_ERROR_NOT_AUTHORIZED => IppStatus::ErrorNotAuthorized, bindings::ipp_status_e_IPP_STATUS_ERROR_NOT_POSSIBLE => IppStatus::ErrorNotPossible, bindings::ipp_status_e_IPP_STATUS_ERROR_TIMEOUT => IppStatus::ErrorTimeout, bindings::ipp_status_e_IPP_STATUS_ERROR_NOT_FOUND => IppStatus::ErrorNotFound, bindings::ipp_status_e_IPP_STATUS_ERROR_GONE => IppStatus::ErrorGone, - bindings::ipp_status_e_IPP_STATUS_ERROR_REQUEST_ENTITY => { - IppStatus::ErrorRequestEntity - } + bindings::ipp_status_e_IPP_STATUS_ERROR_REQUEST_ENTITY => IppStatus::ErrorRequestEntity, bindings::ipp_status_e_IPP_STATUS_ERROR_REQUEST_VALUE => IppStatus::ErrorRequestValue, bindings::ipp_status_e_IPP_STATUS_ERROR_DOCUMENT_FORMAT_NOT_SUPPORTED => { IppStatus::ErrorDocumentFormatNotSupported @@ -320,7 +314,12 @@ impl IppRequest { let name_c = CString::new(name)?; let attr = unsafe { - bindings::ippAddBoolean(self.ipp, group.into(), name_c.as_ptr(), value as ::std::os::raw::c_char) + bindings::ippAddBoolean( + self.ipp, + group.into(), + name_c.as_ptr(), + value as ::std::os::raw::c_char, + ) }; if attr.is_null() { @@ -347,7 +346,8 @@ impl IppRequest { .map(|v| CString::new(*v).map_err(Error::from)) .collect::>>()?; - let values_ptrs: Vec<*const ::std::os::raw::c_char> = values_c.iter().map(|s| s.as_ptr()).collect(); + let values_ptrs: Vec<*const ::std::os::raw::c_char> = + values_c.iter().map(|s| s.as_ptr()).collect(); let attr = unsafe { bindings::ippAddStrings( @@ -464,11 +464,11 @@ impl IppResponse { Err(_) => return None, }; - let group_tag = group.map(|g| g.into()).unwrap_or(bindings::ipp_tag_e_IPP_TAG_ZERO); + let group_tag = group + .map(|g| g.into()) + .unwrap_or(bindings::ipp_tag_e_IPP_TAG_ZERO); - let attr = unsafe { - bindings::ippFindAttribute(self.ipp, name_c.as_ptr(), group_tag) - }; + let attr = unsafe { bindings::ippFindAttribute(self.ipp, name_c.as_ptr(), group_tag) }; if attr.is_null() { None diff --git a/src/job/options.rs b/src/job/options.rs index 7325957..1de3a3f 100644 --- a/src/job/options.rs +++ b/src/job/options.rs @@ -155,7 +155,7 @@ mod tests { let cups_options = options.as_cups_options(); assert_eq!(cups_options.len(), 6); - + // checking all options are present or not let option_map: std::collections::HashMap<&str, &str> = cups_options.into_iter().collect(); assert_eq!(option_map.get("copies"), Some(&"3")); @@ -198,10 +198,16 @@ mod tests { assert_eq!(PrintQuality::High.to_string(), "5"); assert_eq!(DuplexMode::OneSided.to_string(), "one-sided"); - assert_eq!(DuplexMode::TwoSidedPortrait.to_string(), "two-sided-long-edge"); - assert_eq!(DuplexMode::TwoSidedLandscape.to_string(), "two-sided-short-edge"); + assert_eq!( + DuplexMode::TwoSidedPortrait.to_string(), + "two-sided-long-edge" + ); + assert_eq!( + DuplexMode::TwoSidedLandscape.to_string(), + "two-sided-short-edge" + ); assert_eq!(Orientation::Portrait.to_string(), "3"); assert_eq!(Orientation::Landscape.to_string(), "4"); } -} \ No newline at end of file +} diff --git a/src/job/status.rs b/src/job/status.rs index de71c93..c808757 100644 --- a/src/job/status.rs +++ b/src/job/status.rs @@ -76,26 +76,68 @@ mod tests { #[test] fn test_job_status_from_cups_state() { use crate::bindings::*; - assert_eq!(JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_PENDING as i32), JobStatus::Pending); - assert_eq!(JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_PROCESSING as i32), JobStatus::Processing); - assert_eq!(JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_COMPLETED as i32), JobStatus::Completed); - assert_eq!(JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_CANCELED as i32), JobStatus::Canceled); - assert_eq!(JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_ABORTED as i32), JobStatus::Aborted); - assert_eq!(JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_HELD as i32), JobStatus::Held); - assert_eq!(JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_STOPPED as i32), JobStatus::Stopped); + assert_eq!( + JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_PENDING as i32), + JobStatus::Pending + ); + assert_eq!( + JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_PROCESSING as i32), + JobStatus::Processing + ); + assert_eq!( + JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_COMPLETED as i32), + JobStatus::Completed + ); + assert_eq!( + JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_CANCELED as i32), + JobStatus::Canceled + ); + assert_eq!( + JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_ABORTED as i32), + JobStatus::Aborted + ); + assert_eq!( + JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_HELD as i32), + JobStatus::Held + ); + assert_eq!( + JobStatus::from_cups_state(ipp_jstate_e_IPP_JSTATE_STOPPED as i32), + JobStatus::Stopped + ); assert_eq!(JobStatus::from_cups_state(999), JobStatus::Unknown); } #[test] fn test_job_status_to_cups_value() { use crate::bindings::*; - assert_eq!(JobStatus::Pending.to_cups_value(), ipp_jstate_e_IPP_JSTATE_PENDING as i32); - assert_eq!(JobStatus::Processing.to_cups_value(), ipp_jstate_e_IPP_JSTATE_PROCESSING as i32); - assert_eq!(JobStatus::Completed.to_cups_value(), ipp_jstate_e_IPP_JSTATE_COMPLETED as i32); - assert_eq!(JobStatus::Canceled.to_cups_value(), ipp_jstate_e_IPP_JSTATE_CANCELED as i32); - assert_eq!(JobStatus::Aborted.to_cups_value(), ipp_jstate_e_IPP_JSTATE_ABORTED as i32); - assert_eq!(JobStatus::Held.to_cups_value(), ipp_jstate_e_IPP_JSTATE_HELD as i32); - assert_eq!(JobStatus::Stopped.to_cups_value(), ipp_jstate_e_IPP_JSTATE_STOPPED as i32); + assert_eq!( + JobStatus::Pending.to_cups_value(), + ipp_jstate_e_IPP_JSTATE_PENDING as i32 + ); + assert_eq!( + JobStatus::Processing.to_cups_value(), + ipp_jstate_e_IPP_JSTATE_PROCESSING as i32 + ); + assert_eq!( + JobStatus::Completed.to_cups_value(), + ipp_jstate_e_IPP_JSTATE_COMPLETED as i32 + ); + assert_eq!( + JobStatus::Canceled.to_cups_value(), + ipp_jstate_e_IPP_JSTATE_CANCELED as i32 + ); + assert_eq!( + JobStatus::Aborted.to_cups_value(), + ipp_jstate_e_IPP_JSTATE_ABORTED as i32 + ); + assert_eq!( + JobStatus::Held.to_cups_value(), + ipp_jstate_e_IPP_JSTATE_HELD as i32 + ); + assert_eq!( + JobStatus::Stopped.to_cups_value(), + ipp_jstate_e_IPP_JSTATE_STOPPED as i32 + ); assert_eq!(JobStatus::Unknown.to_cups_value(), 0); } diff --git a/src/lib.rs b/src/lib.rs index 0fde071..51cb48b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -174,22 +174,22 @@ pub mod job; /// - Get option values with type conversion pub mod options; -pub use constants::*; pub use connection::{ConnectionFlags, HttpConnection, connect_to_destination}; +pub use constants::*; pub use destination::{ - Destination, DestinationInfo, Destinations, MediaSize, PrinterState, OptionConflict, copy_dest, + Destination, DestinationInfo, Destinations, MediaSize, OptionConflict, PrinterState, copy_dest, enum_destinations, find_destinations, get_all_destinations, get_default_destination, get_destination, remove_dest, }; pub use error::{Error, ErrorCategory, Result}; +pub use ipp::{ + IppAttribute, IppOperation, IppRequest, IppResponse, IppStatus, IppTag, IppValueTag, +}; pub use job::{ ColorMode, DuplexMode, FORMAT_JPEG, FORMAT_PDF, FORMAT_POSTSCRIPT, FORMAT_TEXT, JobInfo, JobStatus, Orientation, PrintOptions, PrintQuality, cancel_job, create_job, create_job_with_options, get_active_jobs, get_completed_jobs, get_job_info, get_jobs, }; -pub use ipp::{ - IppAttribute, IppOperation, IppRequest, IppResponse, IppStatus, IppTag, IppValueTag, -}; pub use options::{ add_integer_option, add_option, encode_option, encode_options, encode_options_with_group, get_integer_option, get_option, parse_options, remove_option, diff --git a/src/options.rs b/src/options.rs index fd942dd..8f58ecc 100644 --- a/src/options.rs +++ b/src/options.rs @@ -31,9 +31,8 @@ pub fn parse_options(arg: &str) -> Result> { let mut num_options: c_int = 0; let mut options_ptr: *mut bindings::cups_option_s = ptr::null_mut(); - let result = unsafe { - bindings::cupsParseOptions(arg_c.as_ptr(), num_options, &mut options_ptr) - }; + let result = + unsafe { bindings::cupsParseOptions(arg_c.as_ptr(), num_options, &mut options_ptr) }; if result < 0 { return Err(Error::ConfigurationError(format!( @@ -83,7 +82,11 @@ pub fn parse_options(arg: &str) -> Result> { /// /// # Returns /// * Updated options vector with the new option added (or replaced if it existed) -pub fn add_option(name: &str, value: &str, mut options: Vec<(String, String)>) -> Vec<(String, String)> { +pub fn add_option( + name: &str, + value: &str, + mut options: Vec<(String, String)>, +) -> Vec<(String, String)> { // Remove existing option with the same name options.retain(|(n, _)| n != name); @@ -104,7 +107,11 @@ pub fn add_option(name: &str, value: &str, mut options: Vec<(String, String)>) - /// /// # Returns /// * Updated options vector with the new option added -pub fn add_integer_option(name: &str, value: i32, options: Vec<(String, String)>) -> Vec<(String, String)> { +pub fn add_integer_option( + name: &str, + value: i32, + options: Vec<(String, String)>, +) -> Vec<(String, String)> { add_option(name, &value.to_string(), options) } @@ -116,7 +123,10 @@ pub fn add_integer_option(name: &str, value: i32, options: Vec<(String, String)> /// /// # Returns /// * `(updated_options, was_removed)` - Updated vector and boolean indicating if option was found -pub fn remove_option(name: &str, mut options: Vec<(String, String)>) -> (Vec<(String, String)>, bool) { +pub fn remove_option( + name: &str, + mut options: Vec<(String, String)>, +) -> (Vec<(String, String)>, bool) { let initial_len = options.len(); options.retain(|(n, _)| n != name); let was_removed = options.len() < initial_len; @@ -182,9 +192,8 @@ pub fn encode_option( let name_c = CString::new(name)?; let value_c = CString::new(value)?; - let attr = unsafe { - bindings::cupsEncodeOption(ipp, group_tag, name_c.as_ptr(), value_c.as_ptr()) - }; + let attr = + unsafe { bindings::cupsEncodeOption(ipp, group_tag, name_c.as_ptr(), value_c.as_ptr()) }; if attr.is_null() { Err(Error::ConfigurationError(format!( @@ -210,10 +219,7 @@ pub fn encode_option( /// # Returns /// * `Ok(())` - Options encoded successfully /// * `Err(Error)` - Encoding failed -pub fn encode_options( - ipp: *mut bindings::_ipp_s, - options: &[(String, String)], -) -> Result<()> { +pub fn encode_options(ipp: *mut bindings::_ipp_s, options: &[(String, String)]) -> Result<()> { if ipp.is_null() { return Err(Error::NullPointer); } @@ -235,11 +241,7 @@ pub fn encode_options( } unsafe { - bindings::cupsEncodeOptions( - ipp, - cups_options.len() as c_int, - cups_options.as_mut_ptr(), - ); + bindings::cupsEncodeOptions(ipp, cups_options.len() as c_int, cups_options.as_mut_ptr()); } Ok(()) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 02f79f5..2c7e308 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,8 +1,8 @@ use cups_rs::*; use serial_test::serial; +use std::io::Write; use std::time::Duration; use tempfile::NamedTempFile; -use std::io::Write; fn cups_available() -> bool { match get_all_destinations() { @@ -16,33 +16,39 @@ fn cups_available() -> bool { fn get_test_printer() -> Result { let destinations = get_all_destinations()?; - + if let Ok(pdf_dest) = get_destination("PDF") { return Ok(pdf_dest); } - - destinations.into_iter().next().ok_or_else(|| { - Error::DestinationNotFound("No test printer available".to_string()) - }) + + destinations + .into_iter() + .next() + .ok_or_else(|| Error::DestinationNotFound("No test printer available".to_string())) } #[test] #[serial] fn test_integration_destination_discovery() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + let destinations = get_all_destinations().expect("Should get destinations"); - assert!(!destinations.is_empty(), "Should have at least one destination"); - + assert!( + !destinations.is_empty(), + "Should have at least one destination" + ); + for dest in &destinations { assert!(!dest.name.is_empty(), "Destination should have a name"); println!("Found destination: {} ({})", dest.full_name(), dest.state()); - + // Test basic properties let _state = dest.state(); let _accepting = dest.is_accepting_jobs(); let _reasons = dest.state_reasons(); - + // Test optional properties if let Some(info) = dest.info() { assert!(!info.is_empty()); @@ -59,15 +65,18 @@ fn test_integration_destination_discovery() { #[test] #[serial] fn test_integration_get_specific_destination() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + let destinations = get_all_destinations().expect("Should get destinations"); - if destinations.is_empty() { return; } - + if destinations.is_empty() { + return; + } + let first_dest = &destinations[0]; - let specific_dest = get_destination(&first_dest.name) - .expect("Should get specific destination"); - + let specific_dest = get_destination(&first_dest.name).expect("Should get specific destination"); + assert_eq!(specific_dest.name, first_dest.name); assert_eq!(specific_dest.is_default, first_dest.is_default); } @@ -75,11 +84,13 @@ fn test_integration_get_specific_destination() { #[test] #[serial] fn test_integration_destination_not_found() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + let result = get_destination("NonExistentPrinter123"); assert!(result.is_err()); - + match result { Err(Error::DestinationNotFound(name)) => { assert_eq!(name, "NonExistentPrinter123"); @@ -91,57 +102,74 @@ fn test_integration_destination_not_found() { #[test] #[serial] fn test_integration_printer_capabilities() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + let printer = match get_test_printer() { Ok(p) => p, Err(_) => return, // Skip if no test printer }; - + // Test getting detailed info let info_result = printer.get_detailed_info(std::ptr::null_mut()); match info_result { Ok(info) => { println!("Successfully got detailed info for {}", printer.name); - + // Test media capabilities - let media_count = info.get_media_count(std::ptr::null_mut(), printer.as_ptr(), MEDIA_FLAGS_DEFAULT); + let media_count = + info.get_media_count(std::ptr::null_mut(), printer.as_ptr(), MEDIA_FLAGS_DEFAULT); println!("Media count: {}", media_count); - + if media_count > 0 { // Test getting all media - let all_media = info.get_all_media(std::ptr::null_mut(), printer.as_ptr(), MEDIA_FLAGS_DEFAULT); + let all_media = + info.get_all_media(std::ptr::null_mut(), printer.as_ptr(), MEDIA_FLAGS_DEFAULT); match all_media { Ok(media_list) => { println!("Found {} media sizes", media_list.len()); for (i, media) in media_list.iter().take(3).enumerate() { - println!(" {}: {} ({:.1}\" x {:.1}\")", - i, media.name, media.width_inches(), media.length_inches()); + println!( + " {}: {} ({:.1}\" x {:.1}\")", + i, + media.name, + media.width_inches(), + media.length_inches() + ); } } Err(e) => println!("Could not get media list: {}", e), } - + // Test getting default media - let default_media = info.get_default_media(std::ptr::null_mut(), printer.as_ptr(), MEDIA_FLAGS_DEFAULT); + let default_media = info.get_default_media( + std::ptr::null_mut(), + printer.as_ptr(), + MEDIA_FLAGS_DEFAULT, + ); match default_media { Ok(media) => { - println!("Default media: {} ({:.1}\" x {:.1}\")", - media.name, media.width_inches(), media.length_inches()); + println!( + "Default media: {} ({:.1}\" x {:.1}\")", + media.name, + media.width_inches(), + media.length_inches() + ); } Err(e) => println!("Could not get default media: {}", e), } } - + // Test option support let supports_copies = printer.is_option_supported(std::ptr::null_mut(), COPIES); let supports_media = printer.is_option_supported(std::ptr::null_mut(), MEDIA); let supports_duplex = printer.is_option_supported(std::ptr::null_mut(), SIDES); - + println!("Supports copies: {}", supports_copies); println!("Supports media: {}", supports_media); println!("Supports duplex: {}", supports_duplex); - + // Cleanup unsafe { let dest_ptr = printer.as_ptr(); @@ -169,8 +197,10 @@ fn test_integration_printer_capabilities() { #[test] #[serial] fn test_integration_job_lifecycle() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + let printer = match get_test_printer() { Ok(p) => p, Err(_) => { @@ -178,16 +208,20 @@ fn test_integration_job_lifecycle() { return; } }; - + println!("Testing job lifecycle with printer: {}", printer.name); - + // temporary test file let mut temp_file = NamedTempFile::new().expect("Should create temp file"); - writeln!(temp_file, "This is a test document for CUPS integration testing.").unwrap(); + writeln!( + temp_file, + "This is a test document for CUPS integration testing." + ) + .unwrap(); writeln!(temp_file, "Created at: {}", chrono::Utc::now()).unwrap(); writeln!(temp_file, "Printer: {}", printer.name).unwrap(); temp_file.flush().unwrap(); - + // Create a job let job_result = create_job(&printer, "Integration Test Job"); let job = match job_result { @@ -200,7 +234,7 @@ fn test_integration_job_lifecycle() { return; // Skip rest of test } }; - + // Submit document to job let submit_result = job.submit_file(temp_file.path(), FORMAT_TEXT); match submit_result { @@ -214,7 +248,7 @@ fn test_integration_job_lifecycle() { return; } } - + // Close the job to start printing let close_result = job.close(); match close_result { @@ -228,40 +262,44 @@ fn test_integration_job_lifecycle() { return; } } - + // Check job status std::thread::sleep(Duration::from_millis(500)); // Give it a moment - + match get_job_info(job.id) { Ok(info) => { - println!("Job {} status: {} (size: {} bytes)", - info.id, info.status, info.size); + println!( + "Job {} status: {} (size: {} bytes)", + info.id, info.status, info.size + ); assert_eq!(info.id, job.id); } Err(e) => { println!("Could not get job info (job may have completed): {}", e); } } - + println!("Job lifecycle test completed successfully"); } #[test] #[serial] fn test_integration_job_with_options() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + let printer = match get_test_printer() { Ok(p) => p, Err(_) => return, }; - + // Create a job with specific options let options = PrintOptions::new() .copies(2) .color_mode(ColorMode::Monochrome) .quality(PrintQuality::Draft); - + let job_result = create_job_with_options(&printer, "Options Test Job", &options); let job = match job_result { Ok(j) => { @@ -273,10 +311,11 @@ fn test_integration_job_with_options() { return; } }; - + // Create test content - let test_content = "Test content for options verification\nCopies: 2\nColor: Monochrome\nQuality: Draft"; - + let test_content = + "Test content for options verification\nCopies: 2\nColor: Monochrome\nQuality: Draft"; + let submit_result = job.submit_data(test_content.as_bytes(), FORMAT_TEXT, "options_test.txt"); match submit_result { Ok(()) => { @@ -293,13 +332,15 @@ fn test_integration_job_with_options() { #[test] #[serial] fn test_integration_job_cancellation() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + let printer = match get_test_printer() { Ok(p) => p, Err(_) => return, }; - + // Create a job let job = match create_job(&printer, "Cancellation Test Job") { Ok(j) => j, @@ -308,9 +349,9 @@ fn test_integration_job_cancellation() { return; } }; - + println!("Created job {} for cancellation test", job.id); - + // Cancel it immediately let cancel_result = job.cancel(); match cancel_result { @@ -326,58 +367,65 @@ fn test_integration_job_cancellation() { #[test] #[serial] fn test_integration_get_jobs() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + // Test getting all jobs let all_jobs = get_jobs(None).unwrap_or_default(); println!("Found {} total jobs", all_jobs.len()); - + // Test getting active jobs let active_jobs = get_active_jobs(None).unwrap_or_default(); println!("Found {} active jobs", active_jobs.len()); - + // Test getting completed jobs let completed_jobs = get_completed_jobs(None).unwrap_or_default(); println!("Found {} completed jobs", completed_jobs.len()); - + // Print some job details for job in active_jobs.iter().take(3) { - println!("Active job {}: {} by {} on {} ({})", - job.id, job.title, job.user, job.dest, job.status); + println!( + "Active job {}: {} by {} on {} ({})", + job.id, job.title, job.user, job.dest, job.status + ); } - + for job in completed_jobs.iter().take(3) { - println!("Completed job {}: {} by {} on {} ({})", - job.id, job.title, job.user, job.dest, job.status); + println!( + "Completed job {}: {} by {} on {} ({})", + job.id, job.title, job.user, job.dest, job.status + ); } } #[test] #[serial] fn test_integration_find_destinations_by_type() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + // Test finding local printers - let local_printers = find_destinations(PRINTER_LOCAL, PRINTER_REMOTE) - .unwrap_or_default(); + let local_printers = find_destinations(PRINTER_LOCAL, PRINTER_REMOTE).unwrap_or_default(); println!("Found {} local printers", local_printers.len()); - + // Test finding color printers - let color_printers = find_destinations(PRINTER_COLOR, PRINTER_COLOR) - .unwrap_or_default(); + let color_printers = find_destinations(PRINTER_COLOR, PRINTER_COLOR).unwrap_or_default(); println!("Found {} color printers", color_printers.len()); - + // Test finding duplex printers - let duplex_printers = find_destinations(PRINTER_DUPLEX, PRINTER_DUPLEX) - .unwrap_or_default(); + let duplex_printers = find_destinations(PRINTER_DUPLEX, PRINTER_DUPLEX).unwrap_or_default(); println!("Found {} duplex printers", duplex_printers.len()); } #[test] #[serial] fn test_integration_error_handling() { - if !cups_available() { return; } - + if !cups_available() { + return; + } + // Non-existent printer let result = get_destination("NonExistentPrinter"); assert!(result.is_err()); @@ -385,18 +433,18 @@ fn test_integration_error_handling() { println!("Expected error for non-existent printer: {}", e); assert!(matches!(e, Error::DestinationNotFound(_))); } - + // Invalid job ID let result = get_job_info(999999); assert!(result.is_err()); if let Err(e) = result { println!("Expected error for invalid job ID: {}", e); } - + // Cancel non-existent job let result = cancel_job(999999); assert!(result.is_err()); if let Err(e) = result { println!("Expected error for canceling non-existent job: {}", e); } -} \ No newline at end of file +} diff --git "a/\342\200\216.github/workflows/rust.yml" "b/\342\200\216.github/workflows/rust.yml" new file mode 100644 index 0000000..dc3cc83 --- /dev/null +++ "b/\342\200\216.github/workflows/rust.yml" @@ -0,0 +1,25 @@ +name: Rust CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y libcups2-dev + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Format check + run: cargo fmt --all -- --check + + - name: Tests + run: cargo test --all -- --nocapture \ No newline at end of file