Skip to main content

padlock_source/frontends/
rust.rs

1// padlock-source/src/frontends/rust.rs
2//
3// Extracts struct layouts from Rust source using syn + the Visit API.
4// Sizes are approximated from type names using the target arch config.
5// Only repr(C) / repr(packed) / plain structs are handled; generics are opaque.
6
7use padlock_core::arch::ArchConfig;
8use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
9use quote::ToTokens;
10use syn::{Fields, ItemEnum, ItemStruct, Type, visit::Visit};
11
12// ── attribute guard extraction ────────────────────────────────────────────────
13
14/// Extract a lock guard name from field attributes.
15///
16/// Recognised forms:
17/// - `#[lock_protected_by = "mu"]`
18/// - `#[protected_by = "mu"]`
19/// - `#[guarded_by("mu")]` or `#[guarded_by(mu)]`
20/// - `#[pt_guarded_by("mu")]` or `#[pt_guarded_by(mu)]` (pointer variant)
21pub fn extract_guard_from_attrs(attrs: &[syn::Attribute]) -> Option<String> {
22    for attr in attrs {
23        let path = attr.path();
24        // Name-value form: #[lock_protected_by = "mu"] / #[protected_by = "mu"]
25        if (path.is_ident("lock_protected_by") || path.is_ident("protected_by"))
26            && let syn::Meta::NameValue(nv) = &attr.meta
27            && let syn::Expr::Lit(syn::ExprLit {
28                lit: syn::Lit::Str(s),
29                ..
30            }) = &nv.value
31        {
32            return Some(s.value());
33        }
34        // List form: #[guarded_by("mu")] / #[guarded_by(mu)] / #[pt_guarded_by(...)]
35        if path.is_ident("guarded_by") || path.is_ident("pt_guarded_by") {
36            // Try string literal first
37            if let Ok(s) = attr.parse_args::<syn::LitStr>() {
38                return Some(s.value());
39            }
40            // Fall back to bare identifier
41            if let Ok(id) = attr.parse_args::<syn::Ident>() {
42                return Some(id.to_string());
43            }
44        }
45    }
46    None
47}
48
49// ── type resolution ───────────────────────────────────────────────────────────
50
51fn rust_type_size_align(ty: &Type, arch: &'static ArchConfig) -> (usize, usize, TypeInfo) {
52    match ty {
53        Type::Path(tp) => {
54            let seg = tp.path.segments.last();
55            let name = seg.map(|s| s.ident.to_string()).unwrap_or_default();
56
57            // Transparent newtypes: size and alignment equal the inner T.
58            // Cell<T>, MaybeUninit<T>, UnsafeCell<T>, Wrapping<T>, Saturating<T>,
59            // ManuallyDrop<T> all have `#[repr(transparent)]` and add no overhead.
60            // Recurse into the first generic type argument when present.
61            if matches!(
62                name.as_str(),
63                "Cell" | "MaybeUninit" | "UnsafeCell" | "Wrapping" | "Saturating" | "ManuallyDrop"
64            ) && let Some(inner_ty) = seg.and_then(|s| {
65                if let syn::PathArguments::AngleBracketed(ref ab) = s.arguments {
66                    ab.args.iter().find_map(|a| {
67                        if let syn::GenericArgument::Type(t) = a {
68                            Some(t)
69                        } else {
70                            None
71                        }
72                    })
73                } else {
74                    None
75                }
76            }) {
77                let (size, align, _) = rust_type_size_align(inner_ty, arch);
78                return (size, align, TypeInfo::Primitive { name, size, align });
79            }
80
81            // Option<T> niche optimisation: when T has a niche (a bit pattern
82            // that cannot occur in a valid T), the compiler encodes the None
83            // discriminant into that niche without adding a separate tag byte.
84            //
85            // Recognised niche-capable types (conservative subset):
86            //   NonZeroU8/NonZeroI8/… — niche at 0
87            //   &T / &mut T          — niche at null pointer
88            //   Box<T> / NonNull<T>  — niche at null pointer
89            //
90            // For all other Option<T> we fall through to pointer-size, which is
91            // a reasonable default (exact size requires full type inference).
92            if name == "Option"
93                && let Some(inner) = seg.and_then(|s| {
94                    if let syn::PathArguments::AngleBracketed(ref ab) = s.arguments {
95                        ab.args.iter().find_map(|a| {
96                            if let syn::GenericArgument::Type(t) = a {
97                                Some(t)
98                            } else {
99                                None
100                            }
101                        })
102                    } else {
103                        None
104                    }
105                })
106            {
107                if let Some((sz, al)) = option_niche_size(inner, arch) {
108                    return (
109                        sz,
110                        al,
111                        TypeInfo::Primitive {
112                            name,
113                            size: sz,
114                            align: al,
115                        },
116                    );
117                }
118                // No niche — conservative: inner size + 1-byte discriminant
119                // rounded up to inner alignment.
120                let (inner_size, inner_align, _) = rust_type_size_align(inner, arch);
121                let sz = if inner_size == 0 {
122                    1 // Option<()> / Option<ZST> = 1 byte
123                } else {
124                    (inner_size + 1).next_multiple_of(inner_align.max(1))
125                };
126                return (
127                    sz,
128                    inner_align.max(1),
129                    TypeInfo::Primitive {
130                        name,
131                        size: sz,
132                        align: inner_align.max(1),
133                    },
134                );
135            }
136
137            // Box<dyn Trait>, Arc<dyn Trait>, Rc<dyn Trait>, Weak<dyn Trait> are fat
138            // pointers: they hold both a data pointer and a vtable pointer (2 words).
139            let is_fat = matches!(name.as_str(), "Box" | "Arc" | "Rc" | "Weak")
140                && seg
141                    .map(|s| {
142                        if let syn::PathArguments::AngleBracketed(ref ab) = s.arguments {
143                            ab.args.iter().any(|a| {
144                                matches!(a, syn::GenericArgument::Type(Type::TraitObject(_)))
145                            })
146                        } else {
147                            false
148                        }
149                    })
150                    .unwrap_or(false);
151            let (size, align) = if is_fat {
152                (arch.pointer_size * 2, arch.pointer_size)
153            } else {
154                primitive_size_align(&name, arch)
155            };
156            (size, align, TypeInfo::Primitive { name, size, align })
157        }
158        Type::Ptr(p) => {
159            let s = arch.pointer_size;
160            // *const dyn Trait / *mut dyn Trait are fat pointers (data + vtable)
161            let is_fat = matches!(*p.elem, Type::TraitObject(_));
162            let sz = if is_fat { s * 2 } else { s };
163            (sz, s, TypeInfo::Pointer { size: sz, align: s })
164        }
165        Type::Reference(r) => {
166            let s = arch.pointer_size;
167            // &dyn Trait / &mut dyn Trait are fat pointers (data + vtable)
168            let is_fat = matches!(*r.elem, Type::TraitObject(_));
169            let sz = if is_fat { s * 2 } else { s };
170            (sz, s, TypeInfo::Pointer { size: sz, align: s })
171        }
172        Type::Array(arr) => {
173            let (elem_size, elem_align, elem_ty) = rust_type_size_align(&arr.elem, arch);
174            let count = array_len_from_expr(&arr.len);
175            let size = elem_size * count;
176            (
177                size,
178                elem_align,
179                TypeInfo::Array {
180                    element: Box::new(elem_ty),
181                    count,
182                    size,
183                    align: elem_align,
184                },
185            )
186        }
187        _ => {
188            let s = arch.pointer_size;
189            (
190                s,
191                s,
192                TypeInfo::Opaque {
193                    name: "(unknown)".into(),
194                    size: s,
195                    align: s,
196                },
197            )
198        }
199    }
200}
201
202/// Return `Some((size, align))` when `Option<inner>` can use a niche optimisation,
203/// meaning the Option occupies no more space than the inner type itself.
204///
205/// Recognised niche-capable types:
206/// - `NonZeroU8` / `NonZeroI8` … `NonZeroUsize` — niche at zero
207/// - `&T` / `&mut T` — niche at null pointer (reference is always non-null)
208/// - `Box<T>` / `NonNull<T>` / `Arc<T>` / `Rc<T>` — non-null pointer niche
209fn option_niche_size(inner: &Type, arch: &'static ArchConfig) -> Option<(usize, usize)> {
210    match inner {
211        Type::Path(tp) => {
212            let name = tp
213                .path
214                .segments
215                .last()
216                .map(|s| s.ident.to_string())
217                .unwrap_or_default();
218            match name.as_str() {
219                "NonZeroU8" | "NonZeroI8" => Some((1, 1)),
220                "NonZeroU16" | "NonZeroI16" => Some((2, 2)),
221                "NonZeroU32" | "NonZeroI32" => Some((4, 4)),
222                "NonZeroU64" | "NonZeroI64" => Some((8, 8)),
223                "NonZeroU128" | "NonZeroI128" => Some((16, 16)),
224                "NonZeroUsize" | "NonZeroIsize" => {
225                    let ps = arch.pointer_size;
226                    Some((ps, ps))
227                }
228                // Box<T>, NonNull<T>, Arc<T>, Rc<T> — non-null pointer niche
229                "Box" | "NonNull" | "Arc" | "Rc" => {
230                    let ps = arch.pointer_size;
231                    Some((ps, ps))
232                }
233                _ => None,
234            }
235        }
236        // &T and &mut T — references are always non-null → niche at null
237        Type::Reference(_) => {
238            let ps = arch.pointer_size;
239            Some((ps, ps))
240        }
241        _ => None,
242    }
243}
244
245fn primitive_size_align(name: &str, arch: &'static ArchConfig) -> (usize, usize) {
246    let ps = arch.pointer_size;
247    match name {
248        // ── language primitives ───────────────────────────────────────────────
249        "bool" | "u8" | "i8" => (1, 1),
250        "u16" | "i16" | "f16" => (2, 2),
251        "u32" | "i32" | "f32" => (4, 4),
252        "u64" | "i64" | "f64" => (8, 8),
253        "u128" | "i128" | "f128" => (16, 16),
254        "usize" | "isize" => (ps, ps),
255        "char" => (4, 4), // Rust char is a Unicode scalar (4 bytes)
256
257        // NonZero integer types — same size/align as the underlying integer.
258        // The niche optimisation means Option<NonZeroU8> == 1 byte, but the
259        // struct field itself is identical in size to the plain integer.
260        "NonZeroU8" | "NonZeroI8" => (1, 1),
261        "NonZeroU16" | "NonZeroI16" => (2, 2),
262        "NonZeroU32" | "NonZeroI32" => (4, 4),
263        "NonZeroU64" | "NonZeroI64" => (8, 8),
264        "NonZeroU128" | "NonZeroI128" => (16, 16),
265        "NonZeroUsize" | "NonZeroIsize" => (ps, ps),
266
267        // Wrapping<T>, Saturating<T> — transparent newtype over T.
268        // The generic arg has already been stripped, so we get the inner
269        // primitive name here; if the stripping didn't happen these fall
270        // through to pointer-size, which is acceptable.
271        "Wrapping" | "Saturating" => (ps, ps),
272
273        // MaybeUninit<T> and UnsafeCell<T> are transparent newtypes —
274        // same size as T. Without knowing T we approximate as pointer-size,
275        // which is correct for the common case of wrapping a pointer-sized value.
276        "MaybeUninit" | "UnsafeCell" => (ps, ps),
277
278        // ── std atomics ───────────────────────────────────────────────────────
279        "AtomicBool" | "AtomicU8" | "AtomicI8" => (1, 1),
280        "AtomicU16" | "AtomicI16" => (2, 2),
281        "AtomicU32" | "AtomicI32" => (4, 4),
282        "AtomicU64" | "AtomicI64" => (8, 8),
283        "AtomicUsize" | "AtomicIsize" | "AtomicPtr" => (ps, ps),
284
285        // ── heap-allocated collections: ptr + len + cap (3 words) ────────────
286        // Size is independent of the element type T (generic arg already stripped).
287        "Vec" | "String" | "OsString" | "CString" | "PathBuf" => (3 * ps, ps),
288        "VecDeque" | "LinkedList" | "BinaryHeap" => (3 * ps, ps),
289        "HashMap" | "HashSet" | "BTreeMap" | "BTreeSet" => (3 * ps, ps),
290
291        // ── single-pointer smart pointers ─────────────────────────────────────
292        // Cell<T> is handled as a transparent newtype in rust_type_size_align;
293        // this entry is a fallback for bare `Cell` without a type argument.
294        "Box" | "Rc" | "Arc" | "Weak" | "NonNull" | "Cell" => (ps, ps),
295
296        // ── interior-mutability / sync wrappers ───────────────────────────────
297        // Size depends on T but pointer-size is a reasonable approximation for
298        // display purposes; use binary analysis for precise results.
299        "RefCell" | "Mutex" | "RwLock" => (ps, ps),
300
301        // ── channels ─────────────────────────────────────────────────────────
302        "Sender" | "Receiver" | "SyncSender" => (ps, ps),
303
304        // ── zero-sized types ──────────────────────────────────────────────────
305        "PhantomData" | "PhantomPinned" => (0, 1),
306
307        // ── common fixed-size stdlib types ────────────────────────────────────
308        // Duration: u64 secs (8B) + u32 nanos (4B) → 12B + 4B trailing = 16B
309        "Duration" => (16, 8),
310        "Instant" | "SystemTime" => (16, 8),
311
312        // ── Pin<T> wraps T, pointer-size approximation ────────────────────────
313        "Pin" => (ps, ps),
314
315        // ── x86 SSE / AVX / AVX-512 SIMD types ───────────────────────────────
316        "__m64" => (8, 8),
317        "__m128" | "__m128d" | "__m128i" => (16, 16),
318        "__m256" | "__m256d" | "__m256i" => (32, 32),
319        "__m512" | "__m512d" | "__m512i" => (64, 64),
320
321        // ── Rust portable SIMD / packed_simd types ────────────────────────────
322        "f32x4" | "i32x4" | "u32x4" => (16, 16),
323        "f64x2" | "i64x2" | "u64x2" => (16, 16),
324        "f32x8" | "i32x8" | "u32x8" => (32, 32),
325        "f64x4" | "i64x4" | "u64x4" => (32, 32),
326        "f32x16" | "i32x16" | "u32x16" => (64, 64),
327
328        // ── unknown / third-party / generic type params (T, E, …) ────────────
329        _ => (ps, ps),
330    }
331}
332
333fn array_len_from_expr(expr: &syn::Expr) -> usize {
334    if let syn::Expr::Lit(syn::ExprLit {
335        lit: syn::Lit::Int(n),
336        ..
337    }) = expr
338    {
339        n.base10_parse::<usize>().unwrap_or(0)
340    } else {
341        0
342    }
343}
344
345// ── struct repr detection ─────────────────────────────────────────────────────
346
347fn is_packed(attrs: &[syn::Attribute]) -> bool {
348    attrs
349        .iter()
350        .any(|a| a.path().is_ident("repr") && a.to_token_stream().to_string().contains("packed"))
351}
352
353/// Returns `true` when the struct has no repr annotation that fixes the layout
354/// (`repr(C)`, `repr(packed)`, `repr(transparent)`).  A struct with only
355/// `repr(align(N))` still has an unspecified field order — the compiler may
356/// reorder fields freely — so it counts as `repr(Rust)` for warning purposes.
357fn is_repr_rust(attrs: &[syn::Attribute]) -> bool {
358    !attrs.iter().any(|a| {
359        if !a.path().is_ident("repr") {
360            return false;
361        }
362        let ts = a.to_token_stream().to_string();
363        ts.contains('C') || ts.contains("packed") || ts.contains("transparent")
364    })
365}
366
367/// Extract the alignment from `#[repr(align(N))]`. Returns `None` if not present.
368fn repr_align(attrs: &[syn::Attribute]) -> Option<usize> {
369    for attr in attrs {
370        if !attr.path().is_ident("repr") {
371            continue;
372        }
373        let ts = attr.to_token_stream().to_string();
374        // Look for `align ( N )` in the token stream string.
375        // The tokeniser adds spaces: "repr (align (64))" etc.
376        if let Some(start) = ts.find("align") {
377            let after = ts[start..].trim_start_matches("align").trim_start();
378            if after.starts_with('(') {
379                let inner = after.trim_start_matches('(');
380                let num_str: String = inner.chars().take_while(|c| c.is_ascii_digit()).collect();
381                if let Ok(n) = num_str.parse::<usize>()
382                    && n > 0
383                    && n.is_power_of_two()
384                {
385                    return Some(n);
386                }
387            }
388        }
389    }
390    None
391}
392
393fn simulate_rust_layout(
394    name: String,
395    fields: &[(String, Type)],
396    packed: bool,
397    forced_align: Option<usize>,
398    arch: &'static ArchConfig,
399) -> StructLayout {
400    let mut offset = 0usize;
401    let mut struct_align = 1usize;
402    let mut out_fields: Vec<Field> = Vec::new();
403
404    for (fname, ty) in fields {
405        let (size, align, type_info) = rust_type_size_align(ty, arch);
406        let effective_align = if packed { 1 } else { align };
407
408        if effective_align > 0 {
409            offset = offset.next_multiple_of(effective_align);
410        }
411        struct_align = struct_align.max(effective_align);
412
413        out_fields.push(Field {
414            name: fname.clone(),
415            ty: type_info,
416            offset,
417            size,
418            align: effective_align,
419            source_file: None,
420            source_line: None,
421            access: AccessPattern::Unknown,
422        });
423        offset += size;
424    }
425
426    // Apply repr(align(N)): raise minimum alignment and add trailing padding.
427    if let Some(fa) = forced_align
428        && fa > struct_align
429    {
430        struct_align = fa;
431    }
432
433    if !packed && struct_align > 0 {
434        offset = offset.next_multiple_of(struct_align);
435    }
436
437    StructLayout {
438        name,
439        total_size: offset,
440        align: struct_align,
441        fields: out_fields,
442        source_file: None,
443        source_line: None,
444        arch,
445        is_packed: packed,
446        is_union: false,
447        is_repr_rust: false, // callers override this after construction
448        suppressed_findings: Vec::new(), // callers may override after construction
449        uncertain_fields: Vec::new(),
450    }
451}
452
453// ── visitor ───────────────────────────────────────────────────────────────────
454
455struct StructVisitor<'src> {
456    arch: &'static ArchConfig,
457    layouts: Vec<StructLayout>,
458    source: &'src str,
459}
460
461impl<'ast, 'src> Visit<'ast> for StructVisitor<'src> {
462    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
463        syn::visit::visit_item_struct(self, node); // recurse into nested items
464
465        // Generic structs (e.g. `struct Foo<T>`) cannot be accurately laid out
466        // without knowing the concrete type arguments. Skip them rather than
467        // producing wrong field sizes for the type parameters.
468        if !node.generics.params.is_empty() {
469            eprintln!(
470                "padlock: note: skipping '{}' — generic struct \
471                 (layout depends on type arguments; use binary analysis for accurate results)",
472                node.ident
473            );
474            return;
475        }
476
477        let name = node.ident.to_string();
478        let packed = is_packed(&node.attrs);
479        let forced_align = repr_align(&node.attrs);
480
481        // Collect (field_name, type, optional_guard, source_line)
482        let fields: Vec<(String, Type, Option<String>, u32)> = match &node.fields {
483            Fields::Named(nf) => nf
484                .named
485                .iter()
486                .map(|f| {
487                    let fname = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
488                    let guard = extract_guard_from_attrs(&f.attrs);
489                    let line = f
490                        .ident
491                        .as_ref()
492                        .map(|i| i.span().start().line as u32)
493                        .unwrap_or(0);
494                    (fname, f.ty.clone(), guard, line)
495                })
496                .collect(),
497            Fields::Unnamed(uf) => uf
498                .unnamed
499                .iter()
500                .enumerate()
501                .map(|(i, f)| {
502                    let guard = extract_guard_from_attrs(&f.attrs);
503                    // Unnamed fields don't have an ident span; use 0 as a sentinel.
504                    (format!("_{i}"), f.ty.clone(), guard, 0u32)
505                })
506                .collect(),
507            Fields::Unit => vec![],
508        };
509
510        let name_ty: Vec<(String, Type)> = fields
511            .iter()
512            .map(|(n, t, _, _)| (n.clone(), t.clone()))
513            .collect();
514        let mut layout = simulate_rust_layout(name, &name_ty, packed, forced_align, self.arch);
515        let struct_line = node.ident.span().start().line as u32;
516        layout.source_line = Some(struct_line);
517        layout.is_repr_rust = is_repr_rust(&node.attrs);
518        layout.suppressed_findings =
519            super::suppress::suppressed_from_source_line(self.source, struct_line);
520
521        // Apply explicit guard annotations and field source lines.
522        for (i, (_, _, guard, field_line)) in fields.iter().enumerate() {
523            if *field_line > 0 {
524                layout.fields[i].source_line = Some(*field_line);
525            }
526            if let Some(g) = guard {
527                layout.fields[i].access = AccessPattern::Concurrent {
528                    guard: Some(g.clone()),
529                    is_atomic: false,
530                    is_annotated: true,
531                };
532            }
533        }
534
535        self.layouts.push(layout);
536    }
537
538    fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
539        syn::visit::visit_item_enum(self, node);
540
541        // Skip generic enums (layout depends on unknown type arguments)
542        if !node.generics.params.is_empty() {
543            eprintln!(
544                "padlock: note: skipping '{}' — generic enum \
545                 (layout depends on type arguments; use binary analysis for accurate results)",
546                node.ident
547            );
548            return;
549        }
550
551        let name = node.ident.to_string();
552        let n_variants = node.variants.len();
553        if n_variants == 0 {
554            return;
555        }
556
557        // Discriminant size: smallest integer that fits the variant count.
558        // Rust defaults to isize but uses the minimal repr in practice.
559        let disc_size: usize = if n_variants <= 256 {
560            1
561        } else if n_variants <= 65536 {
562            2
563        } else {
564            4
565        };
566
567        // Check if all variants are unit (C-like enum, no payload)
568        let all_unit = node
569            .variants
570            .iter()
571            .all(|v| matches!(v.fields, Fields::Unit));
572
573        if all_unit {
574            // Pure discriminant — no payload storage
575            let enum_line = node.ident.span().start().line as u32;
576            let layout = StructLayout {
577                name,
578                total_size: disc_size,
579                align: disc_size,
580                fields: vec![Field {
581                    name: "__discriminant".to_string(),
582                    ty: TypeInfo::Primitive {
583                        name: format!("u{}", disc_size * 8),
584                        size: disc_size,
585                        align: disc_size,
586                    },
587                    offset: 0,
588                    size: disc_size,
589                    align: disc_size,
590                    source_file: None,
591                    source_line: None,
592                    access: AccessPattern::Unknown,
593                }],
594                source_file: None,
595                source_line: Some(enum_line),
596                arch: self.arch,
597                is_packed: false,
598                is_union: false,
599                is_repr_rust: is_repr_rust(&node.attrs),
600                suppressed_findings: super::suppress::suppressed_from_source_line(
601                    self.source,
602                    enum_line,
603                ),
604                uncertain_fields: Vec::new(),
605            };
606            self.layouts.push(layout);
607            return;
608        }
609
610        // Data enum: find the maximum variant payload size and alignment.
611        let mut max_payload_size = 0usize;
612        let mut max_payload_align = 1usize;
613
614        for variant in &node.variants {
615            let var_fields: Vec<(String, Type)> = match &variant.fields {
616                Fields::Named(nf) => nf
617                    .named
618                    .iter()
619                    .map(|f| {
620                        let n = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
621                        (n, f.ty.clone())
622                    })
623                    .collect(),
624                Fields::Unnamed(uf) => uf
625                    .unnamed
626                    .iter()
627                    .enumerate()
628                    .map(|(i, f)| (format!("_{i}"), f.ty.clone()))
629                    .collect(),
630                Fields::Unit => vec![],
631            };
632
633            if !var_fields.is_empty() {
634                let var_layout =
635                    simulate_rust_layout(String::new(), &var_fields, false, None, self.arch);
636                if var_layout.total_size > max_payload_size {
637                    max_payload_size = var_layout.total_size;
638                }
639                max_payload_align = max_payload_align.max(var_layout.align);
640            }
641        }
642
643        // Conservative model: payload first at offset 0, discriminant immediately after.
644        // Rust's actual layout is compiler-controlled (niche optimisation etc.);
645        // this model gives a safe upper-bound for padding analysis.
646        let payload_align = max_payload_align.max(1);
647        let disc_offset = max_payload_size;
648        let total_before_pad = disc_offset + disc_size;
649        let total_align = payload_align.max(disc_size);
650        let total_size = total_before_pad.next_multiple_of(total_align);
651
652        let mut fields: Vec<Field> = Vec::new();
653        if max_payload_size > 0 {
654            fields.push(Field {
655                name: "__payload".to_string(),
656                ty: TypeInfo::Opaque {
657                    name: format!("largest_variant_payload ({}B)", max_payload_size),
658                    size: max_payload_size,
659                    align: payload_align,
660                },
661                offset: 0,
662                size: max_payload_size,
663                align: payload_align,
664                source_file: None,
665                source_line: None,
666                access: AccessPattern::Unknown,
667            });
668        }
669        fields.push(Field {
670            name: "__discriminant".to_string(),
671            ty: TypeInfo::Primitive {
672                name: format!("u{}", disc_size * 8),
673                size: disc_size,
674                align: disc_size,
675            },
676            offset: disc_offset,
677            size: disc_size,
678            align: disc_size,
679            source_file: None,
680            source_line: None,
681            access: AccessPattern::Unknown,
682        });
683
684        let enum_line = node.ident.span().start().line as u32;
685        self.layouts.push(StructLayout {
686            name,
687            total_size,
688            align: total_align,
689            fields,
690            source_file: None,
691            source_line: Some(enum_line),
692            arch: self.arch,
693            is_packed: false,
694            is_union: false,
695            is_repr_rust: is_repr_rust(&node.attrs),
696            suppressed_findings: super::suppress::suppressed_from_source_line(
697                self.source,
698                enum_line,
699            ),
700            uncertain_fields: Vec::new(),
701        });
702    }
703}
704
705// ── public API ────────────────────────────────────────────────────────────────
706
707pub fn parse_rust(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
708    let file: syn::File = syn::parse_str(source)?;
709    let mut visitor = StructVisitor {
710        arch,
711        layouts: Vec::new(),
712        source,
713    };
714    visitor.visit_file(&file);
715    Ok(visitor.layouts)
716}
717
718// ── tests ─────────────────────────────────────────────────────────────────────
719
720#[cfg(test)]
721mod tests {
722    use super::*;
723    use padlock_core::arch::X86_64_SYSV;
724
725    #[test]
726    fn parse_simple_struct() {
727        let src = "struct Foo { a: u8, b: u64, c: u32 }";
728        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
729        assert_eq!(layouts.len(), 1);
730        let l = &layouts[0];
731        assert_eq!(l.name, "Foo");
732        assert_eq!(l.fields.len(), 3);
733        assert_eq!(l.fields[0].size, 1); // u8
734        assert_eq!(l.fields[1].size, 8); // u64
735        assert_eq!(l.fields[2].size, 4); // u32
736    }
737
738    #[test]
739    fn layout_includes_padding() {
740        // u8 then u64: 7 bytes padding inserted
741        let src = "struct T { a: u8, b: u64 }";
742        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
743        let l = &layouts[0];
744        assert_eq!(l.fields[0].offset, 0);
745        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
746        assert_eq!(l.total_size, 16);
747        let gaps = padlock_core::ir::find_padding(l);
748        assert_eq!(gaps[0].bytes, 7);
749    }
750
751    #[test]
752    fn multiple_structs_parsed() {
753        let src = "struct A { x: u32 } struct B { y: u64 }";
754        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
755        assert_eq!(layouts.len(), 2);
756    }
757
758    #[test]
759    fn packed_struct_no_padding() {
760        let src = "#[repr(packed)] struct P { a: u8, b: u64 }";
761        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
762        let l = &layouts[0];
763        assert!(l.is_packed);
764        assert_eq!(l.fields[1].offset, 1); // no padding, b immediately after a
765        let gaps = padlock_core::ir::find_padding(l);
766        assert!(gaps.is_empty());
767    }
768
769    #[test]
770    fn pointer_field_uses_arch_size() {
771        let src = "struct S { p: *const u8 }";
772        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
773        assert_eq!(layouts[0].fields[0].size, 8); // 64-bit pointer
774    }
775
776    // ── attribute guard extraction ─────────────────────────────────────────────
777
778    #[test]
779    fn lock_protected_by_attr_sets_guard() {
780        let src = r#"
781struct Cache {
782    #[lock_protected_by = "mu"]
783    readers: u64,
784    mu: u64,
785}
786"#;
787        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
788        let readers = &layouts[0].fields[0];
789        assert_eq!(readers.name, "readers");
790        if let AccessPattern::Concurrent { guard, .. } = &readers.access {
791            assert_eq!(guard.as_deref(), Some("mu"));
792        } else {
793            panic!("expected Concurrent, got {:?}", readers.access);
794        }
795    }
796
797    #[test]
798    fn guarded_by_string_attr_sets_guard() {
799        let src = r#"
800struct S {
801    #[guarded_by("lock")]
802    value: u32,
803}
804"#;
805        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
806        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
807            assert_eq!(guard.as_deref(), Some("lock"));
808        } else {
809            panic!("expected Concurrent");
810        }
811    }
812
813    #[test]
814    fn guarded_by_ident_attr_sets_guard() {
815        let src = r#"
816struct S {
817    #[guarded_by(mu)]
818    count: u64,
819}
820"#;
821        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
822        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
823            assert_eq!(guard.as_deref(), Some("mu"));
824        } else {
825            panic!("expected Concurrent");
826        }
827    }
828
829    #[test]
830    fn protected_by_attr_sets_guard() {
831        let src = r#"
832struct S {
833    #[protected_by = "lock_a"]
834    x: u64,
835}
836"#;
837        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
838        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
839            assert_eq!(guard.as_deref(), Some("lock_a"));
840        } else {
841            panic!("expected Concurrent");
842        }
843    }
844
845    #[test]
846    fn different_guards_on_same_cache_line_is_false_sharing() {
847        // readers and writers are at offsets 0 and 8 — same cache line (line 0).
848        // They have different explicit guards → confirmed false sharing.
849        let src = r#"
850struct HotPath {
851    #[lock_protected_by = "mu_a"]
852    readers: u64,
853    #[lock_protected_by = "mu_b"]
854    writers: u64,
855}
856"#;
857        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
858        assert!(padlock_core::analysis::false_sharing::has_false_sharing(
859            &layouts[0]
860        ));
861    }
862
863    #[test]
864    fn same_guard_on_same_cache_line_is_not_false_sharing() {
865        let src = r#"
866struct Safe {
867    #[lock_protected_by = "mu"]
868    a: u64,
869    #[lock_protected_by = "mu"]
870    b: u64,
871}
872"#;
873        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
874        assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
875            &layouts[0]
876        ));
877    }
878
879    #[test]
880    fn unannotated_field_stays_unknown() {
881        let src = "struct S { x: u64 }";
882        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
883        assert!(matches!(
884            layouts[0].fields[0].access,
885            AccessPattern::Unknown
886        ));
887    }
888
889    // ── stdlib type sizes ─────────────────────────────────────────────────────
890
891    #[test]
892    fn vec_field_has_three_pointer_size() {
893        // Vec<T> is always ptr + len + cap regardless of T
894        let src = "struct S { items: Vec<u64> }";
895        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
896        assert_eq!(layouts[0].fields[0].size, 24); // 3 × 8 on x86-64
897    }
898
899    #[test]
900    fn string_field_has_three_pointer_size() {
901        let src = "struct S { name: String }";
902        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
903        assert_eq!(layouts[0].fields[0].size, 24);
904    }
905
906    #[test]
907    fn box_field_has_pointer_size() {
908        let src = "struct S { inner: Box<u64> }";
909        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
910        assert_eq!(layouts[0].fields[0].size, 8);
911    }
912
913    #[test]
914    fn arc_field_has_pointer_size() {
915        let src = "struct S { shared: Arc<Vec<u8>> }";
916        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
917        assert_eq!(layouts[0].fields[0].size, 8);
918    }
919
920    #[test]
921    fn phantom_data_is_zero_sized() {
922        let src = "struct S { a: u64, _marker: PhantomData<u8> }";
923        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
924        let marker = layouts[0]
925            .fields
926            .iter()
927            .find(|f| f.name == "_marker")
928            .unwrap();
929        assert_eq!(marker.size, 0);
930    }
931
932    #[test]
933    fn duration_field_is_16_bytes() {
934        let src = "struct S { timeout: Duration }";
935        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
936        assert_eq!(layouts[0].fields[0].size, 16);
937    }
938
939    #[test]
940    fn atomic_u64_has_correct_size() {
941        let src = "struct S { counter: AtomicU64 }";
942        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
943        assert_eq!(layouts[0].fields[0].size, 8);
944    }
945
946    #[test]
947    fn atomic_bool_has_correct_size() {
948        let src = "struct S { flag: AtomicBool }";
949        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
950        assert_eq!(layouts[0].fields[0].size, 1);
951    }
952
953    // ── generic struct skipping ───────────────────────────────────────────────
954
955    #[test]
956    fn generic_struct_is_skipped() {
957        // Cannot accurately lay out struct Foo<T> without knowing T.
958        let src = "struct Wrapper<T> { value: T, count: usize }";
959        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
960        assert!(
961            layouts.is_empty(),
962            "generic structs should be skipped; got {:?}",
963            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
964        );
965    }
966
967    #[test]
968    fn generic_struct_with_multiple_params_is_skipped() {
969        let src = "struct Pair<A, B> { first: A, second: B }";
970        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
971        assert!(layouts.is_empty());
972    }
973
974    #[test]
975    fn non_generic_struct_still_parsed_when_generic_sibling_exists() {
976        let src = r#"
977struct Generic<T> { value: T }
978struct Concrete { a: u32, b: u64 }
979"#;
980        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
981        assert_eq!(layouts.len(), 1);
982        assert_eq!(layouts[0].name, "Concrete");
983    }
984
985    // ── enum data variant support ─────────────────────────────────────────────
986
987    #[test]
988    fn unit_enum_is_just_discriminant() {
989        let src = "enum Color { Red, Green, Blue }";
990        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
991        assert_eq!(layouts.len(), 1);
992        let l = &layouts[0];
993        assert_eq!(l.name, "Color");
994        assert_eq!(l.total_size, 1); // 3 variants → u8 discriminant
995        assert_eq!(l.fields.len(), 1);
996        assert_eq!(l.fields[0].name, "__discriminant");
997    }
998
999    #[test]
1000    fn unit_enum_with_many_variants_uses_u16_discriminant() {
1001        // Build an enum with 300 variants (> 256)
1002        let variants: String = (0..300)
1003            .map(|i| format!("V{i}"))
1004            .collect::<Vec<_>>()
1005            .join(", ");
1006        let src = format!("enum Big {{ {variants} }}");
1007        let layouts = parse_rust(&src, &X86_64_SYSV).unwrap();
1008        let l = &layouts[0];
1009        assert_eq!(l.total_size, 2); // needs u16
1010        assert_eq!(l.fields[0].size, 2);
1011    }
1012
1013    #[test]
1014    fn data_enum_total_size_covers_largest_variant() {
1015        // Quit: no payload; Move: {x: i32, y: i32} = 8B; Write: String = 24B
1016        // Max payload = 24B (String), disc = 1B → total = 32B (aligned to 8)
1017        let src = r#"
1018enum Message {
1019    Quit,
1020    Move { x: i32, y: i32 },
1021    Write(String),
1022}
1023"#;
1024        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1025        let l = &layouts[0];
1026        assert_eq!(l.name, "Message");
1027        // __payload (24B, align 8) + __discriminant (1B) → padded to 32B
1028        assert_eq!(l.total_size, 32);
1029        assert_eq!(l.fields.len(), 2);
1030        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
1031        assert_eq!(payload.size, 24); // String = 3×pointer
1032    }
1033
1034    #[test]
1035    fn generic_enum_is_skipped() {
1036        let src = "enum Wrapper<T> { Some(T), None }";
1037        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1038        assert!(
1039            layouts.is_empty(),
1040            "generic enums should be skipped; got {:?}",
1041            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
1042        );
1043    }
1044
1045    #[test]
1046    fn empty_enum_is_skipped() {
1047        let src = "enum Never {}";
1048        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1049        assert!(layouts.is_empty());
1050    }
1051
1052    #[test]
1053    fn enum_with_only_unit_variants_has_no_payload_field() {
1054        let src = "enum Dir { North, South, East, West }";
1055        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1056        assert!(!layouts[0].fields.iter().any(|f| f.name == "__payload"));
1057    }
1058
1059    #[test]
1060    fn data_enum_and_sibling_struct_both_parsed() {
1061        let src = r#"
1062enum Status { Ok, Err(u32) }
1063struct Conn { port: u16, status: u32 }
1064"#;
1065        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1066        assert_eq!(layouts.len(), 2);
1067        assert!(layouts.iter().any(|l| l.name == "Status"));
1068        assert!(layouts.iter().any(|l| l.name == "Conn"));
1069    }
1070
1071    // ── bad weather: enums ────────────────────────────────────────────────────
1072
1073    #[test]
1074    fn enum_with_only_zero_sized_variants_has_payload_size_zero() {
1075        // All unit variants → treated as unit enum, total = disc_size
1076        let src = "enum E { A, B }";
1077        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1078        let l = &layouts[0];
1079        assert_eq!(l.total_size, 1);
1080    }
1081
1082    #[test]
1083    fn enum_mixed_unit_and_data_includes_max_payload() {
1084        // Mix: unit variant + data variant; payload comes from data variant
1085        let src = "enum E { Nothing, Data(u64) }";
1086        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1087        let l = &layouts[0];
1088        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
1089        assert_eq!(payload.size, 8); // u64
1090    }
1091
1092    // ── repr(align(N)) ────────────────────────────────────────────────────────
1093
1094    #[test]
1095    fn repr_align_raises_struct_alignment() {
1096        let src = "#[repr(align(64))]\nstruct CacheLine { a: u8, b: u32 }";
1097        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1098        let l = &layouts[0];
1099        assert_eq!(
1100            l.align, 64,
1101            "repr(align(64)) must set struct alignment to 64"
1102        );
1103        assert_eq!(l.total_size, 64, "size must be padded to 64 bytes");
1104    }
1105
1106    #[test]
1107    fn repr_align_does_not_shrink_natural_alignment() {
1108        // repr(align(1)) on a struct whose natural align is 8 — must keep 8
1109        let src = "#[repr(align(1))]\nstruct S { a: u64 }";
1110        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1111        let l = &layouts[0];
1112        assert_eq!(
1113            l.align, 8,
1114            "natural align must not be reduced below repr(align)"
1115        );
1116    }
1117
1118    #[test]
1119    fn repr_align_adds_trailing_padding() {
1120        // u8 + u32 = 5 bytes natural, padded to 8 with align(8)
1121        let src = "#[repr(align(8))]\nstruct S { a: u8, b: u32 }";
1122        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1123        let l = &layouts[0];
1124        assert_eq!(l.total_size, 8);
1125    }
1126
1127    #[test]
1128    fn no_repr_align_has_natural_size() {
1129        // Baseline: without repr(align), just natural padding
1130        let src = "struct S { a: u8, b: u32 }";
1131        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1132        let l = &layouts[0];
1133        // a:1 + 3 pad + b:4 = 8; align=4
1134        assert_eq!(l.total_size, 8);
1135        assert_eq!(l.align, 4);
1136    }
1137
1138    // ── tuple structs ─────────────────────────────────────────────────────────
1139
1140    #[test]
1141    fn tuple_struct_fields_named_by_index() {
1142        let src = "struct Pair(u64, u8);";
1143        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1144        let l = &layouts[0];
1145        assert_eq!(l.fields[0].name, "_0");
1146        assert_eq!(l.fields[1].name, "_1");
1147    }
1148
1149    #[test]
1150    fn tuple_struct_layout_follows_alignment() {
1151        // u64 then u8: no padding before u64, 7 bytes trailing
1152        let src = "struct S(u64, u8);";
1153        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1154        let l = &layouts[0];
1155        assert_eq!(l.fields[0].offset, 0);
1156        assert_eq!(l.fields[0].size, 8);
1157        assert_eq!(l.fields[1].offset, 8);
1158        assert_eq!(l.fields[1].size, 1);
1159        assert_eq!(l.total_size, 16);
1160    }
1161
1162    #[test]
1163    fn tuple_struct_with_padding_waste_detected() {
1164        // u8 then u64: 7 bytes padding
1165        let src = "struct S(u8, u64);";
1166        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1167        let l = &layouts[0];
1168        assert_eq!(l.fields[0].offset, 0); // u8 at 0
1169        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
1170        assert_eq!(l.total_size, 16);
1171        let gaps = padlock_core::ir::find_padding(l);
1172        assert_eq!(gaps[0].bytes, 7);
1173    }
1174
1175    // ── type-table tests ──────────────────────────────────────────────────────
1176
1177    #[test]
1178    fn nonzero_types_same_size_as_base() {
1179        assert_eq!(primitive_size_align("NonZeroU8", &X86_64_SYSV), (1, 1));
1180        assert_eq!(primitive_size_align("NonZeroI8", &X86_64_SYSV), (1, 1));
1181        assert_eq!(primitive_size_align("NonZeroU16", &X86_64_SYSV), (2, 2));
1182        assert_eq!(primitive_size_align("NonZeroU32", &X86_64_SYSV), (4, 4));
1183        assert_eq!(primitive_size_align("NonZeroU64", &X86_64_SYSV), (8, 8));
1184        assert_eq!(primitive_size_align("NonZeroU128", &X86_64_SYSV), (16, 16));
1185        assert_eq!(
1186            primitive_size_align("NonZeroUsize", &X86_64_SYSV),
1187            (X86_64_SYSV.pointer_size, X86_64_SYSV.pointer_size)
1188        );
1189    }
1190
1191    #[test]
1192    fn float16_and_float128_correct_size() {
1193        assert_eq!(primitive_size_align("f16", &X86_64_SYSV), (2, 2));
1194        assert_eq!(primitive_size_align("f128", &X86_64_SYSV), (16, 16));
1195    }
1196
1197    #[test]
1198    fn rust_struct_with_nonzero_fields() {
1199        let src = "struct Counts { hits: NonZeroU64, misses: NonZeroU32, flags: u8 }";
1200        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1201        let l = &layouts[0];
1202        assert_eq!(l.fields[0].size, 8); // NonZeroU64
1203        assert_eq!(l.fields[1].size, 4); // NonZeroU32
1204        assert_eq!(l.fields[2].size, 1); // u8
1205        // Total: 8+4+1 = 13, padded to align(8) = 16
1206        assert_eq!(l.total_size, 16);
1207    }
1208
1209    // ── repr(Rust) detection ──────────────────────────────────────────────────
1210
1211    #[test]
1212    fn plain_struct_is_repr_rust() {
1213        let src = "struct Foo { a: u64, b: u32 }";
1214        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1215        assert!(layouts[0].is_repr_rust, "plain struct should be repr(Rust)");
1216    }
1217
1218    #[test]
1219    fn repr_c_struct_is_not_repr_rust() {
1220        let src = "#[repr(C)] struct Foo { a: u64, b: u32 }";
1221        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1222        assert!(
1223            !layouts[0].is_repr_rust,
1224            "repr(C) struct must not be repr(Rust)"
1225        );
1226    }
1227
1228    #[test]
1229    fn repr_packed_struct_is_not_repr_rust() {
1230        let src = "#[repr(packed)] struct Foo { a: u64, b: u32 }";
1231        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1232        assert!(
1233            !layouts[0].is_repr_rust,
1234            "repr(packed) struct must not be repr(Rust)"
1235        );
1236    }
1237
1238    #[test]
1239    fn repr_transparent_struct_is_not_repr_rust() {
1240        let src = "#[repr(transparent)] struct Wrapper(u64);";
1241        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1242        assert!(
1243            !layouts[0].is_repr_rust,
1244            "repr(transparent) struct must not be repr(Rust)"
1245        );
1246    }
1247
1248    #[test]
1249    fn plain_enum_is_repr_rust() {
1250        let src = "enum Color { Red, Green, Blue }";
1251        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1252        assert!(layouts[0].is_repr_rust, "plain enum should be repr(Rust)");
1253    }
1254
1255    #[test]
1256    fn repr_c_enum_is_not_repr_rust() {
1257        let src = "#[repr(C)] enum Dir { North, South }";
1258        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1259        assert!(
1260            !layouts[0].is_repr_rust,
1261            "repr(C) enum must not be repr(Rust)"
1262        );
1263    }
1264
1265    // ── dyn Trait fat pointer sizing ──────────────────────────────────────────
1266
1267    #[test]
1268    fn box_dyn_trait_is_fat_pointer() {
1269        // Box<dyn Trait> = data ptr + vtable ptr = 2 words (16B on x86-64)
1270        let src = "struct S { handler: Box<dyn std::any::Any> }";
1271        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1272        assert_eq!(
1273            layouts[0].fields[0].size, 16,
1274            "Box<dyn Trait> must be 16 bytes (fat pointer)"
1275        );
1276    }
1277
1278    #[test]
1279    fn arc_dyn_trait_is_fat_pointer() {
1280        let src = "struct S { shared: Arc<dyn std::fmt::Display> }";
1281        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1282        assert_eq!(layouts[0].fields[0].size, 16);
1283    }
1284
1285    #[test]
1286    fn ref_dyn_trait_is_fat_pointer() {
1287        // &dyn Trait = data ptr + vtable ptr = 16B
1288        // Use a non-generic struct — structs with any generic params (including
1289        // lifetime params) are skipped by the parser.
1290        let src = "struct S { cb: &'static dyn Fn() }";
1291        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1292        assert_eq!(layouts.len(), 1, "S must be parsed");
1293        assert_eq!(
1294            layouts[0].fields[0].size, 16,
1295            "&dyn Trait must be a 16-byte fat pointer"
1296        );
1297    }
1298
1299    #[test]
1300    fn box_concrete_type_is_single_pointer() {
1301        // Box<u64> is not a fat pointer — just 8B
1302        let src = "struct S { inner: Box<u64> }";
1303        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1304        assert_eq!(
1305            layouts[0].fields[0].size, 8,
1306            "Box<concrete> must remain 8 bytes"
1307        );
1308    }
1309
1310    // ── transparent newtype wrappers ──────────────────────────────────────────
1311
1312    #[test]
1313    fn cell_u8_is_one_byte() {
1314        let src = "struct S { x: Cell<u8> }";
1315        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1316        assert_eq!(
1317            layouts[0].fields[0].size, 1,
1318            "Cell<u8> must be 1 byte, not pointer-sized"
1319        );
1320    }
1321
1322    #[test]
1323    fn maybe_uninit_u32_is_four_bytes() {
1324        let src = "struct S { x: MaybeUninit<u32> }";
1325        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1326        assert_eq!(
1327            layouts[0].fields[0].size, 4,
1328            "MaybeUninit<u32> must be 4 bytes"
1329        );
1330    }
1331
1332    #[test]
1333    fn wrapping_i16_is_two_bytes() {
1334        let src = "struct S { x: Wrapping<i16> }";
1335        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1336        assert_eq!(
1337            layouts[0].fields[0].size, 2,
1338            "Wrapping<i16> must be 2 bytes"
1339        );
1340    }
1341
1342    #[test]
1343    fn manually_drop_u64_is_eight_bytes() {
1344        let src = "struct S { x: ManuallyDrop<u64> }";
1345        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1346        assert_eq!(
1347            layouts[0].fields[0].size, 8,
1348            "ManuallyDrop<u64> must be 8 bytes"
1349        );
1350    }
1351
1352    #[test]
1353    fn unsafe_cell_u32_is_four_bytes() {
1354        let src = "struct S { x: UnsafeCell<u32> }";
1355        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1356        assert_eq!(
1357            layouts[0].fields[0].size, 4,
1358            "UnsafeCell<u32> must be 4 bytes"
1359        );
1360    }
1361
1362    #[test]
1363    fn transparent_wrapper_affects_total_size() {
1364        // bool(1) + pad(1) + Cell<u16>(2) = 4B
1365        let src = "struct S { a: bool, b: Cell<u16> }";
1366        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1367        let l = &layouts[0];
1368        assert_eq!(l.fields[0].size, 1); // bool
1369        assert_eq!(l.fields[1].size, 2); // Cell<u16>
1370        assert_eq!(l.total_size, 4);
1371    }
1372
1373    #[test]
1374    fn struct_with_box_dyn_has_correct_layout() {
1375        // bool(1) + 7-pad + Box<dyn Error>(16) = 24B
1376        let src = "struct Handler { active: bool, err: Box<dyn std::error::Error> }";
1377        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1378        let l = &layouts[0];
1379        assert_eq!(l.fields[0].size, 1); // bool
1380        assert_eq!(l.fields[1].size, 16); // Box<dyn Error>
1381        assert_eq!(l.fields[1].offset, 8); // aligned to pointer_size
1382        assert_eq!(l.total_size, 24);
1383    }
1384}