1use padlock_core::arch::ArchConfig;
8use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
9use quote::ToTokens;
10use syn::{Fields, ItemEnum, ItemStruct, Type, visit::Visit};
11
12pub fn extract_guard_from_attrs(attrs: &[syn::Attribute]) -> Option<String> {
22 for attr in attrs {
23 let path = attr.path();
24 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 if path.is_ident("guarded_by") || path.is_ident("pt_guarded_by") {
36 if let Ok(s) = attr.parse_args::<syn::LitStr>() {
38 return Some(s.value());
39 }
40 if let Ok(id) = attr.parse_args::<syn::Ident>() {
42 return Some(id.to_string());
43 }
44 }
45 }
46 None
47}
48
49fn 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 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 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 let (inner_size, inner_align, _) = rust_type_size_align(inner, arch);
121 let sz = if inner_size == 0 {
122 1 } 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 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 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 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
202fn 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" | "NonNull" | "Arc" | "Rc" => {
230 let ps = arch.pointer_size;
231 Some((ps, ps))
232 }
233 _ => None,
234 }
235 }
236 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 "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), "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" | "Saturating" => (ps, ps),
272
273 "MaybeUninit" | "UnsafeCell" => (ps, ps),
277
278 "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 "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 "Box" | "Rc" | "Arc" | "Weak" | "NonNull" | "Cell" => (ps, ps),
295
296 "RefCell" | "Mutex" | "RwLock" => (ps, ps),
300
301 "Sender" | "Receiver" | "SyncSender" => (ps, ps),
303
304 "PhantomData" | "PhantomPinned" => (0, 1),
306
307 "Duration" => (16, 8),
310 "Instant" | "SystemTime" => (16, 8),
311
312 "Pin" => (ps, ps),
314
315 "__m64" => (8, 8),
317 "__m128" | "__m128d" | "__m128i" => (16, 16),
318 "__m256" | "__m256d" | "__m256i" => (32, 32),
319 "__m512" | "__m512d" | "__m512i" => (64, 64),
320
321 "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 _ => (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
345fn 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
353fn 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
367fn 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 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 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, suppressed_findings: Vec::new(), uncertain_fields: Vec::new(),
450 }
451}
452
453struct 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); 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 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 (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 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 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 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 let all_unit = node
569 .variants
570 .iter()
571 .all(|v| matches!(v.fields, Fields::Unit));
572
573 if all_unit {
574 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 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 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
705pub 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#[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); assert_eq!(l.fields[1].size, 8); assert_eq!(l.fields[2].size, 4); }
737
738 #[test]
739 fn layout_includes_padding() {
740 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); 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); 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); }
775
776 #[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 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 #[test]
892 fn vec_field_has_three_pointer_size() {
893 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); }
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 #[test]
956 fn generic_struct_is_skipped() {
957 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 #[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); 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 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); assert_eq!(l.fields[0].size, 2);
1011 }
1012
1013 #[test]
1014 fn data_enum_total_size_covers_largest_variant() {
1015 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 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); }
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 #[test]
1074 fn enum_with_only_zero_sized_variants_has_payload_size_zero() {
1075 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 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); }
1091
1092 #[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 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 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 let src = "struct S { a: u8, b: u32 }";
1131 let layouts = parse_rust(src, &X86_64_SYSV).unwrap();
1132 let l = &layouts[0];
1133 assert_eq!(l.total_size, 8);
1135 assert_eq!(l.align, 4);
1136 }
1137
1138 #[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 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 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); assert_eq!(l.fields[1].offset, 8); 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 #[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); assert_eq!(l.fields[1].size, 4); assert_eq!(l.fields[2].size, 1); assert_eq!(l.total_size, 16);
1207 }
1208
1209 #[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 #[test]
1268 fn box_dyn_trait_is_fat_pointer() {
1269 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 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 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 #[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 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); assert_eq!(l.fields[1].size, 2); assert_eq!(l.total_size, 4);
1371 }
1372
1373 #[test]
1374 fn struct_with_box_dyn_has_correct_layout() {
1375 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); assert_eq!(l.fields[1].size, 16); assert_eq!(l.fields[1].offset, 8); assert_eq!(l.total_size, 24);
1383 }
1384}