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