use std::process::Command; #[cfg(any(target_os = "windows", target_os = "linux"))] use std::fs; #[cfg(target_os = "linux")] use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; #[cfg(target_os = "windows")] use serde_json::Value; pub const VIRTUAL_PRINTER_NAME: &str = "FieldLens Print"; #[cfg(target_os = "linux")] pub const TEST_VIRTUAL_PRINTER_NAME: &str = "FieldLens Test Capture"; #[cfg(target_os = "linux")] const TEST_VIRTUAL_PRINTER_QUEUE: &str = "FieldLens-Test-Capture"; #[cfg(target_os = "linux")] const TEST_CAPTURE_DEVICE_URI: &str = "socket://127.0.0.1:9101"; const WINDOWS_PORT_NAME: &str = "FieldLensPrint-9100"; #[cfg(target_os = "linux")] const FIELDLENS_CUPS_SPOOL_DIR: &str = "/var/lib/fieldlens-print/capture"; #[cfg(target_os = "linux")] const FIELDLENS_CUPS_FILTER: &str = r#"#!/usr/bin/python3 import os import re import sys CACHE = "/var/lib/fieldlens-print/capture"\nOWNER_UID = __FIELDLENS_OWNER_UID__\nOWNER_GID = __FIELDLENS_OWNER_GID__ def safe(value): value = re.sub(r"[^A-Za-z0-9._-]+", "-", value or "job").strip("-") return value[:80] or "job" job_id = safe(sys.argv[1] if len(sys.argv) > 1 else "job") title = safe(sys.argv[3] if len(sys.argv) > 3 else "print") printer = safe(os.environ.get("PRINTER", "printer")) prefix = "test-" if printer.lower() == "fieldlens-test-capture" else "print-" suffix = "" if title.lower().endswith(".pdf") else ".pdf" name = f"{prefix}{printer}-{job_id}-{title}{suffix}" final_path = os.path.join(CACHE, name) part_path = final_path + ".part" source = None capture = None captured_ok = False try: if len(sys.argv) > 6 and sys.argv[6]: source = open(sys.argv[6], "rb") else: source = sys.stdin.buffer try: os.makedirs(CACHE, exist_ok=True) if not os.path.exists(final_path): capture = open(part_path, "wb") except OSError as error: print(f"WARNING: FieldLens capture cache unavailable: {error}", file=sys.stderr) capture = None while True: chunk = source.read(65536) if not chunk: break if capture is not None: try: capture.write(chunk) except OSError as error: print(f"WARNING: FieldLens capture write failed: {error}", file=sys.stderr) try: capture.close() except OSError: pass capture = None sys.stdout.buffer.write(chunk) sys.stdout.buffer.flush() if capture is not None: capture.flush() capture.close() capture = None os.chmod(part_path, 0o600) try: os.chown(part_path, OWNER_UID, OWNER_GID) except PermissionError: # On this Linux lane CUPS runs filters as root. Keep printing alive # if a future CUPS policy drops privileges; repair can surface it. pass os.replace(part_path, final_path) captured_ok = True print(f"INFO: FieldLens captured {name}", file=sys.stderr) finally: if source is not None and source is not sys.stdin.buffer: source.close() if capture is not None: capture.close() if not captured_ok: try: os.unlink(part_path) except OSError: pass "#; #[cfg(target_os = "linux")] const FIELDLENS_CUPS_REPAIR_HELPER: &str = r#"#!/usr/bin/python3 import os import subprocess import sys import tempfile target_printer = sys.argv[1] target_port = sys.argv[2] temp_filter = sys.argv[3] filter_path = sys.argv[4] owner_uid = int(sys.argv[5]) owner_gid = int(sys.argv[6]) source_ppd = os.path.join("/etc/cups/ppd", f"{target_printer}.ppd") spool_dir = "/var/lib/fieldlens-print/capture" old_spool_dir = "/var/cache/cups/fieldlens-print" with open(source_ppd, "r", encoding="utf-8", errors="replace") as source: original = source.read() cleaned = "\n".join( line for line in original.splitlines() if "fieldlens-capture" not in line ) + "\n" prefilter = ( "*% FieldLens original-document capture before cups-filters universal conversion\n" "*cupsPreFilter: \"application/vnd.universal-input 0 fieldlens-capture\"\n" ) index = cleaned.rfind("*% End") if index >= 0: cleaned = cleaned[:index] + prefilter + cleaned[index:] else: cleaned += prefilter fd, ppd_path = tempfile.mkstemp(prefix="fieldlens-", suffix=".ppd") os.close(fd) try: with open(ppd_path, "w", encoding="utf-8") as target: target.write(cleaned) subprocess.run(["install", "-m", "0755", temp_filter, filter_path], check=True) os.makedirs(spool_dir, exist_ok=True) os.chown(spool_dir, owner_uid, owner_gid) os.chmod(spool_dir, 0o700) # Bring forward captures stranded by 0.1.13/0.1.14 in CUPS' protected # cache. Ignore directories and partial writes. if os.path.isdir(old_spool_dir): for name in os.listdir(old_spool_dir): source = os.path.join(old_spool_dir, name) if not os.path.isfile(source) or name.endswith(".part"): continue target = os.path.join(spool_dir, name) if os.path.exists(target): root, ext = os.path.splitext(name) target = os.path.join(spool_dir, f"{root}-migrated{ext}") try: os.replace(source, target) except OSError: continue try: os.chown(target, owner_uid, owner_gid) os.chmod(target, 0o600) except OSError: pass subprocess.run([ "lpadmin", "-p", "FieldLens-Print", "-E", "-v", target_port, "-P", ppd_path, "-D", "FieldLens Print", ], check=True) subprocess.run([ "lpadmin", "-p", "FieldLens-Test-Capture", "-E", "-v", "socket://127.0.0.1:9101", "-P", ppd_path, "-D", "FieldLens Test Capture", ], check=True) for queue in ("FieldLens-Print", "FieldLens-Test-Capture"): subprocess.run(["cupsenable", queue], check=True) subprocess.run(["cupsaccept", queue], check=True) finally: try: os.unlink(ppd_path) except OSError: pass "#; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SystemPrinter { pub name: String, pub driver: String, pub port: String, pub address: Option, pub is_virtual: bool, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct VirtualPrinterStatus { pub installed: bool, pub name: String, pub detail: String, } fn is_virtual_printer_name(name: &str) -> bool { if name.eq_ignore_ascii_case(VIRTUAL_PRINTER_NAME) || name.eq_ignore_ascii_case("FieldLens-Print") { return true; } #[cfg(target_os = "linux")] { return name.eq_ignore_ascii_case(TEST_VIRTUAL_PRINTER_NAME) || name.eq_ignore_ascii_case(TEST_VIRTUAL_PRINTER_QUEUE); } #[cfg(not(target_os = "linux"))] false } pub fn list_system_printers() -> Result> { #[cfg(target_os = "windows")] { return list_windows_printers(); } #[cfg(target_os = "linux")] { return list_linux_printers(); } #[allow(unreachable_code)] Ok(Vec::new()) } pub fn virtual_printer_status() -> Result { #[cfg(target_os = "linux")] { return linux_virtual_printer_status(); } let installed = list_system_printers()? .iter() .any(|printer| is_virtual_printer_name(&printer.name)); Ok(VirtualPrinterStatus { installed, name: VIRTUAL_PRINTER_NAME.into(), detail: if installed { "Ready in the system print dialog. Keep RAW capture running while printing.".into() } else { "Not installed yet. Choose the physical destination printer, then create it.".into() }, }) } pub fn install_virtual_printer(target_printer: &str) -> Result { let target_printer = target_printer.trim(); if target_printer.is_empty() { return Err(anyhow!("choose the physical printer to forward to first")); } if is_virtual_printer_name(target_printer) { return Err(anyhow!("FieldLens Print cannot forward to itself")); } #[cfg(target_os = "windows")] install_windows_virtual_printer(target_printer)?; #[cfg(target_os = "linux")] install_linux_virtual_printer(target_printer)?; virtual_printer_status() } pub fn remove_virtual_printer() -> Result { #[cfg(target_os = "windows")] remove_windows_virtual_printer()?; #[cfg(target_os = "linux")] remove_linux_virtual_printer()?; virtual_printer_status() } #[cfg(target_os = "windows")] fn list_windows_printers() -> Result> { let script = r#" $items = @( Get-Printer | ForEach-Object { $printer = $_ $port = Get-PrinterPort -Name $printer.PortName -ErrorAction SilentlyContinue $address = $null if ($null -ne $port -and $port.PrinterHostAddress) { $number = if ($port.PortNumber) { $port.PortNumber } else { 9100 } $address = "$($port.PrinterHostAddress):$number" } [pscustomobject]@{ name = $printer.Name driver = $printer.DriverName port = $printer.PortName address = $address isVirtual = ($printer.Name -eq 'FieldLens Print') } } ) $items | ConvertTo-Json -Compress "#; let output = Command::new("powershell") .args(["-NoProfile", "-NonInteractive", "-Command", script]) .output() .context("query Windows printers")?; if !output.status.success() { return Err(anyhow!( "Windows printer query failed: {}", String::from_utf8_lossy(&output.stderr).trim() )); } let stdout = String::from_utf8_lossy(&output.stdout); let value: Value = serde_json::from_str(stdout.trim()).context("decode Windows printer list")?; let values = match value { Value::Array(values) => values, Value::Null => vec![], value => vec![value], }; let mut printers = Vec::new(); for value in values { let printer: SystemPrinter = serde_json::from_value(value)?; printers.push(printer); } printers.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); Ok(printers) } #[cfg(target_os = "windows")] fn install_windows_virtual_printer(target_printer: &str) -> Result<()> { let target = ps_quote(target_printer); let script_path = std::env::temp_dir().join(format!( "fieldlens-printer-setup-{}.ps1", uuid::Uuid::new_v4() )); let script = format!( r#"param([string]$TargetPrinter) $ErrorActionPreference = 'Stop' $target = Get-Printer -Name $TargetPrinter -ErrorAction Stop $portName = '{port}' $queueName = '{queue}' if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) {{ Add-PrinterPort -Name $portName -PrinterHostAddress '127.0.0.1' -PortNumber 9100 }} $existing = Get-Printer -Name $queueName -ErrorAction SilentlyContinue if ($existing) {{ Set-Printer -Name $queueName -DriverName $target.DriverName -PortName $portName }} else {{ Add-Printer -Name $queueName -DriverName $target.DriverName -PortName $portName }} "#, port = WINDOWS_PORT_NAME, queue = VIRTUAL_PRINTER_NAME, ); fs::write(&script_path, script).context("write Windows printer setup script")?; let path = ps_quote(&script_path.to_string_lossy()); let elevated = format!( "$p = Start-Process powershell -Verb RunAs -Wait -PassThru -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{path}','-TargetPrinter','{target}'); exit $p.ExitCode" ); let status = Command::new("powershell") .args(["-NoProfile", "-Command", &elevated]) .status() .context("launch elevated Windows printer setup")?; let _ = fs::remove_file(&script_path); if !status.success() { return Err(anyhow!( "printer setup was cancelled or failed; Windows administrator approval may be required" )); } Ok(()) } #[cfg(target_os = "windows")] fn remove_windows_virtual_printer() -> Result<()> { let script = format!( r#"$ErrorActionPreference = 'Stop' $queueName = '{queue}' $portName = '{port}' if (Get-Printer -Name $queueName -ErrorAction SilentlyContinue) {{ Remove-Printer -Name $queueName }} if (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue) {{ Remove-PrinterPort -Name $portName }} "#, queue = VIRTUAL_PRINTER_NAME, port = WINDOWS_PORT_NAME, ); let script_path = std::env::temp_dir().join(format!( "fieldlens-printer-remove-{}.ps1", uuid::Uuid::new_v4() )); fs::write(&script_path, script)?; let path = ps_quote(&script_path.to_string_lossy()); let elevated = format!( "$p = Start-Process powershell -Verb RunAs -Wait -PassThru -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{path}'); exit $p.ExitCode" ); let status = Command::new("powershell") .args(["-NoProfile", "-Command", &elevated]) .status()?; let _ = fs::remove_file(&script_path); if !status.success() { return Err(anyhow!("removing FieldLens Print was cancelled or failed")); } Ok(()) } #[cfg(target_os = "windows")] fn ps_quote(value: &str) -> String { value.replace('\'', "''") } #[cfg(target_os = "linux")] fn list_linux_printers() -> Result> { let output = match Command::new("lpstat").arg("-v").output() { Ok(output) => output, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { return Err(anyhow!( "CUPS client tools are missing. Install the cups-client package, then reopen FieldLens Print." )) } Err(error) => return Err(error).context("query CUPS printers with lpstat"), }; if !output.status.success() { return Err(anyhow!( "CUPS printer query failed: {}", String::from_utf8_lossy(&output.stderr).trim() )); } let mut printers = Vec::new(); for line in String::from_utf8_lossy(&output.stdout).lines() { let Some(rest) = line.strip_prefix("device for ") else { continue; }; let Some((name, uri)) = rest.split_once(": ") else { continue; }; // Ignore placeholder/dummy queues. They can expose a PPD but are not a // usable physical destination and can make Chromium reject the cloned // FieldLens queue as "not installed correctly". if uri == "///dev/null" || uri == "file:///dev/null" { continue; } let address = socket_uri_to_address(uri); printers.push(SystemPrinter { name: name.to_string(), driver: String::new(), port: uri.to_string(), address, is_virtual: is_virtual_printer_name(name), }); } printers.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); Ok(printers) } #[cfg(target_os = "linux")] fn socket_uri_to_address(uri: &str) -> Option { let value = uri.strip_prefix("socket://")?; let authority = value.split('/').next().unwrap_or(value); if authority.contains(':') { Some(authority.to_string()) } else if authority.is_empty() { None } else { Some(format!("{authority}:9100")) } } #[cfg(target_os = "linux")] fn linux_virtual_printer_status() -> Result { let printers = list_linux_printers()?; let installed = printers .iter() .any(|printer| printer.name.eq_ignore_ascii_case("FieldLens-Print")); if !installed { return Ok(VirtualPrinterStatus { installed: false, name: VIRTUAL_PRINTER_NAME.into(), detail: "Not installed yet. Choose the physical destination printer, then create it.".into(), }); } let state = Command::new("lpstat") .args(["-p", "FieldLens-Print"]) .output() .context("query FieldLens Print CUPS state")?; let accepting = Command::new("lpstat") .args(["-a", "FieldLens-Print"]) .output() .context("query FieldLens Print accepting state")?; let state_text = String::from_utf8_lossy(&state.stdout); let accepting_text = String::from_utf8_lossy(&accepting.stdout); let enabled = state.status.success() && !state_text.to_ascii_lowercase().contains("disabled"); let accepts_jobs = accepting.status.success() && accepting_text.to_ascii_lowercase().contains("accepting requests"); let ready = enabled && accepts_jobs; Ok(VirtualPrinterStatus { installed: ready, name: VIRTUAL_PRINTER_NAME.into(), detail: if ready { "Ready. FieldLens Print sends to the physical printer; FieldLens Test Capture runs the same capture/render path but discards the rendered output.".into() } else { "FieldLens Print exists but CUPS has it disabled or not accepting jobs. Click Create / repair FieldLens Print.".into() }, }) } #[cfg(target_os = "linux")] fn run_cups_repair_command(program: &str, args: &[&str]) -> Result<()> { let direct = Command::new(program).args(args).status(); if matches!(direct, Ok(status) if status.success()) { return Ok(()); } let status = Command::new("pkexec") .arg(program) .args(args) .status() .with_context(|| format!("run elevated {program}"))?; if !status.success() { return Err(anyhow!("{program} failed; administrator approval may be required")); } Ok(()) } #[cfg(target_os = "linux")] fn cups_filter_dir() -> Result<&'static Path> { for candidate in ["/usr/lib/cups/filter", "/usr/libexec/cups/filter"] { let path = Path::new(candidate); if path.is_dir() { return Ok(path); } } Err(anyhow!("CUPS filter directory was not found")) } #[cfg(target_os = "linux")] fn run_linux_admin(program: &str, args: &[String], context: &str) -> Result<()> { let direct = Command::new(program).args(args).status(); if matches!(direct, Ok(status) if status.success()) { return Ok(()); } let status = Command::new("pkexec") .arg(program) .args(args) .status() .with_context(|| format!("run elevated {context}"))?; if !status.success() { return Err(anyhow!("{context} failed; administrator approval may be required")); } Ok(()) } #[cfg(target_os = "linux")] fn install_linux_capture_filter() -> Result<()> { let filter_dir = cups_filter_dir()?; let filter_path = filter_dir.join("fieldlens-capture"); let temp_filter = std::env::temp_dir().join(format!( "fieldlens-capture-{}.py", uuid::Uuid::new_v4() )); fs::write(&temp_filter, FIELDLENS_CUPS_FILTER).context("write FieldLens CUPS pre-filter")?; let install_filter_args = vec![ "-m".to_string(), "0755".to_string(), temp_filter.to_string_lossy().into_owned(), filter_path.to_string_lossy().into_owned(), ]; let filter_result = run_linux_admin( "install", &install_filter_args, "install FieldLens CUPS pre-filter", ); let _ = fs::remove_file(&temp_filter); filter_result?; let spool_args = vec![ "-d".to_string(), "-m".to_string(), "0777".to_string(), FIELDLENS_CUPS_SPOOL_DIR.to_string(), ]; run_linux_admin("install", &spool_args, "create FieldLens CUPS capture spool")?; Ok(()) } #[cfg(target_os = "linux")] fn build_fieldlens_ppd(target_printer: &str) -> Result { let source = PathBuf::from("/etc/cups/ppd").join(format!("{target_printer}.ppd")); if !source.exists() { return Err(anyhow!( "CUPS PPD for '{target_printer}' was not found; FieldLens needs the installed printer definition to preserve browser print capabilities" )); } let original = match fs::read_to_string(&source) { Ok(contents) => contents, Err(direct_error) => { // CUPS commonly stores PPDs as root:lp 0640. Desktop users can // enumerate the queue but cannot necessarily read its PPD. // Reuse the existing PolicyKit setup flow for a read-only fallback. let output = Command::new("pkexec") .arg("cat") .arg(&source) .output() .with_context(|| { format!( "read target printer PPD {} (direct read failed: {direct_error})", source.display() ) })?; if !output.status.success() { return Err(anyhow!( "could not read target printer PPD {}: {}", source.display(), String::from_utf8_lossy(&output.stderr).trim() )); } String::from_utf8(output.stdout) .with_context(|| format!("decode target printer PPD {}", source.display()))? } }; let mut cleaned = original .lines() .filter(|line| !line.contains("fieldlens-capture")) .collect::>() .join("\n"); cleaned.push('\n'); let prefilter = concat!( "*% FieldLens original-document capture before cups-filters universal conversion\n", "*cupsPreFilter: \"application/vnd.universal-input 0 fieldlens-capture\"\n" ); if let Some(index) = cleaned.rfind("*% End") { cleaned.insert_str(index, prefilter); } else { cleaned.push_str(prefilter); } let target = std::env::temp_dir().join(format!( "fieldlens-{}.ppd", uuid::Uuid::new_v4() )); fs::write(&target, cleaned).context("write FieldLens derived PPD")?; Ok(target) } #[cfg(target_os = "linux")] fn install_linux_virtual_printer(target_printer: &str) -> Result<()> { let printers = list_linux_printers()?; let target = printers .iter() .find(|printer| printer.name == target_printer && !printer.is_virtual) .ok_or_else(|| anyhow!( "selected printer '{target_printer}' is not installed or available in CUPS" ))?; let filter_dir = cups_filter_dir()?; let filter_path = filter_dir.join("fieldlens-capture"); let temp_filter = std::env::temp_dir().join(format!( "fieldlens-capture-{}.py", uuid::Uuid::new_v4() )); let temp_helper = std::env::temp_dir().join(format!( "fieldlens-cups-repair-{}.py", uuid::Uuid::new_v4() )); let uid = Command::new("id") .arg("-u") .output() .context("read current Linux user id")?; let gid = Command::new("id") .arg("-g") .output() .context("read current Linux group id")?; if !uid.status.success() || !gid.status.success() { return Err(anyhow!("could not determine the current Linux user for capture ownership")); } let uid = String::from_utf8_lossy(&uid.stdout).trim().to_string(); let gid = String::from_utf8_lossy(&gid.stdout).trim().to_string(); let filter_script = FIELDLENS_CUPS_FILTER .replace("__FIELDLENS_OWNER_UID__", &uid) .replace("__FIELDLENS_OWNER_GID__", &gid); fs::write(&temp_filter, filter_script) .context("write FieldLens CUPS capture filter")?; fs::write(&temp_helper, FIELDLENS_CUPS_REPAIR_HELPER) .context("write FieldLens CUPS repair helper")?; let status = Command::new("pkexec") .arg("python3") .arg(&temp_helper) .arg(target_printer) .arg(&target.port) .arg(&temp_filter) .arg(&filter_path) .arg(&uid) .arg(&gid) .status() .context("run elevated FieldLens CUPS repair")?; let _ = fs::remove_file(&temp_filter); let _ = fs::remove_file(&temp_helper); if !status.success() { return Err(anyhow!( "FieldLens printer repair failed; administrator approval may be required" )); } Ok(()) } #[cfg(target_os = "linux")] fn remove_linux_virtual_printer() -> Result<()> { for queue in ["FieldLens-Print", TEST_VIRTUAL_PRINTER_QUEUE] { let args = vec!["-x".to_string(), queue.to_string()]; let direct = Command::new("lpadmin").args(&args).status(); if matches!(direct, Ok(status) if status.success()) { continue; } let status = Command::new("pkexec") .arg("lpadmin") .args(&args) .status() .context("run elevated CUPS printer removal")?; if !status.success() { return Err(anyhow!("removing FieldLens CUPS printer '{queue}' failed")); } } Ok(()) }