use clap::{Parser, ValueEnum};
use serde_json::{to_writer, to_writer_pretty};
use serde_luaq::{
lua_value, return_statement, script, to_json_value, JsonConversionOptions, LuaValue,
};
use std::{
alloc::{GlobalAlloc, Layout, System},
fs::File,
io::{stdout, BufWriter, Read, Write},
path::PathBuf,
sync::atomic::{AtomicIsize, Ordering::Relaxed},
};
const DEFAULT_SIZE_LIMIT: usize = 64 * 1024 * 1024;
const DEFAULT_MAX_DEPTH: u16 = 16;
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
#[derive(ValueEnum, Debug, Copy, Clone)]
enum LuaInputFormat {
Object,
Return,
Script,
}
#[derive(Parser, Debug)]
#[command(name = "lua_to_json", version, about, long_about = None, verbatim_doc_comment, rename_all = "snake_case")]
struct Args {
#[arg()]
input: PathBuf,
#[arg(short, long)]
format: LuaInputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long)]
pretty: bool,
#[arg(short = 'E', long)]
no_empty_check: bool,
#[arg(long, default_value_t = DEFAULT_SIZE_LIMIT, id = "BYTES")]
lua_size_limit: usize,
#[arg(long, default_value_t = DEFAULT_MAX_DEPTH, id = "DEPTH")]
max_depth: u16,
#[arg(long)]
lossy_string: bool,
#[arg(long)]
no_json: bool,
#[arg(long)]
no_output: bool,
#[arg(long)]
memory_stats: bool,
}
struct Counter;
static ALLOCATED: AtomicIsize = AtomicIsize::new(0);
static PEAK: AtomicIsize = AtomicIsize::new(0);
unsafe impl GlobalAlloc for Counter {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ret = unsafe { System.alloc(layout) };
if !ret.is_null() {
let r = ALLOCATED.fetch_add(layout.size() as isize, Relaxed);
PEAK.fetch_max(r, Relaxed);
}
ret
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe {
System.dealloc(ptr, layout);
}
ALLOCATED.fetch_sub(layout.size() as isize, Relaxed);
}
}
#[global_allocator]
static A: Counter = Counter;
fn main() -> Result {
let args = Args::parse();
let start_bytes = ALLOCATED.load(Relaxed);
if args.memory_stats {
eprintln!("Initial memory usage: {start_bytes} bytes");
}
let opts = JsonConversionOptions {
lossy_string: args.lossy_string,
..Default::default()
};
let mut f = File::open(args.input)?;
let size = f.metadata()?.len() as usize;
if size == 0 {
panic!("Input file is empty");
}
if size > args.lua_size_limit {
panic!(
"Maximum file size exceeded ({size} > {})",
args.lua_size_limit
);
}
let mut buf = Vec::with_capacity(size);
f.read_to_end(&mut buf)?;
let load_lua_bytes = ALLOCATED.load(Relaxed) - start_bytes;
if args.memory_stats {
eprintln!("Reading Lua added: {load_lua_bytes} bytes");
}
let lua_value: LuaValue<'_> = match args.format {
LuaInputFormat::Script => script(&buf, args.max_depth)?.into_iter().collect(),
LuaInputFormat::Object => lua_value(&buf, args.max_depth)?,
LuaInputFormat::Return => return_statement(&buf, args.max_depth)?,
};
let parse_lua_bytes = ALLOCATED.load(Relaxed) - load_lua_bytes;
if args.memory_stats {
let peak = PEAK.load(Relaxed) - load_lua_bytes;
eprintln!("Parsing Lua added: {parse_lua_bytes} bytes, {peak} peak bytes");
}
if args.no_json {
eprintln!("--no_json set, not converting to JSON!");
drop(lua_value);
return Ok(());
}
let json_value = to_json_value(lua_value, &opts)?;
let json_bytes = ALLOCATED.load(Relaxed) - parse_lua_bytes;
if args.memory_stats {
eprintln!("Converting to JSON added: {json_bytes} bytes");
}
if args.no_output {
eprintln!("--no_output set, not outputting the result!");
drop(json_value);
return Ok(());
}
let mut termout = false;
let f: Box<dyn Write> = if let Some(output) = args.output {
Box::new(File::options().create_new(true).write(true).open(output)?)
} else {
termout = true;
Box::new(stdout())
};
let f = BufWriter::new(f);
if args.pretty {
to_writer_pretty(f, &json_value)?;
} else {
to_writer(f, &json_value)?;
}
if termout {
println!();
}
Ok(())
}