diff --git a/Cargo.toml b/Cargo.toml index 89a10af13..5f65e162c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ fluent-syntax = "0.12" naga = { version = "27", features = ["wgsl-in"] } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] } +windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing"] } [target.'cfg(target_os = "linux")'.dependencies] ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] } diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 6f4d3a680..d0ca2ad7e 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -89,8 +89,7 @@ pub fn print_existing_pdf(_path: &std::path::Path, _opts: &PrintOptions) -> Resu } /// Enumerate installed printers. Linux/macOS query CUPS via `lpstat -e`; -/// Windows returns an empty list (the "printto" dispatch targets a named -/// printer directly and the system default is always available). +/// Windows queries the spooler's cached local and connected printer list. #[cfg(not(target_arch = "wasm32"))] pub fn list_printers() -> Vec { #[cfg(not(target_os = "windows"))] @@ -111,7 +110,140 @@ pub fn list_printers() -> Vec { } #[cfg(target_os = "windows")] { - Vec::new() + windows_printers().unwrap_or_else(|error| { + eprintln!("Could not enumerate printers: {error}"); + Vec::new() + }) + } +} + +#[cfg(target_os = "windows")] +fn windows_printers() -> std::io::Result> { + use windows_sys::Win32::Foundation::{GetLastError, ERROR_INSUFFICIENT_BUFFER}; + use windows_sys::Win32::Graphics::Printing::{ + EnumPrintersW, PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, + }; + + let mut bytes_needed = 0; + let mut printer_count = 0; + let mut buffer = Vec::::new(); + // The printer list can grow between the size query and the data query. + for _ in 0..4 { + let buffer_bytes = u32::try_from(std::mem::size_of_val(buffer.as_slice())) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::OutOfMemory))?; + let data = if buffer.is_empty() { + std::ptr::null_mut() + } else { + buffer.as_mut_ptr().cast::() + }; + // SAFETY: data is null for the size query, otherwise it points to an + // aligned, writable allocation of buffer_bytes bytes. + let success = unsafe { + EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null(), + 4, + data, + buffer_bytes, + &mut bytes_needed, + &mut printer_count, + ) + }; + if success != 0 { + return windows_printer_names(&buffer, printer_count as usize); + } + let error = unsafe { GetLastError() }; + if error != ERROR_INSUFFICIENT_BUFFER || bytes_needed <= buffer_bytes { + return Err(std::io::Error::from_raw_os_error(error as i32)); + } + buffer.resize( + (bytes_needed as usize).div_ceil(std::mem::size_of::()), + 0, + ); + } + Err(std::io::Error::from_raw_os_error( + ERROR_INSUFFICIENT_BUFFER as i32, + )) +} + +#[cfg(target_os = "windows")] +fn windows_printer_names(buffer: &[usize], printer_count: usize) -> std::io::Result> { + use std::io::{Error, ErrorKind}; + use windows_sys::Win32::Graphics::Printing::PRINTER_INFO_4W; + + const { + assert!(std::mem::align_of::() >= std::mem::align_of::()); + } + let buffer_bytes = std::mem::size_of_val(buffer); + if printer_count > buffer_bytes / std::mem::size_of::() { + return Err(Error::from(ErrorKind::InvalidData)); + } + // SAFETY: the initialized buffer is aligned and large enough for these + // records. Raw pointer fields are validated before reading any names. + let records = unsafe { + std::slice::from_raw_parts(buffer.as_ptr().cast::(), printer_count) + }; + let mut names = Vec::with_capacity(printer_count); + for record in records { + if record.pPrinterName.is_null() { + continue; + } + let offset = (record.pPrinterName as usize) + .checked_sub(buffer.as_ptr() as usize) + .filter(|offset| *offset < buffer_bytes && offset % 2 == 0) + .ok_or(Error::from(ErrorKind::InvalidData))?; + // SAFETY: offset is UTF-16 aligned and the slice ends within buffer. + let utf16 = unsafe { + std::slice::from_raw_parts( + buffer.as_ptr().cast::().add(offset).cast::(), + (buffer_bytes - offset) / 2, + ) + }; + let length = utf16 + .iter() + .position(|&unit| unit == 0) + .ok_or(Error::from(ErrorKind::InvalidData))?; + if length != 0 { + names.push(String::from_utf16_lossy(&utf16[..length])); + } + } + names.sort(); + names.dedup(); + Ok(names) +} + +#[cfg(all(test, target_os = "windows"))] +mod printer_buffer_tests { + use super::windows_printer_names; + use windows_sys::Win32::Graphics::Printing::PRINTER_INFO_4W; + + #[test] + fn printer_names_stay_within_the_returned_buffer() { + assert!(windows_printer_names(&[], 0).unwrap().is_empty()); + assert!(windows_printer_names(&[], 1).is_err()); + let mut buffer = vec![0usize; 16]; + let name_offset = std::mem::size_of::(); + let name: Vec = "Printer \u{03b1}\0".encode_utf16().collect(); + // SAFETY: the aligned allocation holds the record and UTF-16 name. + unsafe { + let record = buffer.as_mut_ptr().cast::(); + let name_ptr = buffer + .as_mut_ptr() + .cast::() + .add(name_offset) + .cast::(); + std::ptr::copy_nonoverlapping(name.as_ptr(), name_ptr, name.len()); + (*record).pPrinterName = name_ptr; + assert_eq!( + windows_printer_names(&buffer, 1).unwrap(), + ["Printer \u{03b1}"] + ); + (*record).pPrinterName = buffer.as_mut_ptr().cast::().wrapping_sub(1); + assert!(windows_printer_names(&buffer, 1).is_err()); + (*record).pPrinterName = name_ptr; + } + buffer[name_offset / std::mem::size_of::()..].fill(usize::MAX); + assert!(windows_printer_names(&buffer, 1).is_err()); } }