Skip to main content

script/dom/file/
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 dom_struct::dom_struct;
9use encoding_rs::UTF_8;
10use js::context::{JSContext, NoGC};
11use js::jsapi::JSObject;
12use js::realm::CurrentRealm;
13use js::rust::HandleObject;
14use js::typedarray::{ArrayBufferU8, Uint8};
15use net_traits::filemanager_thread::RelativePos;
16use rustc_hash::FxHashMap;
17use script_bindings::reflector::{Reflector, reflect_weak_referenceable_dom_object_with_proto};
18use script_bindings::structuredclone::StructuredData;
19use servo_base::id::{BlobId, BlobIndex};
20use servo_constellation_traits::{BlobData, BlobImpl};
21use uuid::Uuid;
22
23use crate::dom::RootedPromise;
24use crate::dom::bindings::buffer_source::{create_buffer_source, get_buffer_source_slice};
25use crate::dom::bindings::codegen::Bindings::BlobBinding;
26use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods;
27use crate::dom::bindings::codegen::UnionTypes::{
28    ArrayBufferOrArrayBufferViewOrBlobOrString, ArrayBufferViewOrArrayBuffer,
29};
30use crate::dom::bindings::error::{Error, Fallible};
31use crate::dom::bindings::reflector::DomGlobal;
32use crate::dom::bindings::root::DomRoot;
33use crate::dom::bindings::serializable::Serializable;
34use crate::dom::bindings::str::DOMString;
35use crate::dom::encoding::textdecoderstream::TextDecoderStream;
36use crate::dom::globalscope::GlobalScope;
37use crate::dom::promise::Promise;
38use crate::dom::stream::readablestream::{ReadableStream, pipe_through};
39
40/// <https://w3c.github.io/FileAPI/#dfn-Blob>
41#[dom_struct]
42pub(crate) struct Blob {
43    reflector_: Reflector,
44    #[no_trace]
45    blob_id: BlobId,
46}
47
48impl Blob {
49    pub(crate) fn new(
50        cx: &mut JSContext,
51        global: &GlobalScope,
52        blob_impl: BlobImpl,
53    ) -> DomRoot<Blob> {
54        Self::new_with_proto(cx, global, None, blob_impl)
55    }
56
57    fn new_with_proto(
58        cx: &mut JSContext,
59        global: &GlobalScope,
60        proto: Option<HandleObject>,
61        blob_impl: BlobImpl,
62    ) -> DomRoot<Blob> {
63        let dom_blob = reflect_weak_referenceable_dom_object_with_proto(
64            cx,
65            Rc::new(Blob::new_inherited(&blob_impl)),
66            global,
67            proto,
68        );
69        global.track_blob(&dom_blob, blob_impl);
70        dom_blob
71    }
72
73    pub(crate) fn new_inherited(blob_impl: &BlobImpl) -> Blob {
74        Blob {
75            reflector_: Reflector::new(),
76            blob_id: blob_impl.blob_id(),
77        }
78    }
79
80    /// Get a slice to inner data, this might incur synchronous read and caching
81    pub(crate) fn get_bytes(&self) -> Result<Vec<u8>, ()> {
82        self.global().get_blob_bytes(&self.blob_id)
83    }
84
85    /// Get a copy of the type_string
86    pub(crate) fn type_string(&self) -> String {
87        self.global().get_blob_type_string(&self.blob_id)
88    }
89
90    /// Get a FileID representing the Blob content,
91    /// used by URL.createObjectURL
92    pub(crate) fn get_blob_url_id(&self) -> Uuid {
93        self.global().get_blob_url_id(&self.blob_id)
94    }
95
96    /// <https://w3c.github.io/FileAPI/#blob-get-stream>
97    pub(crate) fn get_stream(&self, cx: &mut JSContext) -> Fallible<DomRoot<ReadableStream>> {
98        self.global().get_blob_stream(cx, &self.blob_id)
99    }
100}
101
102impl Serializable for Blob {
103    type Index = BlobIndex;
104    type Data = BlobImpl;
105
106    /// <https://w3c.github.io/FileAPI/#ref-for-serialization-steps>
107    fn serialize(&self, _no_gc: &NoGC) -> Result<(BlobId, BlobImpl), ()> {
108        let blob_id = self.blob_id;
109
110        // 1. Get a clone of the blob impl.
111        let blob_impl = self.global().serialize_blob(&blob_id);
112
113        // We clone the data, but the clone gets its own Id.
114        let new_blob_id = blob_impl.blob_id();
115
116        Ok((new_blob_id, blob_impl))
117    }
118
119    /// <https://w3c.github.io/FileAPI/#ref-for-deserialization-steps>
120    fn deserialize(
121        cx: &mut JSContext,
122        owner: &GlobalScope,
123        serialized: BlobImpl,
124    ) -> Result<DomRoot<Self>, ()> {
125        Ok(Blob::new(cx, owner, serialized))
126    }
127
128    fn serialized_storage<'a>(
129        reader: StructuredData<'a, '_>,
130    ) -> &'a mut Option<FxHashMap<BlobId, Self::Data>> {
131        match reader {
132            StructuredData::Reader(r) => &mut r.blob_impls,
133            StructuredData::Writer(w) => &mut w.blobs,
134        }
135    }
136}
137
138/// <https://w3c.github.io/FileAPI/#convert-line-endings-to-native>
139fn convert_line_endings_to_native(s: &[u8]) -> Vec<u8> {
140    let native_line_ending: &[u8] = if cfg!(target_os = "windows") {
141        // Step 2. If the underlying platform’s conventions are to represent newlines
142        // as a carriage return and line feed sequence,
143        // set native line ending to the code point U+000D CR followed by the code point U+000A LF.
144        b"\r\n"
145    } else {
146        // Step 1. Let native line ending be the code point U+000A LF.
147        b"\n"
148    };
149
150    let len = s.len();
151    // Step 3. Set result to the empty string.
152    let mut result = Vec::with_capacity(len);
153
154    // Step 4. Let position be a position variable for s, initially pointing at the start of s.
155    let mut position = 0;
156
157    // <https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points>
158    let collect_a_sequence_of_code_points = |position: &mut usize| -> &[u8] {
159        let start = *position;
160        while *position < len && s[*position] != b'\r' && s[*position] != b'\n' {
161            *position += 1;
162        }
163        &s[start..*position]
164    };
165
166    // Step 5: Let token be the result of collecting a sequence of code points
167    // that are not equal to U+000A LF or U+000D CR from s given position.
168    // Step 6: Append token to result.
169    result.extend_from_slice(collect_a_sequence_of_code_points(&mut position));
170
171    // Step 7: While position is not past the end of s:
172    while position < len {
173        let byte = s[position];
174        // Step 7.1: If the code point at position within s equals U+000D CR:
175        if byte == b'\r' {
176            // Step 7.1.1: Append native line ending to result.
177            result.extend_from_slice(native_line_ending);
178            // Step 7.1.2: Advance position by 1.
179            position += 1;
180            // Step 7.1.3: If position is not past the end of s and the code point
181            // at position within s equals U+000A LF, advance position by 1.
182            if position < len && s[position] == b'\n' {
183                position += 1;
184            }
185        }
186        // Step 7.2: Otherwise, if the code point at position within s equals U+000A LF:
187        else if byte == b'\n' {
188            // Advance position by 1 and append native line ending to result.
189            position += 1;
190            result.extend_from_slice(native_line_ending);
191        }
192
193        // Step 7.3: Let token be the result of collecting a sequence of code points
194        // that are not equal to U+000A LF or U+000D CR from s given position.
195        // Step 7.4: Append token to result.
196        result.extend_from_slice(collect_a_sequence_of_code_points(&mut position));
197    }
198
199    // Step 8: Return result.
200    result
201}
202
203/// <https://w3c.github.io/FileAPI/#process-blob-parts>
204pub(crate) fn process_blob_parts(
205    no_gc: &NoGC,
206    blobparts: Vec<ArrayBufferOrArrayBufferViewOrBlobOrString>,
207    endings: BlobBinding::EndingType,
208) -> Result<Vec<u8>, ()> {
209    // Step 1. Let bytes be an empty sequence of bytes.
210    let mut bytes = vec![];
211    // Step 2. For each blobpart in blobparts:
212    for blobpart in blobparts {
213        match blobpart {
214            // Step 2.1. If blobpart is a USVString, run the following substeps:
215            ArrayBufferOrArrayBufferViewOrBlobOrString::String(s) => {
216                // Step 2.1.1. Let s be blobpart.
217                // Step 2.1.2. If the endings member of options is "native",
218                // set s to the result of converting line endings to native of blobpart.
219                if endings == BlobBinding::EndingType::Native {
220                    let converted = convert_line_endings_to_native(&s.as_bytes(no_gc));
221                    // Step 2.1.3. Append the result of UTF-8 encoding s to bytes.
222                    bytes.extend(converted);
223                } else {
224                    // Step 2.1.3: Append the result of UTF-8 encoding s to bytes.
225                    bytes.extend_from_slice(&s.as_bytes(no_gc));
226                }
227            },
228            // Step 2.2. If element is a BufferSource,
229            // get a copy of the bytes held by the buffer source,
230            // and append those bytes to bytes.
231            ArrayBufferOrArrayBufferViewOrBlobOrString::ArrayBuffer(a) => {
232                let array_buffer = ArrayBufferViewOrArrayBuffer::ArrayBuffer(a);
233                bytes.extend_from_slice(get_buffer_source_slice(&array_buffer, no_gc));
234            },
235            ArrayBufferOrArrayBufferViewOrBlobOrString::ArrayBufferView(a) => {
236                let array_view = ArrayBufferViewOrArrayBuffer::ArrayBufferView(a);
237                bytes.extend_from_slice(get_buffer_source_slice(&array_view, no_gc));
238            },
239            // Step 2.3. If element is a Blob, append the bytes it represents to bytes.
240            ArrayBufferOrArrayBufferViewOrBlobOrString::Blob(b) => {
241                let blob_bytes = b.get_bytes().unwrap_or(vec![]);
242                bytes.extend(blob_bytes);
243            },
244        }
245    }
246
247    // Step 3. Return bytes.
248    Ok(bytes)
249}
250
251impl BlobMethods<crate::DomTypeHolder> for Blob {
252    // https://w3c.github.io/FileAPI/#constructorBlob
253    #[expect(non_snake_case)]
254    fn Constructor(
255        cx: &mut JSContext,
256        global: &GlobalScope,
257        proto: Option<HandleObject>,
258        blobParts: Option<Vec<ArrayBufferOrArrayBufferViewOrBlobOrString>>,
259        blobPropertyBag: &BlobBinding::BlobPropertyBag,
260    ) -> Fallible<DomRoot<Blob>> {
261        let bytes: Vec<u8> = match blobParts {
262            None => Vec::new(),
263            Some(blobparts) => {
264                match process_blob_parts(cx.no_gc(), blobparts, blobPropertyBag.endings) {
265                    Ok(bytes) => bytes,
266                    Err(_) => return Err(Error::InvalidCharacter(None)),
267                }
268            },
269        };
270
271        let type_string = normalize_type_string(&blobPropertyBag.type_.str());
272        let blob_impl = BlobImpl::new_from_bytes(bytes, type_string);
273
274        Ok(Blob::new_with_proto(cx, global, proto, blob_impl))
275    }
276
277    /// <https://w3c.github.io/FileAPI/#dfn-size>
278    fn Size(&self) -> u64 {
279        self.global().get_blob_size(&self.blob_id)
280    }
281
282    /// <https://w3c.github.io/FileAPI/#dfn-type>
283    fn Type(&self) -> DOMString {
284        DOMString::from(self.type_string())
285    }
286
287    // <https://w3c.github.io/FileAPI/#blob-get-stream>
288    fn Stream(&self, cx: &mut JSContext) -> Fallible<DomRoot<ReadableStream>> {
289        self.get_stream(cx)
290    }
291
292    /// <https://w3c.github.io/FileAPI/#text-stream-method-algo>
293    fn TextStream(&self, cx: &mut JSContext) -> Fallible<DomRoot<ReadableStream>> {
294        // Step 1: Let stream be the result of calling get stream on this.
295        let stream = self.get_stream(cx)?;
296        // Step 2: Let decoder be a new TextDecoderStream in this's relevant realm.
297        // Step 3: Set up decoder with UTF-8.
298        let decoder = TextDecoderStream::new_with_proto(
299            cx,
300            &self.global(),
301            None,
302            UTF_8,
303            false, // fatal
304            false, // ignoreBOM
305        )?;
306        // Step 4: Return the result of calling stream, piped through decoder.
307        Ok(pipe_through(&stream, cx, &self.global(), &decoder))
308    }
309
310    /// <https://w3c.github.io/FileAPI/#slice-method-algo>
311    fn Slice(
312        &self,
313        cx: &mut JSContext,
314        start: Option<i64>,
315        end: Option<i64>,
316        content_type: Option<DOMString>,
317    ) -> DomRoot<Blob> {
318        let global = self.global();
319        let type_string = normalize_type_string(&content_type.unwrap_or_default().str());
320
321        // If our parent is already a sliced blob then we reference the data from the grandparent instead,
322        // to keep the blob ancestry chain short.
323        let (parent, range) = match *global.get_blob_data(&self.blob_id) {
324            BlobData::Sliced(grandparent, parent_range) => {
325                let range = RelativePos {
326                    start: parent_range.start + start.unwrap_or_default(),
327                    end: end.map(|end| end + parent_range.start).or(parent_range.end),
328                };
329                (grandparent, range)
330            },
331            _ => (self.blob_id, RelativePos::from_opts(start, end)),
332        };
333
334        let blob_impl = BlobImpl::new_sliced(range, parent, type_string);
335        Blob::new(cx, &global, blob_impl)
336    }
337
338    /// <https://w3c.github.io/FileAPI/#text-method-algo>
339    fn Text(&self, cx: &mut CurrentRealm) -> RootedPromise {
340        let global = self.global();
341        let p = Promise::new_in_realm(cx);
342        let id = self.get_blob_url_id();
343        global.read_file_async(
344            id,
345            &p,
346            Box::new(|cx, promise, bytes| match bytes {
347                Ok(b) => {
348                    let (text, _) = UTF_8.decode_with_bom_removal(&b);
349                    let text = DOMString::from(text);
350                    promise.resolve_native(cx, &text);
351                },
352                Err(e) => {
353                    promise.reject_error(cx, e);
354                },
355            }),
356        );
357        p
358    }
359
360    /// <https://w3c.github.io/FileAPI/#arraybuffer-method-algo>
361    fn ArrayBuffer(&self, cx: &mut CurrentRealm) -> RootedPromise {
362        let promise = Promise::new_in_realm(cx);
363
364        // 1. Let stream be the result of calling get stream on this.
365        let stream = self.get_stream(cx);
366
367        // 2. Let reader be the result of getting a reader from stream.
368        //    If that threw an exception, return a new promise rejected with that exception.
369        let reader = match stream.and_then(|s| s.acquire_default_reader(cx)) {
370            Ok(reader) => reader,
371            Err(error) => {
372                promise.reject_error(cx, error);
373                return promise;
374            },
375        };
376
377        // 3. Let promise be the result of reading all bytes from stream with reader.
378        let success_promise = promise.clone();
379        let failure_promise = promise.clone();
380        reader.read_all_bytes(
381            cx,
382            Rc::new(move |cx, bytes| {
383                rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
384                // 4. Return the result of transforming promise by a fulfillment handler that returns a new
385                //    [ArrayBuffer]
386                let array_buffer =
387                    create_buffer_source::<ArrayBufferU8>(cx, bytes, js_object.handle_mut())
388                        .expect("Converting input to ArrayBufferU8 should never fail");
389                success_promise.resolve_native(cx, &array_buffer);
390            }),
391            Rc::new(move |cx, value| {
392                failure_promise.reject(cx, value);
393            }),
394        );
395
396        promise
397    }
398
399    /// <https://w3c.github.io/FileAPI/#dom-blob-bytes>
400    fn Bytes(&self, cx: &mut CurrentRealm) -> RootedPromise {
401        let p = Promise::new_in_realm(cx);
402
403        // 1. Let stream be the result of calling get stream on this.
404        let stream = self.get_stream(cx);
405
406        // 2. Let reader be the result of getting a reader from stream.
407        //    If that threw an exception, return a new promise rejected with that exception.
408        let reader = match stream.and_then(|s| s.acquire_default_reader(cx)) {
409            Ok(r) => r,
410            Err(e) => {
411                p.reject_error(cx, e);
412                return p;
413            },
414        };
415
416        // 3. Let promise be the result of reading all bytes from stream with reader.
417        let p_success = p.clone();
418        let p_failure = p.clone();
419        reader.read_all_bytes(
420            cx,
421            Rc::new(move |cx, bytes| {
422                rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
423                let arr = create_buffer_source::<Uint8>(cx, bytes, js_object.handle_mut())
424                    .expect("Converting input to uint8 array should never fail");
425                p_success.resolve_native(cx, &arr);
426            }),
427            Rc::new(move |cx, v| {
428                p_failure.reject(cx, v);
429            }),
430        );
431        p
432    }
433}
434
435/// Get the normalized, MIME-parsable type string
436/// <https://w3c.github.io/FileAPI/#dfn-type>
437/// XXX: We will relax the restriction here,
438/// since the spec has some problem over this part.
439/// see <https://github.com/w3c/FileAPI/issues/43>
440pub(crate) fn normalize_type_string(s: &str) -> String {
441    if is_ascii_printable(s) {
442        s.to_ascii_lowercase()
443        // match s_lower.parse() as Result<Mime, ()> {
444        // Ok(_) => s_lower,
445        // Err(_) => String::new()
446    } else {
447        String::new()
448    }
449}
450
451fn is_ascii_printable(string: &str) -> bool {
452    // Step 5.1 in Sec 5.1 of File API spec
453    // <https://w3c.github.io/FileAPI/#constructorBlob>
454    string.chars().all(|c| ('\x20'..='\x7E').contains(&c))
455}