diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-05 19:34:24 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-05 19:34:24 +0300 |
| commit | 2863b715132fe29e53089e06f105943d1df0b3b4 (patch) | |
| tree | 6b4af2f6fdec6de2888fd2bff3c0829d6719b727 /src/platform/windows | |
| parent | eb93ec8ce62b05de776f751f4d2f52ff60f93f16 (diff) | |
feat(windows): support remote attach to unix hosts (#2329)
* feat(windows): support remote attach to unix hosts
* fix(remote): preserve unix bridge behavior
Diffstat (limited to 'src/platform/windows')
| -rw-r--r-- | src/platform/windows/clipboard_image.rs | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/src/platform/windows/clipboard_image.rs b/src/platform/windows/clipboard_image.rs new file mode 100644 index 00000000..f8e17aed --- /dev/null +++ b/src/platform/windows/clipboard_image.rs @@ -0,0 +1,409 @@ +use std::io::{self, Cursor, Write as _}; + +use crate::protocol::MAX_CLIPBOARD_IMAGE_PAYLOAD; + +pub(super) const MAX_CLIPBOARD_ALLOCATION: usize = 64 * 1024 * 1024; + +const PNG_SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n"; +const MAX_IMAGE_DIMENSION: u32 = 16_384; +const MAX_IMAGE_PIXELS: usize = 16 * 1024 * 1024; +const BI_RGB: u32 = 0; +const BI_BITFIELDS: u32 = 3; +const BI_ALPHABITFIELDS: u32 = 6; + +pub(super) fn validated_png(bytes: &[u8]) -> Option<Vec<u8>> { + let logical_len = png_logical_len(bytes)?; + if logical_len > MAX_CLIPBOARD_IMAGE_PAYLOAD { + return None; + } + let bytes = &bytes[..logical_len]; + let mut decoder = png::Decoder::new_with_limits( + Cursor::new(bytes), + png::Limits { + bytes: MAX_CLIPBOARD_IMAGE_PAYLOAD, + }, + ); + decoder.set_ignore_text_chunk(true); + decoder.set_ignore_iccp_chunk(true); + let mut reader = decoder.read_info().ok()?; + validate_dimensions(reader.info().width, reader.info().height)?; + if reader.info().animation_control.is_some() { + return None; + } + while reader.next_interlaced_row().ok()?.is_some() {} + reader.finish().ok()?; + Some(bytes.to_vec()) +} + +fn png_logical_len(bytes: &[u8]) -> Option<usize> { + if !bytes.starts_with(PNG_SIGNATURE) { + return None; + } + let mut offset = PNG_SIGNATURE.len(); + while offset < bytes.len() { + let length = usize::try_from(read_u32_be(bytes, offset)?).ok()?; + let kind_start = offset.checked_add(4)?; + let data_start = kind_start.checked_add(4)?; + let data_end = data_start.checked_add(length)?; + let chunk_end = data_end.checked_add(4)?; + if chunk_end > bytes.len() { + return None; + } + if bytes.get(kind_start..data_start)? == b"IEND" { + return (length == 0).then_some(chunk_end); + } + offset = chunk_end; + } + None +} + +pub(super) fn dib_to_png(bytes: &[u8]) -> Option<Vec<u8>> { + Dib::parse(bytes)?.encode_png() +} + +struct Dib<'a> { + width: u32, + height: u32, + top_down: bool, + stride: usize, + pixels: &'a [u8], + format: PixelFormat, +} + +#[derive(Clone, Copy)] +enum PixelFormat { + Bgr24, + Bgrx32, + Masked32 { + red: ChannelMask, + green: ChannelMask, + blue: ChannelMask, + alpha: Option<ChannelMask>, + }, +} + +#[derive(Clone, Copy)] +struct ChannelMask { + mask: u32, + shift: u32, + maximum: u32, +} + +impl ChannelMask { + fn parse(mask: u32) -> Option<Self> { + if mask == 0 { + return None; + } + let shift = mask.trailing_zeros(); + let maximum = mask >> shift; + if maximum & maximum.wrapping_add(1) != 0 { + return None; + } + Some(Self { + mask, + shift, + maximum, + }) + } + + fn extract(self, pixel: u32) -> u8 { + let value = (pixel & self.mask) >> self.shift; + ((u64::from(value) * 255 + u64::from(self.maximum) / 2) / u64::from(self.maximum)) as u8 + } +} + +impl<'a> Dib<'a> { + fn parse(bytes: &'a [u8]) -> Option<Self> { + let header_size = usize::try_from(read_u32_le(bytes, 0)?).ok()?; + if !matches!(header_size, 40 | 52 | 56 | 108 | 124) || bytes.len() < header_size { + return None; + } + let signed_width = read_i32_le(bytes, 4)?; + let signed_height = read_i32_le(bytes, 8)?; + if signed_width <= 0 || signed_height == 0 || signed_height == i32::MIN { + return None; + } + let width = u32::try_from(signed_width).ok()?; + let height = signed_height.unsigned_abs(); + validate_dimensions(width, height)?; + if read_u16_le(bytes, 12)? != 1 { + return None; + } + + let bit_count = read_u16_le(bytes, 14)?; + let compression = read_u32_le(bytes, 16)?; + let colors_used = usize::try_from(read_u32_le(bytes, 32)?).ok()?; + let (format, external_mask_bytes) = + pixel_format(bytes, header_size, bit_count, compression)?; + let palette_bytes = colors_used.checked_mul(4)?; + let pixel_offset = header_size + .checked_add(external_mask_bytes)? + .checked_add(palette_bytes)?; + let row_bits = usize::try_from(width) + .ok()? + .checked_mul(usize::from(bit_count))?; + let stride = row_bits.checked_add(31)?.checked_div(32)?.checked_mul(4)?; + let image_size = stride.checked_mul(usize::try_from(height).ok()?)?; + let pixels = bytes.get(pixel_offset..pixel_offset.checked_add(image_size)?)?; + + Some(Self { + width, + height, + top_down: signed_height < 0, + stride, + pixels, + format, + }) + } + + fn encode_png(&self) -> Option<Vec<u8>> { + let mut output = LimitedWriter::new(MAX_CLIPBOARD_IMAGE_PAYLOAD); + { + let mut encoder = png::Encoder::new(&mut output, self.width, self.height); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().ok()?; + let mut stream = writer.stream_writer().ok()?; + let mut rgba = vec![0_u8; usize::try_from(self.width).ok()?.checked_mul(4)?]; + for output_row in 0..usize::try_from(self.height).ok()? { + let source_row = if self.top_down { + output_row + } else { + usize::try_from(self.height) + .ok()? + .checked_sub(output_row + 1)? + }; + let start = source_row.checked_mul(self.stride)?; + let source = self.pixels.get(start..start.checked_add(self.stride)?)?; + self.decode_row(source, &mut rgba)?; + stream.write_all(&rgba).ok()?; + } + stream.finish().ok()?; + writer.finish().ok()?; + } + Some(output.into_inner()) + } + + fn decode_row(&self, source: &[u8], output: &mut [u8]) -> Option<()> { + match self.format { + PixelFormat::Bgr24 => { + for (pixel, rgba) in source + .chunks_exact(3) + .take(usize::try_from(self.width).ok()?) + .zip(output.chunks_exact_mut(4)) + { + rgba.copy_from_slice(&[pixel[2], pixel[1], pixel[0], 255]); + } + } + PixelFormat::Bgrx32 => { + for (pixel, rgba) in source + .chunks_exact(4) + .take(usize::try_from(self.width).ok()?) + .zip(output.chunks_exact_mut(4)) + { + rgba.copy_from_slice(&[pixel[2], pixel[1], pixel[0], 255]); + } + } + PixelFormat::Masked32 { + red, + green, + blue, + alpha, + } => { + for (pixel, rgba) in source + .chunks_exact(4) + .take(usize::try_from(self.width).ok()?) + .zip(output.chunks_exact_mut(4)) + { + let pixel = u32::from_le_bytes(pixel.try_into().ok()?); + rgba.copy_from_slice(&[ + red.extract(pixel), + green.extract(pixel), + blue.extract(pixel), + alpha.map_or(255, |mask| mask.extract(pixel)), + ]); + } + } + } + Some(()) + } +} + +fn pixel_format( + bytes: &[u8], + header_size: usize, + bit_count: u16, + compression: u32, +) -> Option<(PixelFormat, usize)> { + match (bit_count, compression) { + (24, BI_RGB) => return Some((PixelFormat::Bgr24, 0)), + (32, BI_RGB) => return Some((PixelFormat::Bgrx32, 0)), + (32, BI_BITFIELDS | BI_ALPHABITFIELDS) => {} + _ => return None, + } + + let (offset, external_mask_bytes) = if header_size == 40 { + let count = if compression == BI_ALPHABITFIELDS { + 4 + } else { + 3 + }; + (40, count * 4) + } else { + if header_size < 52 || (compression == BI_ALPHABITFIELDS && header_size < 56) { + return None; + } + (40, 0) + }; + let red = ChannelMask::parse(read_u32_le(bytes, offset)?)?; + let green = ChannelMask::parse(read_u32_le(bytes, offset + 4)?)?; + let blue = ChannelMask::parse(read_u32_le(bytes, offset + 8)?)?; + if red.mask & green.mask != 0 || red.mask & blue.mask != 0 || green.mask & blue.mask != 0 { + return None; + } + let alpha = if compression == BI_ALPHABITFIELDS || header_size >= 56 { + match read_u32_le(bytes, offset + 12).filter(|mask| *mask != 0) { + Some(mask) => Some(ChannelMask::parse(mask)?), + None => None, + } + } else { + None + }; + if alpha.is_some_and(|alpha| alpha.mask & (red.mask | green.mask | blue.mask) != 0) { + return None; + } + Some(( + PixelFormat::Masked32 { + red, + green, + blue, + alpha, + }, + external_mask_bytes, + )) +} + +fn validate_dimensions(width: u32, height: u32) -> Option<()> { + if width == 0 || height == 0 || width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION { + return None; + } + let pixels = usize::try_from(width) + .ok()? + .checked_mul(usize::try_from(height).ok()?)?; + (pixels <= MAX_IMAGE_PIXELS).then_some(()) +} + +struct LimitedWriter { + bytes: Vec<u8>, + limit: usize, +} + +impl LimitedWriter { + fn new(limit: usize) -> Self { + Self { + bytes: Vec::new(), + limit, + } + } + + fn into_inner(self) -> Vec<u8> { + self.bytes + } +} + +impl io::Write for LimitedWriter { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + if bytes.len() > self.limit.saturating_sub(self.bytes.len()) { + return Err(io::Error::new( + io::ErrorKind::FileTooLarge, + "encoded clipboard image exceeds protocol limit", + )); + } + self.bytes.extend_from_slice(bytes); + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn read_u16_le(bytes: &[u8], offset: usize) -> Option<u16> { + Some(u16::from_le_bytes( + bytes.get(offset..offset + 2)?.try_into().ok()?, + )) +} + +fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> { + Some(u32::from_le_bytes( + bytes.get(offset..offset + 4)?.try_into().ok()?, + )) +} + +fn read_i32_le(bytes: &[u8], offset: usize) -> Option<i32> { + Some(i32::from_le_bytes( + bytes.get(offset..offset + 4)?.try_into().ok()?, + )) +} + +fn read_u32_be(bytes: &[u8], offset: usize) -> Option<u32> { + Some(u32::from_be_bytes( + bytes.get(offset..offset + 4)?.try_into().ok()?, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn info_header(width: i32, height: i32, bit_count: u16, compression: u32) -> Vec<u8> { + let mut header = vec![0_u8; 40]; + header[0..4].copy_from_slice(&40_u32.to_le_bytes()); + header[4..8].copy_from_slice(&width.to_le_bytes()); + header[8..12].copy_from_slice(&height.to_le_bytes()); + header[12..14].copy_from_slice(&1_u16.to_le_bytes()); + header[14..16].copy_from_slice(&bit_count.to_le_bytes()); + header[16..20].copy_from_slice(&compression.to_le_bytes()); + header + } + + fn decode_rgba(bytes: &[u8]) -> Vec<u8> { + let mut decoder = png::Decoder::new(Cursor::new(bytes)); + decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16); + let mut reader = decoder.read_info().unwrap(); + let mut output = vec![0_u8; reader.output_buffer_size()]; + let info = reader.next_frame(&mut output).unwrap(); + output.truncate(info.buffer_size()); + output + } + + #[test] + fn converts_bottom_up_24_bit_dib_to_png() { + let mut dib = info_header(2, 2, 24, BI_RGB); + dib.extend_from_slice(&[ + 255, 0, 0, 255, 255, 255, 0, 0, // bottom: blue, white, padding + 0, 0, 255, 0, 255, 0, 0, 0, // top: red, green, padding + ]); + + let png = dib_to_png(&dib).unwrap(); + assert_eq!( + decode_rgba(&png), + vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,] + ); + } + + #[test] + fn strips_registered_png_allocator_tail() { + let mut bytes = Vec::new(); + let mut encoder = png::Encoder::new(&mut bytes, 1, 1); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().unwrap(); + writer.write_image_data(&[1, 2, 3, 255]).unwrap(); + writer.finish().unwrap(); + let expected = bytes.clone(); + bytes.extend_from_slice(&[0; 32]); + + assert_eq!(validated_png(&bytes), Some(expected)); + } +} |
