diff options
Diffstat (limited to 'src/platform/mod.rs')
| -rw-r--r-- | src/platform/mod.rs | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 00fa8833..8cf2b08d 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -36,6 +36,50 @@ pub struct ClipboardImage { pub extension: &'static str, } +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum LimitedRead { + Empty, + Complete(Vec<u8>), + Oversized, +} + +pub(crate) fn read_limited_reader( + mut reader: impl std::io::Read, + max_bytes: usize, +) -> std::io::Result<LimitedRead> { + let mut bytes = Vec::new(); + let mut buffer = [0_u8; 8192]; + + while bytes.len() < max_bytes { + let remaining = max_bytes - bytes.len(); + let read_len = remaining.min(buffer.len()); + let bytes_read = match reader.read(&mut buffer[..read_len]) { + Ok(bytes_read) => bytes_read, + Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue, + Err(err) => return Err(err), + }; + if bytes_read == 0 { + return if bytes.is_empty() { + Ok(LimitedRead::Empty) + } else { + Ok(LimitedRead::Complete(bytes)) + }; + } + bytes.extend_from_slice(&buffer[..bytes_read]); + } + + let mut sentinel = [0_u8; 1]; + loop { + return match reader.read(&mut sentinel) { + Ok(0) if bytes.is_empty() => Ok(LimitedRead::Empty), + Ok(0) => Ok(LimitedRead::Complete(bytes)), + Ok(_) => Ok(LimitedRead::Oversized), + Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue, + Err(err) => Err(err), + }; + } +} + #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] @@ -50,3 +94,71 @@ pub use macos::*; mod fallback; #[cfg(not(any(target_os = "linux", target_os = "macos")))] pub use fallback::*; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_limited_reader_returns_complete_data_under_limit() { + let input = std::io::Cursor::new(b"image".to_vec()); + assert_eq!( + read_limited_reader(input, 16).expect("limited read"), + LimitedRead::Complete(b"image".to_vec()) + ); + } + + #[test] + fn read_limited_reader_returns_empty_for_empty_input() { + let input = std::io::Cursor::new(Vec::<u8>::new()); + assert_eq!( + read_limited_reader(input, 16).expect("limited read"), + LimitedRead::Empty + ); + } + + #[test] + fn read_limited_reader_accepts_data_exactly_at_limit() { + let input = std::io::Cursor::new(b"four".to_vec()); + assert_eq!( + read_limited_reader(input, 4).expect("limited read"), + LimitedRead::Complete(b"four".to_vec()) + ); + } + + #[test] + fn read_limited_reader_rejects_data_over_limit() { + let input = std::io::Cursor::new(b"oversized".to_vec()); + assert_eq!( + read_limited_reader(input, 4).expect("limited read"), + LimitedRead::Oversized + ); + } + + #[test] + fn read_limited_reader_retries_interrupted_reads() { + struct InterruptedOnce { + interrupted: bool, + inner: std::io::Cursor<Vec<u8>>, + } + + impl std::io::Read for InterruptedOnce { + fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> { + if !self.interrupted { + self.interrupted = true; + return Err(std::io::ErrorKind::Interrupted.into()); + } + self.inner.read(buffer) + } + } + + let input = InterruptedOnce { + interrupted: false, + inner: std::io::Cursor::new(b"image".to_vec()), + }; + assert_eq!( + read_limited_reader(input, 16).expect("limited read"), + LimitedRead::Complete(b"image".to_vec()) + ); + } +} |
