use omnilua::{Lua, LuaVersion, Value};
const UTF8_VERSIONS: [LuaVersion; 3] = [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55];
fn eval<T: omnilua::FromLuaMulti>(version: LuaVersion, code: &str) -> T {
let lua = Lua::new_versioned(version);
lua.load(code)
.eval()
.unwrap_or_else(|e| panic!("eval of `{code}` failed under {version:?}: {e:?}"))
}
fn eval_bytes(version: LuaVersion, code: &str) -> Vec<u8> {
match eval::<Value>(version, code) {
Value::String(s) => s.as_bytes().expect("string bytes"),
other => panic!("`{code}` under {version:?} returned {other:?}, expected a string"),
}
}
fn probe_call(version: LuaVersion, callee: &str, args: &str) -> Vec<u8> {
let code = format!(
"local ok, r = pcall({callee}, {args})\n\
if ok then return 'ok:' .. tostring(r) else return 'err:' .. tostring(r) end"
);
eval_bytes(version, &code)
}
#[test]
fn charpattern_lead_byte_ceiling_is_version_split() {
let dump = "local p = utf8.charpattern; local o = {}; \
for i = 1, #p do o[i] = string.format('%02X', p:byte(i)) end; \
return table.concat(o)";
assert_eq!(
eval_bytes(LuaVersion::V53, dump),
b"5B002D7FC22DF45D5B802DBF5D2A".to_vec(),
"5.3 charpattern must cap the lead byte at \\xF4 (lutf8lib.c 5.3.6 UTF8PATT)"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
eval_bytes(v, dump),
b"5B002D7FC22DFD5D5B802DBF5D2A".to_vec(),
"5.4+ charpattern must cap the lead byte at \\xFD (extended UTF8PATT)"
);
}
}
#[test]
fn codepoint_lax_bound_is_version_split() {
let (callee, args) = ("utf8.codepoint", "'\\xF4\\x90\\x80\\x80', 1, 1, true");
assert_eq!(
probe_call(LuaVersion::V53, callee, args),
b"err:invalid UTF-8 code".to_vec(),
"5.3 has no extended range: lax 0x110000 must be rejected"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
probe_call(v, callee, args),
b"ok:1114112".to_vec(),
"5.4+ lax must accept the extended codepoint 0x110000"
);
}
}
#[test]
fn codepoint_lax_accepts_maxutf_only_from_54() {
let (callee, args) = ("utf8.codepoint", "'\\xFD\\xBF\\xBF\\xBF\\xBF\\xBF', 1, 1, true");
assert_eq!(
probe_call(LuaVersion::V53, callee, args),
b"err:invalid UTF-8 code".to_vec(),
"5.3 must reject a 6-byte / 0x7FFFFFFF sequence even in lax"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
probe_call(v, callee, args),
b"ok:2147483647".to_vec(),
"5.4+ lax must accept MAXUTF (0x7FFFFFFF)"
);
}
}
#[test]
fn surrogate_strict_default_is_version_split() {
let (callee, args) = ("utf8.codepoint", "'\\u{D800}'");
assert_eq!(
probe_call(LuaVersion::V53, callee, args),
b"ok:55296".to_vec(),
"5.3 has no surrogate guard: default decode of U+D800 must succeed"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
probe_call(v, callee, args),
b"err:invalid UTF-8 code".to_vec(),
"5.4+ strict (default) must reject the surrogate U+D800"
);
}
}
#[test]
fn surrogate_lax_accepted_on_all_versions() {
let (callee, args) = ("utf8.codepoint", "'\\u{D800}', 1, 1, true");
for v in UTF8_VERSIONS {
assert_eq!(
probe_call(v, callee, args),
b"ok:55296".to_vec(),
"lax decode of U+D800 must succeed on every utf8 version"
);
}
}
#[test]
fn len_over_surrogate_is_version_split() {
assert_eq!(
eval::<i64>(LuaVersion::V53, "return utf8.len('\\u{D800}')"),
1,
"5.3 len must count the surrogate as one character"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
let (a, b): (Value, i64) =
eval(v, "local a, b = utf8.len('\\u{D800}'); return a, b");
assert!(
matches!(a, Value::Nil),
"5.4+ len must fail (nil) on the surrogate under {v:?}, got {a:?}"
);
assert_eq!(b, 1, "5.4+ len failure position must be 1 under {v:?}");
}
}
#[test]
fn codes_over_surrogate_is_version_split() {
let code = "local ok, r = pcall(function() \
local c; for _, cc in utf8.codes('\\u{D800}') do c = cc end; return c \
end)\n\
if ok then return 'ok:' .. tostring(r) \
elseif string.find(r, 'invalid UTF%-8 code') then return 'err:invalid UTF-8 code' \
else return 'err:' .. tostring(r) end";
assert_eq!(
eval_bytes(LuaVersion::V53, code),
b"ok:55296".to_vec(),
"5.3 codes() must iterate the surrogate codepoint"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
eval_bytes(v, code),
b"err:invalid UTF-8 code".to_vec(),
"5.4+ codes() must reject the surrogate"
);
}
}
#[test]
fn char_encode_ceiling_is_version_split() {
assert_eq!(
probe_call(LuaVersion::V53, "utf8.char", "0x110000"),
b"err:bad argument #1 to 'utf8.char' (value out of range)".to_vec(),
"5.3 char must reject 0x110000 (> MAXUNICODE)"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
eval::<i64>(v, "return #utf8.char(0x110000)"),
4,
"5.4+ char must encode 0x110000 as 4 bytes under {v:?}"
);
}
}
#[test]
fn char_rejects_above_maxutf_on_all_versions() {
for v in UTF8_VERSIONS {
assert_eq!(
probe_call(v, "utf8.char", "0x80000000"),
b"err:bad argument #1 to 'utf8.char' (value out of range)".to_vec(),
"char must reject 0x80000000 on every utf8 version under {v:?}"
);
}
}
#[test]
fn char_negative_is_rejected_on_all_versions() {
for v in UTF8_VERSIONS {
assert_eq!(
probe_call(v, "utf8.char", "-1"),
b"err:bad argument #1 to 'utf8.char' (value out of range)".to_vec(),
"char must reject a negative codepoint under {v:?}"
);
}
}
#[test]
fn offset_zero_finds_char_start() {
for v in UTF8_VERSIONS {
assert_eq!(
eval::<i64>(v, "return utf8.offset('héllo', 0, 3)"),
2,
"offset(s, 0, 3) must land on the start of 'é' (byte 2) under {v:?}"
);
}
}
#[test]
fn offset_negative_n_counts_back_from_end() {
for v in UTF8_VERSIONS {
assert_eq!(
eval::<i64>(v, "return utf8.offset('héllo', -1)"),
6,
"offset(s, -1) must find the last character start under {v:?}"
);
assert_eq!(
eval::<i64>(v, "return utf8.offset('héllo', -2)"),
5,
"offset(s, -2) must skip back two characters under {v:?}"
);
}
}
#[test]
fn offset_past_end_returns_nil() {
for v in UTF8_VERSIONS {
assert!(
matches!(
eval::<Value>(v, "return utf8.offset('abc', 5)"),
Value::Nil
),
"offset past the end must return nil under {v:?}"
);
}
}
#[test]
fn offset_initial_position_out_of_bounds_wording_is_version_split() {
assert_eq!(
probe_call(LuaVersion::V53, "utf8.offset", "'abc', 1, 5"),
b"err:bad argument #3 to 'utf8.offset' (position out of range)".to_vec(),
"5.3 offset OOB wording"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
probe_call(v, "utf8.offset", "'abc', 1, 5"),
b"err:bad argument #3 to 'utf8.offset' (position out of bounds)".to_vec(),
"5.4+ offset OOB wording under {v:?}"
);
}
}
#[test]
fn offset_into_continuation_byte_errors() {
for v in UTF8_VERSIONS {
assert_eq!(
probe_call(v, "utf8.offset", "'é', 1, 2"),
b"err:initial position is a continuation byte".to_vec(),
"offset into a continuation byte must error under {v:?}"
);
}
}
#[test]
fn len_out_of_bounds_wording_is_version_split() {
assert_eq!(
probe_call(LuaVersion::V53, "utf8.len", "'abc', 0, 2"),
b"err:bad argument #2 to 'utf8.len' (initial position out of string)".to_vec(),
"5.3 len initial-position wording"
);
assert_eq!(
probe_call(LuaVersion::V53, "utf8.len", "'abc', 1, 4"),
b"err:bad argument #3 to 'utf8.len' (final position out of string)".to_vec(),
"5.3 len final-position wording"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
probe_call(v, "utf8.len", "'abc', 0, 2"),
b"err:bad argument #2 to 'utf8.len' (initial position out of bounds)".to_vec(),
"5.4+ len initial-position wording under {v:?}"
);
assert_eq!(
probe_call(v, "utf8.len", "'abc', 1, 4"),
b"err:bad argument #3 to 'utf8.len' (final position out of bounds)".to_vec(),
"5.4+ len final-position wording under {v:?}"
);
}
}
#[test]
fn codepoint_out_of_bounds_wording_is_version_split() {
assert_eq!(
probe_call(LuaVersion::V53, "utf8.codepoint", "'abc', 5"),
b"err:bad argument #3 to 'utf8.codepoint' (out of range)".to_vec(),
"5.3 codepoint OOB wording"
);
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
probe_call(v, "utf8.codepoint", "'abc', 5"),
b"err:bad argument #3 to 'utf8.codepoint' (out of bounds)".to_vec(),
"5.4+ codepoint OOB wording under {v:?}"
);
}
}
#[test]
fn len_reports_first_malformed_position() {
for v in UTF8_VERSIONS {
let (a, b): (Value, i64) =
eval(v, "local a, b = utf8.len('abc\\xE3def'); return a, b");
assert!(
matches!(a, Value::Nil),
"len of a malformed string must fail (nil) under {v:?}, got {a:?}"
);
assert_eq!(b, 4, "len failure position must be the bad lead byte (4) under {v:?}");
}
for v in UTF8_VERSIONS {
let (a, b): (Value, i64) =
eval(v, "local a, b = utf8.len('\\x80hello'); return a, b");
assert!(matches!(a, Value::Nil), "leading cont byte must fail under {v:?}");
assert_eq!(b, 1, "leading-cont failure position must be 1 under {v:?}");
}
}