ktstr/scenario/snapshot/field.rs
1//! [`SnapshotField`] — terminal accessor for a typed read out of a
2//! rendered `crate::monitor::btf_render::RenderedValue` tree, plus
3//! the lazy traversal helpers ([`walk_dotted_path`],
4//! [`lookup_member`], [`peel_pointer`], [`describe_kind`]) and the
5//! [`render_to_u64`] / [`render_to_i64`] coercion paths the
6//! accessors funnel through.
7
8use crate::monitor::btf_render::{RenderedMember, RenderedValue};
9
10use super::{SnapshotError, SnapshotResult};
11
12/// One field's view at the leaf of a dotted-path walk.
13///
14/// Returned by [`super::Snapshot::var`], [`super::SnapshotEntry::get`], and
15/// [`super::SnapshotEntry::key`]. Terminal `as_*` accessors return
16/// [`SnapshotResult`] so a missing or type-mismatched field
17/// surfaces as a recoverable error rather than a panic.
18#[derive(Debug)]
19#[must_use = "SnapshotField is a borrowed view; call as_u64 / as_i64 / etc. to extract"]
20#[non_exhaustive]
21pub enum SnapshotField<'a> {
22 /// Resolved rendered value at the leaf of the path walk.
23 Value(&'a RenderedValue),
24 /// Dedicated per-CPU array key shape (u32, no struct).
25 PercpuKey { key: u32 },
26 /// Path could not be resolved.
27 Missing(SnapshotError),
28}
29
30impl<'a> SnapshotField<'a> {
31 /// Walk into a sub-field. Composable with
32 /// [`super::SnapshotEntry::get`].
33 pub fn get(&self, path: &str) -> SnapshotField<'a> {
34 match self {
35 SnapshotField::Value(v) => walk_dotted_path(v, path),
36 SnapshotField::PercpuKey { .. } => {
37 SnapshotField::Missing(SnapshotError::TypeMismatch {
38 expected: "Struct".to_string(),
39 actual:
40 "Uint(percpu key) — call as_u64/as_i64/as_f64/as_bool for the key value"
41 .to_string(),
42 requested: path.to_string(),
43 })
44 }
45 SnapshotField::Missing(err) => SnapshotField::Missing(err.clone()),
46 }
47 }
48
49 /// True when the field resolved successfully.
50 pub fn is_present(&self) -> bool {
51 !matches!(self, SnapshotField::Missing(_))
52 }
53
54 /// Read as `u64`. Accepts [`RenderedValue::Uint`],
55 /// [`RenderedValue::Int`] (errors on negative),
56 /// [`RenderedValue::Bool`] (0/1), [`RenderedValue::Char`]
57 /// (raw byte), [`RenderedValue::Enum`] (raw enum integer),
58 /// [`RenderedValue::Ptr`] (pointer value), and the
59 /// percpu-array u32 key.
60 pub fn as_u64(&self) -> SnapshotResult<u64> {
61 match self {
62 SnapshotField::Value(v) => render_to_u64(v),
63 SnapshotField::PercpuKey { key } => Ok(u64::from(*key)),
64 SnapshotField::Missing(err) => Err(err.clone()),
65 }
66 }
67
68 /// Read as `i64`.
69 pub fn as_i64(&self) -> SnapshotResult<i64> {
70 match self {
71 SnapshotField::Value(v) => render_to_i64(v),
72 SnapshotField::PercpuKey { key } => Ok(i64::from(*key)),
73 SnapshotField::Missing(err) => Err(err.clone()),
74 }
75 }
76
77 /// Read as `bool`. [`RenderedValue::Bool`] direct, ints / enums
78 /// non-zero is true.
79 pub fn as_bool(&self) -> SnapshotResult<bool> {
80 match self {
81 SnapshotField::Value(v) => match v {
82 RenderedValue::Bool { value } => Ok(*value),
83 RenderedValue::Int { value, .. } => Ok(*value != 0),
84 RenderedValue::Uint { value, .. } => Ok(*value != 0),
85 RenderedValue::Char { value } => Ok(*value != 0),
86 RenderedValue::Enum { value, .. } => Ok(*value != 0),
87 RenderedValue::Ptr { value, .. } => Ok(*value != 0),
88 other => Err(SnapshotError::TypeMismatch {
89 expected: "bool".to_string(),
90 actual: describe_kind(other),
91 requested: String::new(),
92 }),
93 },
94 SnapshotField::PercpuKey { key } => Ok(*key != 0),
95 SnapshotField::Missing(err) => Err(err.clone()),
96 }
97 }
98
99 /// Read as `f64`.
100 pub fn as_f64(&self) -> SnapshotResult<f64> {
101 match self {
102 SnapshotField::Value(v) => match v {
103 RenderedValue::Float { value, .. } => Ok(*value),
104 RenderedValue::Int { value, .. } => Ok(*value as f64),
105 RenderedValue::Uint { value, .. } => Ok(*value as f64),
106 RenderedValue::Enum { value, .. } => Ok(*value as f64),
107 other => Err(SnapshotError::TypeMismatch {
108 expected: "f64".to_string(),
109 actual: describe_kind(other),
110 requested: String::new(),
111 }),
112 },
113 SnapshotField::PercpuKey { key } => Ok(f64::from(*key)),
114 SnapshotField::Missing(err) => Err(err.clone()),
115 }
116 }
117
118 /// Read the variant string for an [`RenderedValue::Enum`] with
119 /// a resolved variant name.
120 pub fn as_str(&self) -> SnapshotResult<&'a str> {
121 match self {
122 SnapshotField::Value(v) => match v {
123 RenderedValue::Enum {
124 variant: Some(name),
125 ..
126 } => Ok(name.as_str()),
127 other => Err(SnapshotError::TypeMismatch {
128 expected: "str (enum variant name)".to_string(),
129 actual: describe_kind(other),
130 requested: String::new(),
131 }),
132 },
133 SnapshotField::PercpuKey { .. } => Err(SnapshotError::TypeMismatch {
134 expected: "str".to_string(),
135 actual: "Uint(percpu key) — call as_u64/as_i64/as_f64/as_bool for the key value"
136 .to_string(),
137 requested: String::new(),
138 }),
139 SnapshotField::Missing(err) => Err(err.clone()),
140 }
141 }
142
143 /// Read as `Vec<u64>` from an [`RenderedValue::Array`] whose
144 /// every element coerces via [`Self::as_u64`]'s rules. Errors
145 /// with [`SnapshotError::TypeMismatch`] when self is not an
146 /// array, or when any element fails the coercion (no partial
147 /// results — the caller cannot tell which element silently
148 /// dropped). Mirrors [`RenderedValue::as_u64_array`] but
149 /// propagates the captured [`SnapshotError`] through the
150 /// [`SnapshotField::Missing`] arm.
151 pub fn as_u64_array(&self) -> SnapshotResult<Vec<u64>> {
152 render_to_typed_array(self, RenderedValue::as_u64, "u64")
153 }
154
155 /// Read as `Vec<u32>` from an array. Mirrors
156 /// [`RenderedValue::as_u32_array`]; out-of-range values
157 /// (Uint exceeding `u32::MAX`) error rather than truncate.
158 pub fn as_u32_array(&self) -> SnapshotResult<Vec<u32>> {
159 render_to_typed_array(
160 self,
161 |v| v.as_u64().and_then(|x| u32::try_from(x).ok()),
162 "u32",
163 )
164 }
165
166 /// Read as `Vec<i64>` from an array. Mirrors
167 /// [`RenderedValue::as_i64_array`].
168 pub fn as_i64_array(&self) -> SnapshotResult<Vec<i64>> {
169 render_to_typed_array(self, RenderedValue::as_i64, "i64")
170 }
171
172 /// Read as `Vec<f64>` from an array. Mirrors
173 /// [`RenderedValue::as_f64_array`].
174 pub fn as_f64_array(&self) -> SnapshotResult<Vec<f64>> {
175 render_to_typed_array(self, RenderedValue::as_f64, "f64")
176 }
177
178 /// Read as `Vec<bool>` from an array. Mirrors
179 /// [`RenderedValue::as_bool_array`].
180 pub fn as_bool_array(&self) -> SnapshotResult<Vec<bool>> {
181 render_to_typed_array(self, RenderedValue::as_bool, "bool")
182 }
183
184 /// Drop into the raw [`RenderedValue`] for direct
185 /// [`RenderedValue::member`] / [`RenderedValue::get`] /
186 /// [`RenderedValue::index`] navigation. Use when the
187 /// pattern-matched-into-known-shape access pattern (Option-
188 /// returning terminals, no rich error context) reads more
189 /// naturally than the SnapshotField's Result-propagating
190 /// chain. `None` for [`SnapshotField::PercpuKey`] (no
191 /// underlying tree) and [`SnapshotField::Missing`].
192 pub fn raw(&self) -> Option<&'a RenderedValue> {
193 match self {
194 SnapshotField::Value(v) => Some(v),
195 _ => None,
196 }
197 }
198
199 /// Iterate the elements of an array-shaped field as
200 /// [`SnapshotField`]s so chained navigation composes:
201 /// `field.iter_members().filter_map(|el| el.get("name").as_u64().ok())`.
202 /// Bridges the gap left by the scalar `as_*_array` terminals
203 /// on array-of-struct shapes: those terminals coerce each
204 /// element to a scalar via the shared coercion helper and
205 /// return [`SnapshotError::TypeMismatch`] on the first
206 /// non-scalar element, which is exactly what an array-of-struct
207 /// triggers. `iter_members` instead hands the caller each raw
208 /// element so they can chain `.get(field).as_u64()` per element.
209 /// Peels [`RenderedValue::Ptr`] dereferences and
210 /// [`RenderedValue::Truncated`] partial-array wrappers the
211 /// same way [`Self::as_u64_array`] does.
212 ///
213 /// Yields nothing for non-array shapes, percpu-key fields, or
214 /// missing fields — the empty iterator pattern is the natural
215 /// "no elements to walk" representation when the chain just
216 /// wants to fold over what's there. `iter_members` itself never
217 /// surfaces [`SnapshotError::TypeMismatch`]; callers needing to
218 /// distinguish "absent" from "empty" check [`Self::is_present`]
219 /// or [`Self::error`] explicitly.
220 pub fn iter_members(&self) -> impl Iterator<Item = SnapshotField<'a>> + '_ {
221 let elements = match self {
222 SnapshotField::Value(v) => array_elements_of(v),
223 _ => &[],
224 };
225 elements.iter().map(SnapshotField::Value)
226 }
227
228 /// Error reference when the field is missing; `None`
229 /// otherwise.
230 pub fn error(&self) -> Option<&SnapshotError> {
231 match self {
232 SnapshotField::Missing(err) => Some(err),
233 _ => None,
234 }
235 }
236}
237
238// ---------------------------------------------------------------------------
239// JSON dotted-path accessor (mirrors SnapshotField for stats values)
240// ---------------------------------------------------------------------------
241
242/// Walk a [`RenderedValue`] tree along a dotted path. Each
243/// component matches a [`RenderedMember::name`] inside a
244/// [`RenderedValue::Struct`]; [`RenderedValue::Ptr`] dereferences
245/// are followed transparently. An empty path returns the root.
246pub(crate) fn walk_dotted_path<'a>(root: &'a RenderedValue, path: &str) -> SnapshotField<'a> {
247 if path.is_empty() {
248 return SnapshotField::Value(root);
249 }
250 let mut cursor: &RenderedValue = root;
251 let mut walked = String::new();
252 for component in path.split('.') {
253 if component.is_empty() {
254 return SnapshotField::Missing(SnapshotError::EmptyPathComponent {
255 requested: path.to_string(),
256 });
257 }
258 cursor = peel_pointer(cursor);
259 let RenderedValue::Struct { members, .. } = cursor else {
260 return SnapshotField::Missing(SnapshotError::NotAStruct {
261 requested: path.to_string(),
262 walked: walked.clone(),
263 component: component.to_string(),
264 kind: describe_kind(cursor),
265 });
266 };
267 let next = members.iter().find(|m| m.name == component);
268 let Some(member) = next else {
269 let names: Vec<String> = members.iter().map(|m| m.name.clone()).collect();
270 return SnapshotField::Missing(SnapshotError::FieldNotFound {
271 requested: path.to_string(),
272 walked: walked.clone(),
273 component: component.to_string(),
274 available: names,
275 });
276 };
277 cursor = &member.value;
278 if !walked.is_empty() {
279 walked.push('.');
280 }
281 walked.push_str(component);
282 }
283 SnapshotField::Value(cursor)
284}
285
286/// Look up a single top-level member by exact name. Used by
287/// [`super::Snapshot::var`].
288pub(super) fn lookup_member<'a>(value: &'a RenderedValue, name: &str) -> Option<&'a RenderedValue> {
289 let v = peel_pointer(value);
290 let RenderedValue::Struct { members, .. } = v else {
291 return None;
292 };
293 members
294 .iter()
295 .find(|m: &&RenderedMember| m.name == name)
296 .map(|m| &m.value)
297}
298
299/// Peel through any [`RenderedValue::Ptr`] layers whose `deref`
300/// is `Some`. Stops at the first non-pointer (or a pointer
301/// without a chased deref).
302fn peel_pointer(mut v: &RenderedValue) -> &RenderedValue {
303 let mut steps = 0;
304 while let RenderedValue::Ptr {
305 deref: Some(inner), ..
306 } = v
307 {
308 v = inner.as_ref();
309 steps += 1;
310 if steps > 16 {
311 break;
312 }
313 }
314 v
315}
316
317/// Human-readable variant name used in error messages.
318fn describe_kind(v: &RenderedValue) -> String {
319 match v {
320 RenderedValue::Int { .. } => "Int",
321 RenderedValue::Uint { .. } => "Uint",
322 RenderedValue::Bool { .. } => "Bool",
323 RenderedValue::Char { .. } => "Char",
324 RenderedValue::Float { .. } => "Float",
325 RenderedValue::Enum { .. } => "Enum",
326 RenderedValue::Struct { .. } => "Struct",
327 RenderedValue::Array { .. } => "Array",
328 RenderedValue::CpuList { .. } => "CpuList",
329 RenderedValue::Ptr { .. } => "Ptr",
330 RenderedValue::Bytes { .. } => "Bytes",
331 RenderedValue::Truncated { .. } => "Truncated",
332 RenderedValue::Unsupported { .. } => "Unsupported",
333 }
334 .to_string()
335}
336
337/// Shared array-elements walker: peel [`RenderedValue::Ptr`]'s
338/// deref and [`RenderedValue::Truncated`]'s partial recursively
339/// to reach an [`RenderedValue::Array`], returning the elements
340/// slice on success. On a non-array variant (after peeling),
341/// `Err` carries the unwrapped inner value so callers that want
342/// a typed mismatch diagnostic can name the actual variant via
343/// [`describe_kind`].
344fn array_elements_or_mismatch(v: &RenderedValue) -> Result<&[RenderedValue], &RenderedValue> {
345 match v {
346 RenderedValue::Array { elements, .. } => Ok(elements.as_slice()),
347 RenderedValue::Ptr {
348 deref: Some(inner), ..
349 } => array_elements_or_mismatch(inner.as_ref()),
350 RenderedValue::Truncated { partial, .. } => array_elements_or_mismatch(partial.as_ref()),
351 other => Err(other),
352 }
353}
354
355/// Borrow the elements slice of an [`RenderedValue::Array`],
356/// peeling [`RenderedValue::Ptr`]'s deref and
357/// [`RenderedValue::Truncated`]'s partial. Returns the empty
358/// slice for any non-array variant so the caller's iterator
359/// chain yields no elements cleanly. Thin wrapper over
360/// [`array_elements_or_mismatch`] that swallows the typed
361/// mismatch — appropriate for [`SnapshotField::iter_members`]
362/// whose empty-iterator contract distinguishes absent vs empty
363/// via [`SnapshotField::is_present`] / [`SnapshotField::error`]
364/// rather than via a returned error.
365fn array_elements_of(v: &RenderedValue) -> &[RenderedValue] {
366 array_elements_or_mismatch(v).unwrap_or(&[])
367}
368
369/// Shared typed-array coercion used by [`SnapshotField::as_u64_array`]
370/// and siblings. `coerce` is the per-element scalar extractor that
371/// returns `None` when the element fails the coercion (matches the
372/// [`RenderedValue`] inherent `.as_*` Option-returning shape).
373/// `type_name` names the requested element type for diagnostics.
374fn render_to_typed_array<T, F>(
375 field: &SnapshotField<'_>,
376 coerce: F,
377 type_name: &'static str,
378) -> SnapshotResult<Vec<T>>
379where
380 F: Fn(&RenderedValue) -> Option<T>,
381{
382 let value = match field {
383 SnapshotField::Value(v) => *v,
384 SnapshotField::PercpuKey { .. } => {
385 return Err(SnapshotError::TypeMismatch {
386 expected: format!("[{type_name}]"),
387 actual: "Uint(percpu key) — call as_u64/as_i64/as_f64/as_bool for the key value"
388 .to_string(),
389 requested: String::new(),
390 });
391 }
392 SnapshotField::Missing(err) => return Err(err.clone()),
393 };
394 let elements = array_elements_or_mismatch(value).map_err(|other| {
395 // Diagnostic wrapping mirrors the operator-facing form the
396 // legacy code emitted for the common one-deep cases:
397 // - top-level `Truncated{partial: NonArray}` reports
398 // `Truncated(partial=<inner-kind>)` so the operator
399 // can tell the partial wrapper hid a non-array shape
400 // (vs the top level just not being an array).
401 // - all other paths (top-level non-array, top-level Ptr,
402 // and any deeper-nested wrapper combination) report
403 // the unwrapped leaf kind directly.
404 // The shared walker recurses through Ptr+Truncated, so
405 // arbitrary nesting around an Array now succeeds (matches
406 // RenderedValue::array_elements semantics at
407 // src/monitor/btf_render/mod.rs). On failure of a nested
408 // shape (e.g. Ptr→Truncated→NonArray), the diagnostic
409 // collapses to the leaf kind rather than narrating the
410 // wrapper stack — sufficient context for the operator
411 // since the wrapper structure is renderer-internal and
412 // not load-bearing for assertions.
413 let actual = match value {
414 RenderedValue::Truncated { .. } => {
415 format!("Truncated(partial={})", describe_kind(other))
416 }
417 _ => describe_kind(other),
418 };
419 SnapshotError::TypeMismatch {
420 expected: format!("[{type_name}]"),
421 actual,
422 requested: String::new(),
423 }
424 })?;
425 let mut out = Vec::with_capacity(elements.len());
426 for (i, element) in elements.iter().enumerate() {
427 let v = coerce(element).ok_or_else(|| SnapshotError::TypeMismatch {
428 expected: format!("[{type_name}]"),
429 actual: format!("{}[{i}]={}", "Array", describe_kind(element)),
430 requested: String::new(),
431 })?;
432 out.push(v);
433 }
434 Ok(out)
435}
436
437/// Shared u64 coercion used by [`SnapshotField::as_u64`].
438fn render_to_u64(v: &RenderedValue) -> SnapshotResult<u64> {
439 match v {
440 RenderedValue::Uint { value, .. } => Ok(*value),
441 RenderedValue::Int { value, .. } => {
442 if *value < 0 {
443 Err(SnapshotError::TypeMismatch {
444 expected: "u64".to_string(),
445 actual: "Int(negative)".to_string(),
446 requested: String::new(),
447 })
448 } else {
449 Ok(*value as u64)
450 }
451 }
452 RenderedValue::Bool { value } => Ok(u64::from(*value)),
453 RenderedValue::Char { value } => Ok(u64::from(*value)),
454 RenderedValue::Enum {
455 value, is_signed, ..
456 } => {
457 // Mirror RenderedValue::as_u64's enum dispatch so the two
458 // surfaces agree on signedness handling: unsigned enums
459 // reinterpret the bit pattern (the renderer stores i64,
460 // so an unsigned u64 wire value with the high bit set
461 // arrives here as a negative i64 — `as u64` recovers the
462 // bits); signed enums reject negative variants as
463 // out-of-range. Without this branch, an unsigned 64-bit
464 // enum at u64::MAX (stored as i64=-1) returned
465 // TypeMismatch from this path while RenderedValue::as_u64
466 // returned Some(u64::MAX) — same value, two surfaces
467 // disagreed.
468 if *is_signed && *value < 0 {
469 Err(SnapshotError::TypeMismatch {
470 expected: "u64".to_string(),
471 actual: "Enum(signed-negative)".to_string(),
472 requested: String::new(),
473 })
474 } else {
475 Ok(*value as u64)
476 }
477 }
478 RenderedValue::Ptr { value, .. } => Ok(*value),
479 other => Err(SnapshotError::TypeMismatch {
480 expected: "u64".to_string(),
481 actual: describe_kind(other),
482 requested: String::new(),
483 }),
484 }
485}
486
487/// Shared i64 coercion used by [`SnapshotField::as_i64`].
488fn render_to_i64(v: &RenderedValue) -> SnapshotResult<i64> {
489 match v {
490 RenderedValue::Int { value, .. } => Ok(*value),
491 RenderedValue::Uint { value, .. } => {
492 if *value > i64::MAX as u64 {
493 Err(SnapshotError::TypeMismatch {
494 expected: "i64".to_string(),
495 actual: "Uint(>i64::MAX)".to_string(),
496 requested: String::new(),
497 })
498 } else {
499 Ok(*value as i64)
500 }
501 }
502 RenderedValue::Bool { value } => Ok(i64::from(*value)),
503 RenderedValue::Char { value } => Ok(i64::from(*value)),
504 RenderedValue::Enum { value, .. } => Ok(*value),
505 other => Err(SnapshotError::TypeMismatch {
506 expected: "i64".to_string(),
507 actual: describe_kind(other),
508 requested: String::new(),
509 }),
510 }
511}
512
513#[cfg(test)]
514mod tests_coercion {
515 //! Host-pure coverage for the [`SnapshotField`] coercion paths
516 //! ([`render_to_u64`] / [`render_to_i64`], the inline `as_bool` /
517 //! `as_f64` / `as_str` matches, the dotted-path walk, and the
518 //! typed-array terminals). These decide whether a BTF-snapshot
519 //! read SUCCEEDS or surfaces a typed [`SnapshotError`]; a swapped
520 //! arm silently coerces a value the accessor should reject.
521 use super::*;
522
523 fn ptr(value: u64) -> RenderedValue {
524 RenderedValue::Ptr {
525 value,
526 deref: None,
527 deref_skipped_reason: None,
528 cast_annotation: None,
529 }
530 }
531
532 /// REGRESSION: the scalar `SnapshotField::as_bool` and the array
533 /// `as_bool_array` (which coerces each element via
534 /// `RenderedValue::as_bool`) must AGREE on a pointer. Before the
535 /// `Ptr` arm landed on `RenderedValue::as_bool`, the scalar
536 /// accepted a pointer (non-null test) while the array path errored
537 /// on the first pointer element — `field.as_bool()` succeeded and
538 /// `field.as_bool_array()` failed on the same shape.
539 #[test]
540 fn as_bool_scalar_and_array_agree_on_pointer() {
541 let non_null = ptr(0x1000);
542 let null = ptr(0);
543 // Scalar: pointer coerces as a non-null test.
544 assert!(SnapshotField::Value(&non_null).as_bool().unwrap());
545 assert!(!SnapshotField::Value(&null).as_bool().unwrap());
546 // Array: the same per-element coercion, no TypeMismatch.
547 let arr = RenderedValue::Array {
548 len: 2,
549 elements: vec![ptr(0x2000), ptr(0)],
550 };
551 assert_eq!(
552 SnapshotField::Value(&arr).as_bool_array().unwrap(),
553 vec![true, false],
554 );
555 }
556
557 /// `as_u64` accepts the pointer / char / bool scalar variants
558 /// (pointer as its numeric address, char as the raw byte, bool as
559 /// 0/1) — the wider integer-coercion set.
560 #[test]
561 fn as_u64_accepts_ptr_char_bool() {
562 assert_eq!(SnapshotField::Value(&ptr(0xdead)).as_u64().unwrap(), 0xdead);
563 let c = RenderedValue::Char { value: 65 };
564 assert_eq!(SnapshotField::Value(&c).as_u64().unwrap(), 65);
565 let b = RenderedValue::Bool { value: true };
566 assert_eq!(SnapshotField::Value(&b).as_u64().unwrap(), 1);
567 }
568
569 /// `render_to_u64`'s enum arm mirrors `RenderedValue::as_u64`:
570 /// an unsigned 64-bit enum at `u64::MAX` (stored as `i64 = -1`)
571 /// reinterprets the bit pattern, while a signed-negative enum is
572 /// rejected as out-of-range.
573 #[test]
574 fn as_u64_enum_signedness() {
575 let unsigned_max = RenderedValue::Enum {
576 bits: 64,
577 value: -1,
578 variant: None,
579 is_signed: false,
580 };
581 assert_eq!(
582 SnapshotField::Value(&unsigned_max).as_u64().unwrap(),
583 u64::MAX,
584 );
585 let signed_neg = RenderedValue::Enum {
586 bits: 32,
587 value: -5,
588 variant: None,
589 is_signed: true,
590 };
591 assert!(matches!(
592 SnapshotField::Value(&signed_neg).as_u64(),
593 Err(SnapshotError::TypeMismatch { .. })
594 ));
595 }
596
597 /// `as_u64` rejects a negative `Int`; `as_i64` rejects a `Uint`
598 /// above `i64::MAX` — the sign-loss boundaries.
599 #[test]
600 fn integer_sign_boundaries_error() {
601 let neg = RenderedValue::Int {
602 bits: 32,
603 value: -1,
604 };
605 assert!(matches!(
606 SnapshotField::Value(&neg).as_u64(),
607 Err(SnapshotError::TypeMismatch { .. })
608 ));
609 let big = RenderedValue::Uint {
610 bits: 64,
611 value: u64::MAX,
612 };
613 assert!(matches!(
614 SnapshotField::Value(&big).as_i64(),
615 Err(SnapshotError::TypeMismatch { .. })
616 ));
617 }
618
619 /// `as_f64` is narrower than `as_u64`: it accepts Float / Int /
620 /// Uint / Enum but rejects Char, Bool, and Ptr (a float of a
621 /// pointer or a char is not meaningful).
622 #[test]
623 fn as_f64_rejects_char_bool_ptr() {
624 let f = RenderedValue::Float {
625 bits: 64,
626 value: 1.5,
627 };
628 assert_eq!(SnapshotField::Value(&f).as_f64().unwrap(), 1.5);
629 for v in [
630 RenderedValue::Char { value: 1 },
631 RenderedValue::Bool { value: true },
632 ptr(0x10),
633 ] {
634 assert!(
635 matches!(
636 SnapshotField::Value(&v).as_f64(),
637 Err(SnapshotError::TypeMismatch { .. })
638 ),
639 "as_f64 must reject {v:?}",
640 );
641 }
642 }
643
644 /// `as_str` reads an enum's resolved variant name and rejects
645 /// non-enum / nameless-enum / percpu-key shapes.
646 #[test]
647 fn as_str_reads_enum_variant_else_errors() {
648 let named = RenderedValue::Enum {
649 bits: 32,
650 value: 2,
651 variant: Some("SCX_OPS_ENABLED".to_string()),
652 is_signed: false,
653 };
654 assert_eq!(
655 SnapshotField::Value(&named).as_str().unwrap(),
656 "SCX_OPS_ENABLED"
657 );
658 let nameless = RenderedValue::Enum {
659 bits: 32,
660 value: 2,
661 variant: None,
662 is_signed: false,
663 };
664 assert!(SnapshotField::Value(&nameless).as_str().is_err());
665 assert!(SnapshotField::PercpuKey { key: 3 }.as_str().is_err());
666 }
667
668 /// The dotted-path walk peels `Ptr{deref: Some}` to the pointed-at
669 /// struct, resolves a member, and surfaces structured errors for a
670 /// non-struct cursor and an empty path component.
671 #[test]
672 fn walk_dotted_path_peels_pointer_and_reports_errors() {
673 let inner = RenderedValue::Struct {
674 type_name: Some("scx_bss".to_string()),
675 members: vec![RenderedMember {
676 name: "stall".to_string(),
677 value: RenderedValue::Uint { bits: 8, value: 1 },
678 }],
679 };
680 let through_ptr = RenderedValue::Ptr {
681 value: 0xffff_0000,
682 deref: Some(Box::new(inner)),
683 deref_skipped_reason: None,
684 cast_annotation: None,
685 };
686 assert_eq!(
687 SnapshotField::Value(&through_ptr)
688 .get("stall")
689 .as_u64()
690 .unwrap(),
691 1,
692 );
693 // Walking into a scalar surfaces NotAStruct.
694 let scalar = RenderedValue::Uint { bits: 8, value: 5 };
695 assert!(matches!(
696 SnapshotField::Value(&scalar).get("x"),
697 SnapshotField::Missing(SnapshotError::NotAStruct { .. })
698 ));
699 // An empty component (`a..b`) surfaces EmptyPathComponent.
700 let s = RenderedValue::Struct {
701 type_name: None,
702 members: vec![RenderedMember {
703 name: "a".to_string(),
704 value: RenderedValue::Uint { bits: 8, value: 0 },
705 }],
706 };
707 assert!(matches!(
708 SnapshotField::Value(&s).get("a..b"),
709 SnapshotField::Missing(SnapshotError::EmptyPathComponent { .. })
710 ));
711 }
712
713 /// REGRESSION: `peel_pointer`'s runaway-depth guard
714 /// (`if steps > 16 { break }`) stops chasing a pointer chain
715 /// deeper than the cap. A chain of 18 nested `Ptr{deref: Some}`
716 /// layers wrapping a final struct never reaches the struct: the
717 /// loop peels 17 layers then breaks with the cursor still a
718 /// `Ptr`, so the dotted-path walk lands on the Ptr (NotAStruct)
719 /// rather than the deeply-nested member. This is the load-bearing
720 /// defense against a malformed / cyclic rendered tree spinning
721 /// forever during host-side traversal of an attacker-influenced
722 /// BTF render. Without the cap, the walk would peel all 18 and
723 /// resolve `x`.
724 #[test]
725 fn peel_pointer_breaks_at_sixteen_step_cap_on_pointer_cycle() {
726 // Innermost: a struct carrying the target member.
727 let mut deep = RenderedValue::Struct {
728 type_name: Some("scx_bss".to_string()),
729 members: vec![RenderedMember {
730 name: "x".to_string(),
731 value: RenderedValue::Uint { bits: 8, value: 1 },
732 }],
733 };
734 // Wrap 18 times in Ptr{deref: Some(prev)} — past the 16-step cap.
735 for _ in 0..18 {
736 deep = RenderedValue::Ptr {
737 value: 0xff,
738 deref: Some(Box::new(deep)),
739 deref_skipped_reason: None,
740 cast_annotation: None,
741 };
742 }
743 let f = SnapshotField::Value(&deep).get("x");
744 // Peeling stopped at the cap with the cursor still a Ptr, so
745 // the struct member is unreachable: NotAStruct, not Value.
746 assert!(
747 matches!(f, SnapshotField::Missing(SnapshotError::NotAStruct { .. })),
748 "16-step cap must leave the cursor on a Ptr (NotAStruct), got {f:?}"
749 );
750 }
751
752 /// `get` on a percpu-key field surfaces a TypeMismatch naming the
753 /// percpu-key shape (no struct to walk into); `is_present`/`raw`/
754 /// `error` reflect each variant.
755 #[test]
756 fn percpu_key_navigation_and_view_helpers() {
757 let pk = SnapshotField::PercpuKey { key: 7 };
758 assert!(matches!(
759 pk.get("x"),
760 SnapshotField::Missing(SnapshotError::TypeMismatch { .. })
761 ));
762 assert!(pk.is_present());
763 assert!(pk.raw().is_none());
764 assert!(pk.error().is_none());
765 assert_eq!(pk.as_u64().unwrap(), 7);
766
767 let missing = SnapshotField::Missing(SnapshotError::EmptyPathComponent {
768 requested: "x".to_string(),
769 });
770 assert!(!missing.is_present());
771 assert!(missing.error().is_some());
772 assert!(matches!(
773 missing.as_u64(),
774 Err(SnapshotError::EmptyPathComponent { .. })
775 ));
776 }
777
778 /// `iter_members` peels `Truncated{partial: Array}` and yields the
779 /// preserved elements; a non-array (after peeling) yields nothing.
780 #[test]
781 fn iter_members_peels_truncated_array_else_empty() {
782 let truncated = RenderedValue::Truncated {
783 needed: 32,
784 had: 16,
785 partial: Box::new(RenderedValue::Array {
786 len: 4,
787 elements: vec![
788 RenderedValue::Uint { bits: 8, value: 10 },
789 RenderedValue::Uint { bits: 8, value: 20 },
790 ],
791 }),
792 };
793 let got: Vec<u64> = SnapshotField::Value(&truncated)
794 .iter_members()
795 .map(|el| el.as_u64().unwrap())
796 .collect();
797 assert_eq!(got, vec![10, 20]);
798 // A scalar yields no elements.
799 let scalar = RenderedValue::Uint { bits: 8, value: 1 };
800 assert_eq!(SnapshotField::Value(&scalar).iter_members().count(), 0);
801 }
802}