use crate::numfmt;
use crate::options::Options;
use crate::quote::quote;
use crate::value::{Ident, Key, Table, Value};
use std::collections::HashMap;
const KEYWORDS: &[&str] = &[
"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in",
"local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
];
fn is_keyword(s: &str) -> bool {
KEYWORDS.contains(&s)
}
struct Serializer<'o> {
opts: &'o Options,
space: &'static str,
huge: bool,
maxl: usize,
maxlen: Option<i64>,
numformat: String,
iname: String,
comm: Option<usize>,
seen: HashMap<Ident, String>,
sref: Vec<String>,
syms: HashMap<String, usize>,
symn: usize,
fatal_value: Option<String>,
}
#[derive(Clone, Copy)]
struct Frame<'a> {
name: Option<&'a Key>,
indent: Option<&'a str>,
insref: Option<&'a str>,
path: Option<&'a str>,
plainindex: bool,
level: usize,
}
impl<'o> Serializer<'o> {
fn new(opts: &'o Options) -> Self {
let name = opts.name.clone().unwrap_or_default();
let iname = format!("_{name}");
let space = if opts.compact { "" } else { " " };
Serializer {
space,
huge: !opts.nohuge,
maxl: opts.maxlevel.unwrap_or(usize::MAX),
maxlen: opts.maxlength,
numformat: opts
.numformat
.clone()
.unwrap_or_else(|| "%.17g".to_string()),
sref: vec![format!("local {iname}={{}}")],
iname,
comm: opts.comment,
seen: HashMap::new(),
syms: HashMap::new(),
symn: 0,
fatal_value: None,
opts,
}
}
fn gensym(&mut self, raw: &str) -> String {
let stripped: String = raw.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
let remapped = self.remap_runs(&stripped);
format!("_{remapped}")
}
fn remap_runs(&mut self, s: &str) -> String {
let b = s.as_bytes();
let mut out = String::new();
let mut i = 0;
while i < b.len() {
if b[i].is_ascii_digit() {
let start = i;
i += 1;
let mut count = 0;
while i < b.len() && b[i].is_ascii_alphanumeric() {
i += 1;
count += 1;
}
let run = &s[start..i];
if count >= 1 {
let n = *self.syms.entry(run.to_string()).or_insert_with(|| {
self.symn += 1;
self.symn
});
out.push_str(&n.to_string());
} else {
out.push_str(run);
}
} else {
out.push(b[i] as char);
i += 1;
}
}
out
}
fn safestr(&self, v: &Value) -> String {
match v {
Value::Number(n) => {
if self.huge {
if n.is_infinite() {
return if *n > 0.0 {
"1/0 --[[math.huge]]".to_string()
} else {
"-1/0 --[[-math.huge]]".to_string()
};
}
if n.is_nan() {
return "0/0".to_string();
}
}
numfmt::format(&self.numformat, *n)
}
Value::Str(s) => quote(s),
Value::Bool(b) => b.to_string(),
Value::Nil => "nil".to_string(),
other => quote(tostring(other).as_bytes()),
}
}
fn comment(&self, text: &str, level: usize) -> String {
match self.comm {
Some(threshold) if level < threshold => format!(" --[[{text}]]"),
_ => String::new(),
}
}
fn safename(&self, path: Option<&str>, name: Option<&Key>) -> (String, String) {
let plain_name = match name {
Some(Key::Str(s)) => std::str::from_utf8(s)
.ok()
.filter(|n| is_plain_ident(n) && !is_keyword(n))
.map(str::to_string),
_ => None,
};
let (safe, is_plain) = match &plain_name {
Some(n) => (n.clone(), true),
None => {
let key_val = name.map_or(Value::Str(Vec::new()), Key::to_value);
(format!("[{}]", self.safestr(&key_val)), false)
}
};
let joiner = if is_plain && path.is_some() { "." } else { "" };
let full = format!("{}{}{}", path.unwrap_or(""), joiner, safe);
(full, safe)
}
fn val2str(&mut self, t: &Value, frame: Frame) -> String {
let level = frame.level;
let (spath, sname) = self.safename(frame.path, frame.name);
let tag = self.build_tag(frame.name, &sname, frame.plainindex);
if let Some(id) = t.ident() {
if let Some(prev) = self.seen.get(&id).cloned() {
self.sref
.push(format!("{spath}{}={}{prev}", self.space, self.space));
return format!("{tag}nil{}", self.comment("ref", level));
}
}
let replaced = self.apply_metamethods(t, frame.insref.unwrap_or(&spath));
let t = replaced.as_ref().unwrap_or(t);
match t {
Value::Table(tab) => self.table_to_str(tab, &tag, &spath, frame),
Value::Global(_) => {
if let Some(id) = t.ident() {
self.seen
.insert(id, frame.insref.unwrap_or(&spath).to_string());
}
format!("{tag}{}", self.globerr(t, level))
}
Value::Function(f) => {
if let Some(id) = t.ident() {
self.seen
.insert(id, frame.insref.unwrap_or(&spath).to_string());
}
if self.opts.nocode {
return format!(
"{tag}function() --[[..skipped..]] end{}",
self.comment("", level)
);
}
match &f.bytecode {
Some(code) => {
let quoted = quote(code);
format!(
"{tag}((loadstring or load)({quoted},'@serialized')){}",
self.comment("", level)
)
}
None => format!("{tag}{}", self.globerr(t, level)),
}
}
_ => format!("{tag}{}", self.safestr(t)),
}
}
fn build_tag(&self, name: Option<&Key>, sname: &str, plainindex: bool) -> String {
if plainindex {
match name {
Some(Key::Number(_)) => String::new(),
Some(k) => {
let raw = tostring(&k.to_value());
format!("{raw}{}={}", self.space, self.space)
}
None => String::new(),
}
} else if name.is_some() {
format!("{sname}{}={}", self.space, self.space)
} else {
String::new()
}
}
fn apply_metamethods(&mut self, t: &Value, seen_path: &str) -> Option<Value> {
let Value::Table(tab) = t else {
return None;
};
if self.opts.metatostring == Some(false) {
return None;
}
let ser = tab.serialize_meta();
let tos = tab.tostring_meta();
if ser.is_none() && tos.is_none() {
return None;
}
let result = ser
.and_then(|f| f().ok())
.or_else(|| tos.and_then(|f| f().ok()))?;
if let Some(id) = t.ident() {
self.seen.insert(id, seen_path.to_string());
}
Some(result)
}
fn globerr(&mut self, v: &Value, level: usize) -> String {
if let Value::Global(g) = v {
if !g.name.is_empty() {
return format!("{}{}", g.name, self.comment(&g.name, level));
}
}
let text = match v {
Value::Global(g) if g.name.is_empty() => format!("userdata: 0x{:012x}", g.id),
_ => tostring(v),
};
if !self.opts.fatal {
return self.safestr(&Value::Str(text.into_bytes()));
}
if self.fatal_value.is_none() {
self.fatal_value = Some(text.clone());
}
self.safestr(&Value::Str(text.into_bytes()))
}
fn table_to_str(&mut self, tab: &Table, tag: &str, spath: &str, frame: Frame) -> String {
let Frame {
indent,
insref,
level,
..
} = frame;
if level >= self.maxl {
return format!("{tag}{{}}{}", self.comment("maxlvl", level));
}
let id = Ident::Table(tab.id());
self.seen.insert(id, insref.unwrap_or(spath).to_string());
if tab.is_empty() {
let text = tab_tostring(tab);
return format!("{tag}{{}}{}", self.comment(&text, level));
}
if let Some(ml) = self.maxlen {
if ml < 0 {
return format!("{tag}{{}}{}", self.comment("maxlen", level));
}
}
let entries = tab.entries();
if let Some((key, _)) = entries
.iter()
.find(|(key, _)| matches!(key, Key::Number(n) if n.is_nan()))
{
self.fatal_value = Some(tostring(&key.to_value()));
return String::new();
}
let border = tab.border();
let maxn = match self.opts.maxnum {
Some(m) => border.min(m),
None => border,
};
let index: HashMap<KeyId, usize> = entries
.iter()
.enumerate()
.filter_map(|(i, (k, _))| key_id(k).map(|id| (id, i)))
.collect();
let lookup = |key: &Key| -> Option<Value> {
key_id(key)
.and_then(|id| index.get(&id))
.map(|&i| entries[i].1.clone())
};
let mut o: Vec<Key> = (1..=maxn).map(|i| Key::Number(i as f64)).collect();
if self.opts.maxnum.is_none() || o.len() < self.opts.maxnum.unwrap() {
for (k, _) in &entries {
if !is_array_index(k, maxn) {
o.push(k.clone());
}
}
}
if let Some(m) = self.opts.maxnum {
if o.len() > m {
o.truncate(m);
}
}
if (self.opts.sortkeys || self.opts.sortfn.is_some()) && o.len() > maxn {
self.sort_keys(&mut o, &entries);
}
let sparse = self.opts.sparse && o.len() > maxn;
let seen_t = self.seen.get(&Ident::Table(tab.id())).cloned();
let mut out: Vec<String> = Vec::new();
for (n, key) in o.iter().enumerate() {
let idx = n + 1;
let value = lookup(key);
let plainindex = idx <= maxn && !sparse;
if self.should_skip(key, value.as_ref(), sparse) {
continue;
}
let is_complex = matches!(key, Key::Table(_) | Key::Function(_) | Key::Global(_));
if is_complex {
self.emit_complex_key(tab, key, value, indent);
} else {
let v = value.unwrap_or(Value::Nil);
let text = self.val2str(
&v,
Frame {
name: Some(key),
indent,
insref: None,
path: seen_t.as_deref(),
plainindex,
level: level + 1,
},
);
let len = text.len() as i64;
out.push(text);
if let Some(ml) = self.maxlen.as_mut() {
*ml -= len;
if *ml < 0 {
break;
}
}
}
}
self.assemble_table(tab, tag, indent, &out, level)
}
fn emit_complex_key(
&mut self,
tab: &Table,
key: &Key,
value: Option<Value>,
indent: Option<&str>,
) {
let key_val = key.to_value();
let key_id = key_val.ident();
let key_seen = key_id.as_ref().and_then(|i| self.seen.get(i)).cloned();
let key_global = match key {
Key::Global(g) => Some(g.name.clone()),
_ => None,
};
if key_seen.is_none() && key_global.is_none() {
self.sref.push("placeholder".to_string());
let idx = self.sref.len() - 1;
let sym = self.gensym(&tostring(&key_val));
let iname = self.iname.clone();
let (sname, _) = self.safename(Some(&iname), Some(&Key::Str(sym.into_bytes())));
let stmt = self.val2str(
&key_val,
Frame {
name: Some(&Key::Str(sname.clone().into_bytes())),
indent,
insref: Some(&sname),
path: Some(&iname),
plainindex: true,
level: 0,
},
);
self.sref[idx] = stmt;
}
self.sref.push("placeholder".to_string());
let idx = self.sref.len() - 1;
let seen_t = self
.seen
.get(&Ident::Table(tab.id()))
.cloned()
.unwrap_or_default();
let key_seen_now = key_id.as_ref().and_then(|i| self.seen.get(i)).cloned();
let key_ref = key_seen_now
.or(key_global)
.unwrap_or_else(|| self.gensym(&tostring(&key_val)));
let path = format!("{seen_t}[{key_ref}]");
let value = value.unwrap_or(Value::Nil);
let val_seen = value.ident().and_then(|i| self.seen.get(&i)).cloned();
let rhs = match val_seen {
Some(p) => p,
None => self.val2str(
&value,
Frame {
name: None,
indent,
insref: Some(&path),
path: None,
plainindex: false,
level: 0,
},
),
};
self.sref[idx] = format!("{path}{}={}{rhs}", self.space, self.space);
}
fn assemble_table(
&self,
tab: &Table,
tag: &str,
indent: Option<&str>,
out: &[String],
level: usize,
) -> String {
let prefix = indent.map_or(String::new(), |ind| ind.repeat(level));
let head = match indent {
Some(ind) => format!("{{\n{prefix}{ind}"),
None => "{".to_string(),
};
let sep = match indent {
Some(ind) => format!(",\n{prefix}{ind}"),
None => format!(",{}", self.space),
};
let body = out.join(&sep);
let tail = match indent {
Some(_) => format!("\n{prefix}}}"),
None => "}".to_string(),
};
let table_text = match &self.opts.custom {
Some(f) => f(tag, &head, &body, &tail, level),
None => format!("{tag}{head}{body}{tail}"),
};
let text = tab_tostring(tab);
format!("{table_text}{}", self.comment(&text, level))
}
fn should_skip(&self, key: &Key, value: Option<&Value>, sparse: bool) -> bool {
if let Some(v) = value {
if let Some(ig) = &self.opts.valignore {
if let Some(id) = v.ident() {
if ig.contains(&id) {
return true;
}
}
}
if let Some(ig) = &self.opts.valignore_scalar {
if ig.iter().any(|x| x == v) {
return true;
}
}
}
if let Some(allow) = &self.opts.keyallow {
if !allow.iter().any(|k| k == key) {
return true;
}
}
if let Some(ignore) = &self.opts.keyignore {
if ignore.iter().any(|k| k == key) {
return true;
}
}
if let Some(tignore) = &self.opts.valtypeignore {
let tn = value.map_or("nil", type_name);
if tignore.contains(tn) {
return true;
}
}
if sparse && value.is_none() {
return true;
}
false
}
fn sort_keys(&self, o: &mut Vec<Key>, entries: &[(Key, Value)]) {
if let Some(f) = &self.opts.sortfn {
f(o, entries);
return;
}
let len = o.len();
o.sort_by_key(|a| sortkey(a, len));
}
}
fn tab_tostring(tab: &Table) -> String {
format!("table: 0x{:012x}", tab.id())
}
fn tostring(v: &Value) -> String {
match v {
Value::Nil => "nil".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => {
if n.is_nan() {
"nan".to_string()
} else if n.is_infinite() {
if *n > 0.0 {
"inf".to_string()
} else {
"-inf".to_string()
}
} else {
numfmt::format("%.14g", *n)
}
}
Value::Str(s) => String::from_utf8_lossy(s).into_owned(),
Value::Table(t) => tab_tostring(t),
Value::Function(f) => format!("function: 0x{:012x}", f.id),
Value::Global(g) => g.name.clone(),
}
}
fn type_name(v: &Value) -> &'static str {
match v {
Value::Nil => "nil",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::Str(_) => "string",
Value::Table(_) => "table",
Value::Function(_) => "function",
Value::Global(_) => "userdata",
}
}
fn is_plain_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn is_array_index(k: &Key, maxn: usize) -> bool {
match k {
Key::Number(x) => {
let f = *x;
f.fract() == 0.0 && f >= 1.0 && f <= maxn as f64
}
_ => false,
}
}
#[derive(PartialEq, Eq, Hash)]
enum KeyId {
Bool(bool),
Number(u64),
Str(Vec<u8>),
Table(usize),
Function(usize),
Global(usize),
}
fn key_id(k: &Key) -> Option<KeyId> {
Some(match k {
Key::Bool(b) => KeyId::Bool(*b),
Key::Number(n) if n.is_nan() => return None,
Key::Number(n) => KeyId::Number(if *n == 0.0 { 0.0_f64 } else { *n }.to_bits()),
Key::Str(s) => KeyId::Str(s.clone()),
Key::Table(t) => KeyId::Table(t.id()),
Key::Function(f) => KeyId::Function(f.id),
Key::Global(g) => KeyId::Global(g.id),
})
}
fn sortkey(k: &Key, list_len: usize) -> String {
let on_filled_slot = match k {
Key::Number(x) => x.fract() == 0.0 && *x >= 1.0 && *x <= list_len as f64,
_ => false,
};
let prefix = if on_filled_slot {
"0"
} else {
match k {
Key::Number(_) => "a",
Key::Str(_) => "b",
_ => "z",
}
};
let body = pad_numbers(&key_tostring(k));
format!("{prefix}{body}")
}
fn pad_numbers(s: &str) -> String {
let mut out = String::new();
let b = s.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i].is_ascii_digit() {
let start = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
let num = &s[start..i];
let val: u64 = num.parse().unwrap_or(0);
out.push_str(&format!("{val:012}"));
} else {
out.push(b[i] as char);
i += 1;
}
}
out
}
fn key_tostring(k: &Key) -> String {
tostring(&k.to_value())
}
pub fn serialize(t: &Value, opts: &Options) -> Result<String, SerError> {
let mut s = Serializer::new(opts);
let name = opts.name.clone();
let indent = opts.indent.clone();
let sepr = match &indent {
Some(_) => "\n".to_string(),
None => format!(";{}", s.space),
};
let top_name = name.as_ref().map(|n| Key::Str(n.clone().into_bytes()));
let body = s.val2str(
t,
Frame {
name: top_name.as_ref(),
indent: indent.as_deref(),
insref: None,
path: None,
plainindex: false,
level: 0,
},
);
if let Some(value) = s.fatal_value.take() {
return Err(SerError::NotSerializable(value));
}
let tail = if s.sref.len() > 1 {
format!("{}{sepr}", s.sref.join(&sepr))
} else {
String::new()
};
let warn = if opts.comment.is_some() && s.sref.len() > 1 {
format!(
"{}--[[incomplete output with shared/self-references skipped]]",
s.space
)
} else {
String::new()
};
let result = match name {
None => format!("{body}{warn}"),
Some(n) => format!("do local {body}{sepr}{tail}return {n}{sepr}end"),
};
Ok(result)
}
#[derive(Debug, PartialEq, Eq)]
pub enum SerError {
NotSerializable(String),
}
impl std::fmt::Display for SerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SerError::NotSerializable(value) => write!(f, "Can't serialize {value}"),
}
}
}
impl std::error::Error for SerError {}
#[cfg(test)]
mod tests {
use super::SerError;
use crate::value::{Key, Table, Value};
use crate::{dump, line};
#[test]
fn nan_table_keys_return_error() {
let table = Table::new();
table.set(Key::Number(f64::NAN), Value::Number(7.0));
assert_eq!(
line(&Value::Table(table.clone())).unwrap_err(),
SerError::NotSerializable("nan".to_string())
);
assert_eq!(
dump(&Value::Table(table)).unwrap_err(),
SerError::NotSerializable("nan".to_string())
);
}
}