Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 22 additions & 107 deletions ghostscope-compiler/src/ebpf/codegen/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComplexArg<'ctx>> = 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<usize> {
if spec.chars().all(|c| c.is_ascii_digit()) {
return spec.parse::<usize>().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<ComplexArg<'ctx>> =
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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) => (
Expand Down Expand Up @@ -639,7 +555,6 @@ impl<'ctx, 'dw> EbpfContext<'ctx, 'dw> {
ai += 1;
}
}
let _ = wants_ascii; // reserved for future per-arg metadata
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion ghostscope-compiler/src/ebpf/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
119 changes: 10 additions & 109 deletions ghostscope-compiler/src/script/format_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down
Loading
Loading