Skip to main content

webui_desktop/
response_content.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4use std::fs::File;
5use std::io::{self, Read, Seek, SeekFrom};
6
7use crate::DesktopResponseBody;
8
9/// Owned protocol content. Files retain an open handle and a fixed delivery
10/// length; native adapters must not reopen their paths or eagerly buffer them.
11#[derive(Debug)]
12pub enum DesktopResponseContent {
13    /// Bytes, including any IPC response-lifetime reservation.
14    Bytes(DesktopResponseBody),
15    /// An already-opened, length-bounded file.
16    File(DesktopResponseFile),
17}
18
19/// A file response bounded to the length observed when it was opened.
20/// Growth is ignored; premature EOF is an explicit I/O error.
21#[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    /// Return the fixed response length.
38    #[must_use]
39    pub fn len(&self) -> u64 {
40        self.length
41    }
42
43    /// Whether the response has no bytes.
44    #[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    /// Borrow buffered bytes. File responses return `None` without performing I/O.
86    #[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    /// Materialize content explicitly for a non-native consumer.
95    ///
96    /// # Errors
97    /// Returns an I/O error if reading a file fails or its contents were truncated.
98    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}