diff --git a/src/lua.rs b/src/lua.rs index 85ad7bfe..7b34f98b 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -5,7 +5,7 @@ use gc_arena::{metrics::Metrics, Arena, Collect, CollectionPhase, Mutation, Root use crate::{ finalizers::Finalizers, registry::{Fetchable, Stashable}, - stdlib::{load_base, load_coroutine, load_io, load_math, load_string, load_table}, + stdlib::{load_base, load_coroutine, load_io, load_math, load_string, load_table, load_utf8}, string::InternedStringSet, Error, FromMultiValue, Fuel, IntoValue, InvalidTableKey, Registry, Singleton, StashedExecutor, StaticError, String, Table, Value, @@ -134,6 +134,7 @@ impl Lua { load_coroutine(ctx); load_math(ctx); load_string(ctx); + load_utf8(ctx); load_table(ctx); }) } diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index aa766153..d7695ee4 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -4,8 +4,9 @@ mod io; mod math; mod string; mod table; +mod utf8; pub use self::{ base::load_base, coroutine::load_coroutine, io::load_io, math::load_math, string::load_string, - table::load_table, + table::load_table, utf8::load_utf8, }; diff --git a/src/stdlib/utf8.rs b/src/stdlib/utf8.rs new file mode 100644 index 00000000..841baa76 --- /dev/null +++ b/src/stdlib/utf8.rs @@ -0,0 +1,60 @@ +use crate::{Callback, CallbackReturn, Context, IntoValue, Table, TypeError, Value}; + +pub fn load_utf8<'gc>(ctx: Context<'gc>) { + let utf8 = Table::new(&ctx); + + utf8.set( + ctx, + "char", + Callback::from_fn(&ctx, |ctx, mut exec, mut stack| { + let mut string = String::new(); + for argn in 0..stack.len() { + exec.fuel().consume(1); + let codepoint = match stack.pop_front() { + Value::Integer(i) if i >= 0 && i <= char::MAX as i64 => Ok(i as u32), + Value::Number(n) + if n >= 0.0 && n.fract() == 0.0 && n <= char::MAX as u32 as f64 => + { + Ok(n as u32) + } + Value::String(s) => String::from_utf8(s.to_vec()) + .map_err(|_| format!("failed to decode argument #{argn} as UTF-8")) + .and_then(|s| { + s.parse::() + .map_err(|_| { + format!("failed to parse argument #{argn} as a number") + }) + .and_then(|f| { + (f >= 0.0 && f.fract() == 0.0 && f <= char::MAX as u32 as f64) + .then_some(f as u32) + .ok_or(format!( + "argument #{argn} has no integer representation" + )) + }) + }), + v => Err(TypeError { + expected: "valid UTF-8 codepoint (string, number, or integer)", + found: v.type_name(), + } + .to_string()), + } + .map_err(|s| s.into_value(ctx))?; + + if let Some(c) = char::from_u32(codepoint) { + string.push(c); + } else { + return Err(format!( + "argument #{argn}: {codepoint:x} is not a valid codepoint" + ) + .into_value(ctx) + .into()); + } + } + stack.replace(ctx, string); + Ok(CallbackReturn::Return) + }), + ) + .unwrap(); + + ctx.set_global("utf8", utf8).unwrap(); +} diff --git a/tests/scripts/utf8.lua b/tests/scripts/utf8.lua new file mode 100644 index 00000000..5a8532b2 --- /dev/null +++ b/tests/scripts/utf8.lua @@ -0,0 +1,9 @@ +function is_err(f) + return pcall(f) == false +end + +do + assert(is_err(function() return utf8.char(0x110000) end) and + is_err(function() return utf8.char(0.1) end) and + utf8.char(72, 69, 76.0, "76", 79) == "HELLO") +end