servo-script 0.4.0

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::ptr;
use std::rc::Rc;

use dom_struct::dom_struct;
use js::context::JSContext;
use js::jsapi::{Heap, IsPromiseObject, JSObject};
use js::jsval::{JSVal, UndefinedValue};
use js::rust::{Handle as SafeHandle, HandleObject, HandleValue as SafeHandleValue, IntoHandle};
use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};

use super::byteteeunderlyingsource::ByteTeeUnderlyingSource;
use crate::dom::bindings::callback::ExceptionHandling;
use crate::dom::bindings::codegen::Bindings::UnderlyingSourceBinding::UnderlyingSource as JsUnderlyingSource;
use crate::dom::bindings::codegen::UnionTypes::ReadableStreamDefaultControllerOrReadableByteStreamController as Controller;
use crate::dom::bindings::error::Error;
use crate::dom::bindings::reflector::DomGlobal;
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::globalscope::GlobalScope;
use crate::dom::messageport::MessagePort;
use crate::dom::promise::Promise;
use crate::dom::stream::defaultteeunderlyingsource::DefaultTeeUnderlyingSource;
use crate::dom::stream::transformstream::TransformStream;

/// A variation of [UnderlyingSourceType] used for storing state within UnderlyingContainer.
/// All variants have identical meanings to [UnderlyingSourceType].
#[derive(JSTraceable)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
enum UnderlyingSource {
    Memory(usize),
    Blob(usize),
    FetchResponse,
    Js(JsUnderlyingSource, Heap<*mut JSObject>),
    Tee(Dom<DefaultTeeUnderlyingSource>),
    Transfer(Dom<MessagePort>),
    Transform(Dom<TransformStream>, Rc<Promise>),
    TeeByte(Dom<ByteTeeUnderlyingSource>),
}

impl UnderlyingSource {
    /// Does the source have all data in memory?
    fn in_memory(&self) -> bool {
        matches!(self, UnderlyingSource::Memory(_))
    }
}

impl From<UnderlyingSourceType<'_>> for UnderlyingSource {
    fn from(underlying_source_type: UnderlyingSourceType<'_>) -> Self {
        match underlying_source_type {
            UnderlyingSourceType::Memory(size) => UnderlyingSource::Memory(size),
            UnderlyingSourceType::Blob(size) => UnderlyingSource::Blob(size),
            UnderlyingSourceType::FetchResponse => UnderlyingSource::FetchResponse,
            UnderlyingSourceType::Js(source) => UnderlyingSource::Js(source, Heap::default()),
            UnderlyingSourceType::Tee(source) => UnderlyingSource::Tee(Dom::from_ref(source)),
            UnderlyingSourceType::Transfer(port) => UnderlyingSource::Transfer(Dom::from_ref(port)),
            UnderlyingSourceType::Transform(stream, promise) => {
                UnderlyingSource::Transform(Dom::from_ref(stream), promise)
            },
            UnderlyingSourceType::TeeByte(source) => {
                UnderlyingSource::TeeByte(Dom::from_ref(source))
            },
        }
    }
}

