Skip to main content

gibblox_web_file/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3extern crate alloc;
4
5#[cfg(target_arch = "wasm32")]
6mod wasm {
7    use alloc::{boxed::Box, format, string::String};
8    use async_trait::async_trait;
9    use core::{
10        fmt,
11        future::Future,
12        pin::Pin,
13        task::{Context, Poll},
14    };
15    use gibblox_core::{
16        BlockByteReader, BlockReader, BlockReaderConfigIdentity, ByteReader, GibbloxError,
17        GibbloxErrorKind, GibbloxResult, ReadContext,
18    };
19    use js_sys::{Promise, Uint8Array};
20    use tracing::{debug, trace};
21    use wasm_bindgen::JsValue;
22    use wasm_bindgen_futures::JsFuture;
23    use web_sys::{Blob, File};
24
25    const JS_SAFE_INTEGER_MAX: u64 = 9_007_199_254_740_991;
26
27    struct SendJsFuture(JsFuture);
28
29    unsafe impl Send for SendJsFuture {}
30
31    impl From<Promise> for SendJsFuture {
32        fn from(promise: Promise) -> Self {
33            Self(JsFuture::from(promise))
34        }
35    }
36
37    impl Future for SendJsFuture {
38        type Output = Result<JsValue, JsValue>;
39
40        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
41            Pin::new(&mut self.0).poll(cx)
42        }
43    }
44
45    #[derive(Clone)]
46    struct SendFile(File);
47
48    unsafe impl Send for SendFile {}
49    unsafe impl Sync for SendFile {}
50
51    impl SendFile {
52        fn as_blob(&self) -> &Blob {
53            self.0.as_ref()
54        }
55
56        fn name(&self) -> String {
57            self.0.name()
58        }
59
60        fn last_modified(&self) -> f64 {
61            self.0.last_modified()
62        }
63    }
64
65    #[derive(Clone, Debug)]
66    pub struct WebFileReaderConfig {
67        pub block_size: u32,
68        pub file_name: String,
69        pub size_bytes: u64,
70        pub last_modified: i64,
71    }
72
73    impl WebFileReaderConfig {
74        fn from_send_file(file: &SendFile, block_size: u32) -> GibbloxResult<Self> {
75            if block_size == 0 || !block_size.is_power_of_two() {
76                return Err(GibbloxError::with_message(
77                    GibbloxErrorKind::InvalidInput,
78                    "block size must be non-zero power of two",
79                ));
80            }
81
82            Ok(Self {
83                block_size,
84                file_name: file.name(),
85                size_bytes: f64_to_u64(file.as_blob().size(), "file size")?,
86                last_modified: f64_to_i64(file.last_modified(), "file last_modified")?,
87            })
88        }
89
90        pub fn from_file(file: &File, block_size: u32) -> GibbloxResult<Self> {
91            Self::from_send_file(&SendFile(file.clone()), block_size)
92        }
93    }
94
95    impl BlockReaderConfigIdentity for WebFileReaderConfig {
96        fn write_identity(&self, out: &mut dyn fmt::Write) -> fmt::Result {
97            write!(
98                out,
99                "web-file:{}:{}:{}",
100                self.file_name, self.size_bytes, self.last_modified
101            )
102        }
103    }
104
105    pub struct WebFileReader {
106        file: SendFile,
107        config: WebFileReaderConfig,
108    }
109
110    impl WebFileReader {
111        pub fn new(file: File, block_size: u32) -> GibbloxResult<Self> {
112            let file = SendFile(file);
113            let config = WebFileReaderConfig::from_send_file(&file, block_size)?;
114            debug!(
115                name = %config.file_name,
116                size_bytes = config.size_bytes,
117                last_modified = config.last_modified,
118                block_size = config.block_size,
119                "opening web file-backed source"
120            );
121
122            Ok(Self { file, config })
123        }
124
125        pub fn config(&self) -> &WebFileReaderConfig {
126            &self.config
127        }
128
129        pub fn size_bytes(&self) -> u64 {
130            self.config.size_bytes
131        }
132    }
133
134    #[async_trait]
135    impl ByteReader for WebFileReader {
136        async fn size_bytes(&self) -> GibbloxResult<u64> {
137            Ok(self.config.size_bytes)
138        }
139
140        fn write_identity(&self, out: &mut dyn fmt::Write) -> fmt::Result {
141            self.config.write_identity(out)
142        }
143
144        async fn read_at(
145            &self,
146            offset: u64,
147            buf: &mut [u8],
148            _ctx: ReadContext,
149        ) -> GibbloxResult<usize> {
150            if buf.is_empty() {
151                return Ok(0);
152            }
153            if offset >= self.config.size_bytes {
154                return Ok(0);
155            }
156
157            let remaining = self.config.size_bytes.checked_sub(offset).ok_or_else(|| {
158                GibbloxError::with_message(GibbloxErrorKind::OutOfRange, "offset out of range")
159            })?;
160            let read_len = (buf.len() as u64).min(remaining) as usize;
161            let end = offset.checked_add(read_len as u64).ok_or_else(|| {
162                GibbloxError::with_message(GibbloxErrorKind::OutOfRange, "read range overflow")
163            })?;
164
165            let promise = self
166                .file
167                .as_blob()
168                .slice_with_f64_and_f64(
169                    to_js_number(offset, "read start")?,
170                    to_js_number(end, "read end")?,
171                )
172                .map_err(js_io("slice file"))?
173                .array_buffer();
174            let buffer = SendJsFuture::from(promise)
175                .await
176                .map_err(js_io("await file read"))?;
177            let bytes = Uint8Array::new(&buffer);
178
179            let available = bytes.length() as usize;
180            if available < read_len {
181                return Err(GibbloxError::with_message(
182                    GibbloxErrorKind::Io,
183                    format!(
184                        "short read while reading web file slice: expected {read_len}, got {available}"
185                    ),
186                ));
187            }
188            bytes
189                .subarray(0, read_len as u32)
190                .copy_to(&mut buf[..read_len]);
191
192            trace!(offset, requested = read_len, "performed web file byte read");
193            Ok(read_len)
194        }
195    }
196
197    #[async_trait]
198    impl BlockReader for WebFileReader {
199        fn block_size(&self) -> u32 {
200            self.config.block_size
201        }
202
203        async fn total_blocks(&self) -> GibbloxResult<u64> {
204            Ok(self
205                .config
206                .size_bytes
207                .div_ceil(self.config.block_size as u64))
208        }
209
210        fn write_identity(&self, out: &mut dyn fmt::Write) -> fmt::Result {
211            self.config.write_identity(out)
212        }
213
214        async fn read_blocks(
215            &self,
216            lba: u64,
217            buf: &mut [u8],
218            ctx: ReadContext,
219        ) -> GibbloxResult<usize> {
220            let adapter = BlockByteReader::new(self, self.config.block_size)?;
221            let read = adapter.read_blocks(lba, buf, ctx).await?;
222            trace!(
223                lba,
224                requested = buf.len(),
225                read,
226                "performed web file block read"
227            );
228            Ok(read)
229        }
230    }
231
232    fn to_js_number(value: u64, label: &'static str) -> GibbloxResult<f64> {
233        if value > JS_SAFE_INTEGER_MAX {
234            return Err(GibbloxError::with_message(
235                GibbloxErrorKind::OutOfRange,
236                format!("{label} exceeds JavaScript safe integer"),
237            ));
238        }
239        Ok(value as f64)
240    }
241
242    fn f64_to_u64(value: f64, label: &'static str) -> GibbloxResult<u64> {
243        if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
244            return Err(GibbloxError::with_message(
245                GibbloxErrorKind::InvalidInput,
246                format!("invalid {label} from web file"),
247            ));
248        }
249        to_js_number(value as u64, label).map(|_| value as u64)
250    }
251
252    fn f64_to_i64(value: f64, label: &'static str) -> GibbloxResult<i64> {
253        if !value.is_finite() || value.fract() != 0.0 {
254            return Err(GibbloxError::with_message(
255                GibbloxErrorKind::InvalidInput,
256                format!("invalid {label} from web file"),
257            ));
258        }
259        if value.abs() > JS_SAFE_INTEGER_MAX as f64
260            || value < i64::MIN as f64
261            || value > i64::MAX as f64
262        {
263            return Err(GibbloxError::with_message(
264                GibbloxErrorKind::OutOfRange,
265                format!("{label} exceeds supported range"),
266            ));
267        }
268        Ok(value as i64)
269    }
270
271    fn js_io(op: &'static str) -> impl FnOnce(JsValue) -> GibbloxError {
272        move |err| {
273            GibbloxError::with_message(
274                GibbloxErrorKind::Io,
275                format!("{op}: {}", js_value_to_string(err)),
276            )
277        }
278    }
279
280    fn js_value_to_string(value: JsValue) -> String {
281        js_sys::JSON::stringify(&value)
282            .ok()
283            .and_then(|s| s.as_string())
284            .unwrap_or_else(|| format!("{value:?}"))
285    }
286}
287
288#[cfg(target_arch = "wasm32")]
289pub use wasm::*;