aboutsummaryrefslogtreecommitdiff
path: root/src/platform/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform/mod.rs')
-rw-r--r--src/platform/mod.rs112
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())
+ );
+ }
+}