From 19bf3aac7de3e782f83a018c029b14fa844fa7ac Mon Sep 17 00:00:00 2001 From: swananan Date: Wed, 29 Jul 2026 19:43:02 +0800 Subject: [PATCH 1/2] refactor: centralize format template parsing Use one typed parser for validation, code generation, rendering, and legacy event formatting while retaining lossy compatibility for existing traces. --- .../src/ebpf/codegen/format/mod.rs | 129 +-- ghostscope-compiler/src/ebpf/codegen/mod.rs | 4 +- .../src/script/format_validator.rs | 119 +-- ghostscope-protocol/src/format_printer.rs | 785 ++++++++---------- ghostscope-protocol/src/format_template.rs | 508 ++++++++++++ ghostscope-protocol/src/lib.rs | 4 + ghostscope-protocol/src/streaming_parser.rs | 124 ++- 7 files changed, 976 insertions(+), 697 deletions(-) create mode 100644 ghostscope-protocol/src/format_template.rs diff --git a/ghostscope-compiler/src/ebpf/codegen/format/mod.rs b/ghostscope-compiler/src/ebpf/codegen/format/mod.rs index d37772aa..413a391d 100644 --- a/ghostscope-compiler/src/ebpf/codegen/format/mod.rs +++ b/ghostscope-compiler/src/ebpf/codegen/format/mod.rs @@ -296,104 +296,14 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { args.len() ); let format_string_index = self.trace_context.add_string(format.to_string())?; - let mut complex_args: Vec> = Vec::with_capacity(args.len()); - - // Parse placeholders from the format string to support extended specifiers - #[derive(Clone, Copy, Debug, PartialEq)] - enum Conv { - Default, - HexLower, - HexUpper, - Ptr, - Ascii, - } - #[derive(Clone, Debug, PartialEq)] - enum LenSpec { - None, - Static(usize), - Star, - Capture(String), - } - - fn parse_static_len(spec: &str) -> Option { - if spec.chars().all(|c| c.is_ascii_digit()) { - return spec.parse::().ok(); - } - if let Some(hex) = spec.strip_prefix("0x") { - if !hex.is_empty() && hex.chars().all(|c| c.is_ascii_hexdigit()) { - return usize::from_str_radix(hex, 16).ok(); - } - } - if let Some(oct) = spec.strip_prefix("0o") { - if !oct.is_empty() && oct.chars().all(|c| matches!(c, '0'..='7')) { - return usize::from_str_radix(oct, 8).ok(); - } - } - if let Some(bin) = spec.strip_prefix("0b") { - if !bin.is_empty() && bin.chars().all(|c| matches!(c, '0' | '1')) { - return usize::from_str_radix(bin, 2).ok(); - } - } - None - } - - fn parse_slots(fmt: &str) -> Vec<(Conv, LenSpec)> { - let mut res = Vec::new(); - let mut it = fmt.chars().peekable(); - while let Some(ch) = it.next() { - if ch == '{' { - if it.peek() == Some(&'{') { - it.next(); - continue; - } - let mut content = String::new(); - for c in it.by_ref() { - if c == '}' { - break; - } - content.push(c); - } - if content.is_empty() { - res.push((Conv::Default, LenSpec::None)); - } else if let Some(rest) = content.strip_prefix(':') { - let mut sit = rest.chars(); - let conv = match sit.next().unwrap_or(' ') { - 'x' => Conv::HexLower, - 'X' => Conv::HexUpper, - 'p' => Conv::Ptr, - 's' => Conv::Ascii, - _ => Conv::Default, - }; - let rest: String = sit.collect(); - let lens = if rest.is_empty() { - LenSpec::None - } else if let Some(r) = rest.strip_prefix('.') { - if r == "*" { - LenSpec::Star - } else if let Some(s) = r.strip_suffix('$') { - LenSpec::Capture(s.to_string()) - } else if let Some(n) = parse_static_len(r) { - LenSpec::Static(n) - } else { - LenSpec::None - } - } else { - LenSpec::None - }; - res.push((conv, lens)); - } else { - res.push((Conv::Default, LenSpec::None)); - } - } - } - res - } - - let slots = parse_slots(format); + let template = FormatTemplate::parse(format) + .map_err(|error| CodeGenError::TypeError(error.to_string()))?; + let mut complex_args: Vec> = + Vec::with_capacity(template.wire_argument_count()); let mut ai = 0usize; // arg cursor - for (conv, lens) in slots.into_iter() { - match conv { - Conv::Default => { + for slot in template.slots() { + match slot.conversion { + FormatConversion::Default => { if ai >= args.len() { break; } @@ -404,7 +314,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { complex_args.push(a); ai += 1; } - Conv::Ptr => { + FormatConversion::Pointer => { if ai >= args.len() { break; } @@ -458,12 +368,18 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { }); ai += 1; } - Conv::HexLower | Conv::HexUpper | Conv::Ascii => { + FormatConversion::LowerHex + | FormatConversion::UpperHex + | FormatConversion::String => { // Memory dump; handle static length at compile time. Other cases use default read and let user space trim. // Handle star: consume length arg (as computed int) then value arg - let wants_ascii = matches!(conv, Conv::Ascii); - match lens { - LenSpec::Static(n) if ai < args.len() => { + match &slot.length { + FormatLength::Static(n) if ai < args.len() => { + let Ok(n) = usize::try_from(*n) else { + return Err(CodeGenError::TypeError(format!( + "capture length {n} does not fit this host" + ))); + }; // Resolve value expr address let expr = &args[ai]; let addr_iv = self.resolve_memory_format_address(expr)?; @@ -493,7 +409,7 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { }); ai += 1; } - LenSpec::Star => { + FormatLength::Dynamic => { // Dynamic length: consume length arg, then create a dynamic mem-dump for value if ai + 1 >= args.len() { break; @@ -555,18 +471,18 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { }); ai += 2; } - LenSpec::Capture(name) => { + FormatLength::Capture(name) => { // Use script variable `name` as length; emit a length argument + a dynamic mem-dump argument if ai >= args.len() { break; } - if !self.variable_exists(&name) { + if !self.variable_exists(name) { return Err(CodeGenError::TypeError(format!( "capture length variable '{name}' not found" ))); } // length as computed int - let len_val = self.load_variable(&name)?; + let len_val = self.load_variable(name)?; let (len_iv, byte_len) = match len_val { BasicValueEnum::IntValue(iv) => (iv, 8usize), BasicValueEnum::PointerValue(pv) => ( @@ -639,7 +555,6 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> { ai += 1; } } - let _ = wants_ascii; // reserved for future per-arg metadata } } } diff --git a/ghostscope-compiler/src/ebpf/codegen/mod.rs b/ghostscope-compiler/src/ebpf/codegen/mod.rs index d1a6af2d..aff54377 100644 --- a/ghostscope-compiler/src/ebpf/codegen/mod.rs +++ b/ghostscope-compiler/src/ebpf/codegen/mod.rs @@ -26,7 +26,9 @@ use ghostscope_protocol::trace_event::{ VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET, VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET, VARIABLE_READ_ERROR_PAYLOAD_LEN, }; -use ghostscope_protocol::{InstructionType, TraceContext, TypeKind}; +use ghostscope_protocol::{ + FormatConversion, FormatLength, FormatTemplate, InstructionType, TraceContext, TypeKind, +}; use inkwell::values::{BasicValueEnum, IntValue, PointerValue}; use inkwell::AddressSpace; use std::collections::HashMap; diff --git a/ghostscope-compiler/src/script/format_validator.rs b/ghostscope-compiler/src/script/format_validator.rs index a345445d..dcd14678 100644 --- a/ghostscope-compiler/src/script/format_validator.rs +++ b/ghostscope-compiler/src/script/format_validator.rs @@ -33,116 +33,17 @@ impl FormatValidator { /// Returns (placeholders, star_extras) where star_extras is the number of additional /// dynamic-length arguments required by `.*` occurrences. fn count_required_args(format: &str) -> Result<(usize, usize), ParseError> { - let mut placeholders = 0usize; - let mut star_extras = 0usize; - let mut chars = format.chars().peekable(); - - while let Some(ch) = chars.next() { - match ch { - '{' => { - if chars.peek() == Some(&'{') { - chars.next(); // Skip escaped '{{' - } else { - // Found a placeholder, look for closing '}' - let mut found_closing = false; - let mut placeholder_content = String::new(); - - for inner_ch in chars.by_ref() { - if inner_ch == '}' { - found_closing = true; - break; - } - placeholder_content.push(inner_ch); - } - - if !found_closing { - return Err(ParseError::InvalidExpression); - } - - // Accept: empty "{}" or extended forms like ":x", ":X", ":p", ":s", optionally with - // a length suffix ".N" (digits) or ".*" (dynamic length consumes one extra argument) - if placeholder_content.is_empty() { - placeholders += 1; - } else { - // Must start with ':' - if !placeholder_content.starts_with(':') { - return Err(ParseError::TypeError(format!( - "Invalid format specifier '{{{placeholder_content}}}': expected ':' prefix" - ))); - } - // Extract conv and optional suffix - let tail = &placeholder_content[1..]; - // conv is first char - let mut iter = tail.chars(); - let conv = iter.next().ok_or_else(|| { - ParseError::TypeError("Empty format after ':'".to_string()) - })?; - match conv { - 'x' | 'X' | 'p' | 's' => {} - _ => { - return Err(ParseError::TypeError(format!( - "Unsupported format conversion '{{:{conv}}}'" - ))); - } - } - // Remaining should be empty or ".N" or ".*" or ".name$" (capture variable) - let rest: String = iter.collect(); - if rest.is_empty() { - // ok - } else if let Some(rem) = rest.strip_prefix('.') { - if rem == "*" { - star_extras += 1; // dynamic length consumes next arg - } else if let Some(name) = rem.strip_suffix('$') { - // capture variable name: [A-Za-z_][A-Za-z0-9_]*$ - let mut chars = name.chars(); - let valid = if let Some(first) = chars.next() { - (first.is_ascii_alphabetic() || first == '_') - && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') - } else { - false - }; - if !valid { - return Err(ParseError::TypeError(format!( - "Invalid capture variable in specifier '{{:{conv}.{rem}}}'" - ))); - } - } else if rem.chars().all(|c| c.is_ascii_digit()) - || (rem.starts_with("0x") - && rem.len() > 2 - && rem[2..].chars().all(|c| c.is_ascii_hexdigit())) - || (rem.starts_with("0o") - && rem.len() > 2 - && rem[2..].chars().all(|c| matches!(c, '0'..='7'))) - || (rem.starts_with("0b") - && rem.len() > 2 - && rem[2..].chars().all(|c| matches!(c, '0' | '1'))) - { - // static length with base support: decimal / 0x.. / 0o.. / 0b.. - } else { - return Err(ParseError::TypeError(format!( - "Invalid length in specifier '{{:{conv}{rest}}}'" - ))); - } - } else { - return Err(ParseError::TypeError(format!( - "Invalid specifier syntax '{{:{conv}{rest}}}'" - ))); - } - placeholders += 1; - } - } - } - '}' => { - if chars.peek() == Some(&'}') { - chars.next(); // Skip escaped '}}' - } else { - return Err(ParseError::InvalidExpression); // Unmatched '}' - } - } - _ => {} + let template = ghostscope_protocol::FormatTemplate::parse(format).map_err(|error| { + if error.is_structure_error() { + ParseError::InvalidExpression + } else { + ParseError::TypeError(error.to_string()) } - } - + })?; + let placeholders = template.slot_count(); + let star_extras = template + .script_argument_count() + .saturating_sub(placeholders); Ok((placeholders, star_extras)) } } diff --git a/ghostscope-protocol/src/format_printer.rs b/ghostscope-protocol/src/format_printer.rs index bd3fb3c4..1a738c5e 100644 --- a/ghostscope-protocol/src/format_printer.rs +++ b/ghostscope-protocol/src/format_printer.rs @@ -11,9 +11,9 @@ use crate::trace_event::{ }; use crate::type_info::TypeInfo; use crate::{ - BTreeEntryPresentation, BTreeFieldPresentation, HashTableBucketOrder, - HashTableEntryPresentation, HashTableFieldPresentation, HashTableOccupancy, - NestedValueChildrenPresentation, NestedValueFieldPresentation, + BTreeEntryPresentation, BTreeFieldPresentation, FormatConversion, FormatLength, FormatPart, + FormatTemplate, HashTableBucketOrder, HashTableEntryPresentation, HashTableFieldPresentation, + HashTableOccupancy, NestedValueChildrenPresentation, NestedValueFieldPresentation, NestedValueHashTableFieldPresentation, NestedValuePresentation, NestedValueVariantFieldPresentation, ValuePresentation, BTREE_CAPTURED_ITEM_COUNT_OFFSET, BTREE_HEADER_SIZE, BTREE_NODE_HEADER_SIZE, BTREE_NODE_HEIGHT_OFFSET, BTREE_NODE_LENGTH_OFFSET, @@ -116,46 +116,21 @@ impl FormatPrinter { /// Simple placeholder applier for tests that don't use complex variables #[cfg(test)] fn apply_format_strings(format_string: &str, formatted_values: &[String]) -> String { + let template = FormatTemplate::parse_lossy(format_string); let mut result = String::new(); - let mut chars = format_string.chars().peekable(); let mut var_index = 0; - while let Some(ch) = chars.next() { - match ch { - '{' => { - if chars.peek() == Some(&'{') { - chars.next(); - result.push('{'); + for part in template.parts() { + match part { + FormatPart::Literal(literal) => result.push_str(literal), + FormatPart::Slot(_) => { + if let Some(value) = formatted_values.get(var_index) { + result.push_str(value); + var_index += 1; } else { - // Skip to closing '}' and substitute - let mut found = false; - for c in chars.by_ref() { - if c == '}' { - found = true; - break; - } - } - if found { - if var_index < formatted_values.len() { - result.push_str(&formatted_values[var_index]); - var_index += 1; - } else { - result.push_str(""); - } - } else { - result.push_str(""); - } + result.push_str(""); } } - '}' => { - if chars.peek() == Some(&'}') { - chars.next(); - result.push('}'); - } else { - result.push('}'); - } - } - _ => result.push(ch), } } result @@ -168,296 +143,219 @@ impl FormatPrinter { vars: &[ParsedComplexVariable], trace_context: &TraceContext, ) -> String { + let template = FormatTemplate::parse_lossy(format_string); let mut result = String::new(); - let mut chars = format_string.chars().peekable(); let mut var_index: usize = 0; - while let Some(ch) = chars.next() { - match ch { - '{' => { - if chars.peek() == Some(&'{') { - chars.next(); - result.push('{'); - } else { - let mut found = false; - let mut content = String::new(); - for c in chars.by_ref() { - if c == '}' { - found = true; - break; - } - content.push(c); - } - if !found { - result.push_str(""); - continue; + for part in template.parts() { + match part { + FormatPart::Literal(literal) => result.push_str(literal), + FormatPart::Slot(slot) => { + if slot.conversion == FormatConversion::Default { + if let Some(variable) = vars.get(var_index) { + let formatted = Self::format_complex_variable_with_status( + variable.var_name_index, + variable.type_index, + &variable.access_path, + &variable.data, + variable.status, + trace_context, + ); + result.push_str(Self::formatted_value_part(&formatted)); + var_index += 1; + } else { + result.push_str(""); } - - if content.is_empty() { - // default {} - if var_index < vars.len() { - let v = &vars[var_index]; - let s = Self::format_complex_variable_with_status( - v.var_name_index, - v.type_index, - &v.access_path, - &v.data, - v.status, - trace_context, - ); - let value_part = Self::formatted_value_part(&s); - result.push_str(value_part); - var_index += 1; + continue; + } + let conv = slot.conversion; + let lenspec = &slot.length; + + // helper: parse signed length from 8-byte little endian, clamp to >=0 + fn parse_len_usize(lenb: &[u8]) -> usize { + if lenb.len() >= 8 { + let arr = [ + lenb[0], lenb[1], lenb[2], lenb[3], lenb[4], lenb[5], lenb[6], + lenb[7], + ]; + let v = i64::from_le_bytes(arr); + if v <= 0 { + 0 } else { - result.push_str(""); + v as usize } - continue; + } else { + 0 } + } - if !content.starts_with(':') { - result.push_str(""); - continue; + // Format statuses that cannot be consumed by a conversion. + let err_value_part = |idx: usize| -> Option { + if idx >= vars.len() { + return None; } - let tail = &content[1..]; - let mut it = tail.chars(); - let conv = it.next().unwrap_or(' '); - let rest: String = it.collect(); - - // (removed) helper to get bytes of current arg; we now surface errors explicitly - - enum Len { - None, - Static(usize), - Star, - Capture, - } - // helper: parse static length supporting decimal/0x.. /0o.. /0b.. - fn parse_static_len(spec: &str) -> Option { - if spec.chars().all(|c| c.is_ascii_digit()) { - return spec.parse::().ok(); - } - if let Some(hex) = spec.strip_prefix("0x") { - if !hex.is_empty() && hex.chars().all(|c| c.is_ascii_hexdigit()) { - return usize::from_str_radix(hex, 16).ok(); - } - } - if let Some(oct) = spec.strip_prefix("0o") { - if !oct.is_empty() && oct.chars().all(|c| matches!(c, '0'..='7')) { - return usize::from_str_radix(oct, 8).ok(); - } - } - if let Some(bin) = spec.strip_prefix("0b") { - if !bin.is_empty() && bin.chars().all(|c| matches!(c, '0' | '1')) { - return usize::from_str_radix(bin, 2).ok(); - } - } + let v = &vars[idx]; + if v.status == VariableStatus::Ok as u8 + || v.status == VariableStatus::ZeroLength as u8 + { None + } else { + let s = Self::format_complex_variable_with_status( + v.var_name_index, + v.type_index, + &v.access_path, + &v.data, + v.status, + trace_context, + ); + Some(Self::formatted_value_part(&s).to_string()) } - - let lenspec = if rest.is_empty() { - Len::None - } else if let Some(r) = rest.strip_prefix('.') { - if r == "*" { - Len::Star - } else if r.ends_with('$') { - Len::Capture - } else if let Some(n) = parse_static_len(r) { - Len::Static(n) - } else { - Len::None - } + }; + let raw_err_value_part = |idx: usize| -> Option { + if vars.get(idx).is_some_and(|variable| { + Self::is_semantic_truncation(variable, trace_context) + }) { + None } else { - Len::None - }; - - // helper: parse signed length from 8-byte little endian, clamp to >=0 - fn parse_len_usize(lenb: &[u8]) -> usize { - if lenb.len() >= 8 { - let arr = [ - lenb[0], lenb[1], lenb[2], lenb[3], lenb[4], lenb[5], lenb[6], - lenb[7], - ]; - let v = i64::from_le_bytes(arr); - if v <= 0 { - 0 - } else { - v as usize - } - } else { - 0 - } + err_value_part(idx) } + }; - // Format statuses that cannot be consumed by a conversion. - let err_value_part = |idx: usize| -> Option { - if idx >= vars.len() { - return None; - } - let v = &vars[idx]; - if v.status == VariableStatus::Ok as u8 - || v.status == VariableStatus::ZeroLength as u8 - { - None - } else { - let s = Self::format_complex_variable_with_status( - v.var_name_index, - v.type_index, - &v.access_path, - &v.data, - v.status, - trace_context, - ); - Some(Self::formatted_value_part(&s).to_string()) - } - }; - let raw_err_value_part = |idx: usize| -> Option { - if vars.get(idx).is_some_and(|variable| { - Self::is_semantic_truncation(variable, trace_context) - }) { - None - } else { - err_value_part(idx) - } - }; - - match conv { - 'x' | 'X' => { - match lenspec { - Len::Star => { - if var_index + 1 >= vars.len() { - result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { - // surface error from length argument - result.push_str(&err); - var_index += 2; - continue; - } else if let Some(err) = err_value_part(var_index + 1) { - // surface error from value argument - result.push_str(&err); - var_index += 2; - continue; + match conv { + FormatConversion::LowerHex | FormatConversion::UpperHex => { + match lenspec { + FormatLength::Dynamic => { + if var_index + 1 >= vars.len() { + result.push_str(""); + } else if let Some(err) = err_value_part(var_index) { + // surface error from length argument + result.push_str(&err); + var_index += 2; + continue; + } else if let Some(err) = err_value_part(var_index + 1) { + // surface error from value argument + result.push_str(&err); + var_index += 2; + continue; + } else { + // both Ok or ZeroLength + let lenb = vars[var_index].data.as_slice(); + let n = parse_len_usize(lenb); + let v = &vars[var_index + 1]; + let full = v.data.as_slice(); + let take = if v.status == VariableStatus::ZeroLength as u8 { + 0 } else { - // both Ok or ZeroLength - let lenb = vars[var_index].data.as_slice(); - let n = parse_len_usize(lenb); - let v = &vars[var_index + 1]; - let full = v.data.as_slice(); - let take = - if v.status == VariableStatus::ZeroLength as u8 { - 0 + std::cmp::min(n, full.len()) + }; + let b = &full[..take]; + let s = b + .iter() + .map(|vv| { + if conv == FormatConversion::LowerHex { + format!("{vv:02x}") } else { - std::cmp::min(n, full.len()) - }; - let b = &full[..take]; - let s = b - .iter() - .map(|vv| { - if conv == 'x' { - format!("{vv:02x}") - } else { - format!("{vv:02X}") - } - }) - .collect::>() - .join(" "); - result.push_str(&s); - var_index += 2; - continue; - } - // when missing one of the args, don't advance to avoid misalignment + format!("{vv:02X}") + } + }) + .collect::>() + .join(" "); + result.push_str(&s); + var_index += 2; + continue; } - Len::Static(n) => { - if var_index >= vars.len() { - result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { - result.push_str(&err); - var_index += 1; - continue; + // when missing one of the args, don't advance to avoid misalignment + } + FormatLength::Static(n) => { + let n = usize::try_from(*n).unwrap_or(usize::MAX); + if var_index >= vars.len() { + result.push_str(""); + } else if let Some(err) = err_value_part(var_index) { + result.push_str(&err); + var_index += 1; + continue; + } else { + let v = &vars[var_index]; + let full = v.data.as_slice(); + let take = if v.status == VariableStatus::ZeroLength as u8 { + 0 } else { - let v = &vars[var_index]; - let full = v.data.as_slice(); - let take = - if v.status == VariableStatus::ZeroLength as u8 { - 0 + std::cmp::min(n, full.len()) + }; + let b = &full[..take]; + let s = b + .iter() + .map(|vv| { + if conv == FormatConversion::LowerHex { + format!("{vv:02x}") } else { - std::cmp::min(n, full.len()) - }; - let b = &full[..take]; - let s = b - .iter() - .map(|vv| { - if conv == 'x' { - format!("{vv:02x}") - } else { - format!("{vv:02X}") - } - }) - .collect::>() - .join(" "); - result.push_str(&s); - var_index += 1; - continue; - } + format!("{vv:02X}") + } + }) + .collect::>() + .join(" "); + result.push_str(&s); + var_index += 1; + continue; } - Len::Capture => { - if var_index + 1 >= vars.len() { - result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { - result.push_str(&err); - var_index += 2; - continue; - } else if let Some(err) = err_value_part(var_index + 1) { - result.push_str(&err); - var_index += 2; - continue; + } + FormatLength::Capture(_) => { + if var_index + 1 >= vars.len() { + result.push_str(""); + } else if let Some(err) = err_value_part(var_index) { + result.push_str(&err); + var_index += 2; + continue; + } else if let Some(err) = err_value_part(var_index + 1) { + result.push_str(&err); + var_index += 2; + continue; + } else { + let lenb = vars[var_index].data.as_slice(); + let n = parse_len_usize(lenb); + let v = &vars[var_index + 1]; + let full = v.data.as_slice(); + let take = if v.status == VariableStatus::ZeroLength as u8 { + 0 } else { - let lenb = vars[var_index].data.as_slice(); - let n = parse_len_usize(lenb); - let v = &vars[var_index + 1]; - let full = v.data.as_slice(); - let take = - if v.status == VariableStatus::ZeroLength as u8 { - 0 + std::cmp::min(n, full.len()) + }; + let b = &full[..take]; + let s = b + .iter() + .map(|vv| { + if conv == FormatConversion::LowerHex { + format!("{vv:02x}") } else { - std::cmp::min(n, full.len()) - }; - let b = &full[..take]; - let s = b - .iter() - .map(|vv| { - if conv == 'x' { - format!("{vv:02x}") - } else { - format!("{vv:02X}") - } - }) - .collect::>() - .join(" "); - result.push_str(&s); - var_index += 2; - continue; - } - // when missing one of the args, don't advance + format!("{vv:02X}") + } + }) + .collect::>() + .join(" "); + result.push_str(&s); + var_index += 2; + continue; } - Len::None => { - if var_index >= vars.len() { - result.push_str(""); - } else if let Some(err) = raw_err_value_part(var_index) { - result.push_str(&err); - var_index += 1; - continue; - } else { - let v = &vars[var_index]; - let truncated = match Self::format_spec_payload_bytes( - v, - trace_context, - ) { + // when missing one of the args, don't advance + } + FormatLength::None => { + if var_index >= vars.len() { + result.push_str(""); + } else if let Some(err) = raw_err_value_part(var_index) { + result.push_str(&err); + var_index += 1; + continue; + } else { + let v = &vars[var_index]; + let truncated = + match Self::format_spec_payload_bytes(v, trace_context) + { Ok(payload) => { let formatted = payload .bytes .iter() .map(|byte| { - if conv == 'x' { + if conv == FormatConversion::LowerHex { format!("{byte:02x}") } else { format!("{byte:02X}") @@ -473,118 +371,115 @@ impl FormatPrinter { false } }; - Self::append_truncation_marker(&mut result, truncated); - var_index += 1; - continue; - } + Self::append_truncation_marker(&mut result, truncated); + var_index += 1; + continue; } } } - 's' => { - let render_bytes = |b: &[u8]| { - let mut out = String::new(); - for &c in b.iter() { - if c == 0 { - break; - } - if (0x20..=0x7e).contains(&c) { - out.push(c as char); - } else { - out.push_str(&format!("\\x{c:02x}")); - } + } + FormatConversion::String => { + let render_bytes = |b: &[u8]| { + let mut out = String::new(); + for &c in b.iter() { + if c == 0 { + break; } - out - }; - - match lenspec { - Len::Star => { - if var_index + 1 >= vars.len() { - result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { - result.push_str(&err); - var_index += 2; - continue; - } else if let Some(err) = err_value_part(var_index + 1) { - result.push_str(&err); - var_index += 2; - continue; - } else { - let lenb = vars[var_index].data.as_slice(); - let n = parse_len_usize(lenb); - let v = &vars[var_index + 1]; - let full = v.data.as_slice(); - let take = - if v.status == VariableStatus::ZeroLength as u8 { - 0 - } else { - std::cmp::min(n, full.len()) - }; - result.push_str(&render_bytes(&full[..take])); - var_index += 2; - continue; - } + if (0x20..=0x7e).contains(&c) { + out.push(c as char); + } else { + out.push_str(&format!("\\x{c:02x}")); } - Len::Static(n) => { - if var_index >= vars.len() { - result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { - result.push_str(&err); - var_index += 1; - continue; + } + out + }; + + match lenspec { + FormatLength::Dynamic => { + if var_index + 1 >= vars.len() { + result.push_str(""); + } else if let Some(err) = err_value_part(var_index) { + result.push_str(&err); + var_index += 2; + continue; + } else if let Some(err) = err_value_part(var_index + 1) { + result.push_str(&err); + var_index += 2; + continue; + } else { + let lenb = vars[var_index].data.as_slice(); + let n = parse_len_usize(lenb); + let v = &vars[var_index + 1]; + let full = v.data.as_slice(); + let take = if v.status == VariableStatus::ZeroLength as u8 { + 0 } else { - let v = &vars[var_index]; - let full = v.data.as_slice(); - let take = - if v.status == VariableStatus::ZeroLength as u8 { - 0 - } else { - std::cmp::min(n, full.len()) - }; - result.push_str(&render_bytes(&full[..take])); - var_index += 1; - continue; - } + std::cmp::min(n, full.len()) + }; + result.push_str(&render_bytes(&full[..take])); + var_index += 2; + continue; } - Len::Capture => { - if var_index + 1 >= vars.len() { - result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { - result.push_str(&err); - var_index += 2; - continue; - } else if let Some(err) = err_value_part(var_index + 1) { - result.push_str(&err); - var_index += 2; - continue; + } + FormatLength::Static(n) => { + let n = usize::try_from(*n).unwrap_or(usize::MAX); + if var_index >= vars.len() { + result.push_str(""); + } else if let Some(err) = err_value_part(var_index) { + result.push_str(&err); + var_index += 1; + continue; + } else { + let v = &vars[var_index]; + let full = v.data.as_slice(); + let take = if v.status == VariableStatus::ZeroLength as u8 { + 0 } else { - let lenb = vars[var_index].data.as_slice(); - let n = parse_len_usize(lenb); - let v = &vars[var_index + 1]; - let full = v.data.as_slice(); - let take = - if v.status == VariableStatus::ZeroLength as u8 { - 0 - } else { - std::cmp::min(n, full.len()) - }; - result.push_str(&render_bytes(&full[..take])); - var_index += 2; - continue; - } + std::cmp::min(n, full.len()) + }; + result.push_str(&render_bytes(&full[..take])); + var_index += 1; + continue; } - Len::None => { - if var_index >= vars.len() { - result.push_str(""); - } else if let Some(err) = raw_err_value_part(var_index) { - result.push_str(&err); - var_index += 1; - continue; + } + FormatLength::Capture(_) => { + if var_index + 1 >= vars.len() { + result.push_str(""); + } else if let Some(err) = err_value_part(var_index) { + result.push_str(&err); + var_index += 2; + continue; + } else if let Some(err) = err_value_part(var_index + 1) { + result.push_str(&err); + var_index += 2; + continue; + } else { + let lenb = vars[var_index].data.as_slice(); + let n = parse_len_usize(lenb); + let v = &vars[var_index + 1]; + let full = v.data.as_slice(); + let take = if v.status == VariableStatus::ZeroLength as u8 { + 0 } else { - let v = &vars[var_index]; - let truncated = match Self::format_spec_payload_bytes( - v, - trace_context, - ) { + std::cmp::min(n, full.len()) + }; + result.push_str(&render_bytes(&full[..take])); + var_index += 2; + continue; + } + } + FormatLength::None => { + if var_index >= vars.len() { + result.push_str(""); + } else if let Some(err) = raw_err_value_part(var_index) { + result.push_str(&err); + var_index += 1; + continue; + } else { + let v = &vars[var_index]; + let truncated = + match Self::format_spec_payload_bytes(v, trace_context) + { Ok(payload) => { result.push_str(&render_bytes( payload.bytes.as_ref(), @@ -596,65 +491,55 @@ impl FormatPrinter { false } }; - Self::append_truncation_marker(&mut result, truncated); - var_index += 1; - continue; - } + Self::append_truncation_marker(&mut result, truncated); + var_index += 1; + continue; } } } - 'p' => { - if var_index >= vars.len() { - result.push_str(""); - } else if let Some(err) = err_value_part(var_index) { - result.push_str(&err); - var_index += 1; - continue; + } + FormatConversion::Pointer => { + if var_index >= vars.len() { + result.push_str(""); + } else if let Some(err) = err_value_part(var_index) { + result.push_str(&err); + var_index += 1; + continue; + } else { + let b = vars[var_index].data.as_slice(); + if b.len() >= 8 { + let addr = u64::from_le_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ]); + result.push_str(&format!("0x{addr:x}")); } else { - let b = vars[var_index].data.as_slice(); - if b.len() >= 8 { - let addr = u64::from_le_bytes([ - b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], - ]); - result.push_str(&format!("0x{addr:x}")); - } else { - result.push_str(""); - } - var_index += 1; - continue; + result.push_str(""); } + var_index += 1; + continue; } - _ => { - // fallback to default formatting - if var_index < vars.len() { - let v = &vars[var_index]; - let s = Self::format_complex_variable_with_status( - v.var_name_index, - v.type_index, - &v.access_path, - &v.data, - v.status, - trace_context, - ); - let value_part = Self::formatted_value_part(&s); - result.push_str(value_part); - var_index += 1; - } else { - result.push_str(""); - } + } + FormatConversion::Default => { + // fallback to default formatting + if var_index < vars.len() { + let v = &vars[var_index]; + let s = Self::format_complex_variable_with_status( + v.var_name_index, + v.type_index, + &v.access_path, + &v.data, + v.status, + trace_context, + ); + let value_part = Self::formatted_value_part(&s); + result.push_str(value_part); + var_index += 1; + } else { + result.push_str(""); } } } } - '}' => { - if chars.peek() == Some(&'}') { - chars.next(); - result.push('}'); - } else { - result.push('}'); - } - } - _ => result.push(ch), } } result diff --git a/ghostscope-protocol/src/format_template.rs b/ghostscope-protocol/src/format_template.rs new file mode 100644 index 00000000..6939462d --- /dev/null +++ b/ghostscope-protocol/src/format_template.rs @@ -0,0 +1,508 @@ +//! Shared parsing for GhostScope's formatted-print template syntax. + +use std::fmt; + +/// Conversion applied to one formatted-print value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatConversion { + Default, + LowerHex, + UpperHex, + Pointer, + String, +} + +impl FormatConversion { + /// Whether the conversion consumes an optional capture length. + pub const fn supports_length(self) -> bool { + matches!(self, Self::LowerHex | Self::UpperHex | Self::String) + } +} + +/// Optional byte length attached to a format conversion. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FormatLength { + None, + Static(u64), + Dynamic, + Capture(String), +} + +/// One typed placeholder in a format template. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatSlot { + pub conversion: FormatConversion, + pub length: FormatLength, +} + +impl FormatSlot { + /// Number of script expressions consumed by this placeholder. + pub const fn script_argument_count(&self) -> usize { + if matches!(self.length, FormatLength::Dynamic) { + 2 + } else { + 1 + } + } + + /// Number of values encoded in the trace event for this placeholder. + pub const fn wire_argument_count(&self) -> usize { + if self.conversion.supports_length() + && matches!( + self.length, + FormatLength::Dynamic | FormatLength::Capture(_) + ) + { + 2 + } else { + 1 + } + } +} + +impl fmt::Display for FormatSlot { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let conversion = match self.conversion { + FormatConversion::Default => return formatter.write_str("{}"), + FormatConversion::LowerHex => 'x', + FormatConversion::UpperHex => 'X', + FormatConversion::Pointer => 'p', + FormatConversion::String => 's', + }; + write!(formatter, "{{:{conversion}")?; + match &self.length { + FormatLength::None => {} + FormatLength::Static(length) => write!(formatter, ".{length}")?, + FormatLength::Dynamic => formatter.write_str(".*")?, + FormatLength::Capture(name) => write!(formatter, ".{name}$")?, + } + formatter.write_str("}") + } +} + +/// A decoded literal or typed placeholder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FormatPart { + Literal(String), + Slot(FormatSlot), +} + +/// Parsed format string shared by validation, code generation, and rendering. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatTemplate { + parts: Vec, +} + +impl FormatTemplate { + /// Parse a template and reject malformed syntax. + pub fn parse(format: &str) -> Result { + Self::parse_with_mode(format, ParseMode::Strict) + } + + /// Parse a template using the renderer's compatibility fallbacks. + pub fn parse_lossy(format: &str) -> Self { + Self::parse_with_mode(format, ParseMode::Lossy) + .expect("lossy format parsing does not return errors") + } + + fn parse_with_mode(format: &str, mode: ParseMode) -> Result { + let mut chars = format.chars().peekable(); + let mut parts = Vec::new(); + let mut literal = String::new(); + + while let Some(ch) = chars.next() { + match ch { + '{' if chars.peek() == Some(&'{') => { + chars.next(); + literal.push('{'); + } + '{' => { + push_literal(&mut parts, &mut literal); + let mut content = String::new(); + let mut found_closing = false; + for inner in chars.by_ref() { + if inner == '}' { + found_closing = true; + break; + } + content.push(inner); + } + if !found_closing { + if mode == ParseMode::Strict { + return Err(FormatTemplateError::UnclosedPlaceholder); + } + literal.push_str(""); + break; + } + push_part(&mut parts, parse_part(&content, mode)?); + } + '}' if chars.peek() == Some(&'}') => { + chars.next(); + literal.push('}'); + } + '}' if mode == ParseMode::Strict => { + return Err(FormatTemplateError::UnmatchedClosingBrace); + } + '}' => literal.push('}'), + _ => literal.push(ch), + } + } + + push_literal(&mut parts, &mut literal); + Ok(Self { parts }) + } + + /// Return decoded literals and typed placeholders in source order. + pub fn parts(&self) -> &[FormatPart] { + &self.parts + } + + /// Iterate over typed placeholders in source order. + pub fn slots(&self) -> impl Iterator { + self.parts.iter().filter_map(|part| match part { + FormatPart::Literal(_) => None, + FormatPart::Slot(slot) => Some(slot), + }) + } + + /// Return the number of placeholders in the template. + pub fn slot_count(&self) -> usize { + self.slots().count() + } + + /// Return the number of expressions required in the trace script. + pub fn script_argument_count(&self) -> usize { + self.slots().map(FormatSlot::script_argument_count).sum() + } + + /// Return the number of values emitted into the trace event. + pub fn wire_argument_count(&self) -> usize { + self.slots().map(FormatSlot::wire_argument_count).sum() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ParseMode { + Strict, + Lossy, +} + +/// Syntax error returned by strict format-template parsing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FormatTemplateError { + UnclosedPlaceholder, + UnmatchedClosingBrace, + InvalidSpecifier { content: String }, + EmptyConversion, + UnsupportedConversion { conversion: char }, + InvalidCaptureVariable { specifier: String }, + InvalidLength { specifier: String }, + InvalidSyntax { specifier: String }, +} + +impl FormatTemplateError { + /// Whether the error is caused by unmatched template delimiters. + pub const fn is_structure_error(&self) -> bool { + matches!( + self, + Self::UnclosedPlaceholder | Self::UnmatchedClosingBrace + ) + } +} + +impl fmt::Display for FormatTemplateError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnclosedPlaceholder => formatter.write_str("unclosed format placeholder"), + Self::UnmatchedClosingBrace => formatter.write_str("unmatched closing brace"), + Self::InvalidSpecifier { content } => write!( + formatter, + "Invalid format specifier '{{{content}}}': expected ':' prefix" + ), + Self::EmptyConversion => formatter.write_str("Empty format after ':'"), + Self::UnsupportedConversion { conversion } => { + write!( + formatter, + "Unsupported format conversion '{{:{conversion}}}'" + ) + } + Self::InvalidCaptureVariable { specifier } => { + write!( + formatter, + "Invalid capture variable in specifier '{specifier}'" + ) + } + Self::InvalidLength { specifier } => { + write!(formatter, "Invalid length in specifier '{specifier}'") + } + Self::InvalidSyntax { specifier } => { + write!(formatter, "Invalid specifier syntax '{specifier}'") + } + } + } +} + +impl std::error::Error for FormatTemplateError {} + +fn push_literal(parts: &mut Vec, literal: &mut String) { + if !literal.is_empty() { + push_part(parts, FormatPart::Literal(std::mem::take(literal))); + } +} + +fn push_part(parts: &mut Vec, part: FormatPart) { + if let FormatPart::Literal(literal) = part { + if let Some(FormatPart::Literal(previous)) = parts.last_mut() { + previous.push_str(&literal); + } else if !literal.is_empty() { + parts.push(FormatPart::Literal(literal)); + } + } else { + parts.push(part); + } +} + +fn parse_part(content: &str, mode: ParseMode) -> Result { + if content.is_empty() { + return Ok(FormatPart::Slot(FormatSlot { + conversion: FormatConversion::Default, + length: FormatLength::None, + })); + } + + let Some(specifier) = content.strip_prefix(':') else { + if mode == ParseMode::Lossy { + return Ok(FormatPart::Literal("".to_string())); + } + return Err(FormatTemplateError::InvalidSpecifier { + content: content.to_string(), + }); + }; + let mut chars = specifier.chars(); + let conversion_char = match chars.next() { + Some(conversion) => conversion, + None if mode == ParseMode::Lossy => ' ', + None => return Err(FormatTemplateError::EmptyConversion), + }; + let conversion = match conversion_char { + 'x' => FormatConversion::LowerHex, + 'X' => FormatConversion::UpperHex, + 'p' => FormatConversion::Pointer, + 's' => FormatConversion::String, + _ if mode == ParseMode::Lossy => FormatConversion::Default, + _ => { + return Err(FormatTemplateError::UnsupportedConversion { + conversion: conversion_char, + }); + } + }; + + let suffix = chars.as_str(); + let length = if suffix.is_empty() { + FormatLength::None + } else if let Some(length) = suffix.strip_prefix('.') { + parse_length(conversion_char, length, mode)? + } else if mode == ParseMode::Lossy { + FormatLength::None + } else { + return Err(FormatTemplateError::InvalidSyntax { + specifier: format!("{{:{conversion_char}{suffix}}}"), + }); + }; + + Ok(FormatPart::Slot(FormatSlot { conversion, length })) +} + +fn parse_length( + conversion: char, + length: &str, + mode: ParseMode, +) -> Result { + if length == "*" { + return Ok(FormatLength::Dynamic); + } + if let Some(name) = length.strip_suffix('$') { + if is_capture_name(name) || mode == ParseMode::Lossy { + return Ok(FormatLength::Capture(name.to_string())); + } + return Err(FormatTemplateError::InvalidCaptureVariable { + specifier: format!("{{:{conversion}.{length}}}"), + }); + } + + match parse_static_length(length) { + Some(Some(value)) => Ok(FormatLength::Static(value)), + // Preserve the previous behavior for syntactically valid values that + // do not fit the numeric parser: accept the slot without a usable + // static bound. + Some(None) => Ok(FormatLength::None), + None if mode == ParseMode::Lossy => Ok(FormatLength::None), + None => Err(FormatTemplateError::InvalidLength { + specifier: format!("{{:{conversion}.{length}}}"), + }), + } +} + +fn parse_static_length(length: &str) -> Option> { + if length.chars().all(|ch| ch.is_ascii_digit()) { + return Some(length.parse::().ok()); + } + if let Some(hex) = length.strip_prefix("0x") { + if !hex.is_empty() && hex.chars().all(|ch| ch.is_ascii_hexdigit()) { + return Some(u64::from_str_radix(hex, 16).ok()); + } + } + if let Some(octal) = length.strip_prefix("0o") { + if !octal.is_empty() && octal.chars().all(|ch| matches!(ch, '0'..='7')) { + return Some(u64::from_str_radix(octal, 8).ok()); + } + } + if let Some(binary) = length.strip_prefix("0b") { + if !binary.is_empty() && binary.chars().all(|ch| matches!(ch, '0' | '1')) { + return Some(u64::from_str_radix(binary, 2).ok()); + } + } + None +} + +fn is_capture_name(name: &str) -> bool { + let mut chars = name.chars(); + chars.next().is_some_and(|first| { + (first.is_ascii_alphabetic() || first == '_') + && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_literals_escapes_and_default_slots() { + let template = + FormatTemplate::parse("prefix {{ {} }} suffix").expect("parse format template"); + + assert_eq!( + template.parts(), + [ + FormatPart::Literal("prefix { ".to_string()), + FormatPart::Slot(FormatSlot { + conversion: FormatConversion::Default, + length: FormatLength::None, + }), + FormatPart::Literal(" } suffix".to_string()), + ] + ); + assert_eq!(template.script_argument_count(), 1); + assert_eq!(template.wire_argument_count(), 1); + } + + #[test] + fn parses_extended_conversions_and_lengths() { + let template = + FormatTemplate::parse("{:x.16} {:X.0x10} {:s.0o20} {:x.0b1000} {:s.*} {:x.len$} {:p}") + .expect("parse extended format template"); + let slots = template.slots().cloned().collect::>(); + + assert_eq!( + slots, + [ + FormatSlot { + conversion: FormatConversion::LowerHex, + length: FormatLength::Static(16), + }, + FormatSlot { + conversion: FormatConversion::UpperHex, + length: FormatLength::Static(16), + }, + FormatSlot { + conversion: FormatConversion::String, + length: FormatLength::Static(16), + }, + FormatSlot { + conversion: FormatConversion::LowerHex, + length: FormatLength::Static(8), + }, + FormatSlot { + conversion: FormatConversion::String, + length: FormatLength::Dynamic, + }, + FormatSlot { + conversion: FormatConversion::LowerHex, + length: FormatLength::Capture("len".to_string()), + }, + FormatSlot { + conversion: FormatConversion::Pointer, + length: FormatLength::None, + }, + ] + ); + assert_eq!(template.script_argument_count(), 8); + assert_eq!(template.wire_argument_count(), 9); + assert_eq!( + slots.iter().map(ToString::to_string).collect::>(), + [ + "{:x.16}", + "{:X.16}", + "{:s.16}", + "{:x.8}", + "{:s.*}", + "{:x.len$}", + "{:p}", + ] + ); + } + + #[test] + fn rejects_invalid_templates() { + assert_eq!( + FormatTemplate::parse("unclosed {"), + Err(FormatTemplateError::UnclosedPlaceholder) + ); + assert_eq!( + FormatTemplate::parse("unmatched }"), + Err(FormatTemplateError::UnmatchedClosingBrace) + ); + assert!(matches!( + FormatTemplate::parse("{value}"), + Err(FormatTemplateError::InvalidSpecifier { .. }) + )); + assert!(matches!( + FormatTemplate::parse("{:q}"), + Err(FormatTemplateError::UnsupportedConversion { conversion: 'q' }) + )); + assert!(matches!( + FormatTemplate::parse("{:x.1bad$}"), + Err(FormatTemplateError::InvalidCaptureVariable { .. }) + )); + assert!(matches!( + FormatTemplate::parse("{:x.nope}"), + Err(FormatTemplateError::InvalidLength { .. }) + )); + } + + #[test] + fn lossy_parsing_preserves_renderer_fallbacks() { + assert_eq!( + FormatTemplate::parse_lossy("prefix {").parts(), + [FormatPart::Literal( + "prefix ".to_string() + )] + ); + assert_eq!( + FormatTemplate::parse_lossy("bad {value} tail").parts(), + [FormatPart::Literal("bad tail".to_string())] + ); + assert_eq!( + FormatTemplate::parse_lossy("{:q}").parts(), + [FormatPart::Slot(FormatSlot { + conversion: FormatConversion::Default, + length: FormatLength::None, + })] + ); + assert_eq!( + FormatTemplate::parse_lossy("closing }").parts(), + [FormatPart::Literal("closing }".to_string())] + ); + } +} diff --git a/ghostscope-protocol/src/lib.rs b/ghostscope-protocol/src/lib.rs index fb4b8feb..40de3e3c 100644 --- a/ghostscope-protocol/src/lib.rs +++ b/ghostscope-protocol/src/lib.rs @@ -4,6 +4,7 @@ // Core modules pub mod bpf_abi; +mod format_template; mod type_kind; mod value_presentation; @@ -92,6 +93,9 @@ pub use trace_context::{ }; pub use format_printer::FormatPrinter; +pub use format_template::{ + FormatConversion, FormatLength, FormatPart, FormatSlot, FormatTemplate, FormatTemplateError, +}; pub use streaming_parser::{ EventSource, ParseState, ParsedBacktraceFrame, ParsedInstruction, ParsedTraceEvent, diff --git a/ghostscope-protocol/src/streaming_parser.rs b/ghostscope-protocol/src/streaming_parser.rs index 0ddc5e90..f1353714 100644 --- a/ghostscope-protocol/src/streaming_parser.rs +++ b/ghostscope-protocol/src/streaming_parser.rs @@ -1,7 +1,7 @@ use crate::format_printer::FormatPrinter; use crate::trace_context::TraceContext; use crate::trace_event::*; -use crate::TypeKind; +use crate::{FormatConversion, FormatTemplate, TypeKind}; use tracing::{debug, warn}; use zerocopy::FromBytes; @@ -93,9 +93,17 @@ impl ParsedTraceEvent { while i < self.instructions.len() { match &self.instructions[i] { ParsedInstruction::PrintString { content } => { - if content.contains("{}") { - let (formatted, consumed) = - self.format_string_with_variables(content, i + 1); + let template = FormatTemplate::parse(content).ok(); + let has_default_slot = template.as_ref().is_some_and(|template| { + template + .slots() + .any(|slot| slot.conversion == FormatConversion::Default) + }); + if has_default_slot { + let (formatted, consumed) = self.format_template_with_variables( + template.as_ref().expect("template parsed above"), + i + 1, + ); emit(&formatted)?; i += consumed; } else { @@ -132,38 +140,37 @@ impl ParsedTraceEvent { } /// Format a format string with following variable instructions - fn format_string_with_variables( + fn format_template_with_variables( &self, - format_string: &str, + template: &FormatTemplate, start_index: usize, ) -> (String, usize) { - // Count placeholders in format string - let placeholder_count = format_string.matches("{}").count(); - let mut consumed = 1; // At least consume the format string itself - let mut result = String::with_capacity(format_string.len()); - let mut remaining = format_string; - - for instruction_index in - start_index..(start_index + placeholder_count).min(self.instructions.len()) - { - let Some(pos) = remaining.find("{}") else { - break; - }; - - if let Some(ParsedInstruction::PrintVariable { - formatted_value, .. - }) = self.instructions.get(instruction_index) - { - result.push_str(&remaining[..pos]); - result.push_str(formatted_value); - consumed += 1; - remaining = &remaining[pos + 2..]; - } else { - break; + let mut result = String::new(); + let mut instruction_index = start_index; + let mut can_substitute = true; + + for part in template.parts() { + match part { + crate::FormatPart::Literal(literal) => result.push_str(literal), + crate::FormatPart::Slot(slot) if slot.conversion == FormatConversion::Default => { + if can_substitute { + if let Some(ParsedInstruction::PrintVariable { + formatted_value, .. + }) = self.instructions.get(instruction_index) + { + result.push_str(formatted_value); + consumed += 1; + instruction_index += 1; + continue; + } + can_substitute = false; + } + result.push_str(&slot.to_string()); + } + crate::FormatPart::Slot(slot) => result.push_str(&slot.to_string()), } } - result.push_str(remaining); (result, consumed) } @@ -1101,4 +1108,61 @@ mod tests { ] ); } + + #[test] + fn escaped_braces_do_not_consume_following_variables() { + let event = ParsedTraceEvent { + trace_id: 1, + timestamp: 0, + pid: 10, + tid: 11, + instructions: vec![ + ParsedInstruction::PrintString { + content: "{{}}".to_string(), + }, + ParsedInstruction::PrintVariable { + name: "value".to_string(), + type_encoding: TypeKind::I32, + formatted_value: "42".to_string(), + raw_data: vec![42], + }, + ParsedInstruction::EndInstruction { + total_instructions: 2, + execution_status: 0, + }, + ], + }; + + assert_eq!( + event.to_formatted_output(), + vec!["{{}}".to_string(), "value (I32): 42".to_string(),] + ); + } + + #[test] + fn escaped_braces_do_not_hide_real_placeholders() { + let event = ParsedTraceEvent { + trace_id: 1, + timestamp: 0, + pid: 10, + tid: 11, + instructions: vec![ + ParsedInstruction::PrintString { + content: "{{}} {}".to_string(), + }, + ParsedInstruction::PrintVariable { + name: "value".to_string(), + type_encoding: TypeKind::I32, + formatted_value: "42".to_string(), + raw_data: vec![42], + }, + ParsedInstruction::EndInstruction { + total_instructions: 2, + execution_status: 0, + }, + ], + }; + + assert_eq!(event.to_formatted_output(), vec!["{} 42".to_string()]); + } } From 92ce5e4b054149d21f5bdbcc62f38146691753cf Mon Sep 17 00:00:00 2001 From: swananan Date: Thu, 30 Jul 2026 08:10:48 +0800 Subject: [PATCH 2/2] refactor: split collection formatters Move hash table, B-tree, and sequence payload decoding into focused format-printer modules without changing rendering behavior. --- ghostscope-protocol/src/format_printer.rs | 606 +----------------- .../src/format_printer/btree.rs | 298 +++++++++ .../src/format_printer/hash_table.rs | 245 +++++++ .../src/format_printer/sequence.rs | 65 ++ ghostscope-protocol/src/lib.rs | 4 +- ghostscope-protocol/src/streaming_parser.rs | 100 +-- ghostscope-ui/src/events.rs | 39 ++ ghostscope-ui/src/events/trace_display.rs | 40 +- 8 files changed, 718 insertions(+), 679 deletions(-) create mode 100644 ghostscope-protocol/src/format_printer/btree.rs create mode 100644 ghostscope-protocol/src/format_printer/hash_table.rs create mode 100644 ghostscope-protocol/src/format_printer/sequence.rs diff --git a/ghostscope-protocol/src/format_printer.rs b/ghostscope-protocol/src/format_printer.rs index 1a738c5e..4d2aea61 100644 --- a/ghostscope-protocol/src/format_printer.rs +++ b/ghostscope-protocol/src/format_printer.rs @@ -11,18 +11,17 @@ use crate::trace_event::{ }; use crate::type_info::TypeInfo; use crate::{ - BTreeEntryPresentation, BTreeFieldPresentation, FormatConversion, FormatLength, FormatPart, - FormatTemplate, HashTableBucketOrder, HashTableEntryPresentation, HashTableFieldPresentation, - HashTableOccupancy, NestedValueChildrenPresentation, NestedValueFieldPresentation, + FormatConversion, FormatLength, FormatPart, FormatTemplate, HashTableEntryPresentation, + NestedValueChildrenPresentation, NestedValueFieldPresentation, NestedValueHashTableFieldPresentation, NestedValuePresentation, - NestedValueVariantFieldPresentation, ValuePresentation, BTREE_CAPTURED_ITEM_COUNT_OFFSET, - BTREE_HEADER_SIZE, BTREE_NODE_HEADER_SIZE, BTREE_NODE_HEIGHT_OFFSET, BTREE_NODE_LENGTH_OFFSET, - BTREE_NODE_SLOT_COUNT_OFFSET, HASH_TABLE_BUCKET_DATA_OFFSET, HASH_TABLE_CAPACITY_OFFSET, - HASH_TABLE_CAPTURED_BUCKETS_OFFSET, HASH_TABLE_HEADER_SIZE, INDIRECT_BYTES_LENGTH_PREFIX_SIZE, - INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET, INDIRECT_SEQUENCE_HEADER_SIZE, + NestedValueVariantFieldPresentation, ValuePresentation, INDIRECT_BYTES_LENGTH_PREFIX_SIZE, NESTED_VALUE_CHILD_HEADER_SIZE, NESTED_VALUE_CHILD_STATUS_OFFSET, }; +mod btree; +mod hash_table; +mod sequence; + struct ActiveVariantMember<'a> { part_index: Option, variant_index: Option, @@ -56,27 +55,6 @@ pub struct ParsedComplexVariable { pub data: Vec, } -struct ParsedHashTablePayload<'a> { - original_count: u64, - captured_buckets: usize, - occupancy: &'a [u8], - buckets: &'a [u8], -} - -struct ParsedBTreeNode<'a> { - height: u64, - length: usize, - keys: &'a [u8], - values: Option<&'a [u8]>, -} - -struct ParsedBTreePayload<'a> { - original_count: u64, - captured_count: u64, - edge_count: usize, - nodes: Vec>>, -} - struct RawPresentationPayload<'data, 'presentation> { data: &'data [u8], presentation: &'presentation ValuePresentation, @@ -89,8 +67,6 @@ struct FormatSpecPayload<'a> { truncated: bool, } -type BTreeEntryBytes<'a> = (&'a [u8], Option<&'a [u8]>); - /// Format printer for converting PrintComplexFormat data to formatted strings pub struct FormatPrinter; @@ -1648,568 +1624,6 @@ impl FormatPrinter { Some(u64::from_le_bytes(data.get(offset..end)?.try_into().ok()?)) } - fn parse_hash_table_payload( - data: &[u8], - entry_stride: u64, - occupancy: HashTableOccupancy, - ) -> Option> { - let original_count = Self::payload_u64(data, 0)?; - let capacity = Self::payload_u64(data, HASH_TABLE_CAPACITY_OFFSET)?; - let captured_buckets = Self::payload_u64(data, HASH_TABLE_CAPTURED_BUCKETS_OFFSET)?; - let bucket_offset = Self::payload_u64(data, HASH_TABLE_BUCKET_DATA_OFFSET)?; - if original_count > capacity || captured_buckets > capacity { - return None; - } - - let captured_buckets = usize::try_from(captured_buckets).ok()?; - let occupancy_width = usize::try_from(occupancy.byte_width()?).ok()?; - let occupancy_len = captured_buckets.checked_mul(occupancy_width)?; - let occupancy_end = HASH_TABLE_HEADER_SIZE.checked_add(occupancy_len)?; - let bucket_offset = usize::try_from(bucket_offset).ok()?; - // The eBPF layout fixes the bucket offset after the maximum reserved - // occupancy region so the verifier sees constant destinations. A small - // runtime table can therefore leave unused occupancy headroom here. - if bucket_offset < occupancy_end { - return None; - } - let stride = usize::try_from(entry_stride).ok()?; - let bucket_len = captured_buckets.checked_mul(stride)?; - let bucket_end = bucket_offset.checked_add(bucket_len)?; - let occupancy_bytes = data.get(HASH_TABLE_HEADER_SIZE..occupancy_end)?; - let buckets = data.get(bucket_offset..bucket_end)?; - let mut occupied = 0_u64; - for bucket_index in 0..captured_buckets { - if Self::hash_table_bucket_occupied(occupancy_bytes, occupancy, bucket_index)? { - occupied = occupied.checked_add(1)?; - } - } - if occupied > original_count - || (u64::try_from(captured_buckets).ok()? == capacity && occupied != original_count) - { - return None; - } - - Some(ParsedHashTablePayload { - original_count, - captured_buckets, - occupancy: occupancy_bytes, - buckets, - }) - } - - fn hash_table_bucket_occupied( - occupancy_bytes: &[u8], - occupancy: HashTableOccupancy, - bucket_index: usize, - ) -> Option { - let width = usize::try_from(occupancy.byte_width()?).ok()?; - let start = bucket_index.checked_mul(width)?; - let end = start.checked_add(width)?; - let bytes = occupancy_bytes.get(start..end)?; - match occupancy { - HashTableOccupancy::ControlByteHighBitClear => { - Some(bytes.first().copied()? & 0x80 == 0) - } - HashTableOccupancy::NonZeroWord { .. } => Some(bytes.iter().any(|byte| *byte != 0)), - } - } - - fn hash_table_bucket<'a>( - payload: &ParsedHashTablePayload<'a>, - entry_stride: u64, - bucket_order: HashTableBucketOrder, - control_index: usize, - ) -> Option<&'a [u8]> { - let stride = usize::try_from(entry_stride).ok()?; - let bucket_index = match bucket_order { - HashTableBucketOrder::Forward => control_index, - HashTableBucketOrder::Reverse => { - payload.captured_buckets.checked_sub(control_index + 1)? - } - }; - let start = bucket_index.checked_mul(stride)?; - let end = start.checked_add(stride)?; - payload.buckets.get(start..end) - } - - fn hash_table_occupied_bucket_bytes( - data: &[u8], - entry_stride: u64, - bucket_order: HashTableBucketOrder, - occupancy: HashTableOccupancy, - ) -> Option> { - let payload = Self::parse_hash_table_payload(data, entry_stride, occupancy)?; - let stride = usize::try_from(entry_stride).ok()?; - let output_capacity = payload.captured_buckets.checked_mul(stride)?; - let mut entries = Vec::with_capacity(output_capacity); - for control_index in 0..payload.captured_buckets { - if Self::hash_table_bucket_occupied(payload.occupancy, occupancy, control_index)? { - entries.extend_from_slice(Self::hash_table_bucket( - &payload, - entry_stride, - bucket_order, - control_index, - )?); - } - } - Some(entries) - } - - fn format_hash_table_field( - entry_data: &[u8], - entry_stride: u64, - field: &HashTableFieldPresentation, - field_index: usize, - control_index: usize, - nested: Option<&NestedHashTableContext<'_>>, - ) -> Option { - if let Some(nested) = nested { - if let Some(nested_field) = nested - .fields - .iter() - .find(|candidate| candidate.field_index == field_index as u64) - { - let field_slot_offset = usize::try_from(nested_field.slot_offset).ok()?; - let slot_offset = control_index - .checked_mul(nested.bucket_slot_stride) - .and_then(|offset| nested.first_slot_offset.checked_add(offset)) - .and_then(|offset| offset.checked_add(field_slot_offset))?; - return Some(Self::format_nested_value_at( - nested.full_data, - slot_offset, - &nested_field.value, - )); - } - } - let field_end = field.offset.checked_add(field.field_type.size())?; - if field_end > entry_stride { - return None; - } - let start = usize::try_from(field.offset).ok()?; - let end = usize::try_from(field_end).ok()?; - Some(Self::format_data_with_type_info_impl( - entry_data.get(start..end)?, - &field.field_type, - 1, - 32, - )) - } - - fn format_hash_table_payload( - data: &[u8], - entry_stride: u64, - bucket_order: HashTableBucketOrder, - occupancy: HashTableOccupancy, - entry: &HashTableEntryPresentation, - nested: Option<&NestedHashTableContext<'_>>, - ) -> String { - let Some(payload) = Self::parse_hash_table_payload(data, entry_stride, occupancy) else { - return "".to_string(); - }; - if nested.is_some_and(|nested| payload.captured_buckets > nested.bucket_count) { - return "".to_string(); - } - let type_name = match entry { - HashTableEntryPresentation::Map { .. } => "HashMap", - HashTableEntryPresentation::Set { .. } => "HashSet", - }; - let mut result = format!("{type_name}(size={}) {{", payload.original_count); - let mut output_index = 0usize; - for control_index in 0..payload.captured_buckets { - let Some(occupied) = - Self::hash_table_bucket_occupied(payload.occupancy, occupancy, control_index) - else { - return "".to_string(); - }; - if !occupied { - continue; - } - let Some(entry_data) = - Self::hash_table_bucket(&payload, entry_stride, bucket_order, control_index) - else { - return "".to_string(); - }; - if output_index > 0 { - result.push_str(", "); - } - match entry { - HashTableEntryPresentation::Map { key, value } => { - let Some(key) = Self::format_hash_table_field( - entry_data, - entry_stride, - key, - 0, - control_index, - nested, - ) else { - return "".to_string(); - }; - let Some(value) = Self::format_hash_table_field( - entry_data, - entry_stride, - value, - 1, - control_index, - nested, - ) else { - return "".to_string(); - }; - result.push_str(&key); - result.push_str(": "); - result.push_str(&value); - } - HashTableEntryPresentation::Set { value } => { - let Some(value) = Self::format_hash_table_field( - entry_data, - entry_stride, - value, - 0, - control_index, - nested, - ) else { - return "".to_string(); - }; - result.push_str(&value); - } - } - output_index += 1; - } - result.push('}'); - result - } - - fn btree_fields( - entry: &BTreeEntryPresentation, - ) -> (&BTreeFieldPresentation, Option<&BTreeFieldPresentation>) { - match entry { - BTreeEntryPresentation::Map { key, value } => (key, Some(value)), - BTreeEntryPresentation::Set { value } => (value, None), - } - } - - fn btree_record_layout( - node_capacity: u64, - entry: &BTreeEntryPresentation, - ) -> Option<(usize, usize, usize)> { - let capacity = usize::try_from(node_capacity).ok()?; - if capacity == 0 { - return None; - } - let (key, value) = Self::btree_fields(entry); - let key_stride = usize::try_from(key.slot_stride).ok()?; - let key_bytes = capacity.checked_mul(key_stride)?; - let value_bytes = match value { - Some(field) => usize::try_from(field.slot_stride) - .ok()? - .checked_mul(capacity)?, - None => 0, - }; - let values_offset = BTREE_NODE_HEADER_SIZE.checked_add(key_bytes)?; - let record_size = values_offset.checked_add(value_bytes)?; - Some((capacity, values_offset, record_size)) - } - - fn validate_btree_field(field: &BTreeFieldPresentation) -> bool { - field - .value_offset - .checked_add(field.field_type.size()) - .is_some_and(|end| { - end <= field.slot_stride - || (field.slot_stride == 0 && field.value_offset == 0 && end == 0) - }) - } - - fn parse_btree_payload<'a>( - data: &'a [u8], - node_capacity: u64, - entry: &BTreeEntryPresentation, - ) -> Option> { - let original_count = Self::payload_u64(data, 0)?; - let node_slots = Self::payload_u64(data, BTREE_NODE_SLOT_COUNT_OFFSET)?; - let captured_count = Self::payload_u64(data, BTREE_CAPTURED_ITEM_COUNT_OFFSET)?; - if captured_count > original_count { - return None; - } - let (key, value) = Self::btree_fields(entry); - if !Self::validate_btree_field(key) - || value.is_some_and(|field| !Self::validate_btree_field(field)) - { - return None; - } - let (capacity, values_offset, record_size) = - Self::btree_record_layout(node_capacity, entry)?; - let node_slots = usize::try_from(node_slots).ok()?; - let records_len = node_slots.checked_mul(record_size)?; - let records_end = BTREE_HEADER_SIZE.checked_add(records_len)?; - let records = data.get(BTREE_HEADER_SIZE..records_end)?; - let key_bytes = values_offset.checked_sub(BTREE_NODE_HEADER_SIZE)?; - let value_bytes = record_size.checked_sub(values_offset)?; - - let mut nodes = Vec::with_capacity(node_slots); - let mut addresses = std::collections::HashSet::with_capacity(node_slots); - let mut parsed_count = 0u64; - for slot in 0..node_slots { - let start = slot.checked_mul(record_size)?; - let record = records.get(start..start.checked_add(record_size)?)?; - let address = Self::payload_u64(record, 0)?; - if address == 0 { - nodes.push(None); - continue; - } - if !addresses.insert(address) { - return None; - } - let height = Self::payload_u64(record, BTREE_NODE_HEIGHT_OFFSET)?; - let length = Self::payload_u64(record, BTREE_NODE_LENGTH_OFFSET)?; - let length = usize::try_from(length).ok()?; - if length > capacity { - return None; - } - parsed_count = parsed_count.checked_add(u64::try_from(length).ok()?)?; - let keys_end = BTREE_NODE_HEADER_SIZE.checked_add(key_bytes)?; - let keys = record.get(BTREE_NODE_HEADER_SIZE..keys_end)?; - let values = match value { - Some(_) => { - Some(record.get(values_offset..values_offset.checked_add(value_bytes)?)?) - } - None => None, - }; - nodes.push(Some(ParsedBTreeNode { - height, - length, - keys, - values, - })); - } - let root_presence_valid = match (original_count, captured_count) { - (0, _) => nodes.iter().all(Option::is_none), - (_, 0) => nodes.iter().all(Option::is_none), - (_, _) => nodes.first().is_some_and(Option::is_some), - }; - if parsed_count != captured_count || !root_presence_valid { - return None; - } - - let edge_count = capacity.checked_add(1)?; - for slot in 1..nodes.len() { - let Some(node) = &nodes[slot] else { - continue; - }; - let parent_slot = (slot - 1) / edge_count; - let parent_edge = (slot - 1) % edge_count; - let Some(Some(parent)) = nodes.get(parent_slot) else { - return None; - }; - if parent.height == 0 - || parent_edge > parent.length - || node.height.checked_add(1) != Some(parent.height) - { - return None; - } - } - for (slot, node) in nodes.iter().enumerate() { - let Some(node) = node else { - continue; - }; - if node.height == 0 { - continue; - } - for edge in 0..=node.length { - let child = slot - .checked_mul(edge_count)? - .checked_add(1)? - .checked_add(edge)?; - if child < nodes.len() && nodes[child].is_none() { - return None; - } - if captured_count == original_count && child >= nodes.len() { - return None; - } - } - } - - Some(ParsedBTreePayload { - original_count, - captured_count, - edge_count, - nodes, - }) - } - - fn btree_field_bytes<'a>( - slots: &'a [u8], - index: usize, - field: &BTreeFieldPresentation, - ) -> Option<&'a [u8]> { - let stride = usize::try_from(field.slot_stride).ok()?; - let value_offset = usize::try_from(field.value_offset).ok()?; - let value_size = usize::try_from(field.field_type.size()).ok()?; - let start = index.checked_mul(stride)?.checked_add(value_offset)?; - slots.get(start..start.checked_add(value_size)?) - } - - fn collect_btree_entries<'a>( - payload: &'a ParsedBTreePayload<'a>, - entry: &'a BTreeEntryPresentation, - node_index: usize, - output: &mut Vec>, - ) -> Option<()> { - let node = payload.nodes.get(node_index)?.as_ref()?; - let (key, value) = Self::btree_fields(entry); - for index in 0..node.length { - if node.height > 0 { - let child = node_index - .checked_mul(payload.edge_count)? - .checked_add(1)? - .checked_add(index)?; - if payload.nodes.get(child).is_some_and(Option::is_some) { - Self::collect_btree_entries(payload, entry, child, output)?; - } - } - let key_data = Self::btree_field_bytes(node.keys, index, key)?; - let value_data = match (value, node.values) { - (Some(field), Some(values)) => Some(Self::btree_field_bytes(values, index, field)?), - (None, None) => None, - _ => return None, - }; - output.push((key_data, value_data)); - } - if node.height > 0 { - let child = node_index - .checked_mul(payload.edge_count)? - .checked_add(1)? - .checked_add(node.length)?; - if payload.nodes.get(child).is_some_and(Option::is_some) { - Self::collect_btree_entries(payload, entry, child, output)?; - } - } - Some(()) - } - - fn btree_entries<'a>( - payload: &'a ParsedBTreePayload<'a>, - entry: &'a BTreeEntryPresentation, - ) -> Option>> { - let mut entries = Vec::with_capacity(usize::try_from(payload.captured_count).ok()?); - if payload.captured_count > 0 { - Self::collect_btree_entries(payload, entry, 0, &mut entries)?; - } - (u64::try_from(entries.len()).ok()? == payload.captured_count).then_some(entries) - } - - fn btree_value_bytes( - data: &[u8], - node_capacity: u64, - entry: &BTreeEntryPresentation, - ) -> Option> { - let payload = Self::parse_btree_payload(data, node_capacity, entry)?; - let entries = Self::btree_entries(&payload, entry)?; - let mut values = Vec::new(); - for (key, value) in entries { - values.extend_from_slice(key); - if let Some(value) = value { - values.extend_from_slice(value); - } - } - Some(values) - } - - fn format_btree_payload( - data: &[u8], - node_capacity: u64, - entry: &BTreeEntryPresentation, - ) -> String { - let Some(payload) = Self::parse_btree_payload(data, node_capacity, entry) else { - return "".to_string(); - }; - let Some(entries) = Self::btree_entries(&payload, entry) else { - return "".to_string(); - }; - let (key, value) = Self::btree_fields(entry); - let type_name = match entry { - BTreeEntryPresentation::Map { .. } => "BTreeMap", - BTreeEntryPresentation::Set { .. } => "BTreeSet", - }; - let mut result = format!("{type_name}(size={}) {{", payload.original_count); - for (index, (key_data, value_data)) in entries.into_iter().enumerate() { - if index > 0 { - result.push_str(", "); - } - let formatted_key = - Self::format_data_with_type_info_impl(key_data, &key.field_type, 1, 32); - result.push_str(&formatted_key); - if let (Some(field), Some(value_data)) = (value, value_data) { - result.push_str(": "); - result.push_str(&Self::format_data_with_type_info_impl( - value_data, - &field.field_type, - 1, - 32, - )); - } - } - result.push('}'); - result - } - - fn parse_sequence_payload(data: &[u8], element_stride: u64) -> Option<(u64, u64, &[u8])> { - let original_count = u64::from_le_bytes(data.get(..8)?.try_into().ok()?); - let captured_count = u64::from_le_bytes( - data.get(INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET..INDIRECT_SEQUENCE_HEADER_SIZE)? - .try_into() - .ok()?, - ); - if captured_count > original_count { - return None; - } - let stride = usize::try_from(element_stride).ok()?; - let captured = usize::try_from(captured_count).ok()?; - let byte_len = captured.checked_mul(stride)?; - let payload = data.get(INDIRECT_SEQUENCE_HEADER_SIZE..)?; - Some((original_count, captured_count, payload.get(..byte_len)?)) - } - - fn format_sequence_payload( - data: &[u8], - element_type: &TypeInfo, - element_stride: u64, - ) -> String { - if element_type.size() != element_stride { - return "".to_string(); - } - let Some((_, captured_count, payload)) = Self::parse_sequence_payload(data, element_stride) - else { - return "".to_string(); - }; - let Ok(captured_count) = usize::try_from(captured_count) else { - return "".to_string(); - }; - let Ok(stride) = usize::try_from(element_stride) else { - return "".to_string(); - }; - - let mut result = String::from("["); - for index in 0..captured_count { - if index > 0 { - result.push_str(", "); - } - let start = index * stride; - let element_data = &payload[start..start + stride]; - if stride == 0 && element_type.type_name() == "()" { - result.push_str("()"); - } else { - result.push_str(&Self::format_data_with_type_info_impl( - element_data, - element_type, - 1, - 32, - )); - } - } - result.push(']'); - result - } - fn discriminant_type_is_unsigned(type_info: &TypeInfo) -> bool { match type_info { TypeInfo::BaseType { encoding, .. } => { @@ -3142,6 +2556,12 @@ impl FormatPrinter { #[cfg(test)] mod tests { use super::*; + use crate::{ + BTreeEntryPresentation, BTreeFieldPresentation, HashTableBucketOrder, + HashTableFieldPresentation, HashTableOccupancy, BTREE_CAPTURED_ITEM_COUNT_OFFSET, + BTREE_HEADER_SIZE, BTREE_NODE_HEADER_SIZE, HASH_TABLE_HEADER_SIZE, + INDIRECT_SEQUENCE_HEADER_SIZE, + }; type BTreeMapNode<'a> = (usize, u64, u64, &'a [i32], &'a [u16]); diff --git a/ghostscope-protocol/src/format_printer/btree.rs b/ghostscope-protocol/src/format_printer/btree.rs new file mode 100644 index 00000000..0f259172 --- /dev/null +++ b/ghostscope-protocol/src/format_printer/btree.rs @@ -0,0 +1,298 @@ +use super::FormatPrinter; +use crate::{ + BTreeEntryPresentation, BTreeFieldPresentation, BTREE_CAPTURED_ITEM_COUNT_OFFSET, + BTREE_HEADER_SIZE, BTREE_NODE_HEADER_SIZE, BTREE_NODE_HEIGHT_OFFSET, BTREE_NODE_LENGTH_OFFSET, + BTREE_NODE_SLOT_COUNT_OFFSET, +}; + +struct ParsedBTreeNode<'a> { + height: u64, + length: usize, + keys: &'a [u8], + values: Option<&'a [u8]>, +} + +pub(super) struct ParsedBTreePayload<'a> { + original_count: u64, + captured_count: u64, + edge_count: usize, + nodes: Vec>>, +} + +type BTreeEntryBytes<'a> = (&'a [u8], Option<&'a [u8]>); + +impl FormatPrinter { + fn btree_fields( + entry: &BTreeEntryPresentation, + ) -> (&BTreeFieldPresentation, Option<&BTreeFieldPresentation>) { + match entry { + BTreeEntryPresentation::Map { key, value } => (key, Some(value)), + BTreeEntryPresentation::Set { value } => (value, None), + } + } + + fn btree_record_layout( + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> Option<(usize, usize, usize)> { + let capacity = usize::try_from(node_capacity).ok()?; + if capacity == 0 { + return None; + } + let (key, value) = Self::btree_fields(entry); + let key_stride = usize::try_from(key.slot_stride).ok()?; + let key_bytes = capacity.checked_mul(key_stride)?; + let value_bytes = match value { + Some(field) => usize::try_from(field.slot_stride) + .ok()? + .checked_mul(capacity)?, + None => 0, + }; + let values_offset = BTREE_NODE_HEADER_SIZE.checked_add(key_bytes)?; + let record_size = values_offset.checked_add(value_bytes)?; + Some((capacity, values_offset, record_size)) + } + + fn validate_btree_field(field: &BTreeFieldPresentation) -> bool { + field + .value_offset + .checked_add(field.field_type.size()) + .is_some_and(|end| { + end <= field.slot_stride + || (field.slot_stride == 0 && field.value_offset == 0 && end == 0) + }) + } + + pub(super) fn parse_btree_payload<'a>( + data: &'a [u8], + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> Option> { + let original_count = Self::payload_u64(data, 0)?; + let node_slots = Self::payload_u64(data, BTREE_NODE_SLOT_COUNT_OFFSET)?; + let captured_count = Self::payload_u64(data, BTREE_CAPTURED_ITEM_COUNT_OFFSET)?; + if captured_count > original_count { + return None; + } + let (key, value) = Self::btree_fields(entry); + if !Self::validate_btree_field(key) + || value.is_some_and(|field| !Self::validate_btree_field(field)) + { + return None; + } + let (capacity, values_offset, record_size) = + Self::btree_record_layout(node_capacity, entry)?; + let node_slots = usize::try_from(node_slots).ok()?; + let records_len = node_slots.checked_mul(record_size)?; + let records_end = BTREE_HEADER_SIZE.checked_add(records_len)?; + let records = data.get(BTREE_HEADER_SIZE..records_end)?; + let key_bytes = values_offset.checked_sub(BTREE_NODE_HEADER_SIZE)?; + let value_bytes = record_size.checked_sub(values_offset)?; + + let mut nodes = Vec::with_capacity(node_slots); + let mut addresses = std::collections::HashSet::with_capacity(node_slots); + let mut parsed_count = 0u64; + for slot in 0..node_slots { + let start = slot.checked_mul(record_size)?; + let record = records.get(start..start.checked_add(record_size)?)?; + let address = Self::payload_u64(record, 0)?; + if address == 0 { + nodes.push(None); + continue; + } + if !addresses.insert(address) { + return None; + } + let height = Self::payload_u64(record, BTREE_NODE_HEIGHT_OFFSET)?; + let length = Self::payload_u64(record, BTREE_NODE_LENGTH_OFFSET)?; + let length = usize::try_from(length).ok()?; + if length > capacity { + return None; + } + parsed_count = parsed_count.checked_add(u64::try_from(length).ok()?)?; + let keys_end = BTREE_NODE_HEADER_SIZE.checked_add(key_bytes)?; + let keys = record.get(BTREE_NODE_HEADER_SIZE..keys_end)?; + let values = match value { + Some(_) => { + Some(record.get(values_offset..values_offset.checked_add(value_bytes)?)?) + } + None => None, + }; + nodes.push(Some(ParsedBTreeNode { + height, + length, + keys, + values, + })); + } + let root_presence_valid = match (original_count, captured_count) { + (0, _) => nodes.iter().all(Option::is_none), + (_, 0) => nodes.iter().all(Option::is_none), + (_, _) => nodes.first().is_some_and(Option::is_some), + }; + if parsed_count != captured_count || !root_presence_valid { + return None; + } + + let edge_count = capacity.checked_add(1)?; + for slot in 1..nodes.len() { + let Some(node) = &nodes[slot] else { + continue; + }; + let parent_slot = (slot - 1) / edge_count; + let parent_edge = (slot - 1) % edge_count; + let Some(Some(parent)) = nodes.get(parent_slot) else { + return None; + }; + if parent.height == 0 + || parent_edge > parent.length + || node.height.checked_add(1) != Some(parent.height) + { + return None; + } + } + for (slot, node) in nodes.iter().enumerate() { + let Some(node) = node else { + continue; + }; + if node.height == 0 { + continue; + } + for edge in 0..=node.length { + let child = slot + .checked_mul(edge_count)? + .checked_add(1)? + .checked_add(edge)?; + if child < nodes.len() && nodes[child].is_none() { + return None; + } + if captured_count == original_count && child >= nodes.len() { + return None; + } + } + } + + Some(ParsedBTreePayload { + original_count, + captured_count, + edge_count, + nodes, + }) + } + + fn btree_field_bytes<'a>( + slots: &'a [u8], + index: usize, + field: &BTreeFieldPresentation, + ) -> Option<&'a [u8]> { + let stride = usize::try_from(field.slot_stride).ok()?; + let value_offset = usize::try_from(field.value_offset).ok()?; + let value_size = usize::try_from(field.field_type.size()).ok()?; + let start = index.checked_mul(stride)?.checked_add(value_offset)?; + slots.get(start..start.checked_add(value_size)?) + } + + fn collect_btree_entries<'a>( + payload: &'a ParsedBTreePayload<'a>, + entry: &'a BTreeEntryPresentation, + node_index: usize, + output: &mut Vec>, + ) -> Option<()> { + let node = payload.nodes.get(node_index)?.as_ref()?; + let (key, value) = Self::btree_fields(entry); + for index in 0..node.length { + if node.height > 0 { + let child = node_index + .checked_mul(payload.edge_count)? + .checked_add(1)? + .checked_add(index)?; + if payload.nodes.get(child).is_some_and(Option::is_some) { + Self::collect_btree_entries(payload, entry, child, output)?; + } + } + let key_data = Self::btree_field_bytes(node.keys, index, key)?; + let value_data = match (value, node.values) { + (Some(field), Some(values)) => Some(Self::btree_field_bytes(values, index, field)?), + (None, None) => None, + _ => return None, + }; + output.push((key_data, value_data)); + } + if node.height > 0 { + let child = node_index + .checked_mul(payload.edge_count)? + .checked_add(1)? + .checked_add(node.length)?; + if payload.nodes.get(child).is_some_and(Option::is_some) { + Self::collect_btree_entries(payload, entry, child, output)?; + } + } + Some(()) + } + + fn btree_entries<'a>( + payload: &'a ParsedBTreePayload<'a>, + entry: &'a BTreeEntryPresentation, + ) -> Option>> { + let mut entries = Vec::with_capacity(usize::try_from(payload.captured_count).ok()?); + if payload.captured_count > 0 { + Self::collect_btree_entries(payload, entry, 0, &mut entries)?; + } + (u64::try_from(entries.len()).ok()? == payload.captured_count).then_some(entries) + } + + pub(super) fn btree_value_bytes( + data: &[u8], + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> Option> { + let payload = Self::parse_btree_payload(data, node_capacity, entry)?; + let entries = Self::btree_entries(&payload, entry)?; + let mut values = Vec::new(); + for (key, value) in entries { + values.extend_from_slice(key); + if let Some(value) = value { + values.extend_from_slice(value); + } + } + Some(values) + } + + pub(super) fn format_btree_payload( + data: &[u8], + node_capacity: u64, + entry: &BTreeEntryPresentation, + ) -> String { + let Some(payload) = Self::parse_btree_payload(data, node_capacity, entry) else { + return "".to_string(); + }; + let Some(entries) = Self::btree_entries(&payload, entry) else { + return "".to_string(); + }; + let (key, value) = Self::btree_fields(entry); + let type_name = match entry { + BTreeEntryPresentation::Map { .. } => "BTreeMap", + BTreeEntryPresentation::Set { .. } => "BTreeSet", + }; + let mut result = format!("{type_name}(size={}) {{", payload.original_count); + for (index, (key_data, value_data)) in entries.into_iter().enumerate() { + if index > 0 { + result.push_str(", "); + } + let formatted_key = + Self::format_data_with_type_info_impl(key_data, &key.field_type, 1, 32); + result.push_str(&formatted_key); + if let (Some(field), Some(value_data)) = (value, value_data) { + result.push_str(": "); + result.push_str(&Self::format_data_with_type_info_impl( + value_data, + &field.field_type, + 1, + 32, + )); + } + } + result.push('}'); + result + } +} diff --git a/ghostscope-protocol/src/format_printer/hash_table.rs b/ghostscope-protocol/src/format_printer/hash_table.rs new file mode 100644 index 00000000..85fe93e4 --- /dev/null +++ b/ghostscope-protocol/src/format_printer/hash_table.rs @@ -0,0 +1,245 @@ +use super::{FormatPrinter, NestedHashTableContext}; +use crate::{ + HashTableBucketOrder, HashTableEntryPresentation, HashTableFieldPresentation, + HashTableOccupancy, HASH_TABLE_BUCKET_DATA_OFFSET, HASH_TABLE_CAPACITY_OFFSET, + HASH_TABLE_CAPTURED_BUCKETS_OFFSET, HASH_TABLE_HEADER_SIZE, +}; + +pub(super) struct ParsedHashTablePayload<'a> { + original_count: u64, + captured_buckets: usize, + occupancy: &'a [u8], + pub(super) buckets: &'a [u8], +} + +impl FormatPrinter { + pub(super) fn parse_hash_table_payload( + data: &[u8], + entry_stride: u64, + occupancy: HashTableOccupancy, + ) -> Option> { + let original_count = Self::payload_u64(data, 0)?; + let capacity = Self::payload_u64(data, HASH_TABLE_CAPACITY_OFFSET)?; + let captured_buckets = Self::payload_u64(data, HASH_TABLE_CAPTURED_BUCKETS_OFFSET)?; + let bucket_offset = Self::payload_u64(data, HASH_TABLE_BUCKET_DATA_OFFSET)?; + if original_count > capacity || captured_buckets > capacity { + return None; + } + + let captured_buckets = usize::try_from(captured_buckets).ok()?; + let occupancy_width = usize::try_from(occupancy.byte_width()?).ok()?; + let occupancy_len = captured_buckets.checked_mul(occupancy_width)?; + let occupancy_end = HASH_TABLE_HEADER_SIZE.checked_add(occupancy_len)?; + let bucket_offset = usize::try_from(bucket_offset).ok()?; + // The eBPF layout fixes the bucket offset after the maximum reserved + // occupancy region so the verifier sees constant destinations. A small + // runtime table can therefore leave unused occupancy headroom here. + if bucket_offset < occupancy_end { + return None; + } + let stride = usize::try_from(entry_stride).ok()?; + let bucket_len = captured_buckets.checked_mul(stride)?; + let bucket_end = bucket_offset.checked_add(bucket_len)?; + let occupancy_bytes = data.get(HASH_TABLE_HEADER_SIZE..occupancy_end)?; + let buckets = data.get(bucket_offset..bucket_end)?; + let mut occupied = 0_u64; + for bucket_index in 0..captured_buckets { + if Self::hash_table_bucket_occupied(occupancy_bytes, occupancy, bucket_index)? { + occupied = occupied.checked_add(1)?; + } + } + if occupied > original_count + || (u64::try_from(captured_buckets).ok()? == capacity && occupied != original_count) + { + return None; + } + + Some(ParsedHashTablePayload { + original_count, + captured_buckets, + occupancy: occupancy_bytes, + buckets, + }) + } + + fn hash_table_bucket_occupied( + occupancy_bytes: &[u8], + occupancy: HashTableOccupancy, + bucket_index: usize, + ) -> Option { + let width = usize::try_from(occupancy.byte_width()?).ok()?; + let start = bucket_index.checked_mul(width)?; + let end = start.checked_add(width)?; + let bytes = occupancy_bytes.get(start..end)?; + match occupancy { + HashTableOccupancy::ControlByteHighBitClear => { + Some(bytes.first().copied()? & 0x80 == 0) + } + HashTableOccupancy::NonZeroWord { .. } => Some(bytes.iter().any(|byte| *byte != 0)), + } + } + + fn hash_table_bucket<'a>( + payload: &ParsedHashTablePayload<'a>, + entry_stride: u64, + bucket_order: HashTableBucketOrder, + control_index: usize, + ) -> Option<&'a [u8]> { + let stride = usize::try_from(entry_stride).ok()?; + let bucket_index = match bucket_order { + HashTableBucketOrder::Forward => control_index, + HashTableBucketOrder::Reverse => { + payload.captured_buckets.checked_sub(control_index + 1)? + } + }; + let start = bucket_index.checked_mul(stride)?; + let end = start.checked_add(stride)?; + payload.buckets.get(start..end) + } + + pub(super) fn hash_table_occupied_bucket_bytes( + data: &[u8], + entry_stride: u64, + bucket_order: HashTableBucketOrder, + occupancy: HashTableOccupancy, + ) -> Option> { + let payload = Self::parse_hash_table_payload(data, entry_stride, occupancy)?; + let stride = usize::try_from(entry_stride).ok()?; + let output_capacity = payload.captured_buckets.checked_mul(stride)?; + let mut entries = Vec::with_capacity(output_capacity); + for control_index in 0..payload.captured_buckets { + if Self::hash_table_bucket_occupied(payload.occupancy, occupancy, control_index)? { + entries.extend_from_slice(Self::hash_table_bucket( + &payload, + entry_stride, + bucket_order, + control_index, + )?); + } + } + Some(entries) + } + + fn format_hash_table_field( + entry_data: &[u8], + entry_stride: u64, + field: &HashTableFieldPresentation, + field_index: usize, + control_index: usize, + nested: Option<&NestedHashTableContext<'_>>, + ) -> Option { + if let Some(nested) = nested { + if let Some(nested_field) = nested + .fields + .iter() + .find(|candidate| candidate.field_index == field_index as u64) + { + let field_slot_offset = usize::try_from(nested_field.slot_offset).ok()?; + let slot_offset = control_index + .checked_mul(nested.bucket_slot_stride) + .and_then(|offset| nested.first_slot_offset.checked_add(offset)) + .and_then(|offset| offset.checked_add(field_slot_offset))?; + return Some(Self::format_nested_value_at( + nested.full_data, + slot_offset, + &nested_field.value, + )); + } + } + let field_end = field.offset.checked_add(field.field_type.size())?; + if field_end > entry_stride { + return None; + } + let start = usize::try_from(field.offset).ok()?; + let end = usize::try_from(field_end).ok()?; + Some(Self::format_data_with_type_info_impl( + entry_data.get(start..end)?, + &field.field_type, + 1, + 32, + )) + } + + pub(super) fn format_hash_table_payload( + data: &[u8], + entry_stride: u64, + bucket_order: HashTableBucketOrder, + occupancy: HashTableOccupancy, + entry: &HashTableEntryPresentation, + nested: Option<&NestedHashTableContext<'_>>, + ) -> String { + let Some(payload) = Self::parse_hash_table_payload(data, entry_stride, occupancy) else { + return "".to_string(); + }; + if nested.is_some_and(|nested| payload.captured_buckets > nested.bucket_count) { + return "".to_string(); + } + let type_name = match entry { + HashTableEntryPresentation::Map { .. } => "HashMap", + HashTableEntryPresentation::Set { .. } => "HashSet", + }; + let mut result = format!("{type_name}(size={}) {{", payload.original_count); + let mut output_index = 0usize; + for control_index in 0..payload.captured_buckets { + let Some(occupied) = + Self::hash_table_bucket_occupied(payload.occupancy, occupancy, control_index) + else { + return "".to_string(); + }; + if !occupied { + continue; + } + let Some(entry_data) = + Self::hash_table_bucket(&payload, entry_stride, bucket_order, control_index) + else { + return "".to_string(); + }; + if output_index > 0 { + result.push_str(", "); + } + match entry { + HashTableEntryPresentation::Map { key, value } => { + let Some(key) = Self::format_hash_table_field( + entry_data, + entry_stride, + key, + 0, + control_index, + nested, + ) else { + return "".to_string(); + }; + let Some(value) = Self::format_hash_table_field( + entry_data, + entry_stride, + value, + 1, + control_index, + nested, + ) else { + return "".to_string(); + }; + result.push_str(&key); + result.push_str(": "); + result.push_str(&value); + } + HashTableEntryPresentation::Set { value } => { + let Some(value) = Self::format_hash_table_field( + entry_data, + entry_stride, + value, + 0, + control_index, + nested, + ) else { + return "".to_string(); + }; + result.push_str(&value); + } + } + output_index += 1; + } + result.push('}'); + result + } +} diff --git a/ghostscope-protocol/src/format_printer/sequence.rs b/ghostscope-protocol/src/format_printer/sequence.rs new file mode 100644 index 00000000..dddc0424 --- /dev/null +++ b/ghostscope-protocol/src/format_printer/sequence.rs @@ -0,0 +1,65 @@ +use super::FormatPrinter; +use crate::{TypeInfo, INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET, INDIRECT_SEQUENCE_HEADER_SIZE}; + +impl FormatPrinter { + pub(super) fn parse_sequence_payload( + data: &[u8], + element_stride: u64, + ) -> Option<(u64, u64, &[u8])> { + let original_count = u64::from_le_bytes(data.get(..8)?.try_into().ok()?); + let captured_count = u64::from_le_bytes( + data.get(INDIRECT_SEQUENCE_CAPTURED_COUNT_OFFSET..INDIRECT_SEQUENCE_HEADER_SIZE)? + .try_into() + .ok()?, + ); + if captured_count > original_count { + return None; + } + let stride = usize::try_from(element_stride).ok()?; + let captured = usize::try_from(captured_count).ok()?; + let byte_len = captured.checked_mul(stride)?; + let payload = data.get(INDIRECT_SEQUENCE_HEADER_SIZE..)?; + Some((original_count, captured_count, payload.get(..byte_len)?)) + } + + pub(super) fn format_sequence_payload( + data: &[u8], + element_type: &TypeInfo, + element_stride: u64, + ) -> String { + if element_type.size() != element_stride { + return "".to_string(); + } + let Some((_, captured_count, payload)) = Self::parse_sequence_payload(data, element_stride) + else { + return "".to_string(); + }; + let Ok(captured_count) = usize::try_from(captured_count) else { + return "".to_string(); + }; + let Ok(stride) = usize::try_from(element_stride) else { + return "".to_string(); + }; + + let mut result = String::from("["); + for index in 0..captured_count { + if index > 0 { + result.push_str(", "); + } + let start = index * stride; + let element_data = &payload[start..start + stride]; + if stride == 0 && element_type.type_name() == "()" { + result.push_str("()"); + } else { + result.push_str(&Self::format_data_with_type_info_impl( + element_data, + element_type, + 1, + 32, + )); + } + } + result.push(']'); + result + } +} diff --git a/ghostscope-protocol/src/lib.rs b/ghostscope-protocol/src/lib.rs index 40de3e3c..f7139fdf 100644 --- a/ghostscope-protocol/src/lib.rs +++ b/ghostscope-protocol/src/lib.rs @@ -98,8 +98,8 @@ pub use format_template::{ }; pub use streaming_parser::{ - EventSource, ParseState, ParsedBacktraceFrame, ParsedInstruction, ParsedTraceEvent, - StreamingTraceParser, + format_legacy_string_with_variables, EventSource, ParseState, ParsedBacktraceFrame, + ParsedInstruction, ParsedTraceEvent, StreamingTraceParser, }; pub use type_info::{ diff --git a/ghostscope-protocol/src/streaming_parser.rs b/ghostscope-protocol/src/streaming_parser.rs index f1353714..2fcd7bff 100644 --- a/ghostscope-protocol/src/streaming_parser.rs +++ b/ghostscope-protocol/src/streaming_parser.rs @@ -1,7 +1,7 @@ use crate::format_printer::FormatPrinter; use crate::trace_context::TraceContext; use crate::trace_event::*; -use crate::{FormatConversion, FormatTemplate, TypeKind}; +use crate::{FormatConversion, FormatPart, FormatTemplate, TypeKind}; use tracing::{debug, warn}; use zerocopy::FromBytes; @@ -77,6 +77,54 @@ pub struct ParsedTraceEvent { pub instructions: Vec, } +/// Format a legacy `PrintString` followed by contiguous `PrintVariable` +/// instructions. +/// +/// Returns `None` when the string is malformed or has no unescaped default +/// placeholders. The consumed count includes the `PrintString` instruction. +pub fn format_legacy_string_with_variables( + format_string: &str, + instructions: &[ParsedInstruction], + start_index: usize, +) -> Option<(String, usize)> { + let template = FormatTemplate::parse(format_string).ok()?; + if !template + .slots() + .any(|slot| slot.conversion == FormatConversion::Default) + { + return None; + } + + let mut consumed = 1; + let mut result = String::new(); + let mut instruction_index = start_index; + let mut can_substitute = true; + + for part in template.parts() { + match part { + FormatPart::Literal(literal) => result.push_str(literal), + FormatPart::Slot(slot) if slot.conversion == FormatConversion::Default => { + if can_substitute { + if let Some(ParsedInstruction::PrintVariable { + formatted_value, .. + }) = instructions.get(instruction_index) + { + result.push_str(formatted_value); + consumed += 1; + instruction_index += 1; + continue; + } + can_substitute = false; + } + result.push_str(&slot.to_string()); + } + FormatPart::Slot(slot) => result.push_str(&slot.to_string()), + } + } + + Some((result, consumed)) +} + impl ParsedTraceEvent { pub fn has_formatted_output(&self) -> bool { self.instructions @@ -93,17 +141,9 @@ impl ParsedTraceEvent { while i < self.instructions.len() { match &self.instructions[i] { ParsedInstruction::PrintString { content } => { - let template = FormatTemplate::parse(content).ok(); - let has_default_slot = template.as_ref().is_some_and(|template| { - template - .slots() - .any(|slot| slot.conversion == FormatConversion::Default) - }); - if has_default_slot { - let (formatted, consumed) = self.format_template_with_variables( - template.as_ref().expect("template parsed above"), - i + 1, - ); + if let Some((formatted, consumed)) = + format_legacy_string_with_variables(content, &self.instructions, i + 1) + { emit(&formatted)?; i += consumed; } else { @@ -138,42 +178,6 @@ impl ParsedTraceEvent { output } - - /// Format a format string with following variable instructions - fn format_template_with_variables( - &self, - template: &FormatTemplate, - start_index: usize, - ) -> (String, usize) { - let mut consumed = 1; // At least consume the format string itself - let mut result = String::new(); - let mut instruction_index = start_index; - let mut can_substitute = true; - - for part in template.parts() { - match part { - crate::FormatPart::Literal(literal) => result.push_str(literal), - crate::FormatPart::Slot(slot) if slot.conversion == FormatConversion::Default => { - if can_substitute { - if let Some(ParsedInstruction::PrintVariable { - formatted_value, .. - }) = self.instructions.get(instruction_index) - { - result.push_str(formatted_value); - consumed += 1; - instruction_index += 1; - continue; - } - can_substitute = false; - } - result.push_str(&slot.to_string()); - } - crate::FormatPart::Slot(slot) => result.push_str(&slot.to_string()), - } - } - - (result, consumed) - } } /// State of ongoing trace event parsing diff --git a/ghostscope-ui/src/events.rs b/ghostscope-ui/src/events.rs index 220c6baf..7fa3e668 100644 --- a/ghostscope-ui/src/events.rs +++ b/ghostscope-ui/src/events.rs @@ -126,4 +126,43 @@ mod tests { ] ); } + + #[test] + fn legacy_escaped_braces_match_protocol_output() { + let event = ParsedTraceEvent { + trace_id: 8, + timestamp: 12, + pid: 44, + tid: 45, + instructions: vec![ + ParsedInstruction::PrintString { + content: "{{}}".to_string(), + }, + ParsedInstruction::PrintVariable { + name: "value".to_string(), + type_encoding: TypeKind::I32, + formatted_value: "42".to_string(), + raw_data: vec![42], + }, + ParsedInstruction::EndInstruction { + total_instructions: 2, + execution_status: 0, + }, + ], + }; + + let protocol_output = event.to_formatted_output(); + let display = UiTraceEvent::from_protocol_event(&event); + + assert!(matches!( + &display.items[..], + [ + TraceDisplayItem::Text { content }, + TraceDisplayItem::Variable(variable), + ] if content == "{{}}" + && variable.name == "value" + && variable.formatted_value == "42" + )); + assert_eq!(display.to_formatted_output(), protocol_output); + } } diff --git a/ghostscope-ui/src/events/trace_display.rs b/ghostscope-ui/src/events/trace_display.rs index b70e42b4..5022c0ae 100644 --- a/ghostscope-ui/src/events/trace_display.rs +++ b/ghostscope-ui/src/events/trace_display.rs @@ -1,4 +1,5 @@ use ghostscope_protocol::{ + format_legacy_string_with_variables, trace_event::{backtrace_error_label, BacktraceStatus}, ParsedInstruction, ParsedTraceEvent, }; @@ -230,9 +231,9 @@ fn protocol_instructions_to_display_items( while index < instructions.len() { match &instructions[index] { ParsedInstruction::PrintString { content } => { - if content.contains("{}") { - let (formatted, consumed) = - format_string_with_variable_items(content, instructions, index + 1); + if let Some((formatted, consumed)) = + format_legacy_string_with_variables(content, instructions, index + 1) + { items.push(TraceDisplayItem::FormattedText { content: formatted }); index += consumed; } else { @@ -302,39 +303,6 @@ fn protocol_instructions_to_display_items( items } -fn format_string_with_variable_items( - format_string: &str, - instructions: &[ParsedInstruction], - start_index: usize, -) -> (String, usize) { - let placeholder_count = format_string.matches("{}").count(); - let mut consumed = 1; - let mut result = String::with_capacity(format_string.len()); - let mut remaining = format_string; - - for instruction_index in start_index..(start_index + placeholder_count).min(instructions.len()) - { - let Some(pos) = remaining.find("{}") else { - break; - }; - - if let Some(ParsedInstruction::PrintVariable { - formatted_value, .. - }) = instructions.get(instruction_index) - { - result.push_str(&remaining[..pos]); - result.push_str(formatted_value); - consumed += 1; - remaining = &remaining[pos + 2..]; - } else { - break; - } - } - result.push_str(remaining); - - (result, consumed) -} - #[derive(Debug, Clone)] pub struct BacktraceDisplay { pub requested_depth: u8,