manen

Fancy Lua REPL
Log | Files | Refs | README | LICENSE

inspect.rs (12133B)


      1 use std::{
      2     collections::{HashMap, HashSet},
      3     fmt::{self, Write},
      4     sync::Arc,
      5 };
      6 
      7 use aho_corasick::AhoCorasick;
      8 use comfy_table::{Table, presets::UTF8_FULL_CONDENSED};
      9 use lazy_static::lazy_static;
     10 use mlua::prelude::*;
     11 use nu_ansi_term::{AnsiString, AnsiStrings, Color};
     12 
     13 lazy_static! {
     14     static ref AC_REPLACEMENTS: (AhoCorasick, Vec<String>) = {
     15         let mut escapes = vec![
     16             String::from("\x07"),
     17             String::from("\x08"),
     18             String::from("\x0C"),
     19             String::from("\n"),
     20             String::from("\r"),
     21             String::from("\t"),
     22             String::from("\x0B"),
     23             String::from("\x7F"),
     24             String::from("\\"),
     25         ];
     26 
     27         let mut replacements = vec![
     28             String::from("\\a"),
     29             String::from("\\b"),
     30             String::from("\\f"),
     31             String::from("\\n"),
     32             String::from("\\r"),
     33             String::from("\\t"),
     34             String::from("\\v"),
     35             String::from("\\127"),
     36             String::from("\\\\"),
     37         ];
     38 
     39         for i in 0..=31 {
     40             escapes.push(String::from_utf8_lossy(&[i]).to_string());
     41             replacements.push(format!("\\{i}"));
     42         }
     43 
     44         (AhoCorasick::new(escapes).unwrap(), replacements)
     45     };
     46     static ref ESCAPER: &'static AhoCorasick = &AC_REPLACEMENTS.0;
     47     static ref REPLACEMENT_COLOR: Vec<String> = AC_REPLACEMENTS
     48         .1
     49         .iter()
     50         .map(|s| format!("{}{}", Color::Cyan.paint(s), Color::Green.prefix()))
     51         .collect();
     52     static ref KEYWORDS: HashSet<&'static str> = HashSet::from_iter([
     53         "and", "break", "do", "else", "elseif", "end", "else", "false", "for", "function", "goto",
     54         "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until",
     55         "while",
     56     ]);
     57 }
     58 
     59 fn escape_control(s: &str) -> String {
     60     ESCAPER
     61         .replace_all(s, &AC_REPLACEMENTS.1)
     62         .replace("\u{FFFD}", "\\x")
     63 }
     64 
     65 fn escape_control_color(s: &str) -> String {
     66     let s = ESCAPER.replace_all(s, &REPLACEMENT_COLOR);
     67     let mut chars = s.chars();
     68     let mut new = String::new();
     69 
     70     while let Some(c) = chars.next() {
     71         if c != '\u{FFFD}' {
     72             new.push(c);
     73             continue;
     74         }
     75 
     76         let (hex1, hex2) = (chars.next(), chars.next());
     77         let escape = format!(
     78             "\\x{}{}",
     79             hex1.unwrap_or_default(),
     80             hex2.unwrap_or_default()
     81         );
     82 
     83         new.push_str(&format!(
     84             "{}{}",
     85             Color::Cyan.paint(escape),
     86             Color::Green.prefix()
     87         ));
     88     }
     89 
     90     new
     91 }
     92 
     93 fn remove_invalid(mut bytes: &[u8]) -> String {
     94     let mut buffer = String::new();
     95 
     96     loop {
     97         match str::from_utf8(bytes) {
     98             Ok(s) => {
     99                 buffer.push_str(s);
    100                 return buffer;
    101             }
    102             Err(e) => {
    103                 let (valid, invalid) = bytes.split_at(e.valid_up_to());
    104 
    105                 if !valid.is_empty() {
    106                     //  SAFETY: We already know the bytes until this point are valid
    107                     buffer.push_str(unsafe { str::from_utf8_unchecked(valid) })
    108                 }
    109 
    110                 let error_len = if let Some(len) = e.error_len() {
    111                     len
    112                 } else {
    113                     return buffer;
    114                 };
    115 
    116                 for bad_byte in &invalid[..error_len] {
    117                     // this *might* cause some false positives
    118                     buffer.push_str(&format!("\u{FFFD}{bad_byte:X?}"));
    119                 }
    120 
    121                 bytes = &invalid[error_len..];
    122             }
    123         }
    124     }
    125 }
    126 
    127 pub fn cleanup_string(lua_str: &LuaString) -> String {
    128     escape_control(&remove_invalid(&lua_str.as_bytes()))
    129 }
    130 
    131 pub fn format_string_bytes(bytes: &[u8], colorize: bool) -> String {
    132     let mut s = remove_invalid(bytes);
    133 
    134     if colorize {
    135         s = escape_control_color(&s);
    136     } else {
    137         s = escape_control(&s);
    138     }
    139 
    140     let pair = (s.contains("'"), s.contains('"'));
    141 
    142     match pair {
    143         (true, true) => format!("\"{}\"", s.replace("\"", "\\\"")),
    144         (false, true) => format!("'{s}'"),
    145         (true, false) | (false, false) => format!("\"{s}\""),
    146     }
    147 }
    148 
    149 fn format_string_lua_string(lua_str: &LuaString, colorize: bool) -> String {
    150     format_string_bytes(&lua_str.as_bytes(), colorize)
    151 }
    152 
    153 fn addr_color(value: &LuaValue) -> Option<(String, Color)> {
    154     match value {
    155         LuaValue::LightUserData(l) => Some((format!("{:?}", l.0), Color::Cyan)),
    156         LuaValue::Table(t) => Some((format!("{:?}", t.to_pointer()), Color::LightBlue)),
    157         LuaValue::Function(f) => Some((format!("{:?}", f.to_pointer()), Color::Purple)),
    158         LuaValue::Thread(t) => Some((format!("{:?}", t.to_pointer()), Color::LightGray)),
    159         LuaValue::UserData(u) => Some((format!("{:?}", u.to_pointer()), Color::Cyan)),
    160         _ => None,
    161     }
    162 }
    163 
    164 fn handle_strings<'a>(colorize: bool, strings: AnsiStrings<'a>) -> String {
    165     if colorize {
    166         strings.to_string()
    167     } else {
    168         nu_ansi_term::unstyle(&strings)
    169     }
    170 }
    171 
    172 pub fn display_basic(value: &LuaValue, colorize: bool) -> String {
    173     match addr_color(value) {
    174         Some((addr, color)) => {
    175             let strings: &[AnsiString<'static>] = &[
    176                 color.paint(value.type_name()),
    177                 Color::Default.paint("@"),
    178                 Color::LightYellow.paint(addr),
    179             ];
    180 
    181             handle_strings(colorize, AnsiStrings(strings))
    182         }
    183         None => {
    184             let strings = &[match value {
    185                 LuaValue::Nil => Color::LightRed.paint("nil"),
    186                 LuaValue::Boolean(b) => Color::LightYellow.paint(b.to_string()),
    187                 LuaValue::Integer(i) => Color::LightYellow.paint(i.to_string()),
    188                 LuaValue::Number(n) => Color::LightYellow.paint(n.to_string()),
    189                 LuaValue::String(s) => Color::Green.paint(format_string_lua_string(s, colorize)),
    190                 val => Color::LightGray.paint(val.to_string().unwrap_or_default()),
    191             }];
    192 
    193             handle_strings(colorize, AnsiStrings(strings))
    194         }
    195     }
    196 }
    197 
    198 fn is_short_printable_inner(tbl: &LuaTable, seen: &mut HashSet<usize>) -> bool {
    199     let addr = tbl.to_pointer() as usize;
    200 
    201     if seen.contains(&addr) {
    202         return false;
    203     }
    204 
    205     seen.insert(addr);
    206 
    207     for (key, value) in tbl.pairs::<LuaValue, LuaValue>().flatten() {
    208         if !key.is_integer() {
    209             return false;
    210         }
    211 
    212         if let LuaValue::Table(inner) = value {
    213             let printable = is_short_printable_inner(&inner, seen);
    214 
    215             if !printable {
    216                 return false;
    217             }
    218         }
    219     }
    220 
    221     true
    222 }
    223 
    224 pub fn is_short_printable(tbl: &LuaTable) -> bool {
    225     let mut seen = HashSet::new();
    226 
    227     is_short_printable_inner(tbl, &mut seen)
    228 }
    229 
    230 pub fn print_array(tbl: &LuaTable, colorize: bool) -> String {
    231     let mut buff = Vec::new();
    232 
    233     if tbl.is_empty() {
    234         return String::from("{}");
    235     }
    236 
    237     for (_, value) in tbl.pairs::<LuaValue, LuaValue>().flatten() {
    238         if let LuaValue::Table(inner) = value {
    239             buff.push(print_array(&inner, colorize));
    240         } else {
    241             buff.push(display_basic(&value, colorize));
    242         }
    243     }
    244 
    245     format!("{{ {} }}", buff.join(", "))
    246 }
    247 
    248 fn is_valid_identifier(s: &str) -> bool {
    249     if KEYWORDS.contains(s) {
    250         return false;
    251     }
    252 
    253     let mut chars = s.chars();
    254     let first = if let Some(c) = chars.next() {
    255         c
    256     } else {
    257         return false;
    258     };
    259 
    260     //  [A-Z_a-z]
    261     if !first.is_ascii_alphabetic() && first != '_' {
    262         return false;
    263     }
    264 
    265     let s = chars.as_str();
    266 
    267     if s.is_empty() {
    268         return true;
    269     }
    270 
    271     // [A-Z_a-z0-9]
    272     if s.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
    273         .is_some()
    274     {
    275         return false;
    276     }
    277 
    278     true
    279 }
    280 
    281 fn display_table_inner(
    282     tbl: &LuaTable,
    283     colorize: bool,
    284     seen: &mut HashMap<usize, usize>,
    285     indent: usize,
    286 ) -> Result<String, fmt::Error> {
    287     let ptr = tbl.to_pointer() as usize;
    288     if let Some(id) = seen.get(&ptr) {
    289         return Ok(format!("<{id}>"));
    290     }
    291 
    292     let id = seen.len();
    293     seen.insert(ptr, id);
    294 
    295     let printable = is_short_printable(tbl);
    296 
    297     if printable {
    298         return Ok(print_array(tbl, colorize));
    299     }
    300 
    301     let mut buffer = String::new();
    302 
    303     // TODO; only output id if necessary
    304     writeln!(&mut buffer, "<{id}>{{")?;
    305 
    306     for (key, value) in tbl.pairs::<LuaValue, LuaValue>().flatten() {
    307         buffer.push_str(&("   ".repeat(indent + 1)));
    308 
    309         if let LuaValue::String(ref s) = key {
    310             let clean = cleanup_string(s);
    311 
    312             if is_valid_identifier(&clean) {
    313                 write!(&mut buffer, "{clean} = ")?
    314             } else {
    315                 write!(&mut buffer, "[{}] = ", display_basic(&key, colorize))?
    316             }
    317         } else {
    318             write!(&mut buffer, "[{}] = ", display_basic(&key, colorize))?;
    319         }
    320 
    321         if let LuaValue::Table(t) = value {
    322             writeln!(
    323                 &mut buffer,
    324                 "{},",
    325                 display_table_inner(&t, colorize, seen, indent + 1)?
    326             )?;
    327         } else {
    328             writeln!(&mut buffer, "{},", display_basic(&value, colorize))?;
    329         }
    330     }
    331 
    332     write!(&mut buffer, "{}}}", "   ".repeat(indent))?;
    333 
    334     Ok(buffer)
    335 }
    336 
    337 pub fn display_table(tbl: &LuaTable, colorize: bool) -> LuaResult<String> {
    338     let mut seen = HashMap::new();
    339 
    340     display_table_inner(tbl, colorize, &mut seen, 0)
    341         .map_err(|e| LuaError::ExternalError(Arc::new(e)))
    342 }
    343 
    344 pub fn inspect(value: &LuaValue, colorize: bool) -> LuaResult<String> {
    345     match value {
    346         LuaValue::Table(tbl) => display_table(tbl, colorize),
    347         value => Ok(display_basic(value, colorize)),
    348     }
    349 }
    350 
    351 #[derive(Clone, Copy)]
    352 pub enum TableFormat {
    353     ComfyTable,
    354     Inspect,
    355     Address,
    356 }
    357 
    358 fn comfy_table_inner(
    359     tbl: &LuaTable,
    360     recursive: bool,
    361     visited: &mut HashMap<usize, usize>,
    362 ) -> LuaResult<String> {
    363     let addr = tbl.to_pointer() as usize;
    364 
    365     if let Some(id) = visited.get(&addr) {
    366         return Ok(format!("<table {id}>"));
    367     }
    368 
    369     let id = visited.len();
    370     visited.insert(addr, id);
    371 
    372     let printable = is_short_printable(tbl);
    373 
    374     if printable {
    375         return Ok(print_array(tbl, false));
    376     }
    377 
    378     let mut table = Table::new();
    379     table.load_preset(UTF8_FULL_CONDENSED);
    380     table.set_header(vec![format!("<table {id}>")]);
    381 
    382     for (key, value) in tbl.pairs::<LuaValue, LuaValue>().flatten() {
    383         let (key_str, value_str) = if let LuaValue::Table(sub) = value {
    384             if recursive {
    385                 (
    386                     display_basic(&key, false),
    387                     comfy_table_inner(&sub, recursive, visited)?,
    388                 )
    389             } else {
    390                 (
    391                     display_basic(&key, false),
    392                     display_basic(&LuaValue::Table(sub), false),
    393                 )
    394             }
    395         } else {
    396             (display_basic(&key, false), display_basic(&value, false))
    397         };
    398 
    399         table.add_row(vec![key_str, value_str]);
    400     }
    401 
    402     if table.is_empty() {
    403         Ok(String::from("{}"))
    404     } else {
    405         Ok(table.to_string())
    406     }
    407 }
    408 
    409 pub fn comfy_table(tbl: &LuaTable, recursive: bool) -> LuaResult<String> {
    410     let mut visited = HashMap::new();
    411     comfy_table_inner(tbl, recursive, &mut visited)
    412 }
    413 
    414 impl TableFormat {
    415     pub fn format(&self, tbl: &LuaTable, colorize: bool) -> LuaResult<String> {
    416         match self {
    417             TableFormat::Address => {
    418                 if colorize {
    419                     Ok(format!(
    420                         "{}{}{}",
    421                         Color::LightBlue.paint("table"),
    422                         Color::Default.paint("@"),
    423                         Color::LightYellow.paint(format!("{:?}", tbl.to_pointer()))
    424                     ))
    425                 } else {
    426                     Ok(format!("table@{:?}", tbl.to_pointer()))
    427                 }
    428             }
    429             TableFormat::Inspect => display_table(tbl, colorize).map_err(LuaError::external),
    430             TableFormat::ComfyTable => comfy_table(tbl, true),
    431         }
    432     }
    433 }