Merge pull request #1276 from CiberNin/fix/windows-printers

This commit is contained in:
Hakan Seven committed 2026-09-15 12:29:27 +03:00
commit 279288ab71
2 files changed
+136 -4

No files matched your search

+1 -1
View File
@@ -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"] }
+135 -3
View File
@@ -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<String> {
#[cfg(not(target_os = "windows"))]
@@ -111,7 +110,140 @@ pub fn list_printers() -> Vec<String> {
}
#[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<Vec<String>> {
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::<usize>::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::<u8>()
};
// 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::<usize>()),
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<Vec<String>> {
use std::io::{Error, ErrorKind};
use windows_sys::Win32::Graphics::Printing::PRINTER_INFO_4W;
const {
assert!(std::mem::align_of::<usize>() >= std::mem::align_of::<PRINTER_INFO_4W>());
}
let buffer_bytes = std::mem::size_of_val(buffer);
if printer_count > buffer_bytes / std::mem::size_of::<PRINTER_INFO_4W>() {
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_INFO_4W>(), 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::<u8>().add(offset).cast::<u16>(),
(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::<PRINTER_INFO_4W>();
let name: Vec<u16> = "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::<PRINTER_INFO_4W>();
let name_ptr = buffer
.as_mut_ptr()
.cast::<u8>()
.add(name_offset)
.cast::<u16>();
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::<u16>().wrapping_sub(1);
assert!(windows_printer_names(&buffer, 1).is_err());
(*record).pPrinterName = name_ptr;
}
buffer[name_offset / std::mem::size_of::<usize>()..].fill(usize::MAX);
assert!(windows_printer_names(&buffer, 1).is_err());
}
}