/// <https://streams.spec.whatwg.org/#underlying-source-api>
/// The `Js` variant corresponds to
/// the JavaScript object representing the underlying source.
/// The other variants are native sources in Rust.
pub(crate) enum UnderlyingSourceType<'a> {
    /// Facilitate partial integration with sources
    /// that are currently read into memory.
    Memory(usize),
    /// A blob as underlying source, with a known total size.
    Blob(usize),
    /// A fetch response as underlying source.
    FetchResponse,
    /// A struct representing a JS object as underlying source,
    /// and the actual JS object for use as `thisArg` in callbacks.
    Js(JsUnderlyingSource),
    /// Tee
    Tee(&'a DefaultTeeUnderlyingSource),
    /// Transfer, with the port used in some of the algorithms.
    Transfer(&'a MessagePort),
    /// A struct representing a JS object as underlying source,
    /// and the actual JS object for use as `thisArg` in callbacks.
    /// This is used for the `TransformStream` API.
    Transform(&'a TransformStream, Rc<Promise>),
    /// Tee Byte
    TeeByte(&'a ByteTeeUnderlyingSource),
}

impl UnderlyingSourceType<'_> {
    /// Is the source backed by a Rust native source?
    pub(crate) fn is_native(&self) -> bool {
        matches!(
            self,
            UnderlyingSourceType::Memory(_) |
                UnderlyingSourceType::Blob(_) |
                UnderlyingSourceType::FetchResponse |
                UnderlyingSourceType::Transfer(_)
        )
    }
}

/// Wrapper around the underlying source.
#[dom_struct]
pub(crate) struct UnderlyingSourceContainer {
    reflector_: Reflector,
    #[ignore_malloc_size_of = "JsUnderlyingSource implemented in SM."]
    underlying_source_type: UnderlyingSource,
}

impl UnderlyingSourceContainer {
    fn new_inherited(underlying_source_type: UnderlyingSourceType) -> UnderlyingSourceContainer {
        UnderlyingSourceContainer {
            reflector_: Reflector::new(),
            underlying_source_type: underlying_source_type.into(),
        }
    }

    pub(crate) fn new(
        cx: &mut JSContext,
        global: &GlobalScope,
        underlying_source_type: UnderlyingSourceType,
    ) -> DomRoot<UnderlyingSourceContainer> {
        // TODO: setting the underlying source dict as the prototype of the
        // `UnderlyingSourceContainer`, as it is later used as the "this" in Call_.
        // Is this a good idea?
        reflect_dom_object_with_cx(
            Box::new(UnderlyingSourceContainer::new_inherited(
                underlying_source_type,
            )),
            global,
            cx,
        )
    }

    /// Setting the JS object after the heap has settled down.
    pub(crate) fn set_underlying_source_this_object(&self, object: HandleObject) {
        if let UnderlyingSource::Js(_source, this_obj) = &self.underlying_source_type {
            this_obj.set(*object);
        }
    }

    /// <https://streams.spec.whatwg.org/#dom-underlyingsource-cancel>
    #[expect(unsafe_code)]
    pub(crate) fn call_cancel_algorithm(
        &self,
        cx: &mut JSContext,
        global: &GlobalScope,
        reason: SafeHandleValue,
    ) -> Option<Result<Rc<Promise>, Error>> {
        match &self.underlying_source_type {
            UnderlyingSource::Js(source, this_obj) => {
                if let Some(algo) = &source.cancel {
                    let result = unsafe {
                        algo.Call_(
                            cx,
                            &SafeHandle::from_raw(this_obj.handle()),
                            Some(reason),
                            ExceptionHandling::Rethrow,
                        )
                    };
                    return Some(result);
                }
                None
            },
            UnderlyingSource::Tee(tee_underlying_source) => {
                // Call the cancel algorithm for the appropriate branch.
                tee_underlying_source.cancel_algorithm(cx, global, reason)
            },
            UnderlyingSource::Transform(stream, _) => {
                // Return ! TransformStreamDefaultSourceCancelAlgorithm(stream, reason).
                Some(stream.transform_stream_default_source_cancel(cx, global, reason))
            },
            UnderlyingSource::Transfer(port) => {
                // Let cancelAlgorithm be the following steps, taking a reason argument:
                // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable

                // Let result be PackAndPostMessageHandlingError(port, "error", reason).
                let result = port.pack_and_post_message_handling_error(cx, "error", reason);

                // Disentangle port.
                self.global().disentangle_port(cx, port);

                let promise = Promise::new(cx, &self.global());

                // If result is an abrupt completion,
                if let Err(error) = result {
                    // Return a promise rejected with result.[[Value]].
                    promise.reject_error(cx, error);
                } else {
                    // Otherwise, return a promise resolved with undefined.
                    promise.resolve_native(cx, &());
                }
                Some(Ok(promise))
            },
            UnderlyingSource::TeeByte(tee_underlyin_source) => {
                // Call the cancel algorithm for the appropriate branch.
                tee_underlyin_source.cancel_algorithm(cx, reason)
            },
            _ => None,
        }
    }

    /// <https://streams.spec.whatwg.org/#dom-underlyingsource-pull>
    #[expect(unsafe_code)]
    pub(crate) fn call_pull_algorithm(
        &self,
        cx: &mut JSContext,
        controller: Controller,
    ) -> Option<Result<Rc<Promise>, Error>> {
        match &self.underlying_source_type {
            UnderlyingSource::Js(source, this_obj) => {
                if let Some(algo) = &source.pull {
                    let result = unsafe {
                        algo.Call_(
                            cx,
                            &SafeHandle::from_raw(this_obj.handle()),
                            controller,
                            ExceptionHandling::Rethrow,
                        )
                    };
                    return Some(result);
                }
                None
            },
            UnderlyingSource::Tee(tee_underlying_source) => {
                // Call the pull algorithm for the appropriate branch.
                Some(Ok(tee_underlying_source.pull_algorithm(cx)))
            },
            UnderlyingSource::Transfer(port) => {
                // Let pullAlgorithm be the following steps:
                // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable

                // Perform ! PackAndPostMessage(port, "pull", undefined).
                rooted!(&in(cx) let mut value = UndefinedValue());
                port.pack_and_post_message(cx, "pull", value.handle())
                    .expect("Sending pull should not fail.");

                // Return a promise resolved with undefined.
                let promise = Promise::new_resolved(cx, &self.global(), ());
                Some(Ok(promise))
            },
            UnderlyingSource::TeeByte(tee_underlyin_source) => {
                // Call the pull algorithm for the appropriate branch.
                Some(Ok(tee_underlyin_source.pull_algorithm(cx, None)))
            },
            // Note: other source type have no pull steps for now.
            UnderlyingSource::Transform(stream, _) => {
                // Return ! TransformStreamDefaultSourcePullAlgorithm(stream).
                Some(stream.transform_stream_default_source_pull(cx, &self.global()))
            },
            _ => None,
        }
    }

    /// <https://streams.spec.whatwg.org/#dom-underlyingsource-start>
    ///
    /// Note: The algorithm can return any value, including a promise,
    /// we always transform the result into a promise for convenience,
    /// and it is also how to spec deals with the situation.
    /// see "Let startPromise be a promise resolved with startResult."
    /// at <https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller>
    #[expect(unsafe_code)]
    pub(crate) fn call_start_algorithm(
        &self,
        cx: &mut JSContext,
        controller: Controller,
    ) -> Option<Result<Rc<Promise>, Error>> {
        match &self.underlying_source_type {
            UnderlyingSource::Js(source, this_obj) => {
                if let Some(start) = &source.start {
                    rooted!(&in(cx) let mut result_object = ptr::null_mut::<JSObject>());
                    rooted!(&in(cx) let mut result: JSVal);
                    unsafe {
                        if let Err(error) = start.Call_(
                            cx,
                            &SafeHandle::from_raw(this_obj.handle()),
                            controller,
                            result.handle_mut(),
                            ExceptionHandling::Rethrow,
                        ) {
                            return Some(Err(error));
                        }
                    }
                    let is_promise = unsafe {
                        if result.is_object() {
                            result_object.set(result.to_object());
                            IsPromiseObject(result_object.handle().into_handle())
                        } else {
                            false
                        }
                    };
                    let promise = if is_promise {
                        Promise::new_with_js_promise(cx, result_object.handle())
                    } else {
                        Promise::new_resolved(cx, &self.global(), result.get())
                    };
                    return Some(Ok(promise));
                }
                None
            },
            UnderlyingSource::Tee(_) => {
                // Let startAlgorithm be an algorithm that returns undefined.
                None
            },
            UnderlyingSource::Transfer(_) => {
                // Let startAlgorithm be an algorithm that returns undefined.
                // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
                None
            },
            UnderlyingSource::Transform(_, start_promise) => {
                // Let startAlgorithm be an algorithm that returns startPromise.
                Some(Ok(start_promise.clone()))
            },
            _ => None,
        }
    }

    /// <https://streams.spec.whatwg.org/#dom-underlyingsource-autoallocatechunksize>
    pub(crate) fn auto_allocate_chunk_size(&self) -> Option<u64> {
        match &self.underlying_source_type {
            UnderlyingSource::Js(source, _) => source.autoAllocateChunkSize,
            _ => None,
        }
    }

    /// Does the source have all data in memory?
    pub(crate) fn in_memory(&self) -> bool {
        self.underlying_source_type.in_memory()
    }
}