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 name = tp
55                .path
56                .segments
57                .last()
58                .map(|s| s.ident.to_string())
59                .unwrap_or_default();
60            let (size, align) = primitive_size_align(&name, arch);
61            (size, align, TypeInfo::Primitive { name, size, align })
62        }
63        Type::Ptr(_) | Type::Reference(_) => {
64            let s = arch.pointer_size;
65            (s, s, TypeInfo::Pointer { size: s, align: s })
66        }
67        Type::Array(arr) => {
68            let (elem_size, elem_align, elem_ty) = rust_type_size_align(&arr.elem, arch);
69            let count = array_len_from_expr(&arr.len);
70            let size = elem_size * count;
71            (
72                size,
73                elem_align,
74                TypeInfo::Array {
75                    element: Box::new(elem_ty),
76                    count,
77                    size,
78                    align: elem_align,
79                },
80            )
81        }
82        _ => {
83            let s = arch.pointer_size;
84            (
85                s,
86                s,
87                TypeInfo::Opaque {
88                    name: "(unknown)".into(),
89                    size: s,
90                    align: s,
91                },
92            )
93        }
94    }
95}
96
97fn primitive_size_align(name: &str, arch: &'static ArchConfig) -> (usize, usize) {
98    let ps = arch.pointer_size;
99    match name {
100        // ── language primitives ───────────────────────────────────────────────
101        "bool" | "u8" | "i8" => (1, 1),
102        "u16" | "i16" | "f16" => (2, 2),
103        "u32" | "i32" | "f32" => (4, 4),
104        "u64" | "i64" | "f64" => (8, 8),
105        "u128" | "i128" | "f128" => (16, 16),
106        "usize" | "isize" => (ps, ps),
107        "char" => (4, 4), // Rust char is a Unicode scalar (4 bytes)
108
109        // NonZero integer types — same size/align as the underlying integer.
110        // The niche optimisation means Option<NonZeroU8> == 1 byte, but the
111        // struct field itself is identical in size to the plain integer.
112        "NonZeroU8" | "NonZeroI8" => (1, 1),
113        "NonZeroU16" | "NonZeroI16" => (2, 2),
114        "NonZeroU32" | "NonZeroI32" => (4, 4),
115        "NonZeroU64" | "NonZeroI64" => (8, 8),
116        "NonZeroU128" | "NonZeroI128" => (16, 16),
117        "NonZeroUsize" | "NonZeroIsize" => (ps, ps),
118
119        // Wrapping<T>, Saturating<T> — transparent newtype over T.
120        // The generic arg has already been stripped, so we get the inner
121        // primitive name here; if the stripping didn't happen these fall
122        // through to pointer-size, which is acceptable.
123        "Wrapping" | "Saturating" => (ps, ps),
124
125        // MaybeUninit<T> and UnsafeCell<T> are transparent newtypes —
126        // same size as T. Without knowing T we approximate as pointer-size,
127        // which is correct for the common case of wrapping a pointer-sized value.
128        "MaybeUninit" | "UnsafeCell" => (ps, ps),
129
130        // ── std atomics ───────────────────────────────────────────────────────
131        "AtomicBool" | "AtomicU8" | "AtomicI8" => (1, 1),
132        "AtomicU16" | "AtomicI16" => (2, 2),
133        "AtomicU32" | "AtomicI32" => (4, 4),
134        "AtomicU64" | "AtomicI64" => (8, 8),
135        "AtomicUsize" | "AtomicIsize" | "AtomicPtr" => (ps, ps),
136
137        // ── heap-allocated collections: ptr + len + cap (3 words) ────────────
138        // Size is independent of the element type T (generic arg already stripped).
139        "Vec" | "String" | "OsString" | "CString" | "PathBuf" => (3 * ps, ps),
140        "VecDeque" | "LinkedList" | "BinaryHeap" => (3 * ps, ps),
141        "HashMap" | "HashSet" | "BTreeMap" | "BTreeSet" => (3 * ps, ps),
142
143        // ── single-pointer smart pointers ─────────────────────────────────────
144        "Box" | "Rc" | "Arc" | "Weak" | "NonNull" | "Cell" => (ps, ps),
145
146        // ── interior-mutability / sync wrappers ───────────────────────────────
147        // Size depends on T but pointer-size is a reasonable approximation for
148        // display purposes; use binary analysis for precise results.
149        "RefCell" | "Mutex" | "RwLock" => (ps, ps),
150
151        // ── channels ─────────────────────────────────────────────────────────
152        "Sender" | "Receiver" | "SyncSender" => (ps, ps),
153
154        // ── zero-sized types ──────────────────────────────────────────────────
155        "PhantomData" | "PhantomPinned" => (0, 1),
156
157        // ── common fixed-size stdlib types ────────────────────────────────────
158        // Duration: u64 secs (8B) + u32 nanos (4B) → 12B + 4B trailing = 16B
159        "Duration" => (16, 8),
160        "Instant" | "SystemTime" => (16, 8),
161
162        // ── Pin<T> wraps T, pointer-size approximation ────────────────────────
163        "Pin" => (ps, ps),
164
165        // ── x86 SSE / AVX / AVX-512 SIMD types ───────────────────────────────
166        "__m64" => (8, 8),
167        "__m128" | "__m128d" | "__m128i" => (16, 16),
168        "__m256" | "__m256d" | "__m256i" => (32, 32),
169        "__m512" | "__m512d" | "__m512i" => (64, 64),
170
171        // ── Rust portable SIMD / packed_simd types ────────────────────────────
172        "f32x4" | "i32x4" | "u32x4" => (16, 16),
173        "f64x2" | "i64x2" | "u64x2" => (16, 16),
174        "f32x8" | "i32x8" | "u32x8" => (32, 32),
175        "f64x4" | "i64x4" | "u64x4" => (32, 32),
176        "f32x16" | "i32x16" | "u32x16" => (64, 64),
177
178        // ── unknown / third-party / generic type params (T, E, …) ────────────
179        _ => (ps, ps),
180    }
181}
182
183fn array_len_from_expr(expr: &syn::Expr) -> usize {
184    if let syn::Expr::Lit(syn::ExprLit {
185        lit: syn::Lit::Int(n),
186        ..
187    }) = expr
188    {
189        n.base10_parse::<usize>().unwrap_or(0)
190    } else {
191        0
192    }
193}
194
195// ── struct repr detection ─────────────────────────────────────────────────────
196
197fn is_packed(attrs: &[syn::Attribute]) -> bool {
198    attrs
199        .iter()
200        .any(|a| a.path().is_ident("repr") && a.to_token_stream().to_string().contains("packed"))
201}
202
203/// Returns `true` when the struct has no repr annotation that fixes the layout
204/// (`repr(C)`, `repr(packed)`, `repr(transparent)`).  A struct with only
205/// `repr(align(N))` still has an unspecified field order — the compiler may
206/// reorder fields freely — so it counts as `repr(Rust)` for warning purposes.
207fn is_repr_rust(attrs: &[syn::Attribute]) -> bool {
208    !attrs.iter().any(|a| {
209        if !a.path().is_ident("repr") {
210            return false;
211        }
212        let ts = a.to_token_stream().to_string();
213        ts.contains('C') || ts.contains("packed") || ts.contains("transparent")
214    })
215}
216
217/// Extract the alignment from `#[repr(align(N))]`. Returns `None` if not present.
218fn repr_align(attrs: &[syn::Attribute]) -> Option<usize> {
219    for attr in attrs {
220        if !attr.path().is_ident("repr") {
221            continue;
222        }
223        let ts = attr.to_token_stream().to_string();
224        // Look for `align ( N )` in the token stream string.
225        // The tokeniser adds spaces: "repr (align (64))" etc.
226        if let Some(start) = ts.find("align") {
227            let after = ts[start..].trim_start_matches("align").trim_start();
228            if after.starts_with('(') {
229                let inner = after.trim_start_matches('(');
230                let num_str: String = inner.chars().take_while(|c| c.is_ascii_digit()).collect();
231                if let Ok(n) = num_str.parse::<usize>()
232                    && n > 0
233                    && n.is_power_of_two()
234                {
235                    return Some(n);
236                }
237            }
238        }
239    }
240    None
241}
242
243fn simulate_rust_layout(
244    name: String,
245    fields: &[(String, Type)],
246    packed: bool,
247    forced_align: Option<usize>,
248    arch: &'static ArchConfig,
249) -> StructLayout {
250    let mut offset = 0usize;
251    let mut struct_align = 1usize;
252    let mut out_fields: Vec<Field> = Vec::new();
253
254    for (fname, ty) in fields {
255        let (size, align, type_info) = rust_type_size_align(ty, arch);
256        let effective_align = if packed { 1 } else { align };
257
258        if effective_align > 0 {
259            offset = offset.next_multiple_of(effective_align);
260        }
261        struct_align = struct_align.max(effective_align);
262
263        out_fields.push(Field {
264            name: fname.clone(),
265            ty: type_info,
266            offset,
267            size,
268            align: effective_align,
269            source_file: None,
270            source_line: None,
271            access: AccessPattern::Unknown,
272        });
273        offset += size;
274    }
275
276    // Apply repr(align(N)): raise minimum alignment and add trailing padding.
277    if let Some(fa) = forced_align
278        && fa > struct_align
279    {
280        struct_align = fa;
281    }
282
283    if !packed && struct_align > 0 {
284        offset = offset.next_multiple_of(struct_align);
285    }
286
287    StructLayout {
288        name,
289        total_size: offset,
290        align: struct_align,
291        fields: out_fields,
292        source_file: None,
293        source_line: None,
294        arch,
295        is_packed: packed,
296        is_union: false,
297        is_repr_rust: false, // callers override this after construction
298        suppressed_findings: Vec::new(), // callers may override after construction
299    }
300}
301
302// ── visitor ───────────────────────────────────────────────────────────────────
303
304struct StructVisitor<'src> {
305    arch: &'static ArchConfig,
306    layouts: Vec<StructLayout>,
307    source: &'src str,
308}
309
310impl<'ast, 'src> Visit<'ast> for StructVisitor<'src> {
311    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
312        syn::visit::visit_item_struct(self, node); // recurse into nested items
313
314        // Generic structs (e.g. `struct Foo<T>`) cannot be accurately laid out
315        // without knowing the concrete type arguments. Skip them rather than
316        // producing wrong field sizes for the type parameters.
317        if !node.generics.params.is_empty() {
318            return;
319        }
320
321        let name = node.ident.to_string();
322        let packed = is_packed(&node.attrs);
323        let forced_align = repr_align(&node.attrs);
324
325        // Collect (field_name, type, optional_guard, source_line)
326        let fields: Vec<(String, Type, Option<String>, u32)> = match &node.fields {
327            Fields::Named(nf) => nf
328                .named
329                .iter()
330                .map(|f| {
331                    let fname = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
332                    let guard = extract_guard_from_attrs(&f.attrs);
333                    let line = f
334                        .ident
335                        .as_ref()
336                        .map(|i| i.span().start().line as u32)
337                        .unwrap_or(0);
338                    (fname, f.ty.clone(), guard, line)
339                })
340                .collect(),
341            Fields::Unnamed(uf) => uf
342                .unnamed
343                .iter()
344                .enumerate()
345                .map(|(i, f)| {
346                    let guard = extract_guard_from_attrs(&f.attrs);
347                    // Unnamed fields don't have an ident span; use 0 as a sentinel.
348                    (format!("_{i}"), f.ty.clone(), guard, 0u32)
349                })
350                .collect(),
351            Fields::Unit => vec![],
352        };
353
354        let name_ty: Vec<(String, Type)> = fields
355            .iter()
356            .map(|(n, t, _, _)| (n.clone(), t.clone()))
357            .collect();
358        let mut layout = simulate_rust_layout(name, &name_ty, packed, forced_align, self.arch);
359        let struct_line = node.ident.span().start().line as u32;
360        layout.source_line = Some(struct_line);
361        layout.is_repr_rust = is_repr_rust(&node.attrs);
362        layout.suppressed_findings =
363            super::suppress::suppressed_from_source_line(self.source, struct_line);
364
365        // Apply explicit guard annotations and field source lines.
366        for (i, (_, _, guard, field_line)) in fields.iter().enumerate() {
367            if *field_line > 0 {
368                layout.fields[i].source_line = Some(*field_line);
369            }
370            if let Some(g) = guard {
371                layout.fields[i].access = AccessPattern::Concurrent {
372                    guard: Some(g.clone()),
373                    is_atomic: false,
374                    is_annotated: true,
375                };
376            }
377        }
378
379        self.layouts.push(layout);
380    }
381
382    fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
383        syn::visit::visit_item_enum(self, node);
384
385        // Skip generic enums (layout depends on unknown type arguments)
386        if !node.generics.params.is_empty() {
387            return;
388        }
389
390        let name = node.ident.to_string();
391        let n_variants = node.variants.len();
392        if n_variants == 0 {
393            return;
394        }
395
396        // Discriminant size: smallest integer that fits the variant count.
397        // Rust defaults to isize but uses the minimal repr in practice.
398        let disc_size: usize = if n_variants <= 256 {
399            1
400        } else if n_variants <= 65536 {
401            2
402        } else {
403            4
404        };
405
406        // Check if all variants are unit (C-like enum, no payload)
407        let all_unit = node
408            .variants
409            .iter()
410            .all(|v| matches!(v.fields, Fields::Unit));
411
412        if all_unit {
413            // Pure discriminant — no payload storage
414            let enum_line = node.ident.span().start().line as u32;
415            let layout = StructLayout {
416                name,
417                total_size: disc_size,
418                align: disc_size,
419                fields: vec![Field {
420                    name: "__discriminant".to_string(),
421                    ty: TypeInfo::Primitive {
422                        name: format!("u{}", disc_size * 8),
423                        size: disc_size,
424                        align: disc_size,
425                    },
426                    offset: 0,
427                    size: disc_size,
428                    align: disc_size,
429                    source_file: None,
430                    source_line: None,
431                    access: AccessPattern::Unknown,
432                }],
433                source_file: None,
434                source_line: Some(enum_line),
435                arch: self.arch,
436                is_packed: false,
437                is_union: false,
438                is_repr_rust: is_repr_rust(&node.attrs),
439                suppressed_findings: super::suppress::suppressed_from_source_line(
440                    self.source,
441                    enum_line,
442                ),
443            };
444            self.layouts.push(layout);
445            return;
446        }
447
448        // Data enum: find the maximum variant payload size and alignment.
449        let mut max_payload_size = 0usize;
450        let mut max_payload_align = 1usize;
451
452        for variant in &node.variants {
453            let var_fields: Vec<(String, Type)> = match &variant.fields {
454                Fields::Named(nf) => nf
455                    .named
456                    .iter()
457                    .map(|f| {
458                        let n = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
459                        (n, f.ty.clone())
460                    })
461                    .collect(),
462                Fields::Unnamed(uf) => uf
463                    .unnamed
464                    .iter()
465                    .enumerate()
466                    .map(|(i, f)| (format!("_{i}"), f.ty.clone()))
467                    .collect(),
468                Fields::Unit => vec![],
469            };
470
471            if !var_fields.is_empty() {
472                let var_layout =
473                    simulate_rust_layout(String::new(), &var_fields, false, None, self.arch);
474                if var_layout.total_size > max_payload_size {
475                    max_payload_size = var_layout.total_size;
476                }
477                max_payload_align = max_payload_align.max(var_layout.align);
478            }
479        }
480
481        // Conservative model: payload first at offset 0, discriminant immediately after.
482        // Rust's actual layout is compiler-controlled (niche optimisation etc.);
483        // this model gives a safe upper-bound for padding analysis.
484        let payload_align = max_payload_align.max(1);
485        let disc_offset = max_payload_size;
486        let total_before_pad = disc_offset + disc_size;
487        let total_align = payload_align.max(disc_size);
488        let total_size = total_before_pad.next_multiple_of(total_align);
489
490        let mut fields: Vec<Field> = Vec::new();
491        if max_payload_size > 0 {
492            fields.push(Field {
493                name: "__payload".to_string(),
494                ty: TypeInfo::Opaque {
495                    name: format!("largest_variant_payload ({}B)", max_payload_size),
496                    size: max_payload_size,
497                    align: payload_align,
498                },
499                offset: 0,
500                size: max_payload_size,
501                align: payload_align,
502                source_file: None,
503                source_line: None,
504                access: AccessPattern::Unknown,
505            });
506        }
507        fields.push(Field {
508            name: "__discriminant".to_string(),
509            ty: TypeInfo::Primitive {
510                name: format!("u{}", disc_size * 8),
511                size: disc_size,
512                align: disc_size,
513            },
514            offset: disc_offset,
515            size: disc_size,
516            align: disc_size,
517            source_file: None,
518            source_line: None,
519            access: AccessPattern::Unknown,
520        });
521
522        let enum_line = node.ident.span().start().line as u32;
523        self.layouts.push(StructLayout {
524            name,
525            total_size,
526            align: total_align,
527            fields,
528            source_file: None,
529            source_line: Some(enum_line),
530            arch: self.arch,
531            is_packed: false,
532            is_union: false,
533            is_repr_rust: is_repr_rust(&node.attrs),
534            suppressed_findings: super::suppress::suppressed_from_source_line(
535                self.source,
536                enum_line,
537            ),
538        });
539    }
540}
541
542// ── public API ────────────────────────────────────────────────────────────────
543
544pub fn parse_rust(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
545    let file: syn::File = syn::parse_str(source)?;
546    let mut visitor = StructVisitor {
547        arch,
548        layouts: Vec::new(),
549        source,
550    };
551    visitor.visit_file(&file);
552    Ok(visitor.layouts)
553}
554
555// ── tests ─────────────────────────────────────────────────────────────────────
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use padlock_core::arch::X86_64_SYSV;
561
562    #[test]
563    fn parse_simple_struct() {
564        let src = "struct Foo { a: u8, b: u64, c: u32 }";
565        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
566        assert_eq!(layouts.len(), 1);
567        let l = &layouts[0];
568        assert_eq!(l.name, "Foo");
569        assert_eq!(l.fields.len(), 3);
570        assert_eq!(l.fields[0].size, 1); // u8
571        assert_eq!(l.fields[1].size, 8); // u64
572        assert_eq!(l.fields[2].size, 4); // u32
573    }
574
575    #[test]
576    fn layout_includes_padding() {
577        // u8 then u64: 7 bytes padding inserted
578        let src = "struct T { a: u8, b: u64 }";
579        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
580        let l = &layouts[0];
581        assert_eq!(l.fields[0].offset, 0);
582        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
583        assert_eq!(l.total_size, 16);
584        let gaps = padlock_core::ir::find_padding(l);
585        assert_eq!(gaps[0].bytes, 7);
586    }
587
588    #[test]
589    fn multiple_structs_parsed() {
590        let src = "struct A { x: u32 } struct B { y: u64 }";
591        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
592        assert_eq!(layouts.len(), 2);
593    }
594
595    #[test]
596    fn packed_struct_no_padding() {
597        let src = "#[repr(packed)] struct P { a: u8, b: u64 }";
598        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
599        let l = &layouts[0];
600        assert!(l.is_packed);
601        assert_eq!(l.fields[1].offset, 1); // no padding, b immediately after a
602        let gaps = padlock_core::ir::find_padding(l);
603        assert!(gaps.is_empty());
604    }
605
606    #[test]
607    fn pointer_field_uses_arch_size() {
608        let src = "struct S { p: *const u8 }";
609        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
610        assert_eq!(layouts[0].fields[0].size, 8); // 64-bit pointer
611    }
612
613    // ── attribute guard extraction ─────────────────────────────────────────────
614
615    #[test]
616    fn lock_protected_by_attr_sets_guard() {
617        let src = r#"
618struct Cache {
619    #[lock_protected_by = "mu"]
620    readers: u64,
621    mu: u64,
622}
623"#;
624        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
625        let readers = &layouts[0].fields[0];
626        assert_eq!(readers.name, "readers");
627        if let AccessPattern::Concurrent { guard, .. } = &readers.access {
628            assert_eq!(guard.as_deref(), Some("mu"));
629        } else {
630            panic!("expected Concurrent, got {:?}", readers.access);
631        }
632    }
633
634    #[test]
635    fn guarded_by_string_attr_sets_guard() {
636        let src = r#"
637struct S {
638    #[guarded_by("lock")]
639    value: u32,
640}
641"#;
642        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
643        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
644            assert_eq!(guard.as_deref(), Some("lock"));
645        } else {
646            panic!("expected Concurrent");
647        }
648    }
649
650    #[test]
651    fn guarded_by_ident_attr_sets_guard() {
652        let src = r#"
653struct S {
654    #[guarded_by(mu)]
655    count: u64,
656}
657"#;
658        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
659        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
660            assert_eq!(guard.as_deref(), Some("mu"));
661        } else {
662            panic!("expected Concurrent");
663        }
664    }
665
666    #[test]
667    fn protected_by_attr_sets_guard() {
668        let src = r#"
669struct S {
670    #[protected_by = "lock_a"]
671    x: u64,
672}
673"#;
674        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
675        if let AccessPattern::Concurrent { guard, .. } = &layouts[0].fields[0].access {
676            assert_eq!(guard.as_deref(), Some("lock_a"));
677        } else {
678            panic!("expected Concurrent");
679        }
680    }
681
682    #[test]
683    fn different_guards_on_same_cache_line_is_false_sharing() {
684        // readers and writers are at offsets 0 and 8 — same cache line (line 0).
685        // They have different explicit guards → confirmed false sharing.
686        let src = r#"
687struct HotPath {
688    #[lock_protected_by = "mu_a"]
689    readers: u64,
690    #[lock_protected_by = "mu_b"]
691    writers: u64,
692}
693"#;
694        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
695        assert!(padlock_core::analysis::false_sharing::has_false_sharing(
696            &layouts[0]
697        ));
698    }
699
700    #[test]
701    fn same_guard_on_same_cache_line_is_not_false_sharing() {
702        let src = r#"
703struct Safe {
704    #[lock_protected_by = "mu"]
705    a: u64,
706    #[lock_protected_by = "mu"]
707    b: u64,
708}
709"#;
710        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
711        assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
712            &layouts[0]
713        ));
714    }
715
716    #[test]
717    fn unannotated_field_stays_unknown() {
718        let src = "struct S { x: u64 }";
719        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
720        assert!(matches!(
721            layouts[0].fields[0].access,
722            AccessPattern::Unknown
723        ));
724    }
725
726    // ── stdlib type sizes ─────────────────────────────────────────────────────
727
728    #[test]
729    fn vec_field_has_three_pointer_size() {
730        // Vec<T> is always ptr + len + cap regardless of T
731        let src = "struct S { items: Vec<u64> }";
732        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
733        assert_eq!(layouts[0].fields[0].size, 24); // 3 × 8 on x86-64
734    }
735
736    #[test]
737    fn string_field_has_three_pointer_size() {
738        let src = "struct S { name: String }";
739        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
740        assert_eq!(layouts[0].fields[0].size, 24);
741    }
742
743    #[test]
744    fn box_field_has_pointer_size() {
745        let src = "struct S { inner: Box<u64> }";
746        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
747        assert_eq!(layouts[0].fields[0].size, 8);
748    }
749
750    #[test]
751    fn arc_field_has_pointer_size() {
752        let src = "struct S { shared: Arc<Vec<u8>> }";
753        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
754        assert_eq!(layouts[0].fields[0].size, 8);
755    }
756
757    #[test]
758    fn phantom_data_is_zero_sized() {
759        let src = "struct S { a: u64, _marker: PhantomData<u8> }";
760        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
761        let marker = layouts[0]
762            .fields
763            .iter()
764            .find(|f| f.name == "_marker")
765            .unwrap();
766        assert_eq!(marker.size, 0);
767    }
768
769    #[test]
770    fn duration_field_is_16_bytes() {
771        let src = "struct S { timeout: Duration }";
772        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
773        assert_eq!(layouts[0].fields[0].size, 16);
774    }
775
776    #[test]
777    fn atomic_u64_has_correct_size() {
778        let src = "struct S { counter: AtomicU64 }";
779        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
780        assert_eq!(layouts[0].fields[0].size, 8);
781    }
782
783    #[test]
784    fn atomic_bool_has_correct_size() {
785        let src = "struct S { flag: AtomicBool }";
786        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
787        assert_eq!(layouts[0].fields[0].size, 1);
788    }
789
790    // ── generic struct skipping ───────────────────────────────────────────────
791
792    #[test]
793    fn generic_struct_is_skipped() {
794        // Cannot accurately lay out struct Foo<T> without knowing T.
795        let src = "struct Wrapper<T> { value: T, count: usize }";
796        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
797        assert!(
798            layouts.is_empty(),
799            "generic structs should be skipped; got {:?}",
800            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
801        );
802    }
803
804    #[test]
805    fn generic_struct_with_multiple_params_is_skipped() {
806        let src = "struct Pair<A, B> { first: A, second: B }";
807        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
808        assert!(layouts.is_empty());
809    }
810
811    #[test]
812    fn non_generic_struct_still_parsed_when_generic_sibling_exists() {
813        let src = r#"
814struct Generic<T> { value: T }
815struct Concrete { a: u32, b: u64 }
816"#;
817        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
818        assert_eq!(layouts.len(), 1);
819        assert_eq!(layouts[0].name, "Concrete");
820    }
821
822    // ── enum data variant support ─────────────────────────────────────────────
823
824    #[test]
825    fn unit_enum_is_just_discriminant() {
826        let src = "enum Color { Red, Green, Blue }";
827        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
828        assert_eq!(layouts.len(), 1);
829        let l = &layouts[0];
830        assert_eq!(l.name, "Color");
831        assert_eq!(l.total_size, 1); // 3 variants → u8 discriminant
832        assert_eq!(l.fields.len(), 1);
833        assert_eq!(l.fields[0].name, "__discriminant");
834    }
835
836    #[test]
837    fn unit_enum_with_many_variants_uses_u16_discriminant() {
838        // Build an enum with 300 variants (> 256)
839        let variants: String = (0..300)
840            .map(|i| format!("V{i}"))
841            .collect::<Vec<_>>()
842            .join(", ");
843        let src = format!("enum Big {{ {variants} }}");
844        let layouts = parse_rust(&src, &X86_64_SYSV).unwrap();
845        let l = &layouts[0];
846        assert_eq!(l.total_size, 2); // needs u16
847        assert_eq!(l.fields[0].size, 2);
848    }
849
850    #[test]
851    fn data_enum_total_size_covers_largest_variant() {
852        // Quit: no payload; Move: {x: i32, y: i32} = 8B; Write: String = 24B
853        // Max payload = 24B (String), disc = 1B → total = 32B (aligned to 8)
854        let src = r#"
855enum Message {
856    Quit,
857    Move { x: i32, y: i32 },
858    Write(String),
859}
860"#;
861        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
862        let l = &layouts[0];
863        assert_eq!(l.name, "Message");
864        // __payload (24B, align 8) + __discriminant (1B) → padded to 32B
865        assert_eq!(l.total_size, 32);
866        assert_eq!(l.fields.len(), 2);
867        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
868        assert_eq!(payload.size, 24); // String = 3×pointer
869    }
870
871    #[test]
872    fn generic_enum_is_skipped() {
873        let src = "enum Wrapper<T> { Some(T), None }";
874        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
875        assert!(
876            layouts.is_empty(),
877            "generic enums should be skipped; got {:?}",
878            layouts.iter().map(|l| &l.name).collect::<Vec<_>>()
879        );
880    }
881
882    #[test]
883    fn empty_enum_is_skipped() {
884        let src = "enum Never {}";
885        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
886        assert!(layouts.is_empty());
887    }
888
889    #[test]
890    fn enum_with_only_unit_variants_has_no_payload_field() {
891        let src = "enum Dir { North, South, East, West }";
892        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
893        assert!(!layouts[0].fields.iter().any(|f| f.name == "__payload"));
894    }
895
896    #[test]
897    fn data_enum_and_sibling_struct_both_parsed() {
898        let src = r#"
899enum Status { Ok, Err(u32) }
900struct Conn { port: u16, status: u32 }
901"#;
902        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
903        assert_eq!(layouts.len(), 2);
904        assert!(layouts.iter().any(|l| l.name == "Status"));
905        assert!(layouts.iter().any(|l| l.name == "Conn"));
906    }
907
908    // ── bad weather: enums ────────────────────────────────────────────────────
909
910    #[test]
911    fn enum_with_only_zero_sized_variants_has_payload_size_zero() {
912        // All unit variants → treated as unit enum, total = disc_size
913        let src = "enum E { A, B }";
914        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
915        let l = &layouts[0];
916        assert_eq!(l.total_size, 1);
917    }
918
919    #[test]
920    fn enum_mixed_unit_and_data_includes_max_payload() {
921        // Mix: unit variant + data variant; payload comes from data variant
922        let src = "enum E { Nothing, Data(u64) }";
923        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
924        let l = &layouts[0];
925        let payload = l.fields.iter().find(|f| f.name == "__payload").unwrap();
926        assert_eq!(payload.size, 8); // u64
927    }
928
929    // ── repr(align(N)) ────────────────────────────────────────────────────────
930
931    #[test]
932    fn repr_align_raises_struct_alignment() {
933        let src = "#[repr(align(64))]\nstruct CacheLine { a: u8, b: u32 }";
934        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
935        let l = &layouts[0];
936        assert_eq!(
937            l.align, 64,
938            "repr(align(64)) must set struct alignment to 64"
939        );
940        assert_eq!(l.total_size, 64, "size must be padded to 64 bytes");
941    }
942
943    #[test]
944    fn repr_align_does_not_shrink_natural_alignment() {
945        // repr(align(1)) on a struct whose natural align is 8 — must keep 8
946        let src = "#[repr(align(1))]\nstruct S { a: u64 }";
947        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
948        let l = &layouts[0];
949        assert_eq!(
950            l.align, 8,
951            "natural align must not be reduced below repr(align)"
952        );
953    }
954
955    #[test]
956    fn repr_align_adds_trailing_padding() {
957        // u8 + u32 = 5 bytes natural, padded to 8 with align(8)
958        let src = "#[repr(align(8))]\nstruct S { a: u8, b: u32 }";
959        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
960        let l = &layouts[0];
961        assert_eq!(l.total_size, 8);
962    }
963
964    #[test]
965    fn no_repr_align_has_natural_size() {
966        // Baseline: without repr(align), just natural padding
967        let src = "struct S { a: u8, b: u32 }";
968        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
969        let l = &layouts[0];
970        // a:1 + 3 pad + b:4 = 8; align=4
971        assert_eq!(l.total_size, 8);
972        assert_eq!(l.align, 4);
973    }
974
975    // ── tuple structs ─────────────────────────────────────────────────────────
976
977    #[test]
978    fn tuple_struct_fields_named_by_index() {
979        let src = "struct Pair(u64, u8);";
980        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
981        let l = &layouts[0];
982        assert_eq!(l.fields[0].name, "_0");
983        assert_eq!(l.fields[1].name, "_1");
984    }
985
986    #[test]
987    fn tuple_struct_layout_follows_alignment() {
988        // u64 then u8: no padding before u64, 7 bytes trailing
989        let src = "struct S(u64, u8);";
990        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
991        let l = &layouts[0];
992        assert_eq!(l.fields[0].offset, 0);
993        assert_eq!(l.fields[0].size, 8);
994        assert_eq!(l.fields[1].offset, 8);
995        assert_eq!(l.fields[1].size, 1);
996        assert_eq!(l.total_size, 16);
997    }
998
999    #[test]
1000    fn tuple_struct_with_padding_waste_detected() {
1001        // u8 then u64: 7 bytes padding
1002        let src = "struct S(u8, u64);";
1003        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1004        let l = &layouts[0];
1005        assert_eq!(l.fields[0].offset, 0); // u8 at 0
1006        assert_eq!(l.fields[1].offset, 8); // u64 aligned to 8
1007        assert_eq!(l.total_size, 16);
1008        let gaps = padlock_core::ir::find_padding(l);
1009        assert_eq!(gaps[0].bytes, 7);
1010    }
1011
1012    // ── type-table tests ──────────────────────────────────────────────────────
1013
1014    #[test]
1015    fn nonzero_types_same_size_as_base() {
1016        assert_eq!(primitive_size_align("NonZeroU8", &X86_64_SYSV), (1, 1));
1017        assert_eq!(primitive_size_align("NonZeroI8", &X86_64_SYSV), (1, 1));
1018        assert_eq!(primitive_size_align("NonZeroU16", &X86_64_SYSV), (2, 2));
1019        assert_eq!(primitive_size_align("NonZeroU32", &X86_64_SYSV), (4, 4));
1020        assert_eq!(primitive_size_align("NonZeroU64", &X86_64_SYSV), (8, 8));
1021        assert_eq!(primitive_size_align("NonZeroU128", &X86_64_SYSV), (16, 16));
1022        assert_eq!(
1023            primitive_size_align("NonZeroUsize", &X86_64_SYSV),
1024            (X86_64_SYSV.pointer_size, X86_64_SYSV.pointer_size)
1025        );
1026    }
1027
1028    #[test]
1029    fn float16_and_float128_correct_size() {
1030        assert_eq!(primitive_size_align("f16", &X86_64_SYSV), (2, 2));
1031        assert_eq!(primitive_size_align("f128", &X86_64_SYSV), (16, 16));
1032    }
1033
1034    #[test]
1035    fn rust_struct_with_nonzero_fields() {
1036        let src = "struct Counts { hits: NonZeroU64, misses: NonZeroU32, flags: u8 }";
1037        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1038        let l = &layouts[0];
1039        assert_eq!(l.fields[0].size, 8); // NonZeroU64
1040        assert_eq!(l.fields[1].size, 4); // NonZeroU32
1041        assert_eq!(l.fields[2].size, 1); // u8
1042        // Total: 8+4+1 = 13, padded to align(8) = 16
1043        assert_eq!(l.total_size, 16);
1044    }
1045
1046    // ── repr(Rust) detection ──────────────────────────────────────────────────
1047
1048    #[test]
1049    fn plain_struct_is_repr_rust() {
1050        let src = "struct Foo { a: u64, b: u32 }";
1051        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1052        assert!(layouts[0].is_repr_rust, "plain struct should be repr(Rust)");
1053    }
1054
1055    #[test]
1056    fn repr_c_struct_is_not_repr_rust() {
1057        let src = "#[repr(C)] struct Foo { a: u64, b: u32 }";
1058        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1059        assert!(
1060            !layouts[0].is_repr_rust,
1061            "repr(C) struct must not be repr(Rust)"
1062        );
1063    }
1064
1065    #[test]
1066    fn repr_packed_struct_is_not_repr_rust() {
1067        let src = "#[repr(packed)] struct Foo { a: u64, b: u32 }";
1068        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1069        assert!(
1070            !layouts[0].is_repr_rust,
1071            "repr(packed) struct must not be repr(Rust)"
1072        );
1073    }
1074
1075    #[test]
1076    fn repr_transparent_struct_is_not_repr_rust() {
1077        let src = "#[repr(transparent)] struct Wrapper(u64);";
1078        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1079        assert!(
1080            !layouts[0].is_repr_rust,
1081            "repr(transparent) struct must not be repr(Rust)"
1082        );
1083    }
1084
1085    #[test]
1086    fn plain_enum_is_repr_rust() {
1087        let src = "enum Color { Red, Green, Blue }";
1088        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1089        assert!(layouts[0].is_repr_rust, "plain enum should be repr(Rust)");
1090    }
1091
1092    #[test]
1093    fn repr_c_enum_is_not_repr_rust() {
1094        let src = "#[repr(C)] enum Dir { North, South }";
1095        let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1096        assert!(
1097            !layouts[0].is_repr_rust,
1098            "repr(C) enum must not be repr(Rust)"
1099        );
1100    }
1101}