webui_desktop/
response_content.rs1use std::fs::File;
5use std::io::{self, Read, Seek, SeekFrom};
6
7use crate::DesktopResponseBody;
8
9#[derive(Debug)]
12pub enum DesktopResponseContent {
13 Bytes(DesktopResponseBody),
15 File(DesktopResponseFile),
17}
18
19#[derive(Debug)]
22pub struct DesktopResponseFile {
23 file: File,
24 length: u64,
25 position: u64,
26}
27
28impl DesktopResponseFile {
29 pub(crate) fn new(file: File, length: u64) -> Self {
30 Self {
31 file,
32 length,
33 position: 0,
34 }
35 }
36
37 #[must_use]
39 pub fn len(&self) -> u64 {
40 self.length
41 }
42
43 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 self.length == 0
47 }
48}
49
50impl Read for DesktopResponseFile {
51 fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
52 let remaining = self.length.saturating_sub(self.position);
53 let count = buffer
54 .len()
55 .min(usize::try_from(remaining).unwrap_or(usize::MAX));
56 if count == 0 {
57 return Ok(0);
58 }
59 let read = self.file.read(&mut buffer[..count])?;
60 if read == 0 {
61 return Err(io::Error::new(
62 io::ErrorKind::UnexpectedEof,
63 "desktop asset was truncated during delivery",
64 ));
65 }
66 self.position += read as u64;
67 Ok(read)
68 }
69}
70
71impl Seek for DesktopResponseFile {
72 fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
73 let target = match position {
74 SeekFrom::Start(value) => Some(value),
75 SeekFrom::Current(delta) => self.position.checked_add_signed(delta),
76 SeekFrom::End(delta) => self.length.checked_add_signed(delta),
77 }
78 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid desktop asset seek"))?;
79 self.position = self.file.seek(SeekFrom::Start(target))?;
80 Ok(self.position)
81 }
82}
83
84impl DesktopResponseContent {
85 #[must_use]
87 pub fn as_bytes(&self) -> Option<&DesktopResponseBody> {
88 match self {
89 Self::Bytes(bytes) => Some(bytes),
90 Self::File(_) => None,
91 }
92 }
93
94 pub fn into_bytes(self) -> io::Result<DesktopResponseBody> {
99 match self {
100 Self::Bytes(bytes) => Ok(bytes),
101 Self::File(mut file) => {
102 let mut bytes = Vec::new();
103 file.read_to_end(&mut bytes)?;
104 Ok(bytes.into())
105 }
106 }
107 }
108}
109
110impl From<Vec<u8>> for DesktopResponseContent {
111 fn from(bytes: Vec<u8>) -> Self {
112 Self::Bytes(bytes.into())
113 }
114}
115
116impl From<DesktopResponseBody> for DesktopResponseContent {
117 fn from(bytes: DesktopResponseBody) -> Self {
118 Self::Bytes(bytes)
119 }
120}
121
122impl<T: AsRef<[u8]> + ?Sized> PartialEq<T> for DesktopResponseContent {
123 fn eq(&self, other: &T) -> bool {
124 self.as_bytes()
125 .is_some_and(|bytes| bytes.as_slice() == other.as_ref())
126 }
127}
128
129#[cfg(test)]
130#[allow(clippy::disallowed_methods)]
131mod tests {
132 use super::*;
133 use std::io::Write;
134
135 #[test]
136 fn file_is_lazy_length_bounded_and_owns_its_handle() {
137 let mut source = tempfile::tempfile().unwrap();
138 source.write_all(b"abcdef").unwrap();
139 source.rewind().unwrap();
140 let body = DesktopResponseContent::File(DesktopResponseFile::new(source, 3));
141 assert!(body.as_bytes().is_none());
142 assert_eq!(body.into_bytes().unwrap(), b"abc");
143 }
144
145 #[test]
146 fn file_truncation_is_an_error_not_successful_short_delivery() {
147 let mut file = DesktopResponseFile::new(tempfile::tempfile().unwrap(), 3);
148 assert_eq!(
149 file.read(&mut [0; 3]).unwrap_err().kind(),
150 io::ErrorKind::UnexpectedEof
151 );
152 }
153
154 #[test]
155 fn file_seek_and_empty_contract() {
156 let mut file = DesktopResponseFile::new(tempfile::tempfile().unwrap(), 0);
157 assert!(file.is_empty());
158 assert_eq!(file.len(), 0);
159 assert!(file.seek(SeekFrom::Current(-1)).is_err());
160 assert_eq!(file.seek(SeekFrom::End(0)).unwrap(), 0);
161 assert_eq!(file.read(&mut [0; 1]).unwrap(), 0);
162 }
163}