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 type_node_size_align(source: &str, node: Node<'_>, arch: &'static ArchConfig) -> (usize, usize) {
66 match node.kind() {
67 "builtin_type" | "identifier" => {
68 let text = source[node.byte_range()].trim();
69 zig_type_size_align(text, arch)
70 }
71 "pointer_type" => (arch.pointer_size, arch.pointer_size),
73 "nullable_type" => {
76 if let Some(inner) = find_child_by_kinds(node, &["pointer_type"]) {
78 let _ = inner; (arch.pointer_size, arch.pointer_size)
80 } else if let Some(inner) = find_first_type_child(source, node) {
81 let (sz, al) = type_node_size_align(source, inner, arch);
82 let tagged = (sz + 1).next_multiple_of(al.max(1));
84 (tagged, al.max(1))
85 } else {
86 (arch.pointer_size, arch.pointer_size)
87 }
88 }
89 "slice_type" => (arch.pointer_size * 2, arch.pointer_size),
91 "array_type" => {
93 if let Some((count, elem_sz, elem_al)) = parse_array_type(source, node, arch) {
94 (elem_sz * count, elem_al)
95 } else {
96 (arch.pointer_size, arch.pointer_size)
97 }
98 }
99 "error_union" => (arch.pointer_size * 2, arch.pointer_size),
101 _ => (arch.pointer_size, arch.pointer_size),
102 }
103}
104
105fn parse_array_type(
107 source: &str,
108 node: Node<'_>,
109 arch: &'static ArchConfig,
110) -> Option<(usize, usize, usize)> {
111 let mut count: Option<usize> = None;
113 let mut elem: Option<(usize, usize)> = None;
114
115 for i in 0..node.child_count() {
116 let child = node.child(i)?;
117 match child.kind() {
118 "integer" | "integer_literal" => {
119 let text = source[child.byte_range()].trim();
120 count = text.parse::<usize>().ok();
121 }
122 "builtin_type" | "identifier" | "pointer_type" | "slice_type" | "array_type"
123 | "nullable_type" => {
124 elem = Some(type_node_size_align(source, child, arch));
125 }
126 _ => {}
127 }
128 }
129
130 let count = count?;
131 let (esz, eal) = elem.unwrap_or((arch.pointer_size, arch.pointer_size));
132 Some((count, esz, eal))
133}
134
135fn find_child_by_kinds<'a>(node: Node<'a>, kinds: &[&str]) -> Option<Node<'a>> {
136 for i in 0..node.child_count() {
137 if let Some(c) = node.child(i)
138 && kinds.contains(&c.kind())
139 {
140 return Some(c);
141 }
142 }
143 None
144}
145
146fn find_first_type_child<'a>(source: &str, node: Node<'a>) -> Option<Node<'a>> {
147 let _ = source;
148 for i in 0..node.child_count() {
149 if let Some(c) = node.child(i) {
150 match c.kind() {
151 "builtin_type" | "identifier" | "pointer_type" | "slice_type" | "array_type"
152 | "nullable_type" | "error_union" => return Some(c),
153 _ => {}
154 }
155 }
156 }
157 None
158}
159
160fn extract_structs(source: &str, root: Node<'_>, arch: &'static ArchConfig) -> Vec<StructLayout> {
163 let mut layouts = Vec::new();
164 let mut stack = vec![root];
165
166 while let Some(node) = stack.pop() {
167 for i in (0..node.child_count()).rev() {
168 if let Some(c) = node.child(i) {
169 stack.push(c);
170 }
171 }
172
173 if node.kind() == "variable_declaration"
174 && let Some(layout) = parse_variable_declaration(source, node, arch)
175 {
176 layouts.push(layout);
177 }
178 }
179 layouts
180}
181
182fn parse_variable_declaration(
183 source: &str,
184 node: Node<'_>,
185 arch: &'static ArchConfig,
186) -> Option<StructLayout> {
187 let source_line = node.start_position().row as u32 + 1;
188 let decl_start_byte = node.start_byte();
189 let mut name: Option<String> = None;
190 let mut struct_node: Option<Node> = None;
191 let mut union_node: Option<Node> = None;
192
193 for i in 0..node.child_count() {
194 let child = node.child(i)?;
195 match child.kind() {
196 "identifier" => {
197 if name.is_none() {
199 name = Some(source[child.byte_range()].to_string());
200 }
201 }
202 "struct_declaration" => struct_node = Some(child),
203 "union_declaration" => union_node = Some(child),
204 _ => {}
205 }
206 }
207
208 let name = name?;
209 let mut layout = if let Some(sn) = struct_node {
210 parse_struct_declaration(source, sn, name, arch, source_line)?
211 } else if let Some(un) = union_node {
212 parse_union_declaration(source, un, name, arch, source_line)?
213 } else {
214 return None;
215 };
216 layout.suppressed_findings =
217 super::suppress::suppressed_from_preceding_source(source, decl_start_byte);
218 Some(layout)
219}
220
221fn parse_union_declaration(
229 source: &str,
230 node: Node<'_>,
231 name: String,
232 arch: &'static ArchConfig,
233 source_line: u32,
234) -> Option<StructLayout> {
235 let mut is_tagged = false;
236 let mut raw_fields: Vec<(String, String, usize, usize, u32)> = Vec::new();
237
238 for i in 0..node.child_count() {
239 let child = node.child(i)?;
240 match child.kind() {
241 "enum" => is_tagged = true,
243 "container_field" => {
247 if let Some(f) = parse_container_field(source, child, arch, false) {
248 raw_fields.push(f);
249 }
250 }
251 _ => {}
252 }
253 }
254
255 if raw_fields.is_empty() {
256 return None;
257 }
258
259 let max_size = raw_fields
261 .iter()
262 .map(|(_, _, sz, _, _)| *sz)
263 .max()
264 .unwrap_or(0);
265 let max_align = raw_fields
266 .iter()
267 .map(|(_, _, _, al, _)| *al)
268 .max()
269 .unwrap_or(1);
270 let total_size = if max_align > 0 {
271 max_size.next_multiple_of(max_align)
272 } else {
273 max_size
274 };
275
276 let mut fields: Vec<Field> = raw_fields
277 .into_iter()
278 .map(|(fname, type_text, size, align, field_line)| Field {
279 name: fname,
280 ty: TypeInfo::Primitive {
281 name: type_text,
282 size,
283 align,
284 },
285 offset: 0,
286 size,
287 align,
288 source_file: None,
289 source_line: Some(field_line),
290 access: padlock_core::ir::AccessPattern::Unknown,
291 })
292 .collect();
293
294 if is_tagged {
297 let n = fields.len();
298 let tag_size: usize = if n <= 256 {
299 1
300 } else if n <= 65536 {
301 2
302 } else {
303 4
304 };
305 fields.push(Field {
306 name: "__tag".to_string(),
307 ty: TypeInfo::Primitive {
308 name: format!("u{}", tag_size * 8),
309 size: tag_size,
310 align: tag_size,
311 },
312 offset: total_size, size: tag_size,
314 align: tag_size,
315 source_file: None,
316 source_line: None,
317 access: padlock_core::ir::AccessPattern::Unknown,
318 });
319 }
320
321 let struct_align = max_align; let final_size = if is_tagged {
324 let tag_size = fields.last().map(|f| f.size).unwrap_or(0);
325 (total_size + tag_size).next_multiple_of(struct_align.max(1))
326 } else {
327 total_size
328 };
329
330 Some(StructLayout {
331 name,
332 total_size: final_size,
333 align: struct_align,
334 fields,
335 source_file: None,
336 source_line: Some(source_line),
337 arch,
338 is_packed: false,
339 is_union: true,
340 is_repr_rust: false,
341 suppressed_findings: Vec::new(), })
343}
344
345fn parse_struct_declaration(
346 source: &str,
347 node: Node<'_>,
348 name: String,
349 arch: &'static ArchConfig,
350 source_line: u32,
351) -> Option<StructLayout> {
352 let mut is_packed = false;
353 let mut is_extern = false;
354 let mut raw_fields: Vec<(String, String, usize, usize, u32)> = Vec::new();
356
357 for i in 0..node.child_count() {
358 let child = node.child(i)?;
359 match child.kind() {
360 "packed" => is_packed = true,
361 "extern" => is_extern = true,
362 "container_field" => {
363 if let Some(f) = parse_container_field(source, child, arch, is_packed) {
364 raw_fields.push(f);
365 }
366 }
367 _ => {}
368 }
369 }
370
371 if raw_fields.is_empty() {
372 return None;
373 }
374
375 let mut offset = 0usize;
380 let mut struct_align = 1usize;
381 let mut fields: Vec<Field> = Vec::new();
382
383 for (fname, type_text, size, align, field_line) in raw_fields {
384 let eff_align = if is_packed { 1 } else { align };
385 if eff_align > 0 {
386 offset = offset.next_multiple_of(eff_align);
387 }
388 struct_align = struct_align.max(eff_align);
389 fields.push(Field {
390 name: fname,
391 ty: TypeInfo::Primitive {
392 name: type_text,
393 size,
394 align,
395 },
396 offset,
397 size,
398 align: eff_align,
399 source_file: None,
400 source_line: Some(field_line),
401 access: padlock_core::ir::AccessPattern::Unknown,
402 });
403 offset += size;
404 }
405
406 if !is_packed && struct_align > 0 {
407 offset = offset.next_multiple_of(struct_align);
408 }
409
410 let _ = is_extern; Some(StructLayout {
413 name,
414 total_size: offset,
415 align: struct_align,
416 fields,
417 source_file: None,
418 source_line: Some(source_line),
419 arch,
420 is_packed,
421 is_union: false,
422 is_repr_rust: false,
423 suppressed_findings: Vec::new(), })
425}
426
427fn parse_container_field(
429 source: &str,
430 node: Node<'_>,
431 arch: &'static ArchConfig,
432 is_packed: bool,
433) -> Option<(String, String, usize, usize, u32)> {
434 let mut field_name: Option<String> = None;
435 let mut type_text: Option<String> = None;
436 let mut size_align: Option<(usize, usize)> = None;
437
438 for i in 0..node.child_count() {
439 let child = node.child(i)?;
440 match child.kind() {
441 "identifier" if field_name.is_none() => {
442 field_name = Some(source[child.byte_range()].to_string());
443 }
444 "builtin_type" | "pointer_type" | "nullable_type" | "slice_type" | "array_type"
445 | "error_union" => {
446 let text = source[child.byte_range()].to_string();
447 size_align = Some(type_node_size_align(source, child, arch));
448 type_text = Some(text);
449 }
450 "identifier" => {
451 let text = source[child.byte_range()].trim().to_string();
453 size_align = Some(zig_type_size_align(&text, arch));
454 type_text = Some(text);
455 }
456 _ => {}
457 }
458 }
459
460 let name = field_name.filter(|n| !n.is_empty())?;
463 let ty = type_text.unwrap_or_else(|| "anyopaque".to_string());
464 let (mut size, align) = size_align.unwrap_or((arch.pointer_size, arch.pointer_size));
465 let field_line = node.start_position().row as u32 + 1;
466
467 if is_packed && size == 0 {
468 size = 0; }
470
471 Some((name, ty, size, align, field_line))
472}
473
474pub fn parse_zig(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
477 let mut parser = Parser::new();
478 parser.set_language(&tree_sitter_zig::LANGUAGE.into())?;
479 let tree = parser
480 .parse(source, None)
481 .ok_or_else(|| anyhow::anyhow!("tree-sitter-zig parse failed"))?;
482 Ok(extract_structs(source, tree.root_node(), arch))
483}
484
485#[cfg(test)]
488mod tests {
489 use super::*;
490 use padlock_core::arch::X86_64_SYSV;
491
492 #[test]
493 fn parse_simple_zig_struct() {
494 let src = "const Point = struct { x: u32, y: u32 };";
495 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
496 assert_eq!(layouts.len(), 1);
497 assert_eq!(layouts[0].name, "Point");
498 assert_eq!(layouts[0].fields.len(), 2);
499 assert_eq!(layouts[0].total_size, 8);
500 }
501
502 #[test]
503 fn zig_layout_with_padding() {
504 let src = "const T = struct { a: bool, b: u64 };";
505 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
506 assert_eq!(layouts.len(), 1);
507 let l = &layouts[0];
508 assert_eq!(l.fields[0].offset, 0); assert_eq!(l.fields[1].offset, 8); assert_eq!(l.total_size, 16);
511 }
512
513 #[test]
514 fn zig_packed_struct_no_padding() {
515 let src = "const Packed = packed struct { a: u8, b: u32 };";
516 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
517 assert_eq!(layouts.len(), 1);
518 let l = &layouts[0];
519 assert!(l.is_packed);
520 assert_eq!(l.fields[0].offset, 0);
521 assert_eq!(l.fields[1].offset, 1); assert_eq!(l.total_size, 5);
523 }
524
525 #[test]
526 fn zig_extern_struct_detected() {
527 let src = "const Extern = extern struct { x: i32, y: f64 };";
528 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
529 assert_eq!(layouts.len(), 1);
530 let l = &layouts[0];
531 assert_eq!(l.fields[0].offset, 0);
533 assert_eq!(l.fields[1].offset, 8);
534 assert_eq!(l.total_size, 16);
535 }
536
537 #[test]
538 fn zig_pointer_field_is_pointer_sized() {
539 let src = "const S = struct { ptr: *u8 };";
540 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
541 assert_eq!(layouts[0].fields[0].size, 8);
542 assert_eq!(layouts[0].fields[0].align, 8);
543 }
544
545 #[test]
546 fn zig_optional_pointer_is_pointer_sized() {
547 let src = "const S = struct { opt: ?*u8 };";
548 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
549 assert_eq!(layouts[0].fields[0].size, 8);
550 }
551
552 #[test]
553 fn zig_slice_is_two_words() {
554 let src = "const S = struct { buf: []u8 };";
555 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
556 assert_eq!(layouts[0].fields[0].size, 16); }
558
559 #[test]
560 fn zig_usize_follows_arch() {
561 let src = "const S = struct { n: usize };";
562 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
563 assert_eq!(layouts[0].fields[0].size, 8);
564 }
565
566 #[test]
567 fn zig_multiple_structs_parsed() {
568 let src = "const A = struct { x: u8 };\nconst B = struct { y: u64 };";
569 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
570 assert_eq!(layouts.len(), 2);
571 assert!(layouts.iter().any(|l| l.name == "A"));
572 assert!(layouts.iter().any(|l| l.name == "B"));
573 }
574
575 #[test]
576 fn zig_array_field_size() {
577 let src = "const S = struct { buf: [4]u32 };";
578 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
579 assert_eq!(layouts[0].fields[0].size, 16); }
581
582 #[test]
585 fn zig_bare_union_parsed_as_union() {
586 let src = "const U = union { a: u8, b: u32 };";
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.name, "U");
591 assert!(l.is_union, "union should have is_union=true");
592 }
593
594 #[test]
595 fn zig_bare_union_total_size_is_max_field() {
596 let src = "const U = union { a: u8, b: u32 };";
598 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
599 let l = &layouts[0];
600 assert_eq!(l.total_size, 4);
601 }
602
603 #[test]
604 fn zig_union_all_fields_at_offset_zero() {
605 let src = "const U = union { a: u8, b: u64 };";
606 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
607 let l = &layouts[0];
608 for field in &l.fields {
609 assert_eq!(
610 field.offset, 0,
611 "union field '{}' should be at offset 0",
612 field.name
613 );
614 }
615 }
616
617 #[test]
618 fn zig_tagged_union_has_tag_field() {
619 let src = "const T = union(enum) { ok: u32, err: void };";
620 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
621 let l = &layouts[0];
622 assert!(
623 l.fields.iter().any(|f| f.name == "__tag"),
624 "tagged union should have a synthetic __tag field"
625 );
626 }
627
628 #[test]
629 fn zig_tagged_union_size_includes_tag() {
630 let src = "const T = union(enum) { ok: u32, err: void };";
633 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
634 let l = &layouts[0];
635 assert_eq!(l.total_size, 8);
637 }
638
639 #[test]
640 fn zig_union_with_largest_field_u64() {
641 let src = "const U = union { a: u8, b: u64, c: u32 };";
643 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
644 let l = &layouts[0];
645 assert_eq!(l.total_size, 8);
646 assert_eq!(l.align, 8);
647 }
648
649 #[test]
650 fn zig_struct_and_union_in_same_file() {
651 let src = "const S = struct { x: u32 };\nconst U = union { a: u8, b: u32 };";
652 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
653 assert_eq!(layouts.len(), 2);
654 assert!(layouts.iter().any(|l| l.name == "S" && !l.is_union));
655 assert!(layouts.iter().any(|l| l.name == "U" && l.is_union));
656 }
657
658 #[test]
661 fn zig_empty_union_returns_none() {
662 let src = "const E = union {};";
664 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
665 assert!(layouts.is_empty(), "empty union should produce no layout");
666 }
667
668 #[test]
669 fn zig_union_no_padding_finding() {
670 let src = "const U = union { a: u8, b: u64 };";
672 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
673 let gaps = padlock_core::ir::find_padding(&layouts[0]);
674 assert!(
675 gaps.is_empty(),
676 "unions should have no padding gaps: {:?}",
677 gaps
678 );
679 }
680
681 #[test]
684 fn zig_c_interop_types_correct_size() {
685 assert_eq!(zig_type_size_align("c_char", &X86_64_SYSV), (1, 1));
686 assert_eq!(zig_type_size_align("c_short", &X86_64_SYSV), (2, 2));
687 assert_eq!(zig_type_size_align("c_ushort", &X86_64_SYSV), (2, 2));
688 assert_eq!(zig_type_size_align("c_int", &X86_64_SYSV), (4, 4));
689 assert_eq!(zig_type_size_align("c_uint", &X86_64_SYSV), (4, 4));
690 assert_eq!(zig_type_size_align("c_long", &X86_64_SYSV), (8, 8));
692 assert_eq!(zig_type_size_align("c_ulong", &X86_64_SYSV), (8, 8));
693 assert_eq!(zig_type_size_align("c_longlong", &X86_64_SYSV), (8, 8));
694 assert_eq!(zig_type_size_align("c_ulonglong", &X86_64_SYSV), (8, 8));
695 assert_eq!(zig_type_size_align("c_float", &X86_64_SYSV), (4, 4));
696 assert_eq!(zig_type_size_align("c_double", &X86_64_SYSV), (8, 8));
697 assert_eq!(zig_type_size_align("c_longdouble", &X86_64_SYSV), (16, 16));
698 }
699
700 #[test]
701 fn zig_arbitrary_width_integers() {
702 assert_eq!(zig_type_size_align("u1", &X86_64_SYSV), (1, 1));
704 assert_eq!(zig_type_size_align("u3", &X86_64_SYSV), (1, 1));
706 assert_eq!(zig_type_size_align("u9", &X86_64_SYSV), (2, 2));
708 assert_eq!(zig_type_size_align("u24", &X86_64_SYSV), (3, 4));
710 assert_eq!(zig_type_size_align("u48", &X86_64_SYSV), (6, 8));
712 assert_eq!(zig_type_size_align("i7", &X86_64_SYSV), (1, 1));
714 assert_eq!(zig_type_size_align("u129", &X86_64_SYSV), (17, 8));
716 }
717
718 #[test]
719 fn zig_struct_with_c_interop_types() {
720 let src = "const Header = extern struct { version: c_uint, length: c_ushort, flags: u8 };";
722 let layouts = parse_zig(src, &X86_64_SYSV).unwrap();
723 assert_eq!(layouts.len(), 1);
724 let l = &layouts[0];
725 assert_eq!(l.fields[0].size, 4); assert_eq!(l.fields[1].size, 2); assert_eq!(l.fields[2].size, 1); }
729}