use super::*;
const B64_STD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const B64_URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum B64Alphabet {
Standard,
Url,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum LastChunk {
Loose,
Strict,
StopBeforePartial,
}
pub(crate) struct DecodeResult {
pub bytes: Vec<u8>,
pub read: usize,
pub error: bool,
}
fn is_ascii_ws(c: u8) -> bool {
matches!(c, b'\t' | b'\n' | b'\x0C' | b'\r' | b' ')
}
fn skip_ascii_ws(units: &[u8], mut index: usize) -> usize {
while index < units.len() && is_ascii_ws(units[index]) {
index += 1;
}
index
}
fn b64_value(c: u8, alphabet: B64Alphabet) -> Option<u8> {
match c {
b'A'..=b'Z' => Some(c - b'A'),
b'a'..=b'z' => Some(c - b'a' + 26),
b'0'..=b'9' => Some(c - b'0' + 52),
b'+' if alphabet == B64Alphabet::Standard => Some(62),
b'/' if alphabet == B64Alphabet::Standard => Some(63),
b'-' if alphabet == B64Alphabet::Url => Some(62),
b'_' if alphabet == B64Alphabet::Url => Some(63),
_ => None,
}
}
pub(crate) fn from_base64(
units: &[u8],
alphabet: B64Alphabet,
last_chunk: LastChunk,
max_len: usize,
) -> DecodeResult {
let length = units.len();
let mut bytes = Vec::new();
if max_len == 0 {
return DecodeResult {
bytes,
read: 0,
error: false,
};
}
let mut chunk = [0u8; 4];
let mut chunk_len = 0usize;
let mut index = 0usize;
let mut read = 0usize;
loop {
index = skip_ascii_ws(units, index);
if index == length {
if chunk_len > 0 {
match last_chunk {
LastChunk::StopBeforePartial => {
return DecodeResult {
bytes,
read,
error: false,
};
}
LastChunk::Loose => {
if chunk_len == 1 {
return DecodeResult {
bytes,
read,
error: true,
};
}
let decoded = decode_chunk(&chunk[..chunk_len], false).unwrap();
bytes.extend_from_slice(&decoded[..chunk_len - 1]);
}
LastChunk::Strict => {
return DecodeResult {
bytes,
read,
error: true,
};
}
}
}
return DecodeResult {
bytes,
read: length,
error: false,
};
}
let c = units[index];
index += 1;
if c == b'=' {
if chunk_len < 2 {
return DecodeResult {
bytes,
read,
error: true,
};
}
index = skip_ascii_ws(units, index);
if chunk_len == 2 {
if index == length {
if last_chunk == LastChunk::StopBeforePartial {
return DecodeResult {
bytes,
read,
error: false,
};
}
return DecodeResult {
bytes,
read,
error: true,
};
}
if units[index] == b'=' {
index = skip_ascii_ws(units, index + 1);
}
}
if index < length {
return DecodeResult {
bytes,
read,
error: true,
};
}
let throw_extra = last_chunk == LastChunk::Strict;
match decode_chunk(&chunk[..chunk_len], throw_extra) {
Some(decoded) => bytes.extend_from_slice(&decoded[..chunk_len - 1]),
None => {
return DecodeResult {
bytes,
read,
error: true,
};
}
}
return DecodeResult {
bytes,
read: length,
error: false,
};
}
let Some(v) = b64_value(c, alphabet) else {
return DecodeResult {
bytes,
read,
error: true,
};
};
let remaining = max_len - bytes.len();
if (remaining == 1 && chunk_len == 2) || (remaining == 2 && chunk_len == 3) {
return DecodeResult {
bytes,
read,
error: false,
};
}
chunk[chunk_len] = v;
chunk_len += 1;
if chunk_len == 4 {
let decoded = decode_chunk(&chunk, false).unwrap();
bytes.extend_from_slice(&decoded);
chunk_len = 0;
read = index;
if bytes.len() == max_len {
return DecodeResult {
bytes,
read,
error: false,
};
}
}
}
}
fn decode_chunk(chunk: &[u8], throw_on_extra: bool) -> Option<[u8; 3]> {
match chunk.len() {
2 => {
if throw_on_extra && (chunk[1] & 0b1111) != 0 {
return None;
}
let b0 = (chunk[0] << 2) | (chunk[1] >> 4);
Some([b0, 0, 0])
}
3 => {
if throw_on_extra && (chunk[2] & 0b11) != 0 {
return None;
}
let b0 = (chunk[0] << 2) | (chunk[1] >> 4);
let b1 = (chunk[1] << 4) | (chunk[2] >> 2);
Some([b0, b1, 0])
}
4 => {
let b0 = (chunk[0] << 2) | (chunk[1] >> 4);
let b1 = (chunk[1] << 4) | (chunk[2] >> 2);
let b2 = (chunk[2] << 6) | chunk[3];
Some([b0, b1, b2])
}
_ => None,
}
}
pub(crate) fn to_base64(bytes: &[u8], alphabet: B64Alphabet, omit_padding: bool) -> Vec<u8> {
let tbl = match alphabet {
B64Alphabet::Standard => B64_STD,
B64Alphabet::Url => B64_URL,
};
let mut out = Vec::with_capacity(bytes.len().div_ceil(3) * 4);
let mut chunks = bytes.chunks_exact(3);
for c in &mut chunks {
let n = (u32::from(c[0]) << 16) | (u32::from(c[1]) << 8) | u32::from(c[2]);
out.push(tbl[(n >> 18) as usize & 63]);
out.push(tbl[(n >> 12) as usize & 63]);
out.push(tbl[(n >> 6) as usize & 63]);
out.push(tbl[n as usize & 63]);
}
let rem = chunks.remainder();
match rem.len() {
1 => {
let n = u32::from(rem[0]) << 16;
out.push(tbl[(n >> 18) as usize & 63]);
out.push(tbl[(n >> 12) as usize & 63]);
if !omit_padding {
out.push(b'=');
out.push(b'=');
}
}
2 => {
let n = (u32::from(rem[0]) << 16) | (u32::from(rem[1]) << 8);
out.push(tbl[(n >> 18) as usize & 63]);
out.push(tbl[(n >> 12) as usize & 63]);
out.push(tbl[(n >> 6) as usize & 63]);
if !omit_padding {
out.push(b'=');
}
}
_ => {}
}
out
}
fn hex_value(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
pub(crate) fn from_hex(units: &[u8], max_len: usize) -> DecodeResult {
let mut bytes = Vec::new();
if !units.len().is_multiple_of(2) {
return DecodeResult {
bytes,
read: 0,
error: true,
};
}
let mut i = 0usize;
while i + 1 < units.len() {
if bytes.len() == max_len {
return DecodeResult {
bytes,
read: i,
error: false,
};
}
let (Some(hi), Some(lo)) = (hex_value(units[i]), hex_value(units[i + 1])) else {
return DecodeResult {
bytes,
read: i,
error: true,
};
};
bytes.push((hi << 4) | lo);
i += 2;
}
DecodeResult {
bytes,
read: i,
error: false,
}
}
pub(crate) fn to_hex(bytes: &[u8]) -> Vec<u8> {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = Vec::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(HEX[(b >> 4) as usize]);
out.push(HEX[(b & 0xF) as usize]);
}
out
}
impl<'a> Interp<'a> {
fn validate_uint8array(&mut self, op: &str) -> Result<Handle, ExecError> {
let this = self.this_val;
match this.as_handle().map(Handle::from_raw) {
Some(h) if self.realm.typed_kind(h) == Some(1) => Ok(h),
_ => Err(self.type_error(&alloc::format!(
"Uint8Array.prototype.{op} called on a non-Uint8Array object"
))),
}
}
fn uint8array_bytes_view(
&mut self,
h: Handle,
op: &str,
) -> Result<(Handle, usize, usize), ExecError> {
if self.typed_array_detached(h) {
return Err(self.type_error(&alloc::format!(
"Uint8Array.prototype.{op} called on a detached ArrayBuffer"
)));
}
let buffer = self.realm.typed_buffer(h).unwrap();
let offset = self.realm.typed_byte_offset(h).unwrap();
let len = self.realm.typed_len(h).unwrap();
Ok((buffer, offset, len))
}
fn uint8array_snapshot(&self, buffer: Handle, offset: usize, len: usize) -> Vec<u8> {
self.realm
.bytes_at(buffer)
.map(|b| b.get(offset..offset + len).unwrap_or(&[]).to_vec())
.unwrap_or_default()
}
fn read_alphabet_option(&mut self, options: NanBox) -> Result<B64Alphabet, ExecError> {
if matches!(options.unpack(), Unpacked::Undefined) {
return Ok(B64Alphabet::Standard);
}
let Some(oh) = options
.as_handle()
.map(Handle::from_raw)
.filter(|_| self.is_object_value(options))
else {
return Err(self.type_error("options is not an object"));
};
let v = self.read_member(oh, "alphabet")?;
if matches!(v.unpack(), Unpacked::Undefined) {
return Ok(B64Alphabet::Standard);
}
match self.opt_string(v).as_deref() {
Some("base64") => Ok(B64Alphabet::Standard),
Some("base64url") => Ok(B64Alphabet::Url),
_ => Err(self.type_error("alphabet must be \"base64\" or \"base64url\"")),
}
}
fn read_last_chunk_option(&mut self, options: NanBox) -> Result<LastChunk, ExecError> {
if matches!(options.unpack(), Unpacked::Undefined) {
return Ok(LastChunk::Loose);
}
let Some(oh) = options
.as_handle()
.map(Handle::from_raw)
.filter(|_| self.is_object_value(options))
else {
return Err(self.type_error("options is not an object"));
};
let v = self.read_member(oh, "lastChunkHandling")?;
if matches!(v.unpack(), Unpacked::Undefined) {
return Ok(LastChunk::Loose);
}
match self.opt_string(v).as_deref() {
Some("loose") => Ok(LastChunk::Loose),
Some("strict") => Ok(LastChunk::Strict),
Some("stop-before-partial") => Ok(LastChunk::StopBeforePartial),
_ => Err(self.type_error(
"lastChunkHandling must be \"loose\", \"strict\", or \"stop-before-partial\"",
)),
}
}
fn opt_string(&self, v: NanBox) -> Option<String> {
v.as_handle()
.map(Handle::from_raw)
.and_then(|h| self.realm.string_value(h))
}
fn require_string_arg(&self, v: NanBox) -> Option<Vec<u8>> {
v.as_handle()
.map(Handle::from_raw)
.and_then(|h| self.realm.string_bytes(h))
}
fn new_uint8array_from(&mut self, bytes: Vec<u8>) -> NanBox {
let buf = self.make_array_buffer_from_bytes(&bytes);
let bytes_h = self.array_buffer_bytes(buf).unwrap();
let view = self.realm.new_typed_array(bytes_h, buf, 0, bytes.len(), 1);
if let Some(proto) = self.intrinsic_proto("Uint8Array") {
self.realm.set_native_proto(view, proto);
}
NanBox::handle(view.to_raw())
}
pub(crate) fn syntax_error(&mut self, message: &str) -> ExecError {
let m = self.new_str(message);
ExecError::Throw(self.make_error(N_SYNTAX_ERROR, Some(m)))
}
fn read_written_record(&mut self, read: usize, written: usize) -> NanBox {
let obj = self.realm.new_object();
self.realm
.set_property(obj, "read", NanBox::number(read as f64));
self.realm
.set_property(obj, "written", NanBox::number(written as f64));
NanBox::handle(obj.to_raw())
}
pub(crate) fn uint8_to_base64(&mut self, options: NanBox) -> Result<NanBox, ExecError> {
let h = self.validate_uint8array("toBase64")?;
let alphabet = self.read_alphabet_option(options)?;
let omit_padding = if matches!(options.unpack(), Unpacked::Undefined) {
false
} else {
let oh = options.as_handle().map(Handle::from_raw).unwrap();
let v = self.read_member(oh, "omitPadding")?;
self.realm.truthy(v)
};
let (buffer, offset, len) = self.uint8array_bytes_view(h, "toBase64")?;
let snapshot = self.uint8array_snapshot(buffer, offset, len);
let out = to_base64(&snapshot, alphabet, omit_padding);
Ok(self.new_str_bytes(out))
}
pub(crate) fn uint8_to_hex(&mut self) -> Result<NanBox, ExecError> {
let h = self.validate_uint8array("toHex")?;
let (buffer, offset, len) = self.uint8array_bytes_view(h, "toHex")?;
let snapshot = self.uint8array_snapshot(buffer, offset, len);
let out = to_hex(&snapshot);
Ok(self.new_str_bytes(out))
}
pub(crate) fn uint8_set_from_base64(
&mut self,
string: NanBox,
options: NanBox,
) -> Result<NanBox, ExecError> {
let h = self.validate_uint8array("setFromBase64")?;
self.guard_view_immutable(h)?;
let Some(units) = self.require_string_arg(string) else {
return Err(self.type_error("setFromBase64 requires a string argument"));
};
let alphabet = self.read_alphabet_option(options)?;
let last_chunk = self.read_last_chunk_option(options)?;
let (buffer, offset, len) = self.uint8array_bytes_view(h, "setFromBase64")?;
let r = from_base64(&units, alphabet, last_chunk, len);
let written = r.bytes.len();
if let Some(store) = self.realm.bytes_at_mut(buffer) {
let dst = &mut store[offset..offset + len];
dst[..written].copy_from_slice(&r.bytes);
}
if r.error {
return Err(self.syntax_error("malformed base64 input"));
}
Ok(self.read_written_record(r.read, written))
}
pub(crate) fn uint8_set_from_hex(&mut self, string: NanBox) -> Result<NanBox, ExecError> {
let h = self.validate_uint8array("setFromHex")?;
self.guard_view_immutable(h)?;
let Some(units) = self.require_string_arg(string) else {
return Err(self.type_error("setFromHex requires a string argument"));
};
let (buffer, offset, len) = self.uint8array_bytes_view(h, "setFromHex")?;
let r = from_hex(&units, len);
let written = r.bytes.len();
if let Some(store) = self.realm.bytes_at_mut(buffer) {
let dst = &mut store[offset..offset + len];
dst[..written].copy_from_slice(&r.bytes);
}
if r.error {
return Err(self.syntax_error("malformed hex input"));
}
Ok(self.read_written_record(r.read, written))
}
pub(crate) fn uint8_from_base64(
&mut self,
string: NanBox,
options: NanBox,
) -> Result<NanBox, ExecError> {
let Some(units) = self.require_string_arg(string) else {
return Err(self.type_error("fromBase64 requires a string argument"));
};
let alphabet = self.read_alphabet_option(options)?;
let last_chunk = self.read_last_chunk_option(options)?;
let r = from_base64(&units, alphabet, last_chunk, usize::MAX);
if r.error {
return Err(self.syntax_error("malformed base64 input"));
}
Ok(self.new_uint8array_from(r.bytes))
}
pub(crate) fn uint8_from_hex(&mut self, string: NanBox) -> Result<NanBox, ExecError> {
let Some(units) = self.require_string_arg(string) else {
return Err(self.type_error("fromHex requires a string argument"));
};
let r = from_hex(&units, usize::MAX);
if r.error {
return Err(self.syntax_error("malformed hex input"));
}
Ok(self.new_uint8array_from(r.bytes))
}
}