Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions crates/presswerk-app/src/pages/add_printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,7 @@ pub fn AddPrinter() -> Element {

/// Probe a printer by attempting Get-Printer-Attributes.
async fn probe_printer(uri: &str) -> Result<(), String> {
let client = presswerk_print::ipp_client::IppClient::new(uri)
.map_err(|e| e.to_string())?;
let client = presswerk_print::ipp_client::IppClient::new(uri).map_err(|e| e.to_string())?;
client
.get_printer_attributes()
.await
Expand Down
9 changes: 2 additions & 7 deletions crates/presswerk-app/src/services/app_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,20 +269,15 @@ impl AppServices {
Ok(remote_id) => {
info!(job_id = %job_id, remote_id, "print job accepted");
if let Ok(queue) = services.job_queue.lock() {
let _ =
queue.update_status(&job_id, JobStatus::Completed, None);
let _ = queue.update_status(&job_id, JobStatus::Completed, None);
}
services.audit("print_completed", &hash, true, None);
}
Err(e) => {
error!(job_id = %job_id, error = %e, "print job failed");
let msg = e.to_string();
if let Ok(queue) = services.job_queue.lock() {
let _ = queue.update_status(
&job_id,
JobStatus::Failed,
Some(&msg),
);
let _ = queue.update_status(&job_id, JobStatus::Failed, Some(&msg));
}
services.audit("print_failed", &hash, false, Some(&msg));
}
Expand Down
15 changes: 12 additions & 3 deletions crates/presswerk-bridge/src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,6 @@ fn get_authority(env: &mut JNIEnv<'_>, activity: &JObject<'_>) -> Result<String>
Ok(format!("{pkg}.fileprovider"))
}


// ---------------------------------------------------------------------------
// Stub implementations for connection types not yet wired to Android APIs
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1095,7 +1094,12 @@ impl NativeThunderboltPrint for AndroidBridge {
Err(PresswerkError::PlatformUnavailable)
}

fn print_thunderbolt(&self, _device_id: &str, _document: &[u8], _mime_type: &str) -> Result<()> {
fn print_thunderbolt(
&self,
_device_id: &str,
_document: &[u8],
_mime_type: &str,
) -> Result<()> {
Err(PresswerkError::PlatformUnavailable)
}
}
Expand Down Expand Up @@ -1151,7 +1155,12 @@ impl NativeUsbDrivePrint for AndroidBridge {
Err(PresswerkError::PlatformUnavailable)
}

fn copy_to_usb_drive(&self, _drive_id: &str, _document: &[u8], _filename: &str) -> Result<String> {
fn copy_to_usb_drive(
&self,
_drive_id: &str,
_document: &[u8],
_filename: &str,
) -> Result<String> {
Err(PresswerkError::PlatformUnavailable)
}
}
15 changes: 12 additions & 3 deletions crates/presswerk-bridge/src/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,6 @@ impl NativeShare for IosBridge {
// Tests
// ---------------------------------------------------------------------------


// ---------------------------------------------------------------------------
// Stub implementations for connection types not yet wired to iOS APIs
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -949,7 +948,12 @@ impl NativeThunderboltPrint for IosBridge {
Err(PresswerkError::PlatformUnavailable)
}

fn print_thunderbolt(&self, _device_id: &str, _document: &[u8], _mime_type: &str) -> Result<()> {
fn print_thunderbolt(
&self,
_device_id: &str,
_document: &[u8],
_mime_type: &str,
) -> Result<()> {
Err(PresswerkError::PlatformUnavailable)
}
}
Expand Down Expand Up @@ -1005,7 +1009,12 @@ impl NativeUsbDrivePrint for IosBridge {
Err(PresswerkError::PlatformUnavailable)
}

fn copy_to_usb_drive(&self, _drive_id: &str, _document: &[u8], _filename: &str) -> Result<String> {
fn copy_to_usb_drive(
&self,
_drive_id: &str,
_document: &[u8],
_filename: &str,
) -> Result<String> {
Err(PresswerkError::PlatformUnavailable)
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/presswerk-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub mod android;
pub mod stub;

/// Retrieves the singleton bridge implementation for the target operating system.
///
///
/// RETURNS: A boxed trait object (`dyn PlatformBridge`) that abstracts away
/// the underlying native SDK details.
pub fn platform_bridge() -> Box<dyn traits::PlatformBridge> {
Expand Down
12 changes: 4 additions & 8 deletions crates/presswerk-core/benches/core_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
// Benchmarks for presswerk-core critical operations.
// Run with: cargo bench -p presswerk-core

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use presswerk_core::{
AppConfig, DocumentType, PrintJob, PrintSettings, JobSource,
};
use criterion::{Criterion, black_box, criterion_group, criterion_main};
use presswerk_core::{AppConfig, DocumentType, JobSource, PrintJob, PrintSettings};

fn bench_config_creation(c: &mut Criterion) {
c.bench_function("config_default", |b| {
Expand Down Expand Up @@ -41,8 +39,7 @@ fn bench_config_serialization(c: &mut Criterion) {

c.bench_function("config_deserialize_json", |b| {
b.iter(|| {
let _config: AppConfig =
serde_json::from_str(&black_box(&json)).expect("deserialize");
let _config: AppConfig = serde_json::from_str(&black_box(&json)).expect("deserialize");
})
});
}
Expand Down Expand Up @@ -101,8 +98,7 @@ fn bench_job_serialization(c: &mut Criterion) {

c.bench_function("job_deserialize_json", |b| {
b.iter(|| {
let _job: PrintJob =
serde_json::from_str(&black_box(&json)).expect("deserialize");
let _job: PrintJob = serde_json::from_str(&black_box(&json)).expect("deserialize");
})
});
}
Expand Down
23 changes: 17 additions & 6 deletions crates/presswerk-core/src/human_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,19 @@ fn humanize_ipp_error(detail: &str) -> HumanError {
} else if lower.contains("server-error") {
HumanError {
message: "The printer reported an internal error.".into(),
suggestion: "Try turning the printer off, waiting 10 seconds, and turning it back on.".into(),
suggestion: "Try turning the printer off, waiting 10 seconds, and turning it back on."
.into(),
retriable: true,
severity: Severity::Transient,
}
} else if lower.contains("client-error-not-possible") || lower.contains("client-error-attributes") {
} else if lower.contains("client-error-not-possible")
|| lower.contains("client-error-attributes")
{
HumanError {
message: "The printer can't handle those settings.".into(),
suggestion: "Try changing the print settings (paper size, duplex, colour) and print again.".into(),
suggestion:
"Try changing the print settings (paper size, duplex, colour) and print again."
.into(),
retriable: false,
severity: Severity::ActionRequired,
}
Expand All @@ -229,7 +234,8 @@ fn humanize_ipp_error(detail: &str) -> HumanError {
} else if lower.contains("invalid uri") || lower.contains("invalid url") {
HumanError {
message: "The printer address doesn't look right.".into(),
suggestion: "Check the printer address and try again. It should look like 192.168.1.100.".into(),
suggestion:
"Check the printer address and try again. It should look like 192.168.1.100.".into(),
retriable: false,
severity: Severity::ActionRequired,
}
Expand All @@ -240,7 +246,10 @@ fn humanize_ipp_error(detail: &str) -> HumanError {
retriable: false,
severity: Severity::ActionRequired,
}
} else if lower.contains("toner-empty") || lower.contains("ink") || lower.contains("marker-supply") {
} else if lower.contains("toner-empty")
|| lower.contains("ink")
|| lower.contains("marker-supply")
{
HumanError {
message: "The printer needs new ink or toner.".into(),
suggestion: "You'll need to buy a replacement cartridge. Check your printer's model number and search online for the right one.".into(),
Expand All @@ -265,7 +274,9 @@ fn humanize_ipp_error(detail: &str) -> HumanError {
// Generic IPP error fallback
HumanError {
message: "The printer had a problem.".into(),
suggestion: format!("Try again. If this keeps happening, try turning the printer off and on again. (Detail: {detail})"),
suggestion: format!(
"Try again. If this keeps happening, try turning the printer off and on again. (Detail: {detail})"
),
retriable: true,
severity: Severity::Transient,
}
Expand Down
30 changes: 24 additions & 6 deletions crates/presswerk-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,17 +368,29 @@ mod tests {
#[test]
fn test_document_type_extensions() {
assert_eq!(DocumentType::from_extension("pdf"), Some(DocumentType::Pdf));
assert_eq!(DocumentType::from_extension("jpg"), Some(DocumentType::Jpeg));
assert_eq!(DocumentType::from_extension("jpeg"), Some(DocumentType::Jpeg));
assert_eq!(
DocumentType::from_extension("jpg"),
Some(DocumentType::Jpeg)
);
assert_eq!(
DocumentType::from_extension("jpeg"),
Some(DocumentType::Jpeg)
);
assert_eq!(DocumentType::from_extension("png"), Some(DocumentType::Png));
assert_eq!(DocumentType::from_extension("txt"), Some(DocumentType::PlainText));
assert_eq!(
DocumentType::from_extension("txt"),
Some(DocumentType::PlainText)
);
assert_eq!(DocumentType::from_extension("unknown"), None);
}

#[test]
fn test_document_type_case_insensitive() {
assert_eq!(DocumentType::from_extension("PDF"), Some(DocumentType::Pdf));
assert_eq!(DocumentType::from_extension("JPG"), Some(DocumentType::Jpeg));
assert_eq!(
DocumentType::from_extension("JPG"),
Some(DocumentType::Jpeg)
);
assert_eq!(DocumentType::from_extension("Pdf"), Some(DocumentType::Pdf));
}

Expand Down Expand Up @@ -410,8 +422,14 @@ mod tests {
#[test]
fn test_duplex_mode_keywords() {
assert_eq!(DuplexMode::Simplex.ipp_sides_keyword(), "one-sided");
assert_eq!(DuplexMode::LongEdge.ipp_sides_keyword(), "two-sided-long-edge");
assert_eq!(DuplexMode::ShortEdge.ipp_sides_keyword(), "two-sided-short-edge");
assert_eq!(
DuplexMode::LongEdge.ipp_sides_keyword(),
"two-sided-long-edge"
);
assert_eq!(
DuplexMode::ShortEdge.ipp_sides_keyword(),
"two-sided-short-edge"
);
}

#[test]
Expand Down
36 changes: 28 additions & 8 deletions crates/presswerk-core/tests/aspect_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Tests security properties, invariants, and edge cases.

use presswerk_core::{
AppConfig, DocumentType, PrintJob, PrintSettings, JobSource, PageRange, PaperSize,
AppConfig, DocumentType, JobSource, PageRange, PaperSize, PrintJob, PrintSettings,
};

// ============================================================================
Expand Down Expand Up @@ -34,8 +34,11 @@ fn aspect_html_injection_in_document_name() {
for name in malicious_names {
// These should be detected as suspicious by the application
assert!(
name.contains('<') || name.contains('>') || name.contains('\'')
|| name.contains('/') || name.contains('.'),
name.contains('<')
|| name.contains('>')
|| name.contains('\'')
|| name.contains('/')
|| name.contains('.'),
"Potential injection in: {}",
name
);
Expand All @@ -55,7 +58,11 @@ fn aspect_path_traversal_prevention() {
// Check for path traversal indicators
let has_traversal = path.contains("../") || path.contains("..\\") || path.starts_with('/');
if should_be_suspicious {
assert!(has_traversal, "Path traversal pattern should be detected in: {}", path);
assert!(
has_traversal,
"Path traversal pattern should be detected in: {}",
path
);
}
}
}
Expand Down Expand Up @@ -90,7 +97,10 @@ fn aspect_page_count_validation() {
"hash".to_string(),
);

job.settings.page_range = Some(PageRange { start: 100, end: 50 });
job.settings.page_range = Some(PageRange {
start: 100,
end: 50,
});

// Invalid range: start > end should be detected
if let Some(range) = &job.settings.page_range {
Expand All @@ -113,7 +123,11 @@ fn aspect_invalid_dpi_detection() {
const MAX_DPI: u32 = 1200;

for dpi in valid_dpis {
assert!(dpi >= MIN_DPI && dpi <= MAX_DPI, "DPI {} should be valid", dpi);
assert!(
dpi >= MIN_DPI && dpi <= MAX_DPI,
"DPI {} should be valid",
dpi
);
}

for dpi in invalid_dpis {
Expand Down Expand Up @@ -177,7 +191,10 @@ fn aspect_error_message_sanitization() {
// Check for common sensitive patterns
let contains_password = msg.contains("password");
let contains_ip = msg.contains("192.168");
assert!(contains_password || contains_ip, "Error message should contain sensitive data (for testing)");
assert!(
contains_password || contains_ip,
"Error message should contain sensitive data (for testing)"
);
// In production, these should be redacted
}
}
Expand Down Expand Up @@ -246,7 +263,10 @@ fn aspect_max_copies_limit() {

// Beyond practical limits would need manual override
let excessive_copies = 10_000u32;
assert!(excessive_copies > MAX_COPIES, "Excessive copies should be detectable");
assert!(
excessive_copies > MAX_COPIES,
"Excessive copies should be detectable"
);
}

#[test]
Expand Down
14 changes: 10 additions & 4 deletions crates/presswerk-core/tests/e2e_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// Tests the complete workflows: configuration, job creation, serialization.

use presswerk_core::{
AppConfig, DocumentType, DuplexMode, Orientation, PaperSize, PageRange,
PrintJob, PrintSettings, JobSource, JobStatus, ErrorClass,
AppConfig, DocumentType, DuplexMode, ErrorClass, JobSource, JobStatus, Orientation, PageRange,
PaperSize, PrintJob, PrintSettings,
};

#[test]
Expand Down Expand Up @@ -164,8 +164,14 @@ fn e2e_paper_size_ipp_mapping() {
#[test]
fn e2e_duplex_mode_ipp_mapping() {
assert_eq!(DuplexMode::Simplex.ipp_sides_keyword(), "one-sided");
assert_eq!(DuplexMode::LongEdge.ipp_sides_keyword(), "two-sided-long-edge");
assert_eq!(DuplexMode::ShortEdge.ipp_sides_keyword(), "two-sided-short-edge");
assert_eq!(
DuplexMode::LongEdge.ipp_sides_keyword(),
"two-sided-long-edge"
);
assert_eq!(
DuplexMode::ShortEdge.ipp_sides_keyword(),
"two-sided-short-edge"
);
}

#[test]
Expand Down
4 changes: 3 additions & 1 deletion crates/presswerk-core/tests/property_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// Property-based tests for presswerk-core using proptest.
// These tests verify invariants that must hold across all valid inputs.

use presswerk_core::{AppConfig, DuplexMode, DocumentType, JobSource, Orientation, PaperSize, PrintJob, PrintSettings};
use presswerk_core::{
AppConfig, DocumentType, DuplexMode, JobSource, Orientation, PaperSize, PrintJob, PrintSettings,
};
use proptest::prelude::*;

// ============================================================================
Expand Down
Loading
Loading