1use padlock_core::arch::ArchConfig;
8use padlock_core::ir::{Field, StructLayout, TypeInfo};
9use tree_sitter::{Node, Parser};
10
11fn zig_type_size_align(ty: &str, arch: &'static ArchConfig) -> (usize, usize) {
14 let ty = ty.trim();
15 match ty {
16 "bool" => (1, 1),
17 "u8" | "i8" => (1, 1),
18 "u16" | "i16" | "f16" => (2, 2),
19 "u32" | "i32" | "f32" => (4, 4),
20 "u64" | "i64" | "f64" => (8, 8),
21 "u128" | "i128" | "f128" => (16, 16),
22 "f80" => (10, 16),
24 "usize" | "isize" => (arch.pointer_size, arch.pointer_size),
25 "void" | "anyopaque" => (0, 1),
26 "type" | "anytype" | "comptime_int" | "comptime_float" => {
28 (arch.pointer_size, arch.pointer_size)
29 }
30
31 "c_char" | "c_uchar" | "c_schar" => (1, 1),
33 "c_short" | "c_ushort" => (2, 2),
34 "c_int" | "c_uint" => (4, 4),
35 "c_long" | "c_ulong" => (arch.pointer_size, arch.pointer_size), "c_longlong" | "c_ulonglong" => (8, 8),
37 "c_float" => (4, 4),
38 "c_double" => (8, 8),
39 "c_longdouble" => (16, 16),
40
41 ty if (ty.starts_with('u') || ty.starts_with('i'))
47 && ty[1..].bytes().all(|b| b.is_ascii_digit())
48 && !ty[1..].is_empty() =>
49 {
50 if let Ok(bits) = ty[1..].parse::<usize>() {
51 let bytes = bits.div_ceil(8).max(1);
52 let align = bytes.next_power_of_two().min(8);
54 (bytes, align)
55 } else {
56 (arch.pointer_size, arch.pointer_size)
57 }
58 }
59
60 _ => (arch.pointer_size, arch.pointer_size),
61 }
62}
63
64fn zig_type_bit_width(ty: &str, arch: &'static ArchConfig) -> usize {
70 let ty = ty.trim();
71 if (ty.starts_with('u') || ty.starts_with('i'))
73 && !ty[1..].is_empty()
74 && ty[1..].bytes().all(|b| b.is_ascii_digit())
75 && let Ok(bits) = ty[1..].parse::<usize>()
76 {
77 return bits;
78 }
79 match ty {
80 "bool" => 1,
81 "f16" => 16,
82 "f32" => 32,
83 "f64" => 64,
84 "f80" => 80,
85 "f128" => 128,
86 _ => {
87 let (bytes, _) = zig_type_size_align(ty, arch);
89 bytes * 8
90 }
91 }
92}
93
94fn type_node_size_align(source: &str, node: Node<'_>, arch: &'static ArchConfig) -> (usize, usize) {
96 match node.kind() {
97 "builtin_type" | "identifier" => {
98 let text = source[node.byte_range()].trim();
99 zig_type_size_align(text, arch)
100 }
101 "pointer_type" => (arch.pointer_size, arch.pointer_size),
103 "nullable_type" => {
106 if let Some(inner) = find_child_by_kinds(node, &["pointer_type"]) {
108 let _ = inner; (arch.pointer_size, arch.pointer_size)
110 } else if let Some(inner) = find_first_type_child(source, node) {
111 let (sz, al) = type_node_size_align(source, inner, arch);
112 let tagged = (sz + 1).next_multiple_of(al.max(1));
114 (tagged, al.max(1))
115 } else {
116 (arch.pointer_size, arch.pointer_size)
117 }
118 }
119 "slice_type" => (arch.pointer_size * 2, arch.pointer_size),
121 "array_type" => {
123 if let Some((count, elem_sz, elem_al)) = parse_array_type(source, node, arch) {
124 (elem_sz * count, elem_al)
125 } else {
126 (arch.pointer_size, arch.pointer_size)
127 }
128 }
129 "error_union" => (arch.pointer_size * 2, arch.pointer_size),
131 _ => (arch.pointer_size, arch.pointer_size),
132 }
133}
134
135fn parse_array_type(
137 source: &str,
138 node: Node<'_>,
139 arch: &'static ArchConfig,
140) -> Option<(usize, usize, usize)> {
141 let mut count: Option<usize> = None;
143 let mut elem: Option<(usize, usize)> = None;
144
145 for i in 0..node.child_count() {
146 let child = node.child(i)?;
147 match child.kind() {
148 "integer" | "integer_literal" => {
149 let text = source[child.byte_range()].trim();
150 count = text.parse::<usize>().ok();
151 }
152 "builtin_type" | "identifier" | "pointer_type" | "slice_type" | "array_type"
153 | "nullable_type" => {
154 elem = Some(type_node_size_align(source, child, arch));
155 }
156 _ => {}
157 }
158 }
159
160 let count = count?;
161 let (esz, eal) = elem.unwrap_or((arch.pointer_size, arch.pointer_size));
162 Some((count, esz, eal))
163}
164
165fn find_child_by_kinds<'a>(node: Node<'a>, kinds: &[&str]) -> Option<Node<'a>> {
166 for i in 0..node.child_count() {
167 if let Some(c) = node.child(i)
168 && kinds.contains(&c.kind())
169 {
170 return Some(c);
171 }
172 }
173 None
174}
175
176fn find_first_type_child<'a>(source: &str, node: Node<'a>) -> Option<Node<'a>> {
177 let _ = source;
178 for i in 0..node.child_count() {
179 if let Some(c) = node.child(i) {
180 match c.kind() {
181 "builtin_type" | "identifier" | "pointer_type" | "slice_type" | "array_type"
182 | "nullable_type" | "error_union" => return Some(c),
183 _ => {}
184 }
185 }
186 }
187 None
188}
189
190fn extract_structs(source: &str, root: Node<'_>, arch: &'static ArchConfig) -> Vec<StructLayout> {
193 let mut layouts = Vec::new();
194 let mut stack = vec![root];
195
196 while let Some(node) = stack.pop() {
197 for i in (0..node.child_count()).rev() {
198 if let Some(c) = node.child(i) {
199 stack.push(c);
200 }
201 }
202
203 if node.kind() == "variable_declaration"
204 && let Some(layout) = parse_variable_declaration(source, node, arch)
205 {
206 layouts.push(layout);
207 }
208 }
209 layouts
210}
211
212fn parse_variable_declaration(
213 source: &str,
214 node: Node<'_>,
215 arch: &'static ArchConfig,
216) -> Option<StructLayout> {
217 let source_line = node.start_position().row as u32 + 1;
218 let decl_start_byte = node.start_byte();
219 let mut name: Option<String> = None;
220 let mut struct_node: Option<Node> = None;
221 let mut union_node: Option<Node> = None;
222
223 for i in 0..node.child_count() {
224 let child = node.child(i)?;
225 match child.kind() {
226 "identifier" if name.is_none() => {
227 name = Some(source[child.byte_range()].to_string());
229 }
230 "struct_declaration" => struct_node = Some(child),
231 "union_declaration" => union_node = Some(child),
232 _ => {}
233 }
234 }
235
236 let name = name?;
237 let mut layout = if let Some(sn) = struct_node {
238 parse_struct_declaration(source, sn, name, arch, source_line)?
239 } else if let Some(un) = union_node {
240 parse_union_declaration(source, un, name, arch, source_line)?
241 } else {
242 return None;
243 };
244 layout.suppressed_findings =
245 super::suppress::suppressed_from_preceding_source(source, decl_start_byte);
246 Some(layout)
247}
248
249fn parse_union_declaration(
257 source: &str,
258 node: Node<'_>,
259 name: String,
260 arch: &'static ArchConfig,
261 source_line: u32,
262) -> Option<StructLayout> {
263 let mut is_tagged = false;
264 let mut raw_fields: Vec<(String, String, usize, usize, u32)> = Vec::new();
265
266 for i in 0..node.child_count() {
267 let child = node.child(i)?;
268 match child.kind() {
269 "enum" => is_tagged = true,
271 "container_field" => {
275 if let Some(f) = parse_container_field(source, child, arch, false) {
276 raw_fields.push(f);
277 }
278 }
279 _ => {}
280 }
281 }
282
283 if raw_fields.is_empty() {
284 return None;
285 }
286
287 let max_size = raw_fields
289 .iter()
290 .map(|(_, _, sz, _, _)| *sz)
291 .max()
292 .unwrap_or(0);
293 let max_align = raw_fields
294 .iter()
295 .map(|(_, _, _, al, _)| *al)
296 .max()
297 .unwrap_or(1);
298 let total_size = if max_align > 0 {
299 max_size.next_multiple_of(max_align)
300 } else {
301 max_size
302 };
303
304 let mut fields: Vec<Field> = raw_fields
305 .into_iter()
306 .map(|(fname, type_text, size, align, field_line)| Field {
307 name: fname,
308 ty: TypeInfo::Primitive {
309 name: type_text,
310 size,
311 align,
312 },
313 offset: 0,
314 size,
315 align,
316 source_file: None,
317 source_line: Some(field_line),
318 access: padlock_core::ir::AccessPattern::Unknown,
319 })
320 .collect();
321
322 if is_tagged {
325 let n = fields.len();
326 let tag_size: usize = if n <= 256 {
327 1
328 } else if n <= 65536 {
329 2
330 } else {
331 4
332 };
333 fields.push(Field {
334 name: "__tag".to_string(),
335 ty: TypeInfo::Primitive {
336 name: format!("u{}", tag_size * 8),
337 size: tag_size,
338 align: tag_size,
339 },
340 offset: total_size, size: tag_size,
342 align: tag_size,
343 source_file: None,
344 source_line: None,
345 access: padlock_core::ir::AccessPattern::Unknown,
346 });
347 }
348
349 let struct_align = max_align; let final_size = if is_tagged {
352 let tag_size = fields.last().map(|f| f.size).unwrap_or(0);
353 (total_size + tag_size).next_multiple_of(struct_align.max(1))
354 } else {
355 total_size
356 };
357
358 Some(StructLayout {
359 name,
360 total_size: final_size,
361 align: struct_align,
362 fields,
363 source_file: None,
364 source_line: Some(source_line),
365 arch,
366 is_packed: false,
367 is_union: true,
368 is_repr_rust: false,
369 suppressed_findings: Vec::new(), uncertain_fields: Vec::new(),
371 })
372}
373
374fn parse_struct_declaration(
375 source: &str,
376 node: Node<'_>,
377 name: String,
378 arch: &'static ArchConfig,
379 source_line: u32,
380) -> Option<StructLayout> {
381 let mut is_packed = false;
382 let mut is_extern = false;
383 let mut raw_fields: Vec<(String, String, usize, usize, u32)> = Vec::new();
385
386 for i in 0..node.child_count() {
387 let child = node.child(i)?;
388 match child.kind() {
389 "packed" => is_packed = true,
390 "extern" => is_extern = true,
391 "container_field" => {
392 if let Some(f) = parse_container_field(source, child, arch, is_packed) {
393 raw_fields.push(f);
394 }
395 }
396 _ => {}
397 }
398 }
399
400 if raw_fields.is_empty() {
401 return None;
402 }
403
404 let mut offset = 0usize;
409 let mut struct_align = 1usize;
410 let mut fields: Vec<Field> = Vec::new();
411
412 if is_packed {
413 let mut bit_offset = 0usize;
417 for (fname, type_text, _byte_size, _byte_align, field_line) in &raw_fields {
418 let bit_width = zig_type_bit_width(type_text, arch);
419 let byte_offset = bit_offset / 8;
420 let byte_size = bit_width
421 .div_ceil(8)
422 .max(if bit_width == 0 { 0 } else { 1 });
423 fields.push(Field {
424 name: fname.clone(),
425 ty: TypeInfo::Primitive {
426 name: type_text.clone(),
427 size: byte_size,
428 align: 1,
429 },
430 offset: byte_offset,
431 size: byte_size,
432 align: 1,
433 source_file: None,
434 source_line: Some(*field_line),
435 access: padlock_core::ir::AccessPattern::Unknown,
436 });
437 bit_offset += bit_width;
438 }
439 offset = bit_offset.div_ceil(8);
440 struct_align = 1;
441 } else {
442 for (fname, type_text, size, align, field_line) in raw_fields {
443 if align > 0 {
444 offset = offset.next_multiple_of(align);
445 }
446 struct_align = struct_align.max(align);
447 fields.push(Field {
448 name: fname,
449 ty: TypeInfo::Primitive {
450 name: type_text,
451 size,
452 align,
453 },
454 offset,
455 size,
456 align,
457 source_file: None,
458 source_line: Some(field_line),
459 access: padlock_core::ir::AccessPattern::Unknown,
460 });
461 offset += size;
462 }
463 if struct_align > 0 {
464 offset = offset.next_multiple_of(struct_align);
465 }
466 }
467
468 let _ = is_extern; Some(StructLayout {
471 name,
472 total_size: offset,
473 align: struct_align,
474 fields,
475 source_file: None,
476 source_line: Some(source_line),
477 arch,
478 is_packed,
479 is_union: false,
480 is_repr_rust: false,
481 suppressed_findings: Vec::new(), uncertain_fields: Vec::new(),
483 })
484}
485
486fn parse_container_field(
488 source: &str,
489 node: Node<'_>,
490 arch: &'static ArchConfig,
491 is_packed: bool,
492) -> Option<(String, String, usize, usize, u32)> {
493 let mut field_name: Option<String> = None;
494 let mut type_text: Option<String> = None;
495 let mut size_align: Option<(usize, usize)> = None;
496
497 for i in 0..node.child_count() {
498 let child = node.child(i)?;
499 match child.kind() {
500 "identifier" if field_name.is_none() => {
501 field_name = Some(source[child.byte_range()].to_string());
502 }
503 "builtin_type" | "pointer_type" | "nullable_type" | "slice_type" | "array_type"
504 | "error_union" => {
505 let text = source[child.byte_range()].to_string();
506 size_align = Some(type_node_size_align(source, child, arch));
507 type_text = Some(text);
508 }
509 "identifier" => {
510 let text = source[child.byte_range()].trim().to_string();
512 size_align = Some(zig_type_size_align(&text, arch));
513 type_text = Some(text);
514 }
515 _ => {}
516 }
517 }
518
519 let name = field_name.filter(|n| !n.is_empty())?;
522 let ty = type_text.unwrap_or_else(|| "anyopaque".to_string());
523 let (mut size, align) = size_align.unwrap_or((arch.pointer_size, arch.pointer_size));
524 let field_line = node.start_position().row as u32 + 1;
525
526 if is_packed && size == 0 {
527 size = 0; }
529
530 Some((name, ty, size, align, field_line))
531}
532
533pub fn parse_zig(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
536 let mut parser = Parser::new();
537 parser.set_language(&tree_sitter_zig::LANGUAGE.into())?;
538 let tree = parser
539 .parse(source, None)
540 .ok_or_else(|| anyhow::anyhow!("tree-sitter-zig parse failed"))?;
541 Ok(extract_structs(source, tree.root_node(), arch))
542}
543
544#[cfg(test)]
547mod tests {
548 use super::*;
549 use padlock_core::arch::X86_64_SYSV;
550
551 #[test]
552 fn parse_simple_zig_struct() {
553 let src = "const Point = struct { x: u32, y: u32 };";
554 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
555 assert_eq!(layouts.len(), 1);
556 assert_eq!(layouts[0].name, "Point");
557 assert_eq!(layouts[0].fields.len(), 2);
558 assert_eq!(layouts[0].total_size, 8);
559 }
560
561 #[test]
562 fn zig_layout_with_padding() {
563 let src = "const T = struct { a: bool, b: u64 };";
564 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
565 assert_eq!(layouts.len(), 1);
566 let l = &layouts[0];
567 assert_eq!(l.fields[0].offset, 0); assert_eq!(l.fields[1].offset, 8); assert_eq!(l.total_size, 16);
570 }
571
572 #[test]
573 fn zig_packed_struct_no_padding() {
574 let src = "const Packed = packed struct { a: u8, b: u32 };";
575 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
576 assert_eq!(layouts.len(), 1);
577 let l = &layouts[0];
578 assert!(l.is_packed);
579 assert_eq!(l.fields[0].offset, 0);
580 assert_eq!(l.fields[1].offset, 1); assert_eq!(l.total_size, 5);
582 }
583
584 #[test]
585 fn zig_extern_struct_detected() {
586 let src = "const Extern = extern struct { x: i32, y: f64 };";
587 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
588 assert_eq!(layouts.len(), 1);
589 let l = &layouts[0];
590 assert_eq!(l.fields[0].offset, 0);
592 assert_eq!(l.fields[1].offset, 8);
593 assert_eq!(l.total_size, 16);
594 }
595
596 #[test]
597 fn zig_pointer_field_is_pointer_sized() {
598 let src = "const S = struct { ptr: *u8 };";
599 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
600 assert_eq!(layouts[0].fields[0].size, 8);
601 assert_eq!(layouts[0].fields[0].align, 8);
602 }
603
604 #[test]
605 fn zig_optional_pointer_is_pointer_sized() {
606 let src = "const S = struct { opt: ?*u8 };";
607 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
608 assert_eq!(layouts[0].fields[0].size, 8);
609 }
610
611 #[test]
612 fn zig_slice_is_two_words() {
613 let src = "const S = struct { buf: []u8 };";
614 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
615 assert_eq!(layouts[0].fields[0].size, 16); }
617
618 #[test]
619 fn zig_usize_follows_arch() {
620 let src = "const S = struct { n: usize };";
621 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
622 assert_eq!(layouts[0].fields[0].size, 8);
623 }
624
625 #[test]
626 fn zig_multiple_structs_parsed() {
627 let src = "const A = struct { x: u8 };\nconst B = struct { y: u64 };";
628 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
629 assert_eq!(layouts.len(), 2);
630 assert!(layouts.iter().any(|l| l.name == "A"));
631 assert!(layouts.iter().any(|l| l.name == "B"));
632 }
633
634 #[test]
635 fn zig_array_field_size() {
636 let src = "const S = struct { buf: [4]u32 };";
637 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
638 assert_eq!(layouts[0].fields[0].size, 16); }
640
641 #[test]
644 fn zig_bare_union_parsed_as_union() {
645 let src = "const U = union { a: u8, b: u32 };";
646 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
647 assert_eq!(layouts.len(), 1);
648 let l = &layouts[0];
649 assert_eq!(l.name, "U");
650 assert!(l.is_union, "union should have is_union=true");
651 }
652
653 #[test]
654 fn zig_bare_union_total_size_is_max_field() {
655 let src = "const U = union { a: u8, b: u32 };";
657 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
658 let l = &layouts[0];
659 assert_eq!(l.total_size, 4);
660 }
661
662 #[test]
663 fn zig_union_all_fields_at_offset_zero() {
664 let src = "const U = union { a: u8, b: u64 };";
665 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
666 let l = &layouts[0];
667 for field in &l.fields {
668 assert_eq!(
669 field.offset, 0,
670 "union field '{}' should be at offset 0",
671 field.name
672 );
673 }
674 }
675
676 #[test]
677 fn zig_tagged_union_has_tag_field() {
678 let src = "const T = union(enum) { ok: u32, err: void };";
679 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
680 let l = &layouts[0];
681 assert!(
682 l.fields.iter().any(|f| f.name == "__tag"),
683 "tagged union should have a synthetic __tag field"
684 );
685 }
686
687 #[test]
688 fn zig_tagged_union_size_includes_tag() {
689 let src = "const T = union(enum) { ok: u32, err: void };";
692 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
693 let l = &layouts[0];
694 assert_eq!(l.total_size, 8);
696 }
697
698 #[test]
699 fn zig_union_with_largest_field_u64() {
700 let src = "const U = union { a: u8, b: u64, c: u32 };";
702 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
703 let l = &layouts[0];
704 assert_eq!(l.total_size, 8);
705 assert_eq!(l.align, 8);
706 }
707
708 #[test]
709 fn zig_struct_and_union_in_same_file() {
710 let src = "const S = struct { x: u32 };\nconst U = union { a: u8, b: u32 };";
711 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
712 assert_eq!(layouts.len(), 2);
713 assert!(layouts.iter().any(|l| l.name == "S" && !l.is_union));
714 assert!(layouts.iter().any(|l| l.name == "U" && l.is_union));
715 }
716
717 #[test]
720 fn zig_empty_union_returns_none() {
721 let src = "const E = union {};";
723 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
724 assert!(layouts.is_empty(), "empty union should produce no layout");
725 }
726
727 #[test]
728 fn zig_union_no_padding_finding() {
729 let src = "const U = union { a: u8, b: u64 };";
731 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
732 let gaps = padlock_core::ir::find_padding(&layouts[0]);
733 assert!(
734 gaps.is_empty(),
735 "unions should have no padding gaps: {:?}",
736 gaps
737 );
738 }
739
740 #[test]
743 fn zig_c_interop_types_correct_size() {
744 assert_eq!(zig_type_size_align("c_char", &X86_64_SYSV), (1, 1));
745 assert_eq!(zig_type_size_align("c_short", &X86_64_SYSV), (2, 2));
746 assert_eq!(zig_type_size_align("c_ushort", &X86_64_SYSV), (2, 2));
747 assert_eq!(zig_type_size_align("c_int", &X86_64_SYSV), (4, 4));
748 assert_eq!(zig_type_size_align("c_uint", &X86_64_SYSV), (4, 4));
749 assert_eq!(zig_type_size_align("c_long", &X86_64_SYSV), (8, 8));
751 assert_eq!(zig_type_size_align("c_ulong", &X86_64_SYSV), (8, 8));
752 assert_eq!(zig_type_size_align("c_longlong", &X86_64_SYSV), (8, 8));
753 assert_eq!(zig_type_size_align("c_ulonglong", &X86_64_SYSV), (8, 8));
754 assert_eq!(zig_type_size_align("c_float", &X86_64_SYSV), (4, 4));
755 assert_eq!(zig_type_size_align("c_double", &X86_64_SYSV), (8, 8));
756 assert_eq!(zig_type_size_align("c_longdouble", &X86_64_SYSV), (16, 16));
757 }
758
759 #[test]
760 fn zig_arbitrary_width_integers() {
761 assert_eq!(zig_type_size_align("u1", &X86_64_SYSV), (1, 1));
763 assert_eq!(zig_type_size_align("u3", &X86_64_SYSV), (1, 1));
765 assert_eq!(zig_type_size_align("u9", &X86_64_SYSV), (2, 2));
767 assert_eq!(zig_type_size_align("u24", &X86_64_SYSV), (3, 4));
769 assert_eq!(zig_type_size_align("u48", &X86_64_SYSV), (6, 8));
771 assert_eq!(zig_type_size_align("i7", &X86_64_SYSV), (1, 1));
773 assert_eq!(zig_type_size_align("u129", &X86_64_SYSV), (17, 8));
775 }
776
777 #[test]
778 fn zig_struct_with_c_interop_types() {
779 let src = "const Header = extern struct { version: c_uint, length: c_ushort, flags: u8 };";
781 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
782 assert_eq!(layouts.len(), 1);
783 let l = &layouts[0];
784 assert_eq!(l.fields[0].size, 4); assert_eq!(l.fields[1].size, 2); assert_eq!(l.fields[2].size, 1); }
788}