Skip to main content

ghostscope_protocol/
format_printer.rs

1//! Format printer for complex print instructions
2//!
3//! Converts PrintComplexVariable/PrintComplexFormat payloads into formatted text in user space.
4
5use crate::trace_context::TraceContext;
6use crate::trace_event::{
7    VariableStatus, VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET,
8    VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET, VARIABLE_READ_ERROR_PAYLOAD_LEN,
9};
10use crate::type_info::TypeInfo;
11
12// Removed legacy simple variable wrapper; use complex paths only.
13
14/// A parsed complex variable from PrintComplexVariable instruction data
15#[derive(Debug, Clone)]
16pub struct ParsedComplexVariable {
17    pub var_name_index: u16,
18    pub type_index: u16,
19    pub access_path: String,
20    pub status: u8, // 0 OK; non-zero means error payload in data
21    pub data: Vec<u8>,
22}
23
24/// Format printer for converting PrintComplexFormat data to formatted strings
25pub struct FormatPrinter;
26
27impl FormatPrinter {
28    /// Format printer for converting PrintComplexFormat data to formatted strings
29    pub fn format_complex_print_data(
30        format_string_index: u16,
31        complex_variables: &[ParsedComplexVariable],
32        trace_context: &TraceContext,
33    ) -> String {
34        // Get the format string from the trace context
35        let format_string = match trace_context.get_string(format_string_index) {
36            Some(s) => s,
37            None => {
38                return format!("<INVALID_FORMAT_INDEX_{format_string_index}>");
39            }
40        };
41
42        // Apply formatting using raw variables to support extended specifiers
43        Self::apply_format_with_specs(format_string, complex_variables, trace_context)
44    }
45
46    /// Simple placeholder applier for tests that don't use complex variables
47    #[cfg(test)]
48    fn apply_format_strings(format_string: &str, formatted_values: &[String]) -> String {
49        let mut result = String::new();
50        let mut chars = format_string.chars().peekable();
51        let mut var_index = 0;
52
53        while let Some(ch) = chars.next() {
54            match ch {
55                '{' => {
56                    if chars.peek() == Some(&'{') {
57                        chars.next();
58                        result.push('{');
59                    } else {
60                        // Skip to closing '}' and substitute
61                        let mut found = false;
62                        for c in chars.by_ref() {
63                            if c == '}' {
64                                found = true;
65                                break;
66                            }
67                        }
68                        if found {
69                            if var_index < formatted_values.len() {
70                                result.push_str(&formatted_values[var_index]);
71                                var_index += 1;
72                            } else {
73                                result.push_str("<MISSING_ARG>");
74                            }
75                        } else {
76                            result.push_str("<MALFORMED_PLACEHOLDER>");
77                        }
78                    }
79                }
80                '}' => {
81                    if chars.peek() == Some(&'}') {
82                        chars.next();
83                        result.push('}');
84                    } else {
85                        result.push('}');
86                    }
87                }
88                _ => result.push(ch),
89            }
90        }
91        result
92    }
93
94    /// Apply formatting with extended specifiers {:x}/{:X}/{:p}/{:s}, and optional
95    /// length suffix .N / .* / .name$.
96    fn apply_format_with_specs(
97        format_string: &str,
98        vars: &[ParsedComplexVariable],
99        trace_context: &TraceContext,
100    ) -> String {
101        let mut result = String::new();
102        let mut chars = format_string.chars().peekable();
103        let mut var_index: usize = 0;
104
105        while let Some(ch) = chars.next() {
106            match ch {
107                '{' => {
108                    if chars.peek() == Some(&'{') {
109                        chars.next();
110                        result.push('{');
111                    } else {
112                        let mut found = false;
113                        let mut content = String::new();
114                        for c in chars.by_ref() {
115                            if c == '}' {
116                                found = true;
117                                break;
118                            }
119                            content.push(c);
120                        }
121                        if !found {
122                            result.push_str("<MALFORMED_PLACEHOLDER>");
123                            continue;
124                        }
125
126                        if content.is_empty() {
127                            // default {}
128                            if var_index < vars.len() {
129                                let v = &vars[var_index];
130                                let s = Self::format_complex_variable_with_status(
131                                    v.var_name_index,
132                                    v.type_index,
133                                    &v.access_path,
134                                    &v.data,
135                                    v.status,
136                                    trace_context,
137                                );
138                                let value_part = s.split(" = ").last().unwrap_or(&s);
139                                result.push_str(value_part);
140                                var_index += 1;
141                            } else {
142                                result.push_str("<MISSING_ARG>");
143                            }
144                            continue;
145                        }
146
147                        if !content.starts_with(':') {
148                            result.push_str("<INVALID_SPEC>");
149                            continue;
150                        }
151                        let tail = &content[1..];
152                        let mut it = tail.chars();
153                        let conv = it.next().unwrap_or(' ');
154                        let rest: String = it.collect();
155
156                        // (removed) helper to get bytes of current arg; we now surface errors explicitly
157
158                        enum Len {
159                            None,
160                            Static(usize),
161                            Star,
162                            Capture,
163                        }
164                        // helper: parse static length supporting decimal/0x.. /0o.. /0b..
165                        fn parse_static_len(spec: &str) -> Option<usize> {
166                            if spec.chars().all(|c| c.is_ascii_digit()) {
167                                return spec.parse::<usize>().ok();
168                            }
169                            if let Some(hex) = spec.strip_prefix("0x") {
170                                if !hex.is_empty() && hex.chars().all(|c| c.is_ascii_hexdigit()) {
171                                    return usize::from_str_radix(hex, 16).ok();
172                                }
173                            }
174                            if let Some(oct) = spec.strip_prefix("0o") {
175                                if !oct.is_empty() && oct.chars().all(|c| matches!(c, '0'..='7')) {
176                                    return usize::from_str_radix(oct, 8).ok();
177                                }
178                            }
179                            if let Some(bin) = spec.strip_prefix("0b") {
180                                if !bin.is_empty() && bin.chars().all(|c| matches!(c, '0' | '1')) {
181                                    return usize::from_str_radix(bin, 2).ok();
182                                }
183                            }
184                            None
185                        }
186
187                        let lenspec = if rest.is_empty() {
188                            Len::None
189                        } else if let Some(r) = rest.strip_prefix('.') {
190                            if r == "*" {
191                                Len::Star
192                            } else if r.ends_with('$') {
193                                Len::Capture
194                            } else if let Some(n) = parse_static_len(r) {
195                                Len::Static(n)
196                            } else {
197                                Len::None
198                            }
199                        } else {
200                            Len::None
201                        };
202
203                        // helper: parse signed length from 8-byte little endian, clamp to >=0
204                        fn parse_len_usize(lenb: &[u8]) -> usize {
205                            if lenb.len() >= 8 {
206                                let arr = [
207                                    lenb[0], lenb[1], lenb[2], lenb[3], lenb[4], lenb[5], lenb[6],
208                                    lenb[7],
209                                ];
210                                let v = i64::from_le_bytes(arr);
211                                if v <= 0 {
212                                    0
213                                } else {
214                                    v as usize
215                                }
216                            } else {
217                                0
218                            }
219                        }
220
221                        // helper: format error value for a var when status != Ok/ZeroLength
222                        let err_value_part = |idx: usize| -> Option<String> {
223                            if idx >= vars.len() {
224                                return None;
225                            }
226                            let v = &vars[idx];
227                            if v.status == VariableStatus::Ok as u8
228                                || v.status == VariableStatus::ZeroLength as u8
229                            {
230                                None
231                            } else {
232                                let s = Self::format_complex_variable_with_status(
233                                    v.var_name_index,
234                                    v.type_index,
235                                    &v.access_path,
236                                    &v.data,
237                                    v.status,
238                                    trace_context,
239                                );
240                                Some(s.split(" = ").last().unwrap_or(&s).to_string())
241                            }
242                        };
243
244                        match conv {
245                            'x' | 'X' => {
246                                match lenspec {
247                                    Len::Star => {
248                                        if var_index + 1 >= vars.len() {
249                                            result.push_str("<MISSING_ARG>");
250                                        } else if let Some(err) = err_value_part(var_index) {
251                                            // surface error from length argument
252                                            result.push_str(&err);
253                                            var_index += 2;
254                                            continue;
255                                        } else if let Some(err) = err_value_part(var_index + 1) {
256                                            // surface error from value argument
257                                            result.push_str(&err);
258                                            var_index += 2;
259                                            continue;
260                                        } else {
261                                            // both Ok or ZeroLength
262                                            let lenb = vars[var_index].data.as_slice();
263                                            let n = parse_len_usize(lenb);
264                                            let v = &vars[var_index + 1];
265                                            let full = v.data.as_slice();
266                                            let take =
267                                                if v.status == VariableStatus::ZeroLength as u8 {
268                                                    0
269                                                } else {
270                                                    std::cmp::min(n, full.len())
271                                                };
272                                            let b = &full[..take];
273                                            let s = b
274                                                .iter()
275                                                .map(|vv| {
276                                                    if conv == 'x' {
277                                                        format!("{vv:02x}")
278                                                    } else {
279                                                        format!("{vv:02X}")
280                                                    }
281                                                })
282                                                .collect::<Vec<_>>()
283                                                .join(" ");
284                                            result.push_str(&s);
285                                            var_index += 2;
286                                            continue;
287                                        }
288                                        // when missing one of the args, don't advance to avoid misalignment
289                                    }
290                                    Len::Static(n) => {
291                                        if var_index >= vars.len() {
292                                            result.push_str("<MISSING_ARG>");
293                                        } else if let Some(err) = err_value_part(var_index) {
294                                            result.push_str(&err);
295                                            var_index += 1;
296                                            continue;
297                                        } else {
298                                            let v = &vars[var_index];
299                                            let full = v.data.as_slice();
300                                            let take =
301                                                if v.status == VariableStatus::ZeroLength as u8 {
302                                                    0
303                                                } else {
304                                                    std::cmp::min(n, full.len())
305                                                };
306                                            let b = &full[..take];
307                                            let s = b
308                                                .iter()
309                                                .map(|vv| {
310                                                    if conv == 'x' {
311                                                        format!("{vv:02x}")
312                                                    } else {
313                                                        format!("{vv:02X}")
314                                                    }
315                                                })
316                                                .collect::<Vec<_>>()
317                                                .join(" ");
318                                            result.push_str(&s);
319                                            var_index += 1;
320                                            continue;
321                                        }
322                                    }
323                                    Len::Capture => {
324                                        if var_index + 1 >= vars.len() {
325                                            result.push_str("<MISSING_ARG>");
326                                        } else if let Some(err) = err_value_part(var_index) {
327                                            result.push_str(&err);
328                                            var_index += 2;
329                                            continue;
330                                        } else if let Some(err) = err_value_part(var_index + 1) {
331                                            result.push_str(&err);
332                                            var_index += 2;
333                                            continue;
334                                        } else {
335                                            let lenb = vars[var_index].data.as_slice();
336                                            let n = parse_len_usize(lenb);
337                                            let v = &vars[var_index + 1];
338                                            let full = v.data.as_slice();
339                                            let take =
340                                                if v.status == VariableStatus::ZeroLength as u8 {
341                                                    0
342                                                } else {
343                                                    std::cmp::min(n, full.len())
344                                                };
345                                            let b = &full[..take];
346                                            let s = b
347                                                .iter()
348                                                .map(|vv| {
349                                                    if conv == 'x' {
350                                                        format!("{vv:02x}")
351                                                    } else {
352                                                        format!("{vv:02X}")
353                                                    }
354                                                })
355                                                .collect::<Vec<_>>()
356                                                .join(" ");
357                                            result.push_str(&s);
358                                            var_index += 2;
359                                            continue;
360                                        }
361                                        // when missing one of the args, don't advance
362                                    }
363                                    Len::None => {
364                                        if var_index >= vars.len() {
365                                            result.push_str("<MISSING_ARG>");
366                                        } else if let Some(err) = err_value_part(var_index) {
367                                            result.push_str(&err);
368                                            var_index += 1;
369                                            continue;
370                                        } else {
371                                            let v = &vars[var_index];
372                                            let b = if v.status == VariableStatus::ZeroLength as u8
373                                            {
374                                                &[][..]
375                                            } else {
376                                                v.data.as_slice()
377                                            };
378                                            let s = b
379                                                .iter()
380                                                .map(|vv| {
381                                                    if conv == 'x' {
382                                                        format!("{vv:02x}")
383                                                    } else {
384                                                        format!("{vv:02X}")
385                                                    }
386                                                })
387                                                .collect::<Vec<_>>()
388                                                .join(" ");
389                                            result.push_str(&s);
390                                            var_index += 1;
391                                            continue;
392                                        }
393                                    }
394                                }
395                            }
396                            's' => {
397                                let mut render_bytes = |b: &[u8]| {
398                                    let mut out = String::new();
399                                    for &c in b.iter() {
400                                        if c == 0 {
401                                            break;
402                                        }
403                                        if (0x20..=0x7e).contains(&c) {
404                                            out.push(c as char);
405                                        } else {
406                                            out.push_str(&format!("\\x{c:02x}"));
407                                        }
408                                    }
409                                    result.push_str(&out);
410                                };
411
412                                match lenspec {
413                                    Len::Star => {
414                                        if var_index + 1 >= vars.len() {
415                                            result.push_str("<MISSING_ARG>");
416                                        } else if let Some(err) = err_value_part(var_index) {
417                                            result.push_str(&err);
418                                            var_index += 2;
419                                            continue;
420                                        } else if let Some(err) = err_value_part(var_index + 1) {
421                                            result.push_str(&err);
422                                            var_index += 2;
423                                            continue;
424                                        } else {
425                                            let lenb = vars[var_index].data.as_slice();
426                                            let n = parse_len_usize(lenb);
427                                            let v = &vars[var_index + 1];
428                                            let full = v.data.as_slice();
429                                            let take =
430                                                if v.status == VariableStatus::ZeroLength as u8 {
431                                                    0
432                                                } else {
433                                                    std::cmp::min(n, full.len())
434                                                };
435                                            render_bytes(&full[..take]);
436                                            var_index += 2;
437                                            continue;
438                                        }
439                                    }
440                                    Len::Static(n) => {
441                                        if var_index >= vars.len() {
442                                            result.push_str("<MISSING_ARG>");
443                                        } else if let Some(err) = err_value_part(var_index) {
444                                            result.push_str(&err);
445                                            var_index += 1;
446                                            continue;
447                                        } else {
448                                            let v = &vars[var_index];
449                                            let full = v.data.as_slice();
450                                            let take =
451                                                if v.status == VariableStatus::ZeroLength as u8 {
452                                                    0
453                                                } else {
454                                                    std::cmp::min(n, full.len())
455                                                };
456                                            render_bytes(&full[..take]);
457                                            var_index += 1;
458                                            continue;
459                                        }
460                                    }
461                                    Len::Capture => {
462                                        if var_index + 1 >= vars.len() {
463                                            result.push_str("<MISSING_ARG>");
464                                        } else if let Some(err) = err_value_part(var_index) {
465                                            result.push_str(&err);
466                                            var_index += 2;
467                                            continue;
468                                        } else if let Some(err) = err_value_part(var_index + 1) {
469                                            result.push_str(&err);
470                                            var_index += 2;
471                                            continue;
472                                        } else {
473                                            let lenb = vars[var_index].data.as_slice();
474                                            let n = parse_len_usize(lenb);
475                                            let v = &vars[var_index + 1];
476                                            let full = v.data.as_slice();
477                                            let take =
478                                                if v.status == VariableStatus::ZeroLength as u8 {
479                                                    0
480                                                } else {
481                                                    std::cmp::min(n, full.len())
482                                                };
483                                            render_bytes(&full[..take]);
484                                            var_index += 2;
485                                            continue;
486                                        }
487                                    }
488                                    Len::None => {
489                                        if var_index >= vars.len() {
490                                            result.push_str("<MISSING_ARG>");
491                                        } else if let Some(err) = err_value_part(var_index) {
492                                            result.push_str(&err);
493                                            var_index += 1;
494                                            continue;
495                                        } else {
496                                            let v = &vars[var_index];
497                                            let b = if v.status == VariableStatus::ZeroLength as u8
498                                            {
499                                                &[][..]
500                                            } else {
501                                                v.data.as_slice()
502                                            };
503                                            render_bytes(b);
504                                            var_index += 1;
505                                            continue;
506                                        }
507                                    }
508                                }
509                            }
510                            'p' => {
511                                if var_index >= vars.len() {
512                                    result.push_str("<MISSING_ARG>");
513                                } else if let Some(err) = err_value_part(var_index) {
514                                    result.push_str(&err);
515                                    var_index += 1;
516                                    continue;
517                                } else {
518                                    let b = vars[var_index].data.as_slice();
519                                    if b.len() >= 8 {
520                                        let addr = u64::from_le_bytes([
521                                            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
522                                        ]);
523                                        result.push_str(&format!("0x{addr:x}"));
524                                    } else {
525                                        result.push_str("<INVALID_POINTER>");
526                                    }
527                                    var_index += 1;
528                                    continue;
529                                }
530                            }
531                            _ => {
532                                // fallback to default formatting
533                                if var_index < vars.len() {
534                                    let v = &vars[var_index];
535                                    let s = Self::format_complex_variable_with_status(
536                                        v.var_name_index,
537                                        v.type_index,
538                                        &v.access_path,
539                                        &v.data,
540                                        v.status,
541                                        trace_context,
542                                    );
543                                    let value_part = s.split(" = ").last().unwrap_or(&s);
544                                    result.push_str(value_part);
545                                    var_index += 1;
546                                } else {
547                                    result.push_str("<MISSING_ARG>");
548                                }
549                            }
550                        }
551                    }
552                }
553                '}' => {
554                    if chars.peek() == Some(&'}') {
555                        chars.next();
556                        result.push('}');
557                    } else {
558                        result.push('}');
559                    }
560                }
561                _ => result.push(ch),
562            }
563        }
564        result
565    }
566
567    /// Format a complex variable with full DWARF type information
568    pub fn format_complex_variable(
569        var_name_index: u16,
570        type_index: u16,
571        access_path: &str,
572        data: &[u8],
573        trace_context: &TraceContext,
574    ) -> String {
575        let var_name = trace_context
576            .get_variable_name(var_name_index)
577            .unwrap_or("<INVALID_VAR_NAME>");
578
579        let type_info = match trace_context.get_type(type_index) {
580            Some(t) => t,
581            None => return format!("<INVALID_TYPE_INDEX_{type_index}>: {var_name}"),
582        };
583
584        let formatted_data = Self::format_data_with_type_info(data, type_info);
585
586        if access_path.is_empty() {
587            format!("{var_name} = {formatted_data}")
588        } else {
589            format!("{var_name}.{access_path} = {formatted_data}")
590        }
591    }
592
593    /// Status-aware complex variable formatting
594    pub fn format_complex_variable_with_status(
595        var_name_index: u16,
596        type_index: u16,
597        access_path: &str,
598        data: &[u8],
599        status: u8,
600        trace_context: &TraceContext,
601    ) -> String {
602        let var_name = trace_context
603            .get_variable_name(var_name_index)
604            .unwrap_or("<INVALID_VAR_NAME>");
605        let type_info = match trace_context.get_type(type_index) {
606            Some(t) => t,
607            None => return format!("<INVALID_TYPE_INDEX_{type_index}>: {var_name}"),
608        };
609
610        // OK path delegates to existing formatter
611        if status == VariableStatus::Ok as u8 {
612            return Self::format_complex_variable(
613                var_name_index,
614                type_index,
615                access_path,
616                data,
617                trace_context,
618            );
619        }
620
621        // Build error prefix based on status and optional payload (errno:i32 + addr:u64)
622        let (errno, addr) = if data.len() >= VARIABLE_READ_ERROR_PAYLOAD_LEN {
623            let errno_start = VARIABLE_READ_ERROR_PAYLOAD_ERRNO_OFFSET;
624            let errno_end = errno_start + std::mem::size_of::<i32>();
625            let addr_start = VARIABLE_READ_ERROR_PAYLOAD_ADDR_OFFSET;
626            let addr_end = addr_start + std::mem::size_of::<u64>();
627            let errno = i32::from_le_bytes(
628                data[errno_start..errno_end]
629                    .try_into()
630                    .expect("read-error errno payload length checked"),
631            );
632            let addr = u64::from_le_bytes(
633                data[addr_start..addr_end]
634                    .try_into()
635                    .expect("read-error addr payload length checked"),
636            );
637            (Some(errno), Some(addr))
638        } else {
639            (None, None)
640        };
641
642        let type_suffix = type_info.type_name();
643        let err_text = match status {
644            s if s == VariableStatus::NullDeref as u8 => {
645                format!("<error: null pointer dereference> ({type_suffix}*)")
646            }
647            s if s == VariableStatus::ReadError as u8 => match (errno, addr) {
648                (Some(e), Some(a)) => {
649                    format!("<read_user failed errno={e} at 0x{a:x}> ({type_suffix}*)")
650                }
651                _ => format!("<read_user failed> ({type_suffix}*)"),
652            },
653            s if s == VariableStatus::AccessError as u8 => {
654                format!("<address compute failed> ({type_suffix}*)")
655            }
656            s if s == VariableStatus::OffsetsUnavailable as u8 => {
657                format!("<proc offsets unavailable> ({type_suffix}*)")
658            }
659            s if s == VariableStatus::Truncated as u8 => format!("<truncated> ({type_suffix}*)"),
660            s if s == VariableStatus::ZeroLength as u8 => format!("<len<=0> ({type_suffix})"),
661            _ => format!("<error status={status}> ({type_suffix}*)"),
662        };
663
664        if access_path.is_empty() {
665            format!("{var_name} = {err_text}")
666        } else {
667            format!("{var_name}.{access_path} = {err_text}")
668        }
669    }
670
671    /// Format data using full DWARF type information
672    pub fn format_data_with_type_info(data: &[u8], type_info: &TypeInfo) -> String {
673        // Relax display limits: increase max depth to print more nested content.
674        Self::format_data_with_type_info_impl(data, type_info, 0, 32)
675    }
676
677    /// Internal implementation with depth control for recursion
678    fn format_data_with_type_info_impl(
679        data: &[u8],
680        type_info: &TypeInfo,
681        current_depth: usize,
682        max_depth: usize,
683    ) -> String {
684        if current_depth > max_depth {
685            return "<MAX_DEPTH_EXCEEDED>".to_string();
686        }
687
688        match type_info {
689            TypeInfo::BaseType { size, encoding, .. } => {
690                Self::format_base_type_data(data, *size, *encoding)
691            }
692            TypeInfo::BitfieldType {
693                underlying_type,
694                bit_offset,
695                bit_size,
696            } => {
697                let u_size = underlying_type.size() as usize;
698                if data.len() < u_size || *bit_size == 0 {
699                    return "<INVALID_BITFIELD>".to_string();
700                }
701                let val =
702                    Self::extract_bits_le(&data[..u_size], *bit_offset as u32, *bit_size as u32);
703                Self::format_bitfield_value(val, underlying_type, *bit_size as u32)
704            }
705            TypeInfo::PointerType { target_type, .. } => {
706                if data.len() < 8 {
707                    "<INVALID_POINTER>".to_string()
708                } else {
709                    let addr = u64::from_le_bytes([
710                        data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
711                    ]);
712                    let ty = format!("{}*", target_type.type_name());
713                    if addr == 0 {
714                        format!("NULL ({ty})")
715                    } else {
716                        format!("0x{addr:x} ({ty})")
717                    }
718                }
719            }
720            TypeInfo::ArrayType {
721                element_type,
722                element_count,
723                ..
724            } => {
725                // Special-case: char arrays -> print as string
726                if Self::is_char_byte_type(element_type) {
727                    return Self::format_char_array_as_string(data, element_count);
728                }
729                let elem_size = element_type.size() as usize;
730                if elem_size == 0 {
731                    return "<ZERO_SIZE_ELEMENT>".to_string();
732                }
733
734                let count = element_count.unwrap_or(data.len() as u64 / elem_size as u64);
735                let actual_count = std::cmp::min(count, data.len() as u64 / elem_size as u64);
736
737                if actual_count == 0 {
738                    return "[]".to_string();
739                }
740
741                let mut result = String::from("[");
742                for i in 0..actual_count {
743                    if i > 0 {
744                        result.push_str(", ");
745                    }
746
747                    let start = i as usize * elem_size;
748                    let end = std::cmp::min(start + elem_size, data.len());
749                    let elem_data = &data[start..end];
750
751                    let formatted_elem = Self::format_data_with_type_info_impl(
752                        elem_data,
753                        element_type,
754                        current_depth + 1,
755                        max_depth,
756                    );
757                    result.push_str(&formatted_elem);
758                }
759                result.push(']');
760                result
761            }
762            TypeInfo::StructType { name, members, .. } => {
763                // Allow deeper nested structures now; cutoff managed by max_depth param
764                if current_depth > max_depth {
765                    return format!("<STRUCT_{name}>");
766                }
767
768                let mut result = format!("{name} {{ ");
769                let mut first = true;
770
771                for member in members.iter() {
772                    if !first {
773                        result.push_str(", ");
774                    }
775                    first = false;
776
777                    let offset = member.offset as usize;
778                    // Prefer explicit BitfieldType on member_type, else use legacy member.bit_* fields
779                    if let TypeInfo::BitfieldType {
780                        underlying_type,
781                        bit_offset,
782                        bit_size,
783                    } = &member.member_type
784                    {
785                        let u_size = underlying_type.size() as usize;
786                        if offset + u_size <= data.len() && *bit_size > 0 && *bit_size <= 64 {
787                            let raw = &data[offset..offset + u_size];
788                            let val_u64 =
789                                Self::extract_bits_le(raw, *bit_offset as u32, *bit_size as u32);
790                            let formatted_value = Self::format_bitfield_value(
791                                val_u64,
792                                underlying_type,
793                                *bit_size as u32,
794                            );
795                            result.push_str(&format!("{}: {}", member.name, formatted_value));
796                        } else {
797                            result.push_str(&format!("{}: <OUT_OF_BOUNDS>", member.name));
798                        }
799                    } else if let (Some(bit_size), maybe_bit_offset) =
800                        (member.bit_size, member.bit_offset)
801                    {
802                        // Handle bitfield member formatting (up to 64 bits)
803                        let bit_size = bit_size as u32;
804                        let bit_offset = maybe_bit_offset.unwrap_or(0) as u32;
805                        let bytes_needed = (bit_offset + bit_size).div_ceil(8) as usize;
806                        if offset + bytes_needed <= data.len() && bit_size > 0 && bit_size <= 64 {
807                            let raw = &data[offset..offset + bytes_needed];
808                            let val_u64 = Self::extract_bits_le(raw, bit_offset, bit_size);
809                            let formatted_value =
810                                Self::format_bitfield_value(val_u64, &member.member_type, bit_size);
811                            result.push_str(&format!("{}: {}", member.name, formatted_value));
812                        } else {
813                            result.push_str(&format!("{}: <OUT_OF_BOUNDS>", member.name));
814                        }
815                    } else {
816                        let member_size = member.member_type.size() as usize;
817                        if offset + member_size <= data.len() {
818                            let member_data = &data[offset..offset + member_size];
819                            let formatted_value = Self::format_data_with_type_info_impl(
820                                member_data,
821                                &member.member_type,
822                                current_depth + 1,
823                                max_depth,
824                            );
825                            result.push_str(&format!("{}: {}", member.name, formatted_value));
826                        } else {
827                            result.push_str(&format!("{}: <OUT_OF_BOUNDS>", member.name));
828                        }
829                    }
830                }
831
832                // No explicit elision; show all available members
833                result.push_str(" }");
834                result
835            }
836            TypeInfo::UnionType { name, members, .. } => {
837                if members.is_empty() {
838                    format!("union {name} {{}}")
839                } else {
840                    // For unions, show the first member interpretation
841                    let first_member = &members[0];
842                    let member_size = first_member.member_type.size() as usize;
843                    let member_data = if member_size <= data.len() {
844                        &data[..member_size]
845                    } else {
846                        data
847                    };
848
849                    let formatted_value = Self::format_data_with_type_info_impl(
850                        member_data,
851                        &first_member.member_type,
852                        current_depth + 1,
853                        max_depth,
854                    );
855                    format!(
856                        "union {} {{ {} = {} }}",
857                        name, first_member.name, formatted_value
858                    )
859                }
860            }
861            TypeInfo::EnumType {
862                name,
863                base_type,
864                variants,
865                ..
866            } => {
867                let base_value = Self::format_data_with_type_info_impl(
868                    data,
869                    base_type,
870                    current_depth + 1,
871                    max_depth,
872                );
873
874                // Try to find matching enum variant and print both type::variant and numeric value
875                if let Ok(int_val) = base_value.parse::<i64>() {
876                    for variant in variants {
877                        if variant.value == int_val {
878                            return format!("{}::{}({})", name, variant.name, base_value);
879                        }
880                    }
881                }
882
883                // No variant matched; still print type name with raw value
884                format!("{name}({base_value})")
885            }
886            TypeInfo::TypedefType {
887                name,
888                underlying_type,
889                ..
890            } => {
891                // Reuse aggregate formatters by substituting display name
892                match &**underlying_type {
893                    TypeInfo::StructType { size, members, .. } => {
894                        let alias_struct = TypeInfo::StructType {
895                            name: name.clone(),
896                            size: *size,
897                            members: members.clone(),
898                        };
899                        Self::format_data_with_type_info_impl(
900                            data,
901                            &alias_struct,
902                            current_depth,
903                            max_depth,
904                        )
905                    }
906                    TypeInfo::UnionType { size, members, .. } => {
907                        let alias_union = TypeInfo::UnionType {
908                            name: name.clone(),
909                            size: *size,
910                            members: members.clone(),
911                        };
912                        Self::format_data_with_type_info_impl(
913                            data,
914                            &alias_union,
915                            current_depth,
916                            max_depth,
917                        )
918                    }
919                    _ => {
920                        let underlying_formatted = Self::format_data_with_type_info_impl(
921                            data,
922                            underlying_type,
923                            current_depth,
924                            max_depth,
925                        );
926                        format!("{name}({underlying_formatted})")
927                    }
928                }
929            }
930            TypeInfo::QualifiedType {
931                underlying_type, ..
932            } => Self::format_data_with_type_info_impl(
933                data,
934                underlying_type,
935                current_depth,
936                max_depth,
937            ),
938            TypeInfo::FunctionType { .. } => {
939                if data.len() >= 8 {
940                    let addr = u64::from_le_bytes([
941                        data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
942                    ]);
943                    format!("<FUNCTION@0x{addr:x}>")
944                } else {
945                    "<INVALID_FUNCTION_POINTER>".to_string()
946                }
947            }
948            TypeInfo::UnknownType { name } => {
949                format!("<UNKNOWN_TYPE_{name}_{}_BYTES>", data.len())
950            }
951            TypeInfo::OptimizedOut { .. } => "<optimized out>".to_string(),
952        }
953    }
954
955    /// Format base type data using DWARF encoding information
956    fn format_base_type_data(data: &[u8], size: u64, encoding: u16) -> String {
957        if encoding == gimli::constants::DW_ATE_boolean.0 as u16 {
958            if data.is_empty() {
959                "<EMPTY_BOOL>".to_string()
960            } else {
961                (data[0] != 0).to_string()
962            }
963        } else if encoding == gimli::constants::DW_ATE_float.0 as u16 {
964            match size {
965                4 => {
966                    if data.len() >= 4 {
967                        let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]];
968                        f32::from_le_bytes(bytes).to_string()
969                    } else {
970                        "<INVALID_F32>".to_string()
971                    }
972                }
973                8 => {
974                    if data.len() >= 8 {
975                        let bytes: [u8; 8] = [
976                            data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
977                        ];
978                        f64::from_le_bytes(bytes).to_string()
979                    } else {
980                        "<INVALID_F64>".to_string()
981                    }
982                }
983                _ => format!("<UNSUPPORTED_FLOAT_SIZE_{size}>"),
984            }
985        } else if encoding == gimli::constants::DW_ATE_signed.0 as u16
986            || encoding == gimli::constants::DW_ATE_signed_char.0 as u16
987        {
988            match size {
989                1 => {
990                    if !data.is_empty() {
991                        (data[0] as i8).to_string()
992                    } else {
993                        "<EMPTY_I8>".to_string()
994                    }
995                }
996                2 => {
997                    if data.len() >= 2 {
998                        let bytes: [u8; 2] = [data[0], data[1]];
999                        i16::from_le_bytes(bytes).to_string()
1000                    } else {
1001                        "<INVALID_I16>".to_string()
1002                    }
1003                }
1004                4 => {
1005                    if data.len() >= 4 {
1006                        let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]];
1007                        i32::from_le_bytes(bytes).to_string()
1008                    } else {
1009                        "<INVALID_I32>".to_string()
1010                    }
1011                }
1012                8 => {
1013                    if data.len() >= 8 {
1014                        let bytes: [u8; 8] = [
1015                            data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
1016                        ];
1017                        i64::from_le_bytes(bytes).to_string()
1018                    } else {
1019                        "<INVALID_I64>".to_string()
1020                    }
1021                }
1022                _ => format!("<UNSUPPORTED_SIGNED_SIZE_{size}>"),
1023            }
1024        } else if encoding == gimli::constants::DW_ATE_unsigned.0 as u16
1025            || encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16
1026        {
1027            match size {
1028                1 => {
1029                    if !data.is_empty() {
1030                        data[0].to_string()
1031                    } else {
1032                        "<EMPTY_U8>".to_string()
1033                    }
1034                }
1035                2 => {
1036                    if data.len() >= 2 {
1037                        let bytes: [u8; 2] = [data[0], data[1]];
1038                        u16::from_le_bytes(bytes).to_string()
1039                    } else {
1040                        "<INVALID_U16>".to_string()
1041                    }
1042                }
1043                4 => {
1044                    if data.len() >= 4 {
1045                        let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]];
1046                        u32::from_le_bytes(bytes).to_string()
1047                    } else {
1048                        "<INVALID_U32>".to_string()
1049                    }
1050                }
1051                8 => {
1052                    if data.len() >= 8 {
1053                        let bytes: [u8; 8] = [
1054                            data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
1055                        ];
1056                        u64::from_le_bytes(bytes).to_string()
1057                    } else {
1058                        "<INVALID_U64>".to_string()
1059                    }
1060                }
1061                _ => format!("<UNSUPPORTED_UNSIGNED_SIZE_{size}>"),
1062            }
1063        } else {
1064            // Handle char-like 1-byte integers as characters (signed or unsigned)
1065            if (encoding == gimli::constants::DW_ATE_signed_char.0 as u16
1066                || encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16)
1067                && size == 1
1068            {
1069                if !data.is_empty() {
1070                    if data[0] >= 32 && data[0] <= 126 {
1071                        format!("'{}'", data[0] as char)
1072                    } else {
1073                        format!("'\\x{:02x}'", data[0])
1074                    }
1075                } else {
1076                    "<EMPTY_CHAR>".to_string()
1077                }
1078            } else {
1079                // Fallback for unknown encodings
1080                format!("<UNKNOWN_ENCODING_{encoding}_SIZE_{size}_BYTES>")
1081            }
1082        }
1083    }
1084
1085    /// Determine if a type is a single-byte character type (signed/unsigned char)
1086    fn is_char_byte_type(t: &TypeInfo) -> bool {
1087        match t {
1088            TypeInfo::BaseType { size, encoding, .. } => {
1089                *size == 1
1090                    && (*encoding == gimli::constants::DW_ATE_signed_char.0 as u16
1091                        || *encoding == gimli::constants::DW_ATE_unsigned_char.0 as u16
1092                        || *encoding == gimli::constants::DW_ATE_unsigned.0 as u16
1093                        || *encoding == gimli::constants::DW_ATE_signed.0 as u16)
1094            }
1095            TypeInfo::TypedefType {
1096                underlying_type, ..
1097            }
1098            | TypeInfo::QualifiedType {
1099                underlying_type, ..
1100            } => Self::is_char_byte_type(underlying_type),
1101            _ => false,
1102        }
1103    }
1104
1105    /// Format a char array as a UTF-8-ish escaped string (best-effort)
1106    fn format_char_array_as_string(data: &[u8], element_count: &Option<u64>) -> String {
1107        let max_len = element_count.map(|c| c as usize).unwrap_or(data.len());
1108        let mut s = String::new();
1109        s.push('"');
1110        let mut i = 0usize;
1111        while i < data.len() && i < max_len {
1112            let b = data[i];
1113            if b == 0 {
1114                break; // C-string termination
1115            }
1116            match b {
1117                b'"' => s.push_str("\\\""),
1118                b'\\' => s.push_str("\\\\"),
1119                0x20..=0x7E => s.push(b as char),
1120                _ => s.push_str(&format!("\\x{b:02x}")),
1121            }
1122            i += 1;
1123        }
1124        s.push('"');
1125        s
1126    }
1127
1128    /// Extract bits from a little-endian byte slice, starting at bit_offset, with length bit_size (<=64)
1129    fn extract_bits_le(raw: &[u8], bit_offset: u32, bit_size: u32) -> u64 {
1130        // Assemble up to 8 bytes into a u64 (little-endian)
1131        let mut word: u64 = 0;
1132        let take = std::cmp::min(8, raw.len());
1133        for (i, byte) in raw.iter().take(take).enumerate() {
1134            word |= (*byte as u64) << (8 * i);
1135        }
1136        let shifted = word >> bit_offset;
1137        let mask: u64 = if bit_size == 64 {
1138            u64::MAX
1139        } else {
1140            (1u64 << bit_size) - 1
1141        };
1142        shifted & mask
1143    }
1144
1145    /// Format bitfield value according to the member's TypeInfo (basic support)
1146    fn format_bitfield_value(val: u64, ty: &TypeInfo, bit_size: u32) -> String {
1147        // Bool by encoding
1148        if let TypeInfo::BaseType { encoding, .. } = ty {
1149            if *encoding == gimli::constants::DW_ATE_boolean.0 as u16 {
1150                return if val != 0 {
1151                    "true".to_string()
1152                } else {
1153                    "false".to_string()
1154                };
1155            }
1156        }
1157
1158        // Enum mapping
1159        if let TypeInfo::EnumType { variants, .. } = ty {
1160            let sval = val as i64; // interpret as non-negative; signed variants must match exact value
1161            for v in variants {
1162                if v.value == sval {
1163                    return v.name.clone();
1164                }
1165            }
1166        }
1167
1168        // Signed extension if base type is signed
1169        let is_signed = ty.is_signed_int();
1170        if is_signed && bit_size > 0 && bit_size <= 64 {
1171            let sign_bit = 1u64 << (bit_size - 1);
1172            let signed_val: i64 = if (val & sign_bit) != 0 {
1173                // negative value, sign-extend
1174                let ext_mask = (!0u64) << bit_size;
1175                (val | ext_mask) as i64
1176            } else {
1177                val as i64
1178            };
1179            return signed_val.to_string();
1180        }
1181
1182        // Default: unsigned decimal
1183        val.to_string()
1184    }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189    use super::*;
1190
1191    #[test]
1192    fn test_apply_format_basic() {
1193        let fmt = "pid: {}, name: {}";
1194        let rendered: Vec<String> = vec!["42".to_string(), "hello".to_string()];
1195        let result = FormatPrinter::apply_format_strings(fmt, &rendered);
1196        assert_eq!(result, "pid: 42, name: hello");
1197    }
1198
1199    #[test]
1200    fn test_apply_format_escape_sequences() {
1201        let rendered: Vec<String> = vec!["123".to_string()];
1202        let result =
1203            FormatPrinter::apply_format_strings("use {{}} for braces, value: {}", &rendered);
1204        assert_eq!(result, "use {} for braces, value: 123");
1205    }
1206
1207    #[test]
1208    fn test_missing_arguments() {
1209        let result = FormatPrinter::apply_format_strings("need arg: {}", &[]);
1210        assert_eq!(result, "need arg: <MISSING_ARG>");
1211    }
1212
1213    #[test]
1214    fn test_format_print_data_with_trace_context() {
1215        let mut trace_context = TraceContext::new();
1216        let format_index = trace_context.add_string("Hello {}, you are {} years old!".to_string());
1217        let rendered: Vec<String> = vec!["Alice".to_string(), "25".to_string()];
1218        let fmt = trace_context.get_string(format_index).unwrap();
1219        let result = FormatPrinter::apply_format_strings(fmt, &rendered);
1220        assert_eq!(result, "Hello Alice, you are 25 years old!");
1221    }
1222
1223    #[test]
1224    fn test_format_complex_variable_struct() {
1225        use crate::type_info::{StructMember, TypeInfo};
1226
1227        let mut trace_context = TraceContext::new();
1228        let var_name_idx = trace_context.add_variable_name("person".to_string());
1229
1230        let person_type = TypeInfo::StructType {
1231            name: "Person".to_string(),
1232            size: 36,
1233            members: vec![
1234                StructMember {
1235                    name: "age".to_string(),
1236                    member_type: TypeInfo::BaseType {
1237                        name: "int".to_string(),
1238                        size: 4,
1239                        encoding: gimli::constants::DW_ATE_signed.0 as u16,
1240                    },
1241                    offset: 0,
1242                    bit_offset: None,
1243                    bit_size: None,
1244                },
1245                StructMember {
1246                    name: "id".to_string(),
1247                    member_type: TypeInfo::BaseType {
1248                        name: "long".to_string(),
1249                        size: 8,
1250                        encoding: gimli::constants::DW_ATE_signed.0 as u16,
1251                    },
1252                    offset: 4,
1253                    bit_offset: None,
1254                    bit_size: None,
1255                },
1256            ],
1257        };
1258
1259        let type_idx = trace_context.add_type(person_type);
1260
1261        // Data: age=25 (4 bytes) + id=12345 (8 bytes)
1262        let data = vec![
1263            25, 0, 0, 0, // age = 25
1264            57, 48, 0, 0, 0, 0, 0, 0, // id = 12345
1265        ];
1266
1267        let result = FormatPrinter::format_complex_variable(
1268            var_name_idx,
1269            type_idx,
1270            "",
1271            &data,
1272            &trace_context,
1273        );
1274
1275        assert!(result.contains("person = Person"));
1276        assert!(result.contains("age: 25"));
1277        assert!(result.contains("id: 12345"));
1278    }
1279
1280    #[test]
1281    fn test_complex_format_char_array() {
1282        use crate::type_info::TypeInfo;
1283
1284        let mut trace_context = TraceContext::new();
1285        let var_name_idx = trace_context.add_variable_name("name".to_string());
1286        // Define char array type: char name[16]
1287        let char_type = TypeInfo::BaseType {
1288            name: "char".to_string(),
1289            size: 1,
1290            encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16,
1291        };
1292        let arr_type = TypeInfo::ArrayType {
1293            element_type: Box::new(char_type),
1294            element_count: Some(16),
1295            total_size: Some(16),
1296        };
1297        let type_idx = trace_context.add_type(arr_type);
1298
1299        // Data buffer with "Alice\0" and padding
1300        let mut data = b"Alice\0".to_vec();
1301        data.resize(16, 0u8);
1302
1303        let fmt_idx = trace_context.add_string("{}".to_string());
1304        let complex_vars = vec![ParsedComplexVariable {
1305            var_name_index: var_name_idx,
1306            type_index: type_idx,
1307            access_path: String::new(),
1308            status: 0,
1309            data,
1310        }];
1311
1312        let result =
1313            FormatPrinter::format_complex_print_data(fmt_idx, &complex_vars, &trace_context);
1314        assert_eq!(result, "\"Alice\"");
1315    }
1316
1317    #[test]
1318    fn test_format_data_with_type_info_array() {
1319        let array_type = TypeInfo::ArrayType {
1320            element_type: Box::new(TypeInfo::BaseType {
1321                name: "int".to_string(),
1322                size: 4,
1323                encoding: gimli::constants::DW_ATE_signed.0 as u16,
1324            }),
1325            element_count: Some(3),
1326            total_size: Some(12),
1327        };
1328
1329        let data = vec![
1330            1, 0, 0, 0, // 1
1331            2, 0, 0, 0, // 2
1332            3, 0, 0, 0, // 3
1333        ];
1334
1335        let result = FormatPrinter::format_data_with_type_info(&data, &array_type);
1336        assert_eq!(result, "[1, 2, 3]");
1337    }
1338
1339    #[test]
1340    fn test_bitfield_value_signed_and_unsigned() {
1341        use crate::type_info::TypeInfo;
1342
1343        // Unsigned 3-bit at bit 0 from a u32 container
1344        let u32_type = TypeInfo::BaseType {
1345            name: "unsigned int".to_string(),
1346            size: 4,
1347            encoding: gimli::constants::DW_ATE_unsigned.0 as u16,
1348        };
1349        let bf_unsigned = TypeInfo::BitfieldType {
1350            underlying_type: Box::new(u32_type.clone()),
1351            bit_offset: 0,
1352            bit_size: 3,
1353        };
1354        let data = [0b0000_0101u8, 0, 0, 0]; // value = 5
1355        let res = FormatPrinter::format_data_with_type_info(&data, &bf_unsigned);
1356        assert_eq!(res, "5");
1357
1358        // Signed 3-bit at bit 0 from an i32 container (0b111 -> -1)
1359        let i32_type = TypeInfo::BaseType {
1360            name: "int".to_string(),
1361            size: 4,
1362            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1363        };
1364        let bf_signed = TypeInfo::BitfieldType {
1365            underlying_type: Box::new(i32_type),
1366            bit_offset: 0,
1367            bit_size: 3,
1368        };
1369        let data_neg1 = [0b0000_0111u8, 0, 0, 0];
1370        let res2 = FormatPrinter::format_data_with_type_info(&data_neg1, &bf_signed);
1371        assert_eq!(res2, "-1");
1372
1373        // Boolean 1-bit at bit 0 from a bool underlying type
1374        let bool_type = TypeInfo::BaseType {
1375            name: "bool".to_string(),
1376            size: 1,
1377            encoding: gimli::constants::DW_ATE_boolean.0 as u16,
1378        };
1379        let bf_bool = TypeInfo::BitfieldType {
1380            underlying_type: Box::new(bool_type),
1381            bit_offset: 0,
1382            bit_size: 1,
1383        };
1384        let data_true = [0x01u8];
1385        let res3 = FormatPrinter::format_data_with_type_info(&data_true, &bf_bool);
1386        assert_eq!(res3, "true");
1387    }
1388
1389    #[test]
1390    fn test_struct_with_bitfields() {
1391        use crate::type_info::{StructMember, TypeInfo};
1392
1393        // Define a struct S with two bitfields in a 32-bit storage at offset 0
1394        let u32_type = TypeInfo::BaseType {
1395            name: "unsigned int".to_string(),
1396            size: 4,
1397            encoding: gimli::constants::DW_ATE_unsigned.0 as u16,
1398        };
1399
1400        let s_type = TypeInfo::StructType {
1401            name: "S".to_string(),
1402            size: 4,
1403            members: vec![
1404                StructMember {
1405                    name: "active".to_string(),
1406                    member_type: TypeInfo::BitfieldType {
1407                        underlying_type: Box::new(u32_type.clone()),
1408                        bit_offset: 0,
1409                        bit_size: 1,
1410                    },
1411                    offset: 0,
1412                    bit_offset: Some(0),
1413                    bit_size: Some(1),
1414                },
1415                StructMember {
1416                    name: "flags".to_string(),
1417                    member_type: TypeInfo::BitfieldType {
1418                        underlying_type: Box::new(u32_type.clone()),
1419                        bit_offset: 1,
1420                        bit_size: 3,
1421                    },
1422                    offset: 0,
1423                    bit_offset: Some(1),
1424                    bit_size: Some(3),
1425                },
1426            ],
1427        };
1428
1429        // Value layout: bit0=1 (active), bits1..3=0b011 (flags=3)
1430        let data = [0b0000_0111u8, 0, 0, 0];
1431        let res = FormatPrinter::format_data_with_type_info(&data, &s_type);
1432        assert!(res.contains("S {"));
1433        assert!(res.contains("active: 1"));
1434        assert!(res.contains("flags: 3"));
1435    }
1436
1437    #[test]
1438    fn test_ext_hex_preserves_null_deref_error() {
1439        let mut trace_context = TraceContext::new();
1440        let fmt_idx = trace_context.add_string("{:x.16}".to_string());
1441
1442        // Array<u8,16> as the value type
1443        let arr_type = TypeInfo::ArrayType {
1444            element_type: Box::new(TypeInfo::BaseType {
1445                name: "u8".to_string(),
1446                size: 1,
1447                encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16,
1448            }),
1449            element_count: Some(16),
1450            total_size: Some(16),
1451        };
1452        let type_idx = trace_context.add_type(arr_type);
1453        let var_name_idx = trace_context.add_variable_name("buf".to_string());
1454
1455        let vars = vec![ParsedComplexVariable {
1456            var_name_index: var_name_idx,
1457            type_index: type_idx,
1458            access_path: String::new(),
1459            status: VariableStatus::NullDeref as u8,
1460            data: vec![],
1461        }];
1462
1463        let out = FormatPrinter::format_complex_print_data(fmt_idx, &vars, &trace_context);
1464        assert!(
1465            out.contains("null pointer dereference"),
1466            "unexpected output: {out}"
1467        );
1468        assert!(
1469            !out.contains("<MISSING_ARG>"),
1470            "should not hide error: {out}"
1471        );
1472    }
1473
1474    #[test]
1475    fn test_ext_s_preserves_read_error_errno_addr() {
1476        let mut trace_context = TraceContext::new();
1477        let fmt_idx = trace_context.add_string("{:s.16}".to_string());
1478
1479        // Array<u8,16>
1480        let arr_type = TypeInfo::ArrayType {
1481            element_type: Box::new(TypeInfo::BaseType {
1482                name: "u8".to_string(),
1483                size: 1,
1484                encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16,
1485            }),
1486            element_count: Some(16),
1487            total_size: Some(16),
1488        };
1489        let type_idx = trace_context.add_type(arr_type);
1490        let var_name_idx = trace_context.add_variable_name("buf".to_string());
1491
1492        // Encode errno:i32 + addr:u64 into data
1493        let errno: i32 = -14; // EFAULT-like
1494        let addr: u64 = 0x1234_5678_9abc_def0;
1495        let mut data = Vec::new();
1496        data.extend_from_slice(&errno.to_le_bytes());
1497        data.extend_from_slice(&addr.to_le_bytes());
1498
1499        let vars = vec![ParsedComplexVariable {
1500            var_name_index: var_name_idx,
1501            type_index: type_idx,
1502            access_path: String::new(),
1503            status: VariableStatus::ReadError as u8,
1504            data,
1505        }];
1506
1507        let out = FormatPrinter::format_complex_print_data(fmt_idx, &vars, &trace_context);
1508        assert!(
1509            out.contains("read_user failed errno=-14"),
1510            "unexpected: {out}"
1511        );
1512        assert!(out.contains("0x123456789abcdef0"), "missing addr: {out}");
1513    }
1514
1515    #[test]
1516    fn test_ext_p_preserves_offsets_unavailable() {
1517        let mut trace_context = TraceContext::new();
1518        let fmt_idx = trace_context.add_string("P={:p}".to_string());
1519
1520        let ptr_type = TypeInfo::PointerType {
1521            target_type: Box::new(TypeInfo::BaseType {
1522                name: "u8".to_string(),
1523                size: 1,
1524                encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16,
1525            }),
1526            size: 8,
1527        };
1528        let type_idx = trace_context.add_type(ptr_type);
1529        let var_name_idx = trace_context.add_variable_name("ptr".to_string());
1530
1531        let vars = vec![ParsedComplexVariable {
1532            var_name_index: var_name_idx,
1533            type_index: type_idx,
1534            access_path: String::new(),
1535            status: VariableStatus::OffsetsUnavailable as u8,
1536            data: vec![],
1537        }];
1538
1539        let out = FormatPrinter::format_complex_print_data(fmt_idx, &vars, &trace_context);
1540        assert!(out.starts_with("P="), "prefix lost: {out}");
1541        assert!(
1542            out.contains("proc offsets unavailable"),
1543            "unexpected: {out}"
1544        );
1545    }
1546
1547    #[test]
1548    fn test_ext_star_len_error_precedence() {
1549        let mut trace_context = TraceContext::new();
1550        let fmt_idx = trace_context.add_string("S={:x.*}".to_string());
1551
1552        // length argument (will surface its error), use base type for simplicity
1553        let len_type = TypeInfo::BaseType {
1554            name: "i64".to_string(),
1555            size: 8,
1556            encoding: gimli::constants::DW_ATE_signed.0 as u16,
1557        };
1558        let len_ty_idx = trace_context.add_type(len_type);
1559        let len_name_idx = trace_context.add_variable_name("len".to_string());
1560
1561        // value argument (OK)
1562        let arr_type = TypeInfo::ArrayType {
1563            element_type: Box::new(TypeInfo::BaseType {
1564                name: "u8".to_string(),
1565                size: 1,
1566                encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16,
1567            }),
1568            element_count: Some(16),
1569            total_size: Some(16),
1570        };
1571        let val_ty_idx = trace_context.add_type(arr_type);
1572        let val_name_idx = trace_context.add_variable_name("buf".to_string());
1573        let val_data: Vec<u8> = (0u8..16).collect();
1574
1575        let vars = vec![
1576            ParsedComplexVariable {
1577                var_name_index: len_name_idx,
1578                type_index: len_ty_idx,
1579                access_path: String::new(),
1580                status: VariableStatus::NullDeref as u8,
1581                data: vec![],
1582            },
1583            ParsedComplexVariable {
1584                var_name_index: val_name_idx,
1585                type_index: val_ty_idx,
1586                access_path: String::new(),
1587                status: VariableStatus::Ok as u8,
1588                data: val_data,
1589            },
1590        ];
1591
1592        let out = FormatPrinter::format_complex_print_data(fmt_idx, &vars, &trace_context);
1593        assert!(out.starts_with("S="), "prefix lost: {out}");
1594        assert!(
1595            out.contains("null pointer"),
1596            "should surface len arg error: {out}"
1597        );
1598        // should not print hex bytes when length errored out
1599        assert!(
1600            !out.contains("00 01 02 03"),
1601            "should not render bytes: {out}"
1602        );
1603    }
1604}