use std::cell::Cell;
use std::num::{NonZero, NonZeroU16};
use std::ptr::{self, NonNull};
use dom_struct::dom_struct;
use js::conversions::latin1_to_string;
use js::jsapi::{
JS_DeprecatedStringHasLatin1Chars, JS_GetTwoByteStringCharsAndLength, JS_IsExceptionPending,
JSObject, JSType, ToPrimitive,
};
use js::jsval::UndefinedValue;
use js::rust::{
HandleObject as SafeHandleObject, HandleValue as SafeHandleValue,
MutableHandleValue as SafeMutableHandleValue, ToString,
};
use js::typedarray::Uint8;
use script_bindings::conversions::SafeToJSValConvertible;
use crate::dom::bindings::buffer_source::create_buffer_source;
use crate::dom::bindings::codegen::Bindings::TextEncoderStreamBinding::TextEncoderStreamMethods;
use crate::dom::bindings::error::{Error, Fallible, throw_dom_exception};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::stream::readablestream::ReadableStream;
use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
use crate::dom::stream::writablestream::WritableStream;
use crate::dom::types::{GlobalScope, TransformStream, TransformStreamDefaultController};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
enum ConvertedInput<'a> {
String(String),
CodeUnits(&'a [u16]),
}
#[expect(unsafe_code)]
fn jsval_to_primitive(
cx: SafeJSContext,
global: &GlobalScope,
chunk: SafeHandleValue,
mut rval: SafeMutableHandleValue,
can_gc: CanGc,
) -> Fallible<()> {
if chunk.is_primitive() {
rval.set(chunk.get());
return Ok(());
}
assert!(chunk.is_object());
rooted!(in(*cx) let obj = chunk.to_object());
let is_success =
unsafe { ToPrimitive(*cx, obj.handle().into(), JSType::JSTYPE_STRING, rval.into()) };
log::debug!("ToPrimitive is_success={:?}", is_success);
if !is_success {
unsafe {
if !JS_IsExceptionPending(*cx) {
throw_dom_exception(
cx,
global,
Error::Type(c"Cannot convert JSObject to primitive".to_owned()),
can_gc,
);
}
}
return Err(Error::JSFailed);
}
Ok(())
}
#[derive(Default, JSTraceable, MallocSizeOf)]
pub(crate) struct Encoder {
leading_surrogate: Cell<Option<NonZeroU16>>,
}
impl Encoder {
fn encode(&self, maybe_ill_formed: ConvertedInput<'_>) -> String {
match maybe_ill_formed {
ConvertedInput::String(s) => {
if !s.is_empty() && self.leading_surrogate.take().is_some() {
let mut output = String::with_capacity(1 + s.len());
output.push('\u{FFFD}');
output.push_str(&s);
return output;
}
s
},
ConvertedInput::CodeUnits(code_units) => self.encode_from_code_units(code_units),
}
}
fn encode_from_code_units(&self, input: &[u16]) -> String {
let mut output = String::with_capacity(input.len());
for result in char::decode_utf16(input.iter().cloned()) {
match result {
Ok(c) => {
if self.leading_surrogate.take().is_some() {
output.push('\u{FFFD}');
}
output.push(c);
},
Err(error) => {
let unpaired_surrogate = error.unpaired_surrogate();
match code_point_type(unpaired_surrogate) {
CodePointType::LeadingSurrogate => {
if self.leading_surrogate.take().is_some() {
output.push('\u{FFFD}');
}
self.leading_surrogate
.replace(NonZero::new(unpaired_surrogate));
},
CodePointType::TrailingSurrogate => match self.leading_surrogate.take() {
Some(leading_surrogate) => {
let c = char::decode_utf16([
leading_surrogate.get(),
unpaired_surrogate,
])
.next()
.expect("A pair of surrogate is supplied")
.expect("Decoding a pair of surrogate cannot fail");
output.push(c);
},
None => output.push('\u{FFFD}'),
},
CodePointType::ScalarValue => unreachable!("Scalar Value won't fail"),
}
},
}
}
output
}
}
enum CodePointType {
ScalarValue,
LeadingSurrogate,
TrailingSurrogate,
}
fn code_point_type(value: u16) -> CodePointType {
match value {
0xD800..=0xDBFF => CodePointType::LeadingSurrogate,
0xDC00..=0xDFFF => CodePointType::TrailingSurrogate,
_ => CodePointType::ScalarValue,
}
}
#[expect(unsafe_code)]
pub(crate) fn encode_and_enqueue_a_chunk(
cx: &mut js::context::JSContext,
global: &GlobalScope,
chunk: SafeHandleValue,
encoder: &Encoder,
controller: &TransformStreamDefaultController,
) -> Fallible<()> {
rooted!(&in(cx) let mut rval = UndefinedValue());
jsval_to_primitive(
cx.into(),
global,
chunk,
rval.handle_mut(),
CanGc::from_cx(cx),
)?;
assert!(!rval.is_object());
rooted!(&in(cx) let jsstr = unsafe { ToString(cx.raw_cx(), rval.handle()) });
if jsstr.is_null() {
unsafe {
if !JS_IsExceptionPending(cx.raw_cx()) {
throw_dom_exception(
cx.into(),
global,
Error::Type(c"Cannot convert JS primitive to string".to_owned()),
CanGc::from_cx(cx),
);
}
}
return Err(Error::JSFailed);
}
let input = unsafe {
if JS_DeprecatedStringHasLatin1Chars(*jsstr) {
let s = NonNull::new(*jsstr).expect("jsstr cannot be null");
ConvertedInput::String(latin1_to_string(cx.raw_cx(), s))
} else {
let mut len = 0;
let data =
JS_GetTwoByteStringCharsAndLength(cx.raw_cx(), std::ptr::null(), *jsstr, &mut len);
let maybe_ill_formed_code_units = std::slice::from_raw_parts(data, len);
ConvertedInput::CodeUnits(maybe_ill_formed_code_units)
}
};
let output = encoder.encode(input);
let output = output.as_bytes();
if output.is_empty() {
return Ok(());
}
rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
let chunk = create_buffer_source::<Uint8>(
cx.into(),
output,
js_object.handle_mut(),
CanGc::from_cx(cx),
)
.map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
rooted!(&in(cx) let mut rval = UndefinedValue());
chunk.safe_to_jsval(cx.into(), rval.handle_mut(), CanGc::from_cx(cx));
controller.enqueue(cx, global, rval.handle())?;
Ok(())
}
pub(crate) fn encode_and_flush(
cx: &mut js::context::JSContext,
global: &GlobalScope,
encoder: &Encoder,
controller: &TransformStreamDefaultController,
) -> Fallible<()> {
if encoder.leading_surrogate.get().is_some() {
rooted!(&in(cx) let mut js_object = ptr::null_mut::<JSObject>());
let chunk = create_buffer_source::<Uint8>(
cx.into(),
&[0xEF_u8, 0xBF, 0xBD],
js_object.handle_mut(),
CanGc::from_cx(cx),
)
.map_err(|_| Error::Type(c"Cannot convert byte sequence to Uint8Array".to_owned()))?;
rooted!(&in(cx) let mut rval = UndefinedValue());
chunk.safe_to_jsval(cx.into(), rval.handle_mut(), CanGc::from_cx(cx));
return controller.enqueue(cx, global, rval.handle());
}
Ok(())
}
#[dom_struct]
pub(crate) struct TextEncoderStream {
reflector_: Reflector,
transform: Dom<TransformStream>,
}
impl TextEncoderStream {
fn new_inherited(transform: &TransformStream) -> TextEncoderStream {
Self {
reflector_: Reflector::new(),
transform: Dom::from_ref(transform),
}
}
fn new_with_proto(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
) -> Fallible<DomRoot<TextEncoderStream>> {
let encoder = Encoder::default();
let transformer_type = TransformerType::Encoder(encoder);
let transform = TransformStream::new_with_proto(global, None, CanGc::from_cx(cx));
transform.set_up(cx, global, transformer_type)?;
Ok(reflect_dom_object_with_proto_and_cx(
Box::new(TextEncoderStream::new_inherited(&transform)),
global,
proto,
cx,
))
}
}
impl TextEncoderStreamMethods<crate::DomTypeHolder> for TextEncoderStream {
fn Constructor(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
) -> Fallible<DomRoot<TextEncoderStream>> {
TextEncoderStream::new_with_proto(cx, global, proto)
}
fn Encoding(&self) -> DOMString {
DOMString::from("utf-8")
}
fn Readable(&self) -> DomRoot<ReadableStream> {
self.transform.get_readable()
}
fn Writable(&self) -> DomRoot<WritableStream> {
self.transform.get_writable()
}
}