1use crate::Sealed;
2use std::{
3 ops::Deref,
4 time::{Duration, SystemTime, UNIX_EPOCH},
5};
6
7use wasm_bindgen::{prelude::*, throw_str, JsCast};
8
9pub trait BlobContents: Sealed {
15 unsafe fn into_jsvalue(self) -> JsValue;
20}
21
22impl<'a> Sealed for &'a str {}
23impl<'a> BlobContents for &'a str {
24 unsafe fn into_jsvalue(self) -> JsValue {
25 self.as_bytes().into_jsvalue()
30 }
31}
32
33impl<'a> Sealed for &'a [u8] {}
34impl<'a> BlobContents for &'a [u8] {
35 unsafe fn into_jsvalue(self) -> JsValue {
36 js_sys::Uint8Array::view(self).into()
37 }
38}
39
40impl Sealed for js_sys::ArrayBuffer {}
41impl BlobContents for js_sys::ArrayBuffer {
42 unsafe fn into_jsvalue(self) -> JsValue {
43 self.into()
44 }
45}
46
47impl Sealed for js_sys::JsString {}
48impl BlobContents for js_sys::JsString {
49 unsafe fn into_jsvalue(self) -> JsValue {
50 self.into()
51 }
52}
53
54impl Sealed for Blob {}
55impl BlobContents for Blob {
56 unsafe fn into_jsvalue(self) -> JsValue {
57 self.into()
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct Blob {
67 inner: web_sys::Blob,
68}
69
70impl Blob {
71 pub fn new<T>(content: T) -> Blob
73 where
74 T: BlobContents,
75 {
76 Blob::new_with_options(content, None)
77 }
78
79 pub fn new_with_options<T>(content: T, mime_type: Option<&str>) -> Blob
82 where
83 T: BlobContents,
84 {
85 let mut properties = web_sys::BlobPropertyBag::new();
86 if let Some(mime_type) = mime_type {
87 properties.type_(mime_type);
88 }
89
90 let parts = js_sys::Array::of1(&unsafe { content.into_jsvalue() });
93 let inner = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &properties);
94
95 Blob::from(inner.unwrap_throw())
96 }
97
98 pub fn slice(&self, start: u64, end: u64) -> Self {
99 let start = safe_u64_to_f64(start);
100 let end = safe_u64_to_f64(end);
101
102 let b: &web_sys::Blob = self.as_ref();
103 Blob::from(b.slice_with_f64_and_f64(start, end).unwrap_throw())
104 }
105
106 pub fn size(&self) -> u64 {
108 safe_f64_to_u64(self.inner.size())
109 }
110
111 #[cfg(feature = "mime")]
114 pub fn mime_type(&self) -> Result<mime::Mime, mime::FromStrError> {
115 self.raw_mime_type().parse()
116 }
117
118 pub fn raw_mime_type(&self) -> String {
121 self.inner.type_()
122 }
123}
124
125impl From<web_sys::Blob> for Blob {
126 fn from(blob: web_sys::Blob) -> Self {
127 Blob { inner: blob }
128 }
129}
130
131impl From<web_sys::File> for Blob {
132 fn from(file: web_sys::File) -> Self {
133 Blob { inner: file.into() }
134 }
135}
136
137impl From<Blob> for web_sys::Blob {
138 fn from(blob: Blob) -> Self {
139 blob.inner
140 }
141}
142
143impl From<Blob> for JsValue {
144 fn from(blob: Blob) -> Self {
145 blob.inner.into()
146 }
147}
148
149impl AsRef<web_sys::Blob> for Blob {
150 fn as_ref(&self) -> &web_sys::Blob {
151 self.inner.as_ref()
152 }
153}
154
155impl AsRef<JsValue> for Blob {
156 fn as_ref(&self) -> &JsValue {
157 self.inner.as_ref()
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct File {
164 inner: Blob,
167}
168
169impl File {
170 pub fn new<T>(name: &str, contents: T) -> File
174 where
175 T: BlobContents,
176 {
177 Self::new_with_options(name, contents, None, None)
178 }
179
180 pub fn new_with_options<T>(
204 name: &str,
205 contents: T,
206 mime_type: Option<&str>,
207 last_modified_time: Option<SystemTime>,
208 ) -> File
209 where
210 T: BlobContents,
211 {
212 let mut options = web_sys::FilePropertyBag::new();
213 if let Some(mime_type) = mime_type {
214 options.type_(mime_type);
215 }
216
217 if let Some(last_modified_time) = last_modified_time {
218 let duration = match last_modified_time.duration_since(UNIX_EPOCH) {
219 Ok(duration) => safe_u128_to_f64(duration.as_millis()),
220 Err(time_err) => -safe_u128_to_f64(time_err.duration().as_millis()),
221 };
222 options.last_modified(duration);
223 }
224
225 let parts = js_sys::Array::of1(&unsafe { contents.into_jsvalue() });
228 let inner = web_sys::File::new_with_u8_array_sequence_and_options(&parts, name, &options)
229 .unwrap_throw();
230
231 File::from(inner)
232 }
233
234 pub fn name(&self) -> String {
236 let f: &web_sys::File = self.as_ref();
237 f.name()
238 }
239
240 pub fn last_modified_time(&self) -> SystemTime {
255 let f: &web_sys::File = self.as_ref();
256 match f.last_modified() {
257 pos if pos >= 0.0 => UNIX_EPOCH + Duration::from_millis(safe_f64_to_u64(pos)),
258 neg => UNIX_EPOCH - Duration::from_millis(safe_f64_to_u64(-neg)),
259 }
260 }
261
262 pub fn slice(&self, start: u64, end: u64) -> Self {
264 let blob = self.deref().slice(start, end);
265
266 let raw_mime_type = self.raw_mime_type();
267 let mime_type = if raw_mime_type.is_empty() {
268 None
269 } else {
270 Some(raw_mime_type)
271 };
272
273 File::new_with_options(
274 &self.name(),
275 blob,
276 mime_type.as_deref(),
277 Some(self.last_modified_time()),
278 )
279 }
280}
281
282impl From<web_sys::File> for File {
283 fn from(file: web_sys::File) -> Self {
284 File {
285 inner: Blob::from(web_sys::Blob::from(file)),
286 }
287 }
288}
289
290impl Deref for File {
291 type Target = Blob;
292
293 fn deref(&self) -> &Self::Target {
294 &self.inner
295 }
296}
297
298impl AsRef<web_sys::File> for File {
299 fn as_ref(&self) -> &web_sys::File {
300 <Blob as AsRef<web_sys::Blob>>::as_ref(&self.inner).unchecked_ref()
301 }
302}
303
304impl AsRef<web_sys::Blob> for File {
305 fn as_ref(&self) -> &web_sys::Blob {
306 self.inner.as_ref()
307 }
308}
309
310impl From<File> for Blob {
311 fn from(file: File) -> Self {
312 file.inner
313 }
314}
315
316fn safe_u64_to_f64(number: u64) -> f64 {
323 if number > (js_sys::Number::MAX_SAFE_INTEGER as u64) {
325 throw_str("a rust number was too large and could not be represented in JavaScript");
326 }
327 number as f64
328}
329
330fn safe_u128_to_f64(number: u128) -> f64 {
331 const MAX_SAFE_INTEGER: u128 = js_sys::Number::MAX_SAFE_INTEGER as u128; if number > MAX_SAFE_INTEGER {
334 throw_str("a rust number was too large and could not be represented in JavaScript");
335 }
336 number as f64
337}
338
339fn safe_f64_to_u64(number: f64) -> u64 {
341 if number > js_sys::Number::MAX_SAFE_INTEGER {
343 throw_str("a rust number was too large and could not be represented in JavaScript");
344 }
345
346 if number.fract() != 0.0 {
347 throw_str(
348 "a number could not be converted to an integer because it was not a whole number",
349 );
350 }
351 number as u64
352}