diff --git a/crates/oxide_builtins/src/lib.rs b/crates/oxide_builtins/src/lib.rs index 18ea5708..552c7c4d 100644 --- a/crates/oxide_builtins/src/lib.rs +++ b/crates/oxide_builtins/src/lib.rs @@ -60,6 +60,8 @@ pub mod string; pub mod stubs; /// Symbol 内置对象实现(constructor/for/keyFor/toString)。 pub mod symbol; +/// TypedArray base64/hex 编解码纯函数(FromBase64/FromHex 状态机与编码输出)。 +pub mod ta_codec; /// Temporal 内置对象实现(目录模块树:Now / Instant / PlainDate / PlainTime / PlainDateTime / PlainMonthDay / PlainYearMonth / ZonedDateTime / Duration)。 pub mod temporal; /// TypedArray 内置对象实现(元素访问、fill/slice/subarray/set 等)。 diff --git a/crates/oxide_builtins/src/ta_codec.rs b/crates/oxide_builtins/src/ta_codec.rs new file mode 100644 index 00000000..04495c1d --- /dev/null +++ b/crates/oxide_builtins/src/ta_codec.rs @@ -0,0 +1,551 @@ +//! Uint8Array base64/hex 编解码纯函数层(无 VM 依赖)。 +//! +//! 承载 FromBase64/FromHex 状态机与编码输出,供 typed_array.rs 的 +//! setFromBase64/setFromHex/toBase64/toHex/fromBase64/fromHex 六个内置共用。 +//! 解码逐块产出 `(read, written)`:`read` 计全部已消费字符(含跳过的 ASCII +//! 空白),`written` 为已写入字节数;`Err(DecodeError)` 统一表示 SyntaxError。已解码 +//! 块在出错前经 `write` 回调逐个写出(setFrom 族"前块已写入后抛错"语义)。 +//! 解码失败统一以 `Err(DecodeError)` 表示,绑定层映射为 SyntaxError。 + +/// 解码失败标记(绑定层映射为 SyntaxError)。 +#[derive(Debug, Clone, Copy)] +pub struct DecodeError; + +/// base64 字母表:标准表(`+/`)与 URL 安全表(`-_`)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Base64Alphabet { + Standard, + Url, +} + +/// 尾块处理模式(lastChunkHandling 三取值)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LastChunkHandling { + Loose, + Strict, + StopBeforePartial, +} + +const B64_ALPHABET_STANDARD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const B64_ALPHABET_URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +fn b64_table(alphabet: Base64Alphabet) -> &'static [u8; 64] { + match alphabet { + Base64Alphabet::Standard => B64_ALPHABET_STANDARD, + Base64Alphabet::Url => B64_ALPHABET_URL, + } +} + +/// 判 ASCII 空白(0x09/0x0A/0x0C/0x0D/0x20 五码;VT 与所有非 ASCII 空格均不算)。 +fn is_base64_whitespace(unit: u16) -> bool { + matches!(unit, 0x09 | 0x0A | 0x0C | 0x0D | 0x20) +} + +/// 取 base64 数据字符的值(0-63);`=` 与任何非字母表字符返回 `None`。 +fn base64_value(unit: u16, alphabet: Base64Alphabet) -> Option { + match unit { + 0x41..=0x5a => Some(unit as u32 - 0x41), + 0x61..=0x7a => Some(unit as u32 - 0x61 + 26), + 0x30..=0x39 => Some(unit as u32 - 0x30 + 52), + 0x2b if alphabet == Base64Alphabet::Standard => Some(62), + 0x2f if alphabet == Base64Alphabet::Standard => Some(63), + 0x2d if alphabet == Base64Alphabet::Url => Some(62), + 0x5f if alphabet == Base64Alphabet::Url => Some(63), + _ => None, + } +} + +/// 解码一个满 4 字符块:校验 padding 位置(`=` 只可居 3/4 位)与数据字符, +/// strict 模式另查 padding 位必须为 0。 +/// +/// # 边界与前提 +/// - `chunk` 恰 4 字符;调用方保证只在此处解码满块。 +/// - 返回 `(24 位值, 解码字节数 1-3)`:24 位值按 4 槽位左对齐(padding 槽补 0), +/// 字节数 = 3 - padding 数。 +fn decode_full_chunk( + chunk: [u16; 4], alphabet: Base64Alphabet, handling: LastChunkHandling, +) -> Result<(u32, usize), DecodeError> { + let pad = match (chunk[2] == 0x3d, chunk[3] == 0x3d) { + (false, false) => 0, + (false, true) => 1, + (true, true) => 2, + // `XX=X`:padding 居第 3 位,四种模式全非法。 + (true, false) => return Err(DecodeError), + }; + let data = &chunk[..4 - pad]; + let mut bits: u32 = 0; + for &c in data { + let v = base64_value(c, alphabet).ok_or(DecodeError)?; + bits = (bits << 6) | v; + } + bits <<= 6 * pad as u32; + if handling == LastChunkHandling::Strict && pad > 0 { + // padding 位即末数据字符的低 2/4 位(pad 1 查 2 位、pad 2 查 4 位)。 + let last = base64_value(*data.last().unwrap(), alphabet).unwrap(); + let mask = if pad == 1 { 0b11 } else { 0b1111 }; + if last & mask != 0 { + return Err(DecodeError); + } + } + Ok((bits, 3 - pad)) +} + +/// 把满块解码出的 24 位值按字节数经回调写出,返回新的已写字节数。 +fn write_chunk_bytes(write: &mut dyn FnMut(usize, u8), written: usize, bits: u32, count: usize) -> usize { + for i in 0..count { + write(written + i, (bits >> (16 - 8 * i)) as u8); + } + written + count +} + +/// 尾块(1-3 个非空白字符,已越过全部满块)的终态裁决。 +/// +/// # 边界与前提 +/// - 1 字符尾块:`=` 三种模式全抛;数据字符仅 stop-before-partial 停(成功); +/// - 含 `=` 尾块:唯一放行形态是 3 字符 `XX=` 且 stop-before-partial(停在不写 +/// 出的块前);`X==`/`X=`/`=` 居首等其余 padding 形态三种模式全抛; +/// - 全数据 2-3 字符尾块:strict 抛、stop-before-partial 停在块前、loose 解 +/// `clen - 1` 字节(2 字符 1 字节、3 字符 2 字节)并写出。 +/// - 成功 `read`:解出字节的 loose 路径取块后消费位置;各"停在块前"路径 +/// (sbp、余量不足)取块前位置 `chunk_start`。 +#[allow(clippy::too_many_arguments)] +fn finish_partial( + chunk: [u16; 4], clen: usize, alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option, + write: &mut dyn FnMut(usize, u8), mut written: usize, chunk_start: usize, read: usize, +) -> Result<(usize, usize), DecodeError> { + if (0..clen).any(|i| chunk[i] == 0x3d) { + let trailing = (0..clen).rev().take_while(|&i| chunk[i] == 0x3d).count(); + if clen == 3 && trailing == 1 && handling == LastChunkHandling::StopBeforePartial { + for &c in &chunk[..2] { + base64_value(c, alphabet).ok_or(DecodeError)?; + } + return Ok((chunk_start, written)); + } + return Err(DecodeError); + } + for &c in &chunk[..clen] { + base64_value(c, alphabet).ok_or(DecodeError)?; + } + match handling { + LastChunkHandling::StopBeforePartial => Ok((chunk_start, written)), + LastChunkHandling::Strict => Err(DecodeError), + LastChunkHandling::Loose => { + if clen == 1 { + return Err(DecodeError); + } + // maxLength 余量不足时不消费该块直接停(不写出)。 + if let Some(m) = max_len { + if written + clen - 1 > m { + return Ok((chunk_start, written)); + } + } + let v = |i: usize| base64_value(chunk[i], alphabet).unwrap(); + write(written, (v(0) << 2 | v(1) >> 4) as u8); + written += 1; + if clen == 3 { + write(written, ((v(1) << 4 | v(2) >> 2) & 0xFF) as u8); + written += 1; + } + Ok((read, written)) + } + } +} + +/// FromBase64 状态机:按块消费输入串,已解码字节经 `write` 回调逐块写出。 +/// +/// # 步骤 +/// 1. `max_len == 0` 短路:输入整体忽略,直接成功空结果。 +/// 2. 循环收集至多 4 个非空白字符(跳过五码 ASCII 空白)组成块。 +/// 3. 满块先过 maxLength 余量判定(不足则块前停、不消费、不写出),再解码写出; +/// 写出后恰好补齐 maxLength 即成功返回。 +/// 4. 块不足 4 字符时走终态裁决([`finish_partial`])。 +/// +/// # 边界与前提 +/// - `units` 为输入串的 UTF-16 单元序列;非 ASCII 码元一律是非法字符。 +/// - 出错时 `write` 已产生的写入保留(调用方承载"前块已写入"语义)。 +/// - `Ok((read, written))` 中 `read` 为成功语义下的消费位置(含跳过空白, +/// 停止路径为块前位置)。 +pub fn decode_base64( + units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option, + write: &mut dyn FnMut(usize, u8), +) -> Result<(usize, usize), DecodeError> { + if max_len == Some(0) { + return Ok((0, 0)); + } + let n = units.len(); + let mut read = 0usize; + let mut written = 0usize; + loop { + let chunk_start = read; + let mut chunk = [0u16; 4]; + let mut clen = 0usize; + while clen < 4 && read < n { + let c = units[read]; + read += 1; + if is_base64_whitespace(c) { + continue; + } + chunk[clen] = c; + clen += 1; + } + match clen { + 4 => { + if chunk[2] == 0x3d || chunk[3] == 0x3d { + // 带 padding 的满块只可作末块:其后还有非空白字符时 + // ("extra padding")在写出该块字节前抛。 + if units[read..].iter().any(|&c| !is_base64_whitespace(c)) { + return Err(DecodeError); + } + } + let (bits, count) = decode_full_chunk(chunk, alphabet, handling)?; + if let Some(m) = max_len { + if written + count > m { + return Ok((chunk_start, written)); + } + } + written = write_chunk_bytes(write, written, bits, count); + if let Some(m) = max_len { + if written == m { + return Ok((read, written)); + } + } + } + 0 => return Ok((read, written)), + _ => return finish_partial(chunk, clen, alphabet, handling, max_len, write, written, chunk_start, read), + } + } +} + +/// 取单个 hex 字符值(大小写宽容);非 hex 字符返回 `None`。 +fn hex_value(unit: u16) -> Option { + match unit { + 0x30..=0x39 => Some(unit as u8 - 0x30), + 0x61..=0x66 => Some(unit as u8 - 0x61 + 10), + 0x41..=0x46 => Some(unit as u8 - 0x41 + 10), + _ => None, + } +} + +/// FromHex 状态机:无空白跳过、无 padding;长度奇数在最前判定 +/// (`read` 0、零写入);坏字符抛错时保留已写出的前字节。 +/// +/// # 边界与前提 +/// - `units` 为输入串的 UTF-16 单元序列;任何非 hex 字符(含所有空白)非法。 +/// - 每对解码前判 `written + 1 > max_len` 则对该对前停(不消费、不写出)。 +pub fn decode_hex( + units: &[u16], max_len: Option, write: &mut dyn FnMut(usize, u8), +) -> Result<(usize, usize), DecodeError> { + if units.len() % 2 == 1 { + return Err(DecodeError); + } + let mut written = 0usize; + let mut read = 0usize; + for (i, pair) in units.chunks_exact(2).enumerate() { + let hi = hex_value(pair[0]).ok_or(DecodeError)?; + let lo = hex_value(pair[1]).ok_or(DecodeError)?; + if let Some(m) = max_len { + if written + 1 > m { + return Ok((i * 2, written)); + } + } + write(written, hi * 16 + lo); + written += 1; + read = i * 2 + 2; + } + Ok((read, written)) +} + +/// 编码 base64:标准表或 URL 表;`omit_padding` 时剥尾部 `=`。 +pub fn encode_base64(bytes: &[u8], alphabet: Base64Alphabet, omit_padding: bool) -> String { + let table = b64_table(alphabet); + let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); + for chunk in bytes.chunks(3) { + let bits = (chunk[0] as u32) << 16 + | (chunk.get(1).copied().unwrap_or(0) as u32) << 8 + | chunk.get(2).copied().unwrap_or(0) as u32; + out.push(table[(bits >> 18) as usize] as char); + out.push(table[((bits >> 12) & 0x3f) as usize] as char); + if chunk.len() > 1 { + out.push(table[((bits >> 6) & 0x3f) as usize] as char); + } else if !omit_padding { + out.push('='); + } + if chunk.len() > 2 { + out.push(table[(bits & 0x3f) as usize] as char); + } else if !omit_padding { + out.push('='); + } + } + out +} + +/// 编码小写两位 hex。 +pub fn encode_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for &b in bytes { + out.push(HEX[(b >> 4) as usize] as char); + out.push(HEX[(b & 0x0f) as usize] as char); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn units(s: &str) -> Vec { + s.chars().map(|c| c as u16).collect() + } + + fn decode( + units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option, + ) -> Result<(usize, Vec), DecodeError> { + let mut bytes = Vec::new(); + decode_base64(units, alphabet, handling, max_len, &mut |_, b| bytes.push(b)) + .map(|(r, w)| (r, bytes.into_iter().take(w).collect())) + } + + fn decode_hex_full(units: &[u16], max_len: Option) -> Result<(usize, Vec), DecodeError> { + let mut bytes = Vec::new(); + decode_hex(units, max_len, &mut |_, b| bytes.push(b)).map(|(r, w)| (r, bytes.into_iter().take(w).collect())) + } + + /// RFC 4648 标准向量:编码/解码双向。 + #[test] + fn rfc4648_vectors_roundtrip() { + let vectors: [(&str, &[u8]); 7] = [ + ("", &[]), + ("Zg==", &[102]), + ("Zm8=", &[102, 111]), + ("Zm9v", &[102, 111, 111]), + ("Zm9vYg==", &[102, 111, 111, 98]), + ("Zm9vYmE=", &[102, 111, 111, 98, 97]), + ("Zm9vYmFy", &[102, 111, 111, 98, 97, 114]), + ]; + for (s, bytes) in vectors { + assert_eq!(encode_base64(bytes, Base64Alphabet::Standard, false), s); + let (read, decoded) = decode(&units(s), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap(); + assert_eq!(decoded, *bytes); + assert_eq!(read, s.chars().count()); + } + } + + #[test] + fn encode_base64_url_alphabet_and_omit_padding() { + assert_eq!(encode_base64(&[199, 239, 242], Base64Alphabet::Standard, false), "x+/y"); + assert_eq!(encode_base64(&[199, 239, 242], Base64Alphabet::Url, false), "x-_y"); + assert_eq!(encode_base64(&[199, 239], Base64Alphabet::Standard, false), "x+8="); + assert_eq!(encode_base64(&[199, 239], Base64Alphabet::Standard, true), "x+8"); + assert_eq!(encode_base64(&[255], Base64Alphabet::Standard, true), "/w"); + assert_eq!(encode_base64(&[255], Base64Alphabet::Url, true), "_w"); + } + + #[test] + fn decode_base64_whitespace_and_read_count() { + for ws in [' ', '\t', '\n', '\u{0C}', '\r'] { + let s = format!("Z{ws}g=="); + let (read, bytes) = decode(&units(&s), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap(); + assert_eq!(bytes, vec![102]); + assert_eq!(read, 5); + } + // VT 不是 ASCII 空白:非法字符。 + assert!(decode(&units("Z\u{0B}g=="), Base64Alphabet::Standard, LastChunkHandling::Loose, None).is_err()); + } + + #[test] + fn decode_base64_strict_padding_bits() { + // 末数据字符低 2 位非 0(`h` = 33 = 0b100001):strict 抛,其余过。 + for (h, ok) in [ + (LastChunkHandling::Loose, true), + (LastChunkHandling::Strict, false), + (LastChunkHandling::StopBeforePartial, true), + ] { + let r = decode(&units("ZXhhZh=="), Base64Alphabet::Standard, h, None); + assert_eq!(r.is_ok(), ok, "ZXhhZh== {h:?}"); + if ok { + assert_eq!(r.unwrap().1, vec![101, 120, 97, 102]); + } + } + // 低 4 位非 0(`h` 居 pad 2 末位):strict 抛;`g` 低 4 位全 0 则过。 + assert!(decode(&units("ZXhhZg=="), Base64Alphabet::Standard, LastChunkHandling::Strict, None).is_ok()); + assert!(decode(&units("Zg1="), Base64Alphabet::Standard, LastChunkHandling::Strict, None).is_err()); + // pad 2 块字节值:`Zg==` → 102。 + assert_eq!( + decode(&units("Zg=="), Base64Alphabet::Standard, LastChunkHandling::Loose, None) + .unwrap() + .1, + vec![102] + ); + // pad 1 块字节值:`Zm8=` → [102, 111]。 + assert_eq!( + decode(&units("Zm8="), Base64Alphabet::Standard, LastChunkHandling::Loose, None) + .unwrap() + .1, + vec![102, 111] + ); + } + + #[test] + fn decode_base64_last_chunk_matrix() { + let cases: [(&str, [bool; 3]); 13] = [ + // (输入, [loose, strict, sbp] 是否成功) + ("A", [false, false, true]), + ("ABCDA", [false, false, true]), + ("AA=", [false, false, true]), + ("aQ=", [false, false, true]), + ("ABCDAA=", [false, false, true]), + ("AAA", [true, false, true]), + ("ZXhhZg", [true, false, true]), + ("ZXhhZg=", [false, false, true]), + ("ZXhhZg===", [false, false, false]), + ("=", [false, false, false]), + ("A==", [false, false, false]), + ("AAAA=", [false, false, false]), + ("ZXhhZgg", [true, false, true]), + ]; + for (s, expect) in cases { + for (h, ok) in [ + (LastChunkHandling::Loose, expect[0]), + (LastChunkHandling::Strict, expect[1]), + (LastChunkHandling::StopBeforePartial, expect[2]), + ] { + let r = decode(&units(s), Base64Alphabet::Standard, h, None); + assert_eq!(r.is_ok(), ok, "{s:?} {h:?}"); + } + } + // sbp 停止路径:前块保留、尾块不消费。 + let (read, bytes) = + decode(&units("ABCDAA="), Base64Alphabet::Standard, LastChunkHandling::StopBeforePartial, None).unwrap(); + assert_eq!((read, bytes), (4, vec![0, 16, 131])); + let (read, bytes) = + decode(&units("ZXhhZg"), Base64Alphabet::Standard, LastChunkHandling::StopBeforePartial, None).unwrap(); + assert_eq!((read, bytes), (4, vec![101, 120, 97])); + // loose 对 2 字符尾块解 1 字节。 + let (read, bytes) = decode(&units("ZXhhZg"), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap(); + assert_eq!((read, bytes), (6, vec![101, 120, 97, 102])); + // loose 对 3 字符尾块解 2 字节。 + let (read, bytes) = + decode(&units("ZXhhZgg"), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap(); + assert_eq!((read, bytes), (7, vec![101, 120, 97, 102, 8])); + } + + #[test] + fn decode_base64_max_length_stop_timing() { + // 块前停:第 2 块 3 字节会超限,不消费(read 停在前值 4)。 + let (read, bytes) = + decode(&units("Zm9vYmFy"), Base64Alphabet::Standard, LastChunkHandling::Loose, Some(5)).unwrap(); + assert_eq!((read, bytes), (4, vec![102, 111, 111])); + // 恰好补齐:`YmE=` 出 2 字节补到 5。 + let (read, bytes) = + decode(&units("Zm9vYmE="), Base64Alphabet::Standard, LastChunkHandling::Loose, Some(5)).unwrap(); + assert_eq!((read, bytes), (8, vec![102, 111, 111, 98, 97])); + // 零长短路:垃圾输入整体忽略。 + let (read, bytes) = + decode(&units("aaaa#"), Base64Alphabet::Standard, LastChunkHandling::Strict, Some(0)).unwrap(); + assert_eq!((read, bytes), (0, Vec::new())); + // 尾 3 字符块余量不足:不消费。 + let (read, bytes) = + decode(&units("Zm9vYmE"), Base64Alphabet::Standard, LastChunkHandling::Loose, Some(3)).unwrap(); + assert_eq!((read, bytes), (4, vec![102, 111, 111])); + } + + #[test] + fn decode_base64_illegal_characters() { + for s in ["Zm.9v", "Zm9v^", "Zg==&", "Z\u{2212}==", "Zg\u{00A0}==", "Zg\u{2009}==", "Zg\u{2028}=="] { + assert!( + decode(&units(s), Base64Alphabet::Standard, LastChunkHandling::Loose, None).is_err(), + "{s:?}" + ); + } + } + + #[test] + fn decode_base64_url_alphabet_decode() { + let (read, bytes) = decode(&units("x-_y"), Base64Alphabet::Url, LastChunkHandling::Loose, None).unwrap(); + assert_eq!((read, bytes), (4, vec![199, 239, 242])); + // URL 表中标准表符号非法。 + assert!(decode(&units("x+/y"), Base64Alphabet::Url, LastChunkHandling::Loose, None).is_err()); + assert!(decode(&units("x-_y"), Base64Alphabet::Standard, LastChunkHandling::Loose, None).is_err()); + } + + #[test] + fn decode_base64_writes_up_to_error() { + // 坏字符前已写出的字节保留(setFrom 语义同)。 + let mut out = Vec::new(); + let r = decode_base64( + &units("MjYyZm.9v"), + Base64Alphabet::Standard, + LastChunkHandling::Loose, + None, + &mut |i, b| out.insert(i, b), + ); + assert!(r.is_err()); + assert_eq!(out, vec![50, 54, 50]); + // 带 padding 满块后随非空白字符:该块字节不写出(extra padding)。 + let mut out = Vec::new(); + let r = decode_base64( + &units("MjYyZg==="), + Base64Alphabet::Standard, + LastChunkHandling::Loose, + None, + &mut |i, b| out.insert(i, b), + ); + assert!(r.is_err()); + assert_eq!(out, vec![50, 54, 50]); + } + + #[test] + fn decode_hex_vectors_and_case_insensitive() { + let cases: [(&str, &[u8]); 5] = [ + ("", &[]), + ("66", &[102]), + ("666f", &[102, 111]), + ("666F", &[102, 111]), + ("666f6f626172", &[102, 111, 111, 98, 97, 114]), + ]; + for (s, bytes) in cases { + let (read, decoded) = decode_hex_full(&units(s), None).unwrap(); + assert_eq!(decoded, *bytes); + assert_eq!(read, s.len()); + } + } + + #[test] + fn decode_hex_odd_and_max_length() { + assert!(decode_hex_full(&units("a"), None).is_err()); + assert!(decode_hex_full(&units("aaa"), None).is_err()); + // 奇数在最前判定:零长目标也抛(零写入)。 + assert!(decode_hex_full(&units("1"), Some(0)).is_err()); + // 对前停:2 字节目标下 `aabbcc` 停在第 3 对前(read 4)。 + let (read, bytes) = decode_hex_full(&units("aabbcc"), Some(2)).unwrap(); + assert_eq!((read, bytes), (4, vec![0xaa, 0xbb])); + // 恰好补齐。 + let (read, bytes) = decode_hex_full(&units("aabbcc"), Some(3)).unwrap(); + assert_eq!((read, bytes), (6, vec![0xaa, 0xbb, 0xcc])); + } + + #[test] + fn decode_hex_illegal_characters() { + for s in ["a.a", "aa^", "a a", "a\ta", "a\u{00A0}a", "a\u{2009}a", "a\u{2028}a"] { + assert!(decode_hex_full(&units(s), None).is_err(), "{s:?}"); + } + } + + #[test] + fn decode_hex_writes_up_to_error() { + // 前对写出、坏对抛错。 + let mut out = Vec::new(); + let r = decode_hex(&units("aaag"), None, &mut |i, b| out.insert(i, b)); + assert!(r.is_err()); + assert_eq!(out, vec![0xaa]); + } + + #[test] + fn encode_hex_lowercase() { + assert_eq!(encode_hex(&[]), ""); + assert_eq!(encode_hex(&[102]), "66"); + assert_eq!(encode_hex(&[102, 111, 111, 98, 97, 114]), "666f6f626172"); + assert_eq!(encode_hex(&[170, 187, 204]), "aabbcc"); + } +} diff --git a/crates/oxide_builtins/src/typed_array.rs b/crates/oxide_builtins/src/typed_array.rs index eb1d3655..4fb91d85 100644 --- a/crates/oxide_builtins/src/typed_array.rs +++ b/crates/oxide_builtins/src/typed_array.rs @@ -1671,3 +1671,233 @@ pub fn typed_array_from(vm: &mut H, args: &[u8]) -> NativeResult { } NativeResult::Ok(new_obj) } + +// ── Uint8Array base64/hex 六方法族 ───────────────────────────────────────────── + +/// ValidateUint8Array:接收者须为 kind 恰为 Uint8 的 TypedArray 对象; +/// Uint8Clamped 与其他种类一律抛 TypeError。 +fn validate_uint8_array(vm: &mut H, this_val: JsValue) -> Result { + let view = get_typed_array_data(vm, this_val)?; + if view.kind != TypedArrayKind::Uint8 { + return Err(type_error(vm, "Uint8Array method called on incompatible receiver")); + } + Ok(view) +} + +/// GetOptionsObject:undefined 直通;其余值过 ToObject,失败抛 TypeError。 +fn ta_options_object(vm: &mut H, value: JsValue) -> Result, JsValue> { + if value.is_undefined() { + return Ok(None); + } + match oxide_runtime_api::to_object(value, vm) { + Ok(obj) => Ok(Some(obj)), + Err(e) => Err(type_error(vm, &e)), + } +} + +/// 读只收字符串原始值的选项字段(alphabet/lastChunkHandling):undefined 取 +/// 默认值;装箱串/抛错 toString 对象等一律抛 TypeError,不做 ToPrimitive。 +fn ta_string_option( + vm: &mut H, opts: &JsObject, opts_val: JsValue, key: &str, default: &'static str, +) -> Result { + let key_si = vm.string_key_si(key); + let v = vm + .ordinary_get(opts, key_si, opts_val) + .map_err(|e| crate::iterator::engine_error(vm, &e))?; + if v.is_undefined() { + return Ok(default.to_string()); + } + if !v.is_string() { + return Err(type_error(vm, &format!("{key} must be a string"))); + } + Ok(vm.lookup_str(v).unwrap_or_default()) +} + +/// ToBoolean:仅 null/undefined/false/NaN/±0 为假。 +fn ta_to_boolean(v: JsValue) -> bool { + if v.is_null() || v.is_undefined() { + return false; + } + if v.is_bool() { + return v.as_bool(); + } + if v.is_int() { + return v.as_int() != 0; + } + if v.is_double() { + let d = v.as_double(); + return !d.is_nan() && d != 0.0; + } + true +} + +/// 取选项中的 alphabet 值并归一到字母表枚举;非 "base64"/"base64url" 抛 TypeError。 +fn ta_alphabet_option( + vm: &mut H, opts: &JsObject, opts_val: JsValue, +) -> Result { + let s = ta_string_option(vm, opts, opts_val, "alphabet", "base64")?; + match s.as_str() { + "base64url" => Ok(crate::ta_codec::Base64Alphabet::Url), + "base64" => Ok(crate::ta_codec::Base64Alphabet::Standard), + _ => Err(type_error(vm, "alphabet must be 'base64' or 'base64url'")), + } +} + +/// 取选项中的 lastChunkHandling 值并归一到模式枚举;非三取值抛 TypeError。 +fn ta_handling_option( + vm: &mut H, opts: &JsObject, opts_val: JsValue, +) -> Result { + let s = ta_string_option(vm, opts, opts_val, "lastChunkHandling", "loose")?; + match s.as_str() { + "strict" => Ok(crate::ta_codec::LastChunkHandling::Strict), + "stop-before-partial" => Ok(crate::ta_codec::LastChunkHandling::StopBeforePartial), + "loose" => Ok(crate::ta_codec::LastChunkHandling::Loose), + _ => Err(type_error(vm, "lastChunkHandling must be 'loose', 'strict', or 'stop-before-partial'")), + } +} + +/// setFromBase64/fromBase64 共用入口:string 类型检查先于选项读取, +/// 选项读完跑 FromBase64 状态机,已解码字节经 `write` 逐块写出; +/// `max_len` 为 None 时静态方法无界。 +fn ta_decode_base64( + vm: &mut H, string: JsValue, options: JsValue, max_len: Option, write: &mut dyn FnMut(usize, u8), +) -> Result<(usize, usize), JsValue> { + if !string.is_string() { + return Err(type_error(vm, "string argument required")); + } + let opts = ta_options_object(vm, options)?; + let mut alphabet = crate::ta_codec::Base64Alphabet::Standard; + let mut handling = crate::ta_codec::LastChunkHandling::Loose; + if let Some(opts_val) = opts { + let obj = unsafe { &*opts_val.as_js_object_ptr() }; + // SAFETY: opts_val 来自 ToObject 成功路径,必为活动对象指针。 + alphabet = ta_alphabet_option(vm, obj, opts_val)?; + handling = ta_handling_option(vm, obj, opts_val)?; + } + let units = vm.string_units(string).into_owned(); + match crate::ta_codec::decode_base64(&units, alphabet, handling, max_len, write) { + Ok(r) => Ok(r), + Err(_) => Err(crate::error::create_syntax_error(vm, "invalid base64 input")), + } +} + +/// setFrom* 结果对象 `{ read, written }`:普通对象 + 两数据属性。 +fn ta_read_written_result(vm: &mut H, read: usize, written: usize) -> JsValue { + let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; + let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto))); + let read_si = vm.kernel_core().perm_interner().intern("read").0; + let written_si = vm.kernel_core().perm_interner().intern("written").0; + let obj_ref = unsafe { &mut *obj }; + vm.set_or_create_prop_value(obj_ref, read_si, JsValue::int(read as i32)); + vm.set_or_create_prop_value(obj_ref, written_si, JsValue::int(written as i32)); + JsValue::from_js_object(obj) +} + +/// `%Uint8Array%.prototype.toBase64(options)`:ValidateUint8Array 先于选项 +/// 副作用,按字母表与 omitPadding 编码视图字节。 +pub fn uint8array_to_base64(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(validate_uint8_array(vm, this_val)); + let opts = native_try!(ta_options_object(vm, if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() })); + let mut alphabet = crate::ta_codec::Base64Alphabet::Standard; + let mut omit_padding = false; + if let Some(opts_val) = opts { + let obj = unsafe { &*opts_val.as_js_object_ptr() }; + // SAFETY: opts_val 来自 ToObject 成功路径,必为活动对象指针。 + alphabet = native_try!(ta_alphabet_option(vm, obj, opts_val)); + let key_si = vm.string_key_si("omitPadding"); + let v = native_try!(vm + .ordinary_get(obj, key_si, opts_val) + .map_err(|e| crate::iterator::engine_error(vm, &e))); + omit_padding = ta_to_boolean(v); + } + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let buffer = unsafe { &*buffer_ptr }; + // SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。 + NativeResult::Ok(vm.new_string_owned(crate::ta_codec::encode_base64( + &buffer[view.byte_offset..view.byte_offset + view.length], + alphabet, + omit_padding, + ))) +} + +/// `%Uint8Array%.prototype.toHex()`:ValidateUint8Array 后编码小写两位 hex。 +pub fn uint8array_to_hex(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(validate_uint8_array(vm, this_val)); + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let buffer = unsafe { &*buffer_ptr }; + // SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。 + NativeResult::Ok( + vm.new_string_owned(crate::ta_codec::encode_hex(&buffer[view.byte_offset..view.byte_offset + view.length])), + ) +} + +/// `%Uint8Array%.prototype.setFromBase64(string, options)`:解码写入视图, +/// 返回 `{ read, written }`;前块已写入后遇错抛 SyntaxError(前字节保留)。 +pub fn uint8array_set_from_base64(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(validate_uint8_array(vm, this_val)); + let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let buffer = unsafe { &mut *buffer_ptr }; + // SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。 + let start = view.byte_offset; + let (read, written) = native_try!(ta_decode_base64(vm, string, options, Some(view.length), &mut |idx, b| { + buffer[start + idx] = b; + })); + NativeResult::Ok(ta_read_written_result(vm, read, written)) +} + +/// `%Uint8Array%.prototype.setFromHex(string)`:无选项;奇数长度最前判定 +/// (零写入)抛 SyntaxError;坏字符保留前字节写入后抛。 +pub fn uint8array_set_from_hex(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(validate_uint8_array(vm, this_val)); + let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if !string.is_string() { + return NativeResult::Err(type_error(vm, "string argument required")); + } + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let buffer = unsafe { &mut *buffer_ptr }; + // SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。 + let start = view.byte_offset; + let units = vm.string_units(string).into_owned(); + let (read, written) = match crate::ta_codec::decode_hex(&units, Some(view.length), &mut |idx, b| { + buffer[start + idx] = b; + }) { + Ok(r) => r, + Err(_) => return NativeResult::Err(crate::error::create_syntax_error(vm, "invalid hex input")), + }; + NativeResult::Ok(ta_read_written_result(vm, read, written)) +} + +/// `Uint8Array.fromBase64(string, options)`:不读 this,结果 proto 恒为 +/// %Uint8Array%.prototype(不经 species/构造器)。 +pub fn uint8array_from_base64(vm: &mut H, args: &[u8]) -> NativeResult { + let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let mut bytes = Vec::new(); + native_try!(ta_decode_base64(vm, string, options, None, &mut |_, b| bytes.push(b))); + let len = bytes.len(); + let buffer = JsValue::from_js_object(new_array_buffer(vm, bytes)); + NativeResult::Ok(JsValue::from_js_object(create_typed_array(vm, TypedArrayKind::Uint8, buffer, 0, len))) +} + +/// `Uint8Array.fromHex(string)`:不读 this;奇数长度最前判定抛 SyntaxError。 +pub fn uint8array_from_hex(vm: &mut H, args: &[u8]) -> NativeResult { + let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if !string.is_string() { + return NativeResult::Err(type_error(vm, "string argument required")); + } + let units = vm.string_units(string).into_owned(); + let mut bytes = Vec::new(); + match crate::ta_codec::decode_hex(&units, None, &mut |_, b| bytes.push(b)) { + Ok(_) => {} + Err(_) => return NativeResult::Err(crate::error::create_syntax_error(vm, "invalid hex input")), + } + let len = bytes.len(); + let buffer = JsValue::from_js_object(new_array_buffer(vm, bytes)); + NativeResult::Ok(JsValue::from_js_object(create_typed_array(vm, TypedArrayKind::Uint8, buffer, 0, len))) +} diff --git a/crates/oxide_kernel/src/builtin/construct.rs b/crates/oxide_kernel/src/builtin/construct.rs index 2328fa86..ca1c216d 100644 --- a/crates/oxide_kernel/src/builtin/construct.rs +++ b/crates/oxide_kernel/src/builtin/construct.rs @@ -159,11 +159,12 @@ fn make_typed_array_abstract_ctor( } pub(crate) fn make_typed_array_family( - string_forge: &PermInterner, shape_forge: &ShapeForge, labels: BuiltinLabels, object_proto: &P, + string_forge: &PermInterner, shape_forge: &ShapeForge, labels: BuiltinLabels, array_proto: &P, ) -> TypedArrayFamily { - let obj_proto_val = JsValue::from_js_object(object_proto.as_ptr() as *mut JsObject); - // 给共享原型开 "constructor" 槽位(占位值在 wire 时填抽象构造器)。 - let mut typed_array_proto_obj = JsObject::new_empty(EMPTY_SHAPE_ID, obj_proto_val); + let array_proto_val = JsValue::from_js_object(array_proto.as_ptr() as *mut JsObject); + // 给共享原型开 "constructor" 槽位(占位值在 wire 时填抽象构造器); + // [[Prototype]] 按规范接 Array.prototype(toString 经绑定层共享同一函数对象)。 + let mut typed_array_proto_obj = JsObject::new_empty(EMPTY_SHAPE_ID, array_proto_val); let ctor_si = labels.constructor; let proto_shape = shape_forge.make_shape(typed_array_proto_obj.shape_id(), ctor_si); typed_array_proto_obj.set_shape_id(proto_shape); @@ -252,9 +253,9 @@ fn set_proto_if_changed(obj: &P, proto: JsValue) { /// TypedArray 家族共享对),经 `wire_ctor_proto` 覆盖占位槽; /// 2. 23 个非 TypedArray 构造器的 `[[Prototype]]` → Function.prototype /// (标准内置函数对象均继承 Function.prototype); -/// 3. 25 个非 Object 原型的 `[[Prototype]]` → Object.prototype,另含 +/// 3. 24 个非 Object 原型的 `[[Prototype]]` → Object.prototype,另含 /// Temporal 命名空间对象、Temporal.now、Console 单例与 Math/JSON -/// 命名空间对象; +/// 命名空间对象;%TypedArray% 共享原型例外接 Array.prototype; /// 4. %IteratorPrototype% → Object.prototype,6 个集合迭代器原型 /// → %IteratorPrototype%; /// 5. TypedArray 家族:11 个具体原型 → %TypedArray% 共享原型,11 个具体 @@ -337,7 +338,7 @@ pub(crate) fn wire_builtin_world_links(world: &BuiltinWorld) { } let obj_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); - let non_object_protos: [&P; 25] = [ + let non_object_protos: [&P; 24] = [ &world.array_proto, &world.function_proto, &world.string_proto, @@ -351,7 +352,6 @@ pub(crate) fn wire_builtin_world_links(world: &BuiltinWorld) { &world.regexp_proto, &world.array_buffer_proto, &world.data_view_proto, - &world.typed_array_proto, &world.instant_proto, &world.plain_date_proto, &world.plain_time_proto, @@ -367,6 +367,9 @@ pub(crate) fn wire_builtin_world_links(world: &BuiltinWorld) { for proto in &non_object_protos { set_proto_if_changed(proto, obj_proto_val); } + // %TypedArray%.prototype → Array.prototype(toString 语义经此继承)。 + let array_proto_val = JsValue::from_js_object(world.array_proto.as_ptr() as *mut JsObject); + set_proto_if_changed(&world.typed_array_proto, array_proto_val); // Temporal 命名空间对象(非构造器)继承 Object.prototype。 set_proto_if_changed(&world.temporal_object, obj_proto_val); @@ -468,7 +471,7 @@ impl BuiltinWorld { let (array_buffer_proto, array_buffer_constructor) = make_named_pair(string_forge, shape_forge, labels, "ArrayBuffer"); let (data_view_proto, data_view_constructor) = make_named_pair(string_forge, shape_forge, labels, "DataView"); - let typed_arrays = make_typed_array_family(string_forge, shape_forge, labels, &object_proto); + let typed_arrays = make_typed_array_family(string_forge, shape_forge, labels, &array_proto); let sym_match = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_replace = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); diff --git a/crates/oxide_kernel/src/builtin/rebuild.rs b/crates/oxide_kernel/src/builtin/rebuild.rs index 56db6f86..b335237a 100644 --- a/crates/oxide_kernel/src/builtin/rebuild.rs +++ b/crates/oxide_kernel/src/builtin/rebuild.rs @@ -311,7 +311,7 @@ impl BuiltinWorld { (current.data_view_proto.clone(), current.data_view_constructor.clone()) }; let typed_arrays = if dirty.typed_array_family { - make_typed_array_family(string_forge, shape_forge, labels, &object_proto) + make_typed_array_family(string_forge, shape_forge, labels, &array_proto) } else { TypedArrayFamily { typed_array_proto: current.typed_array_proto.clone(), diff --git a/crates/oxide_vm/src/bindings/bind_typed_array.rs b/crates/oxide_vm/src/bindings/bind_typed_array.rs index b1d591c0..c0fea76c 100644 --- a/crates/oxide_vm/src/bindings/bind_typed_array.rs +++ b/crates/oxide_vm/src/bindings/bind_typed_array.rs @@ -238,6 +238,103 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: oxide_builtins::typed_array::typed_array_to_string_tag_getter:: as *const (), ); + // 共享原型的 own toString 与 Array.prototype.toString 共享同一函数对象 + // (规范:同一内置函数对象;身份与描述符均按 own 属性验收)。 + let to_string_si = core.perm_interner().intern("toString").0; + let array_proto_ref = unsafe { &*session.builtin_world().array_proto.as_ptr() }; + // SAFETY: array_proto 由 BuiltinWorld 构造期建立,session 存活期内恒有效。 + if let Some(pos) = core.shape_forge().lookup_position(array_proto_ref.shape_id(), to_string_si) { + let value = array_proto_ref.get_prop_at(pos); + let new_shape = core.shape_forge().make_shape(shared_proto.shape_id(), to_string_si); + shared_proto.set_shape_id(new_shape); + let new_pos = shared_proto.push_prop(value); + shared_proto.set_data_meta(new_pos, PropAttributes::new(true, false, true)); + shared_proto.bump_generation(); + } + + // base64/hex 四原型法挂 Uint8Array 专属原型(Uint8ClampedArray 经链继承、 + // kind 校验拒绝);两静态法挂 Uint8Array 构造器。 + let world = session.builtin_world(); + let sh = core.shape_forge().as_ref(); + let sf = core.perm_interner().as_ref(); + let u8_proto_ptr = session.builtin_world().uint8array_proto.as_ptr() as *mut JsObject; + let u8_proto = unsafe { &mut *u8_proto_ptr }; + // SAFETY: 指针来自 session 存活期内的 BuiltinWorld 对象。 + let _ = world.bind_method( + u8_proto, + sh, + sf, + "toBase64", + unsafe { + oxide_types::object::NativeFnPtr::from_raw( + oxide_builtins::typed_array::uint8array_to_base64:: as *const (), + ) + }, + 0, + ); + let _ = world.bind_method( + u8_proto, + sh, + sf, + "toHex", + unsafe { + oxide_types::object::NativeFnPtr::from_raw( + oxide_builtins::typed_array::uint8array_to_hex:: as *const (), + ) + }, + 0, + ); + let _ = world.bind_method( + u8_proto, + sh, + sf, + "setFromBase64", + unsafe { + oxide_types::object::NativeFnPtr::from_raw( + oxide_builtins::typed_array::uint8array_set_from_base64:: as *const (), + ) + }, + 1, + ); + let _ = world.bind_method( + u8_proto, + sh, + sf, + "setFromHex", + unsafe { + oxide_types::object::NativeFnPtr::from_raw( + oxide_builtins::typed_array::uint8array_set_from_hex:: as *const (), + ) + }, + 1, + ); + let u8_ctor_ptr = session.builtin_world().uint8array_constructor.as_ptr() as *mut JsObject; + let u8_ctor = unsafe { &mut *u8_ctor_ptr }; + let _ = world.bind_method( + u8_ctor, + sh, + sf, + "fromBase64", + unsafe { + oxide_types::object::NativeFnPtr::from_raw( + oxide_builtins::typed_array::uint8array_from_base64:: as *const (), + ) + }, + 1, + ); + let _ = world.bind_method( + u8_ctor, + sh, + sf, + "fromHex", + unsafe { + oxide_types::object::NativeFnPtr::from_raw( + oxide_builtins::typed_array::uint8array_from_hex:: as *const (), + ) + }, + 1, + ); + bind_typed_array_abstract_ctor(core, session); // 把 `%TypedArray%` 抽象构造器暴露为全局 `TypedArray`(具体构造器的 [[Prototype]])。 diff --git a/crates/oxide_vm/tests/builtins_typed_array_base64_tests.rs b/crates/oxide_vm/tests/builtins_typed_array_base64_tests.rs new file mode 100644 index 00000000..701894f3 --- /dev/null +++ b/crates/oxide_vm/tests/builtins_typed_array_base64_tests.rs @@ -0,0 +1,430 @@ +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + vm.run(&Arc::new(module)) +} + +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.lookup_str(val).unwrap_or_default() +} + +#[test] +fn to_base64_rfc4648_vectors() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + var vectors = [ + [new Uint8Array([]), ""], + [new Uint8Array([102]), "Zg=="], + [new Uint8Array([102, 111]), "Zm8="], + [new Uint8Array([102, 111, 111]), "Zm9v"], + [new Uint8Array([102, 111, 111, 98]), "Zm9vYg=="], + [new Uint8Array([102, 111, 111, 98, 97]), "Zm9vYmE="], + [new Uint8Array([102, 111, 111, 98, 97, 114]), "Zm9vYmFy"] + ]; + for (var i = 0; i < vectors.length; i++) { + if (vectors[i][0].toBase64() !== vectors[i][1]) return "vector " + i; + } + return true; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn to_base64_alphabet_and_omit_padding() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + var a = new Uint8Array([199, 239, 242]); + if (a.toBase64() !== "x+/y") return "std"; + if (a.toBase64({ alphabet: "base64url" }) !== "x-_y") return "url"; + var b = new Uint8Array([199, 239]); + if (b.toBase64() !== "x+8=") return "pad"; + if (b.toBase64({ omitPadding: true }) !== "x+8") return "omit"; + if (b.toBase64({ omitPadding: 1 }) !== "x+8") return "tobool"; + if (b.toBase64({ omitPadding: 0 }) !== "x+8=") return "tobool0"; + if (new Uint8Array([255]).toBase64({ alphabet: "base64url", omitPadding: true }) !== "_w") return "urlomit"; + return true; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn to_base64_option_coercion_and_receiver() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + // 装箱串与抛错 toString 对象均 TypeError(不做 ToPrimitive)。 + var t1 = (function() { try { new Uint8Array(2).toBase64({ alphabet: Object("base64") }); return false; } catch (e) { return e instanceof TypeError; } })(); + var t2 = (function() { try { new Uint8Array(2).toBase64({ alphabet: { toString: function() { throw 1; } } }); return false; } catch (e) { return e instanceof TypeError; } })(); + var t3 = (function() { try { new Uint8Array(2).toBase64({ alphabet: "other" }); return false; } catch (e) { return e instanceof TypeError; } })(); + // 选项 getter 触发恰一次,且其副作用在数据读取前生效。 + var accesses = 0; + var array = new Uint8Array([0]); + var options = {}; + Object.defineProperty(options, "alphabet", { + get: function() { accesses += 1; array[0] = 255; return "base64"; } + }); + var result = array.toBase64(options); + return t1 && t2 && t3 && accesses === 1 && result === "/w==" && array[0] === 255; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn to_hex_and_receiver_validation() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + if (new Uint8Array([]).toHex() !== "") return "empty"; + if (new Uint8Array([102, 111, 111, 98, 97, 114]).toHex() !== "666f6f626172") return "vector"; + // 非 Uint8 接收者抛 TypeError(kind 校验先于任何副作用)。 + var t1 = (function() { try { Uint8Array.prototype.toHex.call(new Uint8ClampedArray(2), {}); return false; } catch (e) { return e instanceof TypeError; } })(); + var t2 = (function() { try { Uint8Array.prototype.toBase64.call(new Int8Array(2)); return false; } catch (e) { return e instanceof TypeError; } })(); + // 非对象接收者同样抛。 + var t3 = (function() { try { Uint8Array.prototype.toBase64.call([]); return false; } catch (e) { return e instanceof TypeError; } })(); + return t1 && t2 && t3; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn set_from_base64_results_and_target_size() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + // 标准向量:read/written 与内容。 + var pairs = [ + ["", 0, []], + ["Zg==", 4, [102]], + ["Zm8=", 4, [102, 111]], + ["Zm9v", 4, [102, 111, 111]], + ["Zm9vYg==", 8, [102, 111, 111, 98]] + ]; + for (var i = 0; i < pairs.length; i++) { + var target = new Uint8Array(4); + var r = target.setFromBase64(pairs[i][0]); + if (r.read !== pairs[i][1] || r.written !== pairs[i][2].length) return "rw " + i; + for (var k = 0; k < 4; k++) { + var expect = k < pairs[i][2].length ? pairs[i][2][k] : 0; + if (target[k] !== expect) return "body " + i; + } + } + // 小目标:块前停(第 2 块不消费)、恰好补齐。 + var t1 = new Uint8Array([255, 255, 255, 255, 255]); + var r1 = t1.setFromBase64("Zm9vYmFy"); + if (r1.read !== 4 || r1.written !== 3 || t1[3] !== 255) return "small"; + var t2 = new Uint8Array([255, 255, 255, 255, 255]); + var r2 = t2.setFromBase64("Zm9vYmE="); + if (r2.read !== 8 || r2.written !== 5) return "exact"; + // 空白跳过且计入 read。 + var t3 = new Uint8Array(3); + var r3 = t3.setFromBase64("Z g=="); + if (r3.read !== 5 || r3.written !== 1 || t3[0] !== 102) return "ws"; + return true; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn set_from_base64_last_chunk_handling() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + function ok(input, handling, read, written) { + var t = new Uint8Array(6); + var r = t.setFromBase64(input, handling === undefined ? undefined : { lastChunkHandling: handling }); + return r.read === read && r.written === written; + } + function throws(input, handling) { + try { + new Uint8Array(6).setFromBase64(input, handling === undefined ? undefined : { lastChunkHandling: handling }); + return false; + } catch (e) { return e instanceof SyntaxError; } + } + if (!ok("ZXhhZg==", undefined, 8, 4)) return "pad default"; + if (!ok("ZXhhZg==", "strict", 8, 4)) return "pad strict"; + if (!ok("ZXhhZg", "loose", 6, 4)) return "nopad loose"; + if (!ok("ZXhhZg", "stop-before-partial", 4, 3)) return "nopad sbp"; + if (!throws("ZXhhZg", "strict")) return "nopad strict"; + if (!ok("ZXhhZh==", undefined, 8, 4)) return "bits default"; + if (!throws("ZXhhZh==", "strict")) return "bits strict"; + if (!throws("ZXhhZg=")) return "partialpad default"; + if (!ok("ZXhhZg=", "stop-before-partial", 4, 3)) return "partialpad sbp"; + if (!throws("ZXhhZg=", "strict")) return "partialpad strict"; + if (!throws("ZXhhZg===")) return "excesspad default"; + if (!throws("ZXhhZg===", "stop-before-partial")) return "excesspad sbp"; + // 非法字符(含非 ASCII 空格)。 + if (!throws("Zm.9v")) return "illegal"; + if (!throws("Zg\u{00A0}==")) return "nbsp"; + return true; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn set_from_base64_subarray_and_option_order() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + // subarray 偏移生效。 + var base = new Uint8Array([255, 255, 255, 255, 255, 255, 255]); + var sub = base.subarray(2, 5); + var r = sub.setFromBase64("Zm9vYmFy"); + if (r.read !== 4 || r.written !== 3) return "sub rw"; + if (base[2] !== 102 || base[3] !== 111 || base[4] !== 111 || base[5] !== 255) return "sub body"; + // 接收者校验先于选项副作用:抛错 getter 零触发。 + var touched = 0; + var options = {}; + Object.defineProperty(options, "alphabet", { + get: function() { touched += 1; throw new TypeError("no"); } + }); + try { + Uint8Array.prototype.setFromBase64.call(new Int8Array(2), "Zg==", options); + return false; + } catch (e) { + if (!(e instanceof TypeError)) return "type"; + } + if (touched !== 0) return "touched"; + // string 非原始串抛 TypeError,选项零触发。 + var optTouched = 0; + var touchy = {}; + Object.defineProperty(touchy, "alphabet", { + get: function() { optTouched += 1; throw 1; } + }); + try { + new Uint8Array(2).setFromBase64({ toString: function() { throw 1; } }, touchy); + return false; + } catch (e) { + if (!(e instanceof TypeError)) return "str type"; + } + return optTouched === 0; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn set_from_base64_writes_up_to_error() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + var t = new Uint8Array([255, 255, 255, 255, 255]); + var threw = false; + try { t.setFromBase64("MjYyZm.9v"); } catch (e) { threw = e instanceof SyntaxError; } + if (!threw) return "throw"; + return t[0] === 50 && t[1] === 54 && t[2] === 50 && t[3] === 255 && t[4] === 255; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn set_from_hex_results_and_odd_length() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + var t = new Uint8Array(3); + var r = t.setFromHex("aabbcc"); + if (r.read !== 6 || r.written !== 3) return "rw"; + if (t[0] !== 170 || t[1] !== 187 || t[2] !== 204) return "body"; + // 大小写宽容。 + t.fill(0); + var r2 = t.setFromHex("AABB"); + if (r2.read !== 4 || t[0] !== 170 || t[1] !== 187) return "case"; + // 奇数长度最前判定:零写入抛 SyntaxError(含零长目标)。 + var threw1 = false; + try { new Uint8Array(0).setFromHex("1"); } catch (e) { threw1 = e instanceof SyntaxError; } + if (!threw1) return "odd zero"; + var t3 = new Uint8Array(3); + var threw2 = false; + try { t3.setFromHex("aaa"); } catch (e) { threw2 = e instanceof SyntaxError; } + if (!threw2 || t3[0] !== 0) return "odd write"; + // 坏字符保留前字节。 + var t4 = new Uint8Array(3); + var threw3 = false; + try { t4.setFromHex("aaag"); } catch (e) { threw3 = e instanceof SyntaxError; } + if (!threw3 || t4[0] !== 170 || t4[1] !== 0) return "uptoerr"; + return true; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn from_base64_static() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + var vectors = [ + ["", 0], ["Zg==", 1], ["Zm8=", 2], ["Zm9v", 3], + ["Zm9vYg==", 4], ["Zm9vYmE=", 5], ["Zm9vYmFy", 6] + ]; + for (var i = 0; i < vectors.length; i++) { + var arr = Uint8Array.fromBase64(vectors[i][0]); + if (arr.length !== vectors[i][1]) return "len " + i; + if (arr.buffer.byteLength !== vectors[i][1]) return "buf " + i; + if (Object.getPrototypeOf(arr) !== Uint8Array.prototype) return "proto " + i; + } + // 不读 this:子类构造器不触发,结果 proto 恒 %Uint8Array%.prototype。 + var Subclass = new Function("Uint8Array", "class S extends Uint8Array { constructor() { throw new Error('ctor'); } } return S;")(Uint8Array); + var fromSubclass = Subclass.fromBase64("Zg=="); + if (Object.getPrototypeOf(fromSubclass) !== Uint8Array.prototype) return "sub proto"; + // 字符串检查先于选项。 + var threw1 = false; + try { Uint8Array.fromBase64(42, { alphabet: Object("base64") }); } catch (e) { threw1 = e instanceof TypeError; } + if (!threw1) return "strcheck"; + // 尾块矩阵(fromBase64 无 maxLength 面)。 + if (!throws("A")) return "A"; + if (Uint8Array.fromBase64("A", { lastChunkHandling: "stop-before-partial" }).length !== 0) return "A sbp"; + if (Uint8Array.fromBase64("ABCDA", { lastChunkHandling: "stop-before-partial" }).length !== 3) return "ABCDA sbp"; + var t5 = Uint8Array.fromBase64("AA=", { lastChunkHandling: "stop-before-partial" }); + if (t5.length !== 0) return "AA= sbp"; + var t6 = Uint8Array.fromBase64("ZXhhZg", { lastChunkHandling: "stop-before-partial" }); + if (t6.length !== 3) return "ZXhhZg sbp"; + var threw2 = false; + try { Uint8Array.fromBase64("ZXhhZg", { lastChunkHandling: "strict" }); } catch (e) { threw2 = e instanceof SyntaxError; } + if (!threw2) return "strict"; + return true; + function throws(s) { + try { Uint8Array.fromBase64(s); return false; } catch (e) { return e instanceof SyntaxError; } + } + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn from_hex_static() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + var arr = Uint8Array.fromHex("666f6f626172"); + if (arr.length !== 6 || arr.buffer.byteLength !== 6) return "len"; + if (Object.getPrototypeOf(arr) !== Uint8Array.prototype) return "proto"; + if (arr[0] !== 102 || arr[5] !== 114) return "body"; + var threw = false; + try { Uint8Array.fromHex("a"); } catch (e) { threw = e instanceof SyntaxError; } + if (!threw) return "odd"; + var threw2 = false; + try { Uint8Array.fromHex({ toString: function() { throw 1; } }); } catch (e) { threw2 = e instanceof TypeError; } + if (!threw2) return "type"; + return true; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_prototype_to_string_chain() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + // own toString 与 Array.prototype.toString 同一函数对象。 + if (TypedArray.prototype.toString !== Array.prototype.toString) return "identity"; + var d = Object.getOwnPropertyDescriptor(TypedArray.prototype, "toString"); + if (d === undefined) return "not own"; + if (d.writable !== true || d.enumerable !== false || d.configurable !== true) return "attrs"; + // proto 链接 Array.prototype。 + if (Object.getPrototypeOf(TypedArray.prototype) !== Array.prototype) return "chain"; + // 实例 toString 走 Array 语义(经 TA 自身 join)。 + if (new Uint8Array([1, 2, 3]).toString() !== "1,2,3") return "instance"; + if (new Int16Array([7]).toString() !== "7") return "instance16"; + return true; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn clamped_inherits_base64_methods_but_rejects() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + // clamped 接收者被 kind 校验拒绝(原型链当前不继承方法, + // 调用不可见成员同样 TypeError)。 + var threw1 = false; + try { new Uint8ClampedArray(2).toBase64(); } catch (e) { threw1 = e instanceof TypeError; } + var threw2 = false; + try { new Uint8ClampedArray(2).setFromHex("aa"); } catch (e) { threw2 = e instanceof TypeError; } + var threw3 = false; + try { new Uint8ClampedArray(2).setFromBase64("Zg=="); } catch (e) { threw3 = e instanceof TypeError; } + var threw4 = false; + try { new Uint8ClampedArray(2).toHex(); } catch (e) { threw4 = e instanceof TypeError; } + return threw1 && threw2 && threw3 && threw4; + })()"#, + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn base64_method_metadata() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + r#"(function() { + function meta(obj, name, length) { + var m = obj[name]; + if (typeof m !== "function") return name + " missing"; + if (m.name !== name) return name + " name"; + if (m.length !== length) return name + " length"; + var d = Object.getOwnPropertyDescriptor(obj, name); + if (d.enumerable !== false || d.writable !== true || d.configurable !== true) return name + " attrs"; + return null; + } + var bad = meta(Uint8Array.prototype, "toBase64", 0); + if (bad) return bad; + bad = meta(Uint8Array.prototype, "toHex", 0); + if (bad) return bad; + bad = meta(Uint8Array.prototype, "setFromBase64", 1); + if (bad) return bad; + bad = meta(Uint8Array.prototype, "setFromHex", 1); + if (bad) return bad; + bad = meta(Uint8Array, "fromBase64", 1); + if (bad) return bad; + bad = meta(Uint8Array, "fromHex", 1); + return bad === null; + })()"#, + ) + .unwrap(); + assert!(result.as_bool(), "metadata: {}", to_str(&vm, result)); +}