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}