script/dom/
blob.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashMap;
6use std::ptr;
7use std::rc::Rc;
8
9use base::id::{BlobId, BlobIndex};
10use constellation_traits::{BlobData, BlobImpl};
11use dom_struct::dom_struct;
12use encoding_rs::UTF_8;
13use js::jsapi::JSObject;
14use js::rust::HandleObject;
15use js::typedarray::{ArrayBufferU8, Uint8};
16use net_traits::filemanager_thread::RelativePos;
17use uuid::Uuid;
18
19use crate::dom::bindings::buffer_source::create_buffer_source;
20use crate::dom::bindings::codegen::Bindings::BlobBinding;
21use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods;
22use crate::dom::bindings::codegen::UnionTypes::ArrayBufferOrArrayBufferViewOrBlobOrString;
23use crate::dom::bindings::error::{Error, Fallible};
24use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto};
25use crate::dom::bindings::root::DomRoot;
26use crate::dom::bindings::serializable::Serializable;
27use crate::dom::bindings::str::DOMString;
28use crate::dom::bindings::structuredclone::StructuredData;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::promise::Promise;
31use crate::dom::readablestream::ReadableStream;
32use crate::realms::{AlreadyInRealm, InRealm};
33use crate::script_runtime::CanGc;
34
35/// <https://w3c.github.io/FileAPI/#dfn-Blob>
36#[dom_struct]
37pub(crate) struct Blob {
38    reflector_: Reflector,
39    #[no_trace]
40    blob_id: BlobId,
41}
42
43impl Blob {
44    pub(crate) fn new(global: &GlobalScope, blob_impl: BlobImpl, can_gc: CanGc) -> DomRoot<Blob> {
45        Self::new_with_proto(global, None, blob_impl, can_gc)
46    }
47
48    fn new_with_proto(
49        global: &GlobalScope,
50        proto: Option<HandleObject>,
51        blob_impl: BlobImpl,
52        can_gc: CanGc,
53    ) -> DomRoot<Blob> {
54        let dom_blob = reflect_dom_object_with_proto(
55            Box::new(Blob::new_inherited(&blob_impl)),
56            global,
57            proto,
58            can_gc,
59        );
60        global.track_blob(&dom_blob, blob_impl);
61        dom_blob
62    }
63
64    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
65    pub(crate) fn new_inherited(blob_impl: &BlobImpl) -> Blob {
66        Blob {
67            reflector_: Reflector::new(),
68            blob_id: blob_impl.blob_id(),
69        }
70    }
71
72    /// Get a slice to inner data, this might incur synchronous read and caching
73    pub(crate) fn get_bytes(&self) -> Result<Vec<u8>, ()> {
74        self.global().get_blob_bytes(&self.blob_id)
75    }
76
77    /// Get a copy of the type_string
78    pub(crate) fn type_string(&self) -> String {
79        self.global().get_blob_type_string(&self.blob_id)
80    }
81
82    /// Get a FileID representing the Blob content,
83    /// used by URL.createObjectURL
84    pub(crate) fn get_blob_url_id(&self) -> Uuid {
85        self.global().get_blob_url_id(&self.blob_id)
86    }
87
88    /// <https://w3c.github.io/FileAPI/#blob-get-stream>
89    pub(crate) fn get_stream(&self, can_gc: CanGc) -> Fallible<DomRoot<ReadableStream>> {
90        self.global().get_blob_stream(&self.blob_id, can_gc)
91    }
92}
93
94impl Serializable for Blob {
95    type Index = BlobIndex;
96    type Data = BlobImpl;
97
98    /// <https://w3c.github.io/FileAPI/#ref-for-serialization-steps>
99    fn serialize(&self) -> Result<(BlobId, BlobImpl), ()> {
100        let blob_id = self.blob_id;
101
102        // 1. Get a clone of the blob impl.
103        let blob_impl = self.global().serialize_blob(&blob_id);
104
105        // We clone the data, but the clone gets its own Id.
106        let new_blob_id = blob_impl.blob_id();
107
108        Ok((new_blob_id, blob_impl))
109    }
110
111    /// <https://w3c.github.io/FileAPI/#ref-for-deserialization-steps>
112    fn deserialize(
113        owner: &GlobalScope,
114        serialized: BlobImpl,
115        can_gc: CanGc,
116    ) -> Result<DomRoot<Self>, ()> {
117        let deserialized_blob = Blob::new(owner, serialized, can_gc);
118        Ok(deserialized_blob)
119    }
120
121    fn serialized_storage<'a>(
122        reader: StructuredData<'a, '_>,
123    ) -> &'a mut Option<HashMap<BlobId, Self::Data>> {
124        match reader {
125            StructuredData::Reader(r) => &mut r.blob_impls,
126            StructuredData::Writer(w) => &mut w.blobs,
127        }
128    }
129}
130
131/// Extract bytes from BlobParts, used by Blob and File constructor
132/// <https://w3c.github.io/FileAPI/#constructorBlob>
133#[allow(unsafe_code)]
134pub(crate) fn blob_parts_to_bytes(
135    mut blobparts: Vec<ArrayBufferOrArrayBufferViewOrBlobOrString>,
136) -> Result<Vec<u8>, ()> {
137    let mut ret = vec![];
138    for blobpart in &mut blobparts {
139        match blobpart {
140            ArrayBufferOrArrayBufferViewOrBlobOrString::String(s) => {
141                ret.extend(s.as_bytes());
142            },
143            ArrayBufferOrArrayBufferViewOrBlobOrString::Blob(b) => {
144                let bytes = b.get_bytes().unwrap_or(vec![]);
145                ret.extend(bytes);
146            },
147            ArrayBufferOrArrayBufferViewOrBlobOrString::ArrayBuffer(a) => unsafe {
148                let bytes = a.as_slice();
149                ret.extend(bytes);
150            },
151            ArrayBufferOrArrayBufferViewOrBlobOrString::ArrayBufferView(a) => unsafe {
152                let bytes = a.as_slice();
153                ret.extend(bytes);
154            },
155        }
156    }
157
158    Ok(ret)
159}
160
161impl BlobMethods<crate::DomTypeHolder> for Blob {
162    // https://w3c.github.io/FileAPI/#constructorBlob
163    #[allow(non_snake_case)]
164    fn Constructor(
165        global: &GlobalScope,
166        proto: Option<HandleObject>,
167        can_gc: CanGc,
168        blobParts: Option<Vec<ArrayBufferOrArrayBufferViewOrBlobOrString>>,
169        blobPropertyBag: &BlobBinding::BlobPropertyBag,
170    ) -> Fallible<DomRoot<Blob>> {
171        let bytes: Vec<u8> = match blobParts {
172            None => Vec::new(),
173            Some(blobparts) => match blob_parts_to_bytes(blobparts) {
174                Ok(bytes) => bytes,
175                Err(_) => return Err(Error::InvalidCharacter),
176            },
177        };
178
179        let type_string = normalize_type_string(blobPropertyBag.type_.as_ref());
180        let blob_impl = BlobImpl::new_from_bytes(bytes, type_string);
181
182        Ok(Blob::new_with_proto(global, proto, blob_impl, can_gc))
183    }
184
185    // https://w3c.github.io/FileAPI/#dfn-size
186    fn Size(&self) -> u64 {
187        self.global().get_blob_size(&self.blob_id)
188    }
189
190    // https://w3c.github.io/FileAPI/#dfn-type
191    fn Type(&self) -> DOMString {
192        DOMString::from(self.type_string())
193    }
194
195    // <https://w3c.github.io/FileAPI/#blob-get-stream>
196    fn Stream(&self, can_gc: CanGc) -> Fallible<DomRoot<ReadableStream>> {
197        self.get_stream(can_gc)
198    }
199
200    /// <https://w3c.github.io/FileAPI/#slice-method-algo>
201    fn Slice(
202        &self,
203        start: Option<i64>,
204        end: Option<i64>,
205        content_type: Option<DOMString>,
206        can_gc: CanGc,
207    ) -> DomRoot<Blob> {
208        let global = self.global();
209        let type_string = normalize_type_string(&content_type.unwrap_or_default());
210
211        // If our parent is already a sliced blob then we reference the data from the grandparent instead,
212        // to keep the blob ancestry chain short.
213        let (parent, range) = match *global.get_blob_data(&self.blob_id) {
214            BlobData::Sliced(grandparent, parent_range) => {
215                let range = RelativePos {
216                    start: parent_range.start + start.unwrap_or_default(),
217                    end: end.map(|end| end + parent_range.start).or(parent_range.end),
218                };
219                (grandparent, range)
220            },
221            _ => (self.blob_id, RelativePos::from_opts(start, end)),
222        };
223
224        let blob_impl = BlobImpl::new_sliced(range, parent, type_string);
225        Blob::new(&global, blob_impl, can_gc)
226    }
227
228    /// <https://w3c.github.io/FileAPI/#text-method-algo>
229    fn Text(&self, can_gc: CanGc) -> Rc<Promise> {
230        let global = self.global();
231        let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
232        let p = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
233        let id = self.get_blob_url_id();
234        global.read_file_async(
235            id,
236            p.clone(),
237            Box::new(|promise, bytes| match bytes {
238                Ok(b) => {
239                    let (text, _, _) = UTF_8.decode(&b);
240                    let text = DOMString::from(text);
241                    promise.resolve_native(&text, CanGc::note());
242                },
243                Err(e) => {
244                    promise.reject_error(e, CanGc::note());
245                },
246            }),
247        );
248        p
249    }
250
251    // https://w3c.github.io/FileAPI/#arraybuffer-method-algo
252    fn ArrayBuffer(&self, in_realm: InRealm, can_gc: CanGc) -> Rc<Promise> {
253        let cx = GlobalScope::get_cx();
254        let global = GlobalScope::from_safe_context(cx, in_realm);
255        let promise = Promise::new_in_current_realm(in_realm, can_gc);
256
257        // 1. Let stream be the result of calling get stream on this.
258        let stream = self.get_stream(can_gc);
259
260        // 2. Let reader be the result of getting a reader from stream.
261        //    If that threw an exception, return a new promise rejected with that exception.
262        let reader = match stream.and_then(|s| s.acquire_default_reader(can_gc)) {
263            Ok(reader) => reader,
264            Err(error) => {
265                promise.reject_error(error, can_gc);
266                return promise;
267            },
268        };
269
270        // 3. Let promise be the result of reading all bytes from stream with reader.
271        let success_promise = promise.clone();
272        let failure_promise = promise.clone();
273        reader.read_all_bytes(
274            cx,
275            &global,
276            Rc::new(move |bytes| {
277                rooted!(in(*cx) let mut js_object = ptr::null_mut::<JSObject>());
278                // 4. Return the result of transforming promise by a fulfillment handler that returns a new
279                //    [ArrayBuffer]
280                let array_buffer = create_buffer_source::<ArrayBufferU8>(
281                    cx,
282                    bytes,
283                    js_object.handle_mut(),
284                    can_gc,
285                )
286                .expect("Converting input to ArrayBufferU8 should never fail");
287                success_promise.resolve_native(&array_buffer, can_gc);
288            }),
289            Rc::new(move |cx, value| {
290                failure_promise.reject(cx, value, can_gc);
291            }),
292            in_realm,
293            can_gc,
294        );
295
296        promise
297    }
298
299    /// <https://w3c.github.io/FileAPI/#dom-blob-bytes>
300    fn Bytes(&self, in_realm: InRealm, can_gc: CanGc) -> Rc<Promise> {
301        let cx = GlobalScope::get_cx();
302        let global = GlobalScope::from_safe_context(cx, in_realm);
303        let p = Promise::new_in_current_realm(in_realm, can_gc);
304
305        // 1. Let stream be the result of calling get stream on this.
306        let stream = self.get_stream(can_gc);
307
308        // 2. Let reader be the result of getting a reader from stream.
309        //    If that threw an exception, return a new promise rejected with that exception.
310        let reader = match stream.and_then(|s| s.acquire_default_reader(can_gc)) {
311            Ok(r) => r,
312            Err(e) => {
313                p.reject_error(e, can_gc);
314                return p;
315            },
316        };
317
318        // 3. Let promise be the result of reading all bytes from stream with reader.
319        let p_success = p.clone();
320        let p_failure = p.clone();
321        reader.read_all_bytes(
322            cx,
323            &global,
324            Rc::new(move |bytes| {
325                rooted!(in(*cx) let mut js_object = ptr::null_mut::<JSObject>());
326                let arr = create_buffer_source::<Uint8>(cx, bytes, js_object.handle_mut(), can_gc)
327                    .expect("Converting input to uint8 array should never fail");
328                p_success.resolve_native(&arr, can_gc);
329            }),
330            Rc::new(move |cx, v| {
331                p_failure.reject(cx, v, can_gc);
332            }),
333            in_realm,
334            can_gc,
335        );
336        p
337    }
338}
339
340/// Get the normalized, MIME-parsable type string
341/// <https://w3c.github.io/FileAPI/#dfn-type>
342/// XXX: We will relax the restriction here,
343/// since the spec has some problem over this part.
344/// see <https://github.com/w3c/FileAPI/issues/43>
345pub(crate) fn normalize_type_string(s: &str) -> String {
346    if is_ascii_printable(s) {
347        s.to_ascii_lowercase()
348        // match s_lower.parse() as Result<Mime, ()> {
349        // Ok(_) => s_lower,
350        // Err(_) => "".to_string()
351    } else {
352        "".to_string()
353    }
354}
355
356fn is_ascii_printable(string: &str) -> bool {
357    // Step 5.1 in Sec 5.1 of File API spec
358    // <https://w3c.github.io/FileAPI/#constructorBlob>
359    string.chars().all(|c| ('\x20'..='\x7E').contains(&c))
360}