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 }