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