use std::cell::RefCell; use std::collections::HashMap; use std::convert::Infallible; use std::io; use std::rc::Rc; use avril_core::serde_v8::{JsBuffer, ToJsBuffer}; use avril_core::{Extension, Op, v8}; use encoding_rs::{CoderResult, Decoder, DecoderResult, Encoding, REPLACEMENT, UTF_8}; type DecodeArgs = (Option, String, bool, bool, JsBuffer, bool); struct State { decoder: Decoder, fatal: bool, } impl State { fn decode(&mut self, bytes: &[u8], stream: bool) -> io::Result<(String, bool)> { let capacity = self .decoder .max_utf8_buffer_length(bytes.len()) .ok_or_else(|| io::Error::other("Decoded string is too large"))?; let mut output = String::new(); output.try_reserve(capacity).map_err(io::Error::other)?; let invalid = if self.fatal { let (result, _) = self.decoder .decode_to_string_without_replacement(bytes, &mut output, !stream); match result { DecoderResult::InputEmpty => false, // Browsers and WPT discard the bad chunk's remainder but retain codec state. DecoderResult::Malformed(_, _) => true, DecoderResult::OutputFull => { return Err(io::Error::other("Decoder output exceeded its size bound")); } } } else { let (result, _, _) = self.decoder.decode_to_string(bytes, &mut output, !stream); if result == CoderResult::OutputFull { return Err(io::Error::other("Decoder output exceeded its size bound")); } false }; Ok((output, invalid)) } } #[derive(Default)] struct Decoders { next: u64, states: HashMap, } pub fn init() -> Extension { let decoders = Rc::new(RefCell::new(Decoders::default())); let decode = decoders.clone(); Extension { name: "ext:encoding", dependencies: &["ext:web", "ext:webstreams"], ops: vec![ Op::new("op_encoding_label", |label: String| { Ok::<_, Infallible>( Encoding::for_label(label.as_bytes()) .filter(|encoding| *encoding != REPLACEMENT) .map(|encoding| encoding.name().to_ascii_lowercase()), ) }), Op::new( "op_encoding_decode", move |(id, label, fatal, ignore_bom, bytes, stream): DecodeArgs| { let mut decoders = decode.borrow_mut(); let mut state = if let Some(id) = id { decoders .states .remove(&id) .ok_or_else(|| io::Error::other("Decoder is closed"))? } else { let encoding = Encoding::for_label(label.as_bytes()) .filter(|encoding| *encoding != REPLACEMENT) .ok_or_else(|| io::Error::other("Invalid encoding label"))?; let decoder = if ignore_bom { encoding.new_decoder_without_bom_handling() } else { encoding.new_decoder_with_bom_removal() }; State { decoder, fatal } }; let (text, invalid) = state.decode(&bytes, stream)?; let id = if stream { let id = match id { Some(id) => id, None => { let id = decoders .next .checked_add(1) .filter(|id| *id <= 9_007_199_254_740_991) .ok_or_else(|| { io::Error::other("Encoding resource IDs exhausted") })?; decoders.next = id; id } }; decoders.states.insert(id, state); Some(id) } else { None }; Ok::<_, io::Error>((text, invalid, id)) }, ), Op::new("op_encoding_close", move |id: u64| { decoders.borrow_mut().states.remove(&id); Ok::<_, Infallible>(()) }), Op::try_raw("op_encoding_decode_utf8", |scope, args| { let bytes: JsBuffer = avril_core::serde_v8::from_v8(scope, args.get(0)).map_err(io::Error::other)?; // Body collection already owns fixed, unshared bytes; no snapshot or decoder state. let text = if bytes.is_ascii() { v8::String::new_from_one_byte(scope, &bytes, v8::NewStringType::Normal) } else { let (text, _) = UTF_8.decode_with_bom_removal(&bytes); v8::String::new(scope, &text) } .ok_or_else(|| io::Error::other("Decoded string is too large"))?; Ok::<_, io::Error>(text.into()) }), Op::new("op_encoding_encode", |text: String| { Ok::<_, Infallible>(ToJsBuffer::from(text.into_bytes())) }), Op::try_raw("op_encoding_encode_into", |scope, args| { let text = v8::Local::::try_from(args.get(0)) .map_err(|_| io::Error::other("Expected an encoding source string"))?; let mut destination: JsBuffer = avril_core::serde_v8::from_v8(scope, args.get(1)).map_err(io::Error::other)?; let mut read = 0; let written = text.write_utf8_v2( scope, &mut destination, v8::WriteFlags::kReplaceInvalidUtf8, Some(&mut read), ); #[derive(serde::Serialize)] struct Counts { read: usize, written: usize, } avril_core::serde_v8::to_v8(scope, Counts { read, written }) .map_err(io::Error::other) }), ], modules: &[( "ext:encoding/main.js", include_str!(concat!(env!("OUT_DIR"), "/encoding.js")), )], entry_point: Some("ext:encoding/main.js"), } }