Skip to main content

ferroui_browser/storage/
blob_readable_stream.rs

1use crate::interop::{stream_helper, JsObject};
2use std::io::{self, Read, Seek, SeekFrom};
3
4/// A read stream over a `Blob` of the page.
5///
6/// The page reads a blob only asynchronously: [`read_async`](Self::read_async)
7/// reads a slice of it as upstream's `ReadAsync` does. The synchronous
8/// [`Read`] the storage contracts hand out is served from the content that
9/// [`load_async`](Self::load_async) brought into memory; before that it
10/// fails, as upstream's `Read` does.
11pub struct BlobReadableStream {
12    js_reference: Option<JsObject>,
13    position: u64,
14    length: u64,
15    content: Option<Vec<u8>>,
16}
17
18fn disposed() -> io::Error {
19    io::Error::other("Cannot access a disposed object. Object name: 'BlobReadableStream'.")
20}
21
22/// Upstream's `EndOfStreamException`.
23fn end_of_stream() -> io::Error {
24    io::Error::new(io::ErrorKind::UnexpectedEof, "Failed to read the requested number of bytes from the stream.")
25}
26
27impl BlobReadableStream {
28    /// The stream over the blob `js_stream_reference`.
29    pub fn new(js_stream_reference: JsObject) -> Self {
30        let length = stream_helper::byte_length(&js_stream_reference);
31        Self::with_length(js_stream_reference, length)
32    }
33
34    fn with_length(js_stream_reference: JsObject, length: u64) -> Self {
35        Self { js_reference: Some(js_stream_reference), position: 0, length, content: None }
36    }
37
38    fn js_reference(&self) -> io::Result<&JsObject> {
39        self.js_reference.as_ref().ok_or_else(disposed)
40    }
41
42    pub fn can_read(&self) -> bool {
43        true
44    }
45
46    pub fn can_seek(&self) -> bool {
47        false
48    }
49
50    pub fn can_write(&self) -> bool {
51        false
52    }
53
54    /// The size of the blob in bytes.
55    pub fn length(&self) -> u64 {
56        self.length
57    }
58
59    pub fn position(&self) -> u64 {
60        self.position
61    }
62
63    /// Reads up to `buffer.len()` bytes at the position into `buffer` and
64    /// returns how many were read; `0` at the end of the blob.
65    pub async fn read_async(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
66        let num_bytes_to_read = (buffer.len() as u64).min(self.length.saturating_sub(self.position)) as usize;
67        let js_reference = self.js_reference()?.clone();
68        let bytes_read = stream_helper::slice_async(&js_reference, self.position, num_bytes_to_read)
69            .await
70            .map_err(io::Error::from)?;
71        self.complete_read(&bytes_read, num_bytes_to_read, buffer)
72    }
73
74    fn complete_read(&mut self, bytes_read: &[u8], num_bytes_to_read: usize, buffer: &mut [u8]) -> io::Result<usize> {
75        if bytes_read.len() != num_bytes_to_read {
76            return Err(end_of_stream());
77        }
78
79        self.position += bytes_read.len() as u64;
80        buffer[..bytes_read.len()].copy_from_slice(bytes_read);
81
82        Ok(bytes_read.len())
83    }
84
85    /// Reads the whole blob into memory, after which the stream can be read
86    /// synchronously through [`Read`]. Fails as [`read_async`](Self::read_async)
87    /// does when the blob yields fewer bytes than its length.
88    pub async fn load_async(&mut self) -> io::Result<()> {
89        if self.content.is_some() {
90            return Ok(());
91        }
92        let js_reference = self.js_reference()?.clone();
93        let content = stream_helper::slice_async(&js_reference, 0, self.length as usize).await?;
94        self.set_content(content)
95    }
96
97    fn set_content(&mut self, content: Vec<u8>) -> io::Result<()> {
98        if content.len() as u64 != self.length {
99            return Err(end_of_stream());
100        }
101        self.content = Some(content);
102        Ok(())
103    }
104
105    /// Releases the blob.
106    pub fn dispose(&mut self) {
107        self.js_reference = None;
108        self.content = None;
109    }
110}
111
112impl Read for BlobReadableStream {
113    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
114        self.js_reference()?;
115        let Some(content) = &self.content else {
116            return Err(io::Error::other("Browser supports only ReadAsync"));
117        };
118        let start = self.position.min(content.len() as u64) as usize;
119        let count = buffer.len().min(content.len() - start);
120        buffer[..count].copy_from_slice(&content[start..start + count]);
121        self.position += count as u64;
122        Ok(count)
123    }
124}
125
126impl Seek for BlobReadableStream {
127    fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
128        let position = match position {
129            SeekFrom::Current(offset) => self.position as i64 + offset,
130            SeekFrom::End(offset) => self.length as i64 + offset,
131            SeekFrom::Start(offset) => offset as i64,
132        };
133        if position < 0 {
134            return Err(io::Error::new(
135                io::ErrorKind::InvalidInput,
136                "An attempt was made to move the position before the beginning of the stream.",
137            ));
138        }
139        self.position = position as u64;
140        Ok(self.position)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    // Not from upstream (upstream has no tests of the browser streams): the parts that do not
147    // need a page.
148    use super::*;
149
150    fn stream(content: Option<&[u8]>, length: u64) -> BlobReadableStream {
151        let mut stream = BlobReadableStream::with_length(JsObject::NULL, length);
152        if let Some(content) = content {
153            stream.set_content(content.to_vec()).unwrap();
154        }
155        stream
156    }
157
158    #[test]
159    fn read_before_load_fails_as_upstream_read_does() {
160        let mut stream = stream(None, 4);
161        let error = stream.read(&mut [0; 4]).unwrap_err();
162        assert_eq!(error.to_string(), "Browser supports only ReadAsync");
163    }
164
165    #[test]
166    fn read_after_load_returns_the_content_and_advances() {
167        let mut stream = stream(Some(b"hello"), 5);
168        let mut buffer = [0u8; 3];
169        assert_eq!(stream.read(&mut buffer).unwrap(), 3);
170        assert_eq!(&buffer, b"hel");
171        assert_eq!(stream.position(), 3);
172        let mut rest = Vec::new();
173        stream.read_to_end(&mut rest).unwrap();
174        assert_eq!(rest, b"lo");
175        assert_eq!(stream.read(&mut buffer).unwrap(), 0);
176    }
177
178    #[test]
179    fn seek_moves_relative_to_each_origin() {
180        let mut stream = stream(Some(b"0123456789"), 10);
181        assert_eq!(stream.seek(SeekFrom::Start(4)).unwrap(), 4);
182        assert_eq!(stream.seek(SeekFrom::Current(2)).unwrap(), 6);
183        assert_eq!(stream.seek(SeekFrom::End(-1)).unwrap(), 9);
184        let mut buffer = [0u8; 4];
185        assert_eq!(stream.read(&mut buffer).unwrap(), 1);
186        assert_eq!(buffer[0], b'9');
187        assert!(stream.seek(SeekFrom::Current(-20)).is_err());
188    }
189
190    #[test]
191    fn short_slice_is_an_end_of_stream_error() {
192        let mut stream = stream(None, 10);
193        let mut buffer = [0u8; 4];
194        let error = stream.complete_read(b"ab", 4, &mut buffer).unwrap_err();
195        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
196        assert_eq!(stream.complete_read(b"abcd", 4, &mut buffer).unwrap(), 4);
197        assert_eq!(stream.position(), 4);
198    }
199
200    #[test]
201    fn content_shorter_than_the_blob_is_an_end_of_stream_error() {
202        let mut stream = stream(None, 10);
203        let error = stream.set_content(b"abc".to_vec()).unwrap_err();
204        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
205        assert_eq!(stream.length(), 10);
206        assert!(stream.read(&mut [0; 1]).is_err(), "nothing was loaded");
207    }
208
209    #[test]
210    fn disposed_stream_fails_to_read() {
211        let mut stream = stream(Some(b"abc"), 3);
212        stream.dispose();
213        assert!(stream.read(&mut [0; 1]).is_err());
214    }
215}