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