use std::ffi::c_void;
use std::io::{Seek, SeekFrom, Write};
use std::sync::{Arc, Mutex};
use windows::Win32::Foundation::{E_FAIL, E_INVALIDARG, E_NOTIMPL, E_POINTER, RECT, S_OK};
use windows::Win32::System::Com::*;
use windows_core::{implement, HRESULT};
use wry::WebViewExtWindows as _;
use crate::operations::{HubError, NativeBlobEncoder, OpaqueToken, OperationHub};
use crate::tauri::capture::{CaptureError, NativeCancellation};
struct Encoding {
encoder: Option<NativeBlobEncoder>,
header: [u8; 24],
}
#[implement(IStream)]
struct PngStream(Arc<Mutex<Encoding>>);
#[allow(non_snake_case)]
impl ISequentialStream_Impl for PngStream_Impl {
fn Read(&self, _: *mut c_void, _: u32, read: *mut u32) -> HRESULT {
if !read.is_null() {
unsafe {
*read = 0;
}
}
E_NOTIMPL
}
fn Write(&self, bytes: *const c_void, count: u32, written: *mut u32) -> HRESULT {
if !written.is_null() {
unsafe {
*written = 0;
}
}
if count > 0 && bytes.is_null() {
return E_POINTER;
}
let Ok(mut state) = self.0.lock() else {
return E_FAIL;
};
let Some(encoder) = state.encoder.as_mut() else {
return E_FAIL;
};
let Ok(position) = encoder.stream_position() else {
return E_FAIL;
};
let bytes = if count == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(bytes.cast::<u8>(), count as usize) }
};
if encoder.write_all(bytes).is_err() {
return E_FAIL;
}
if position < state.header.len() as u64 {
let start = position as usize;
let copy = bytes.len().min(state.header.len() - start);
state.header[start..start + copy].copy_from_slice(&bytes[..copy]);
}
if !written.is_null() {
unsafe {
*written = count;
}
}
S_OK
}
}
#[allow(non_snake_case)]
impl IStream_Impl for PngStream_Impl {
fn Seek(
&self,
offset: i64,
origin: STREAM_SEEK,
position: *mut u64,
) -> windows_core::Result<()> {
let from = match origin {
STREAM_SEEK_SET if offset >= 0 => SeekFrom::Start(offset as u64),
STREAM_SEEK_CUR => SeekFrom::Current(offset),
STREAM_SEEK_END => SeekFrom::End(offset),
_ => return Err(E_INVALIDARG.into()),
};
let mut state = self
.0
.lock()
.map_err(|_| windows_core::Error::from(E_FAIL))?;
let value = state
.encoder
.as_mut()
.ok_or_else(|| windows_core::Error::from(E_FAIL))?
.seek(from)
.map_err(|_| windows_core::Error::from(E_INVALIDARG))?;
if !position.is_null() {
unsafe {
*position = value;
}
}
Ok(())
}
fn SetSize(&self, _: u64) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn CopyTo(
&self,
_: windows_core::Ref<IStream>,
_: u64,
_: *mut u64,
_: *mut u64,
) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn Commit(&self, _: &STGC) -> windows_core::Result<()> {
Ok(())
}
fn Revert(&self) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn LockRegion(&self, _: u64, _: u64, _: &LOCKTYPE) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn UnlockRegion(&self, _: u64, _: u64, _: u32) -> windows_core::Result<()> {
Err(E_NOTIMPL.into())
}
fn Stat(&self, output: *mut STATSTG, _: &STATFLAG) -> windows_core::Result<()> {
if output.is_null() {
return Err(E_POINTER.into());
}
let mut state = self
.0
.lock()
.map_err(|_| windows_core::Error::from(E_FAIL))?;
let encoder = state
.encoder
.as_mut()
.ok_or_else(|| windows_core::Error::from(E_FAIL))?;
let position = encoder
.stream_position()
.map_err(|_| windows_core::Error::from(E_FAIL))?;
let size = encoder
.seek(SeekFrom::End(0))
.map_err(|_| windows_core::Error::from(E_FAIL))?;
encoder
.seek(SeekFrom::Start(position))
.map_err(|_| windows_core::Error::from(E_FAIL))?;
unsafe {
*output = STATSTG {
r#type: STGTY_STREAM.0 as u32,
cbSize: size,
grfMode: STGM_WRITE,
..Default::default()
};
}
Ok(())
}
fn Clone(&self) -> windows_core::Result<IStream> {
Err(E_NOTIMPL.into())
}
}
fn check_pixels(width: u64, height: u64, maximum: u64) -> Result<(), CaptureError> {
if width == 0
|| height == 0
|| width
.checked_mul(height)
.is_none_or(|pixels| pixels > maximum)
{
Err(CaptureError::PixelLimit)
} else {
Ok(())
}
}
fn check_png(header: &[u8; 24], maximum: u64) -> Result<(), CaptureError> {
if &header[..8] != b"\x89PNG\r\n\x1a\n" || &header[8..16] != b"\0\0\0\rIHDR" {
return Err(CaptureError::InvalidPng);
}
check_pixels(
u32::from_be_bytes(header[16..20].try_into().unwrap()) as u64,
u32::from_be_bytes(header[20..24].try_into().unwrap()) as u64,
maximum,
)
}
fn map_hub(error: HubError) -> CaptureError {
match error {
HubError::Closed | HubError::Invalid => CaptureError::Cancelled,
_ => CaptureError::BlobLimit,
}
}
pub(crate) fn capture(
view: &wry::WebView,
hub: Arc<OperationHub>,
store: u64,
operation: OpaqueToken,
maximum_pixels: u64,
maximum_bytes: usize,
completed: impl FnOnce(Result<OpaqueToken, CaptureError>) + 'static,
) -> Result<NativeCancellation, CaptureError> {
let mut bounds = RECT::default();
unsafe { view.controller().Bounds(&mut bounds) }.map_err(|_| CaptureError::NativeFailure)?;
let width = i64::from(bounds.right) - i64::from(bounds.left);
let height = i64::from(bounds.bottom) - i64::from(bounds.top);
if width <= 0 || height <= 0 {
return Err(CaptureError::PixelLimit);
}
check_pixels(width as u64, height as u64, maximum_pixels)?;
let encoder =
NativeBlobEncoder::for_operation(hub, store, operation, maximum_bytes).map_err(map_hub)?;
let state = Arc::new(Mutex::new(Encoding {
encoder: Some(encoder),
header: [0; 24],
}));
let stream: IStream = PngStream(Arc::clone(&state)).into();
let callback = webview2_com::CapturePreviewCompletedHandler::create(Box::new(move |status| {
let result = (|| {
let mut state = state.lock().map_err(|_| CaptureError::NativeFailure)?;
let encoder = state.encoder.take().ok_or(CaptureError::Cancelled)?;
if let Some(error) = encoder.failure() {
return Err(map_hub(error));
}
status.map_err(|_| CaptureError::NativeFailure)?;
check_png(&state.header, maximum_pixels)?;
encoder.finish_for_operation(operation).map_err(map_hub)
})();
completed(result);
Ok(())
}));
unsafe { view.webview().CapturePreview(
webview2_com::Microsoft::Web::WebView2::Win32::COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG,
&stream, &callback,
) }.map_err(|_| CaptureError::NativeFailure)?;
Ok(NativeCancellation::new(|| {}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn com_stream_rewrites_and_rejects_quota_and_revocation() {
let hub = OperationHub::with_blob_limits(
4,
3,
crate::operations::BlobLimits::new(4, 8, 8).unwrap(),
)
.unwrap();
let encoder = NativeBlobEncoder::new(Arc::clone(&hub), 7, 8).unwrap();
let state = Arc::new(Mutex::new(Encoding {
encoder: Some(encoder),
header: [0; 24],
}));
let stream: IStream = PngStream(Arc::clone(&state)).into();
let mut written = 99;
unsafe {
stream
.Write(b"abcd".as_ptr().cast(), 4, Some(&mut written))
.ok()
.unwrap();
assert_eq!(written, 4);
stream.Seek(0, STREAM_SEEK_SET, None).unwrap();
stream.Write(b"AB".as_ptr().cast(), 2, None).ok().unwrap();
}
let blob = state
.lock()
.unwrap()
.encoder
.take()
.unwrap()
.finish()
.unwrap();
assert_eq!(hub.blob_read(7, blob, 4).unwrap(), b"ABcd");
hub.close_resource(blob).unwrap();
assert!(unsafe { stream.Write(b"x".as_ptr().cast(), 1, Some(&mut written)) }.is_err());
assert_eq!(written, 0);
assert_eq!(hub.snapshot().retained_transfer_capacity, 0);
let encoder = NativeBlobEncoder::new(Arc::clone(&hub), 7, 8).unwrap();
let state = Arc::new(Mutex::new(Encoding {
encoder: Some(encoder),
header: [0; 24],
}));
let stream: IStream = PngStream(Arc::clone(&state)).into();
assert!(
unsafe { stream.Write(b"too-large".as_ptr().cast(), 9, Some(&mut written)) }.is_err()
);
assert_eq!(written, 0);
assert_eq!(hub.snapshot().buffered_blob_bytes, 0);
drop(state.lock().unwrap().encoder.take());
let (view, open) = hub.begin_external_webview_open(7).unwrap();
hub.finish_external_open(open, view);
hub.observe_terminal(7, open).unwrap().unwrap();
let operation = hub.begin_external_webview_capture(7, view).unwrap();
let encoder = NativeBlobEncoder::for_operation(Arc::clone(&hub), 7, operation, 8).unwrap();
let state = Arc::new(Mutex::new(Encoding {
encoder: Some(encoder),
header: [0; 24],
}));
let stream: IStream = PngStream(Arc::clone(&state)).into();
unsafe { stream.Write(b"part".as_ptr().cast(), 4, None) }
.ok()
.unwrap();
hub.close_resource(view).unwrap();
assert_eq!(hub.snapshot().retained_transfer_capacity, 0);
assert!(unsafe { stream.Write(b"late".as_ptr().cast(), 4, Some(&mut written)) }.is_err());
assert_eq!(written, 0);
assert!(
state.lock().unwrap().encoder.is_some(),
"native owner remains alive during revocation"
);
hub.close_all(crate::operations::Terminal::Closed);
assert_eq!(hub.snapshot().retained_transfer_capacity, 0);
}
#[test]
fn png_header_and_pixel_budget_are_checked() {
let mut header = [0; 24];
assert_eq!(check_png(&header, 64), Err(CaptureError::InvalidPng));
header[..16].copy_from_slice(b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR");
header[16..20].copy_from_slice(&8_u32.to_be_bytes());
header[20..24].copy_from_slice(&8_u32.to_be_bytes());
assert_eq!(check_png(&header, 64), Ok(()));
assert_eq!(check_png(&header, 63), Err(CaptureError::PixelLimit));
assert_eq!(
check_pixels(u64::MAX, 2, u64::MAX),
Err(CaptureError::PixelLimit)
);
}
}