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::*;