1use padlock_core::arch::ArchConfig;
7use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
8use std::collections::HashSet;
9use tree_sitter::{Node, Parser};
10
11fn go_type_size_align(ty: &str, arch: &'static ArchConfig) -> (usize, usize) {
14 match ty.trim() {
15 "bool" => (1, 1),
16 "int8" | "uint8" | "byte" => (1, 1),
17 "int16" | "uint16" => (2, 2),
18 "int32" | "uint32" | "rune" | "float32" => (4, 4),
19 "int64" | "uint64" | "float64" | "complex64" => (8, 8),
20 "complex128" => (16, 16),
21 "int" | "uint" => (arch.pointer_size, arch.pointer_size),
22 "uintptr" => (arch.pointer_size, arch.pointer_size),
23 "string" => (arch.pointer_size * 2, arch.pointer_size), ty if ty.starts_with("[]") => (arch.pointer_size * 3, arch.pointer_size), ty if ty.starts_with("map[") || ty.starts_with("chan ") => {
26 (arch.pointer_size, arch.pointer_size)
27 }
28 ty if ty.starts_with('*') => (arch.pointer_size, arch.pointer_size),
29 "error" | "any" => (arch.pointer_size * 2, arch.pointer_size),
42 ty if ty.starts_with("interface") => (arch.pointer_size * 2, arch.pointer_size),
43 _ => (arch.pointer_size, arch.pointer_size),
44 }
45}
46
47fn collect_go_interface_names(source: &str, root: Node<'_>) -> HashSet<String> {
56 let mut names = HashSet::new();
57 let mut stack = vec![root];
58 while let Some(node) = stack.pop() {
59 for i in (0..node.child_count()).rev() {
60 if let Some(child) = node.child(i) {
61 stack.push(child);
62 }
63 }
64 if node.kind() != "type_spec" {
65 continue;
66 }
67 let mut iface_name: Option<String> = None;
69 let mut is_interface = false;
70 for i in 0..node.child_count() {
71 let Some(child) = node.child(i) else { continue };
72 match child.kind() {
73 "type_identifier" => {
74 iface_name = Some(source[child.byte_range()].to_string());
75 }
76 "interface_type" => {
77 is_interface = true;
78 }
79 _ => {}
80 }
81 }
82 if is_interface && let Some(name) = iface_name {
83 names.insert(name);
84 }
85 }
86 names
87}
88
89fn extract_structs(source: &str, root: Node<'_>, arch: &'static ArchConfig) -> Vec<StructLayout> {
92 let local_interfaces = collect_go_interface_names(source, root);
94
95 let mut layouts = Vec::new();
96 let mut stack = vec![root];
97
98 while let Some(node) = stack.pop() {
99 for i in (0..node.child_count()).rev() {
100 if let Some(c) = node.child(i) {
101 stack.push(c);
102 }
103 }
104
105 if node.kind() == "type_declaration"
107 && let Some(layout) = parse_type_declaration(source, node, arch, &local_interfaces)
108 {
109 layouts.push(layout);
110 }
111 }
112 layouts
113}
114
115fn parse_type_declaration(
116 source: &str,
117 node: Node<'_>,
118 arch: &'static ArchConfig,
119 local_interfaces: &HashSet<String>,
120) -> Option<StructLayout> {
121 let source_line = node.start_position().row as u32 + 1;
122 let decl_start_byte = node.start_byte();
123 for i in 0..node.child_count() {
125 let child = node.child(i)?;
126 if child.kind() == "type_spec" {
127 return parse_type_spec(
128 source,
129 child,
130 arch,
131 source_line,
132 decl_start_byte,
133 local_interfaces,
134 );
135 }
136 }
137 None
138}
139
140fn parse_type_spec(
141 source: &str,
142 node: Node<'_>,
143 arch: &'static ArchConfig,
144 source_line: u32,
145 decl_start_byte: usize,
146 local_interfaces: &HashSet<String>,
147) -> Option<StructLayout> {
148 let mut name: Option<String> = None;
149 let mut struct_node: Option<Node> = None;
150 let mut is_generic = false;
151
152 for i in 0..node.child_count() {
153 let child = node.child(i)?;
154 match child.kind() {
155 "type_identifier" => name = Some(source[child.byte_range()].to_string()),
156 "struct_type" => struct_node = Some(child),
157 "type_parameter_list" => is_generic = true,
158 _ => {}
159 }
160 }
161
162 let name = name?;
163
164 if is_generic {
165 eprintln!(
166 "padlock: note: skipping '{name}' — generic struct \
167 (layout depends on type arguments; use binary analysis for accurate results)"
168 );
169 crate::record_skipped(
170 &name,
171 "generic struct — layout depends on type arguments; \
172 use binary analysis for accurate results",
173 );
174 return None;
175 }
176
177 let struct_node = struct_node?;
178 parse_struct_type(
179 source,
180 struct_node,
181 name,
182 arch,
183 source_line,
184 decl_start_byte,
185 local_interfaces,
186 )
187}
188
189fn parse_struct_type(
190 source: &str,
191 node: Node<'_>,
192 name: String,
193 arch: &'static ArchConfig,
194 source_line: u32,
195 decl_start_byte: usize,
196 local_interfaces: &HashSet<String>,
197) -> Option<StructLayout> {
198 let mut raw_fields: Vec<(String, String, Option<String>, u32)> = Vec::new();
199
200 for i in 0..node.child_count() {
201 let child = node.child(i)?;
202 if child.kind() == "field_declaration_list" {
203 for j in 0..child.child_count() {
204 let field_node = child.child(j)?;
205 if field_node.kind() == "field_declaration" {
206 collect_field_declarations(source, field_node, &mut raw_fields);
207 }
208 }
209 }
210 }
211
212 if raw_fields.is_empty() {
213 return None;
214 }
215
216 let mut offset = 0usize;
218 let mut struct_align = 1usize;
219 let mut fields: Vec<Field> = Vec::new();
220 let mut uncertain_fields: Vec<String> = Vec::new();
221
222 for (fname, ty_name, guard, field_line) in raw_fields {
223 let (mut size, mut align) = go_type_size_align(&ty_name, arch);
224
225 if local_interfaces.contains(ty_name.as_str()) {
228 size = arch.pointer_size * 2;
229 align = arch.pointer_size;
230 }
231
232 let is_pointer = ty_name.starts_with('*');
237 let base_ty = ty_name.trim_start_matches('*');
238 if !is_pointer && base_ty.contains('.') {
239 uncertain_fields.push(fname.clone());
240 }
241
242 if align > 0 {
243 offset = offset.next_multiple_of(align);
244 }
245 struct_align = struct_align.max(align);
246 let access = if let Some(g) = guard {
247 AccessPattern::Concurrent {
248 guard: Some(g),
249 is_atomic: false,
250 is_annotated: true,
251 }
252 } else {
253 AccessPattern::Unknown
254 };
255 fields.push(Field {
256 name: fname,
257 ty: TypeInfo::Primitive {
258 name: ty_name,
259 size,
260 align,
261 },
262 offset,
263 size,
264 align,
265 source_file: None,
266 source_line: Some(field_line),
267 access,
268 });
269 offset += size;
270 }
271 if struct_align > 0 {
272 offset = offset.next_multiple_of(struct_align);
273 }
274
275 Some(StructLayout {
276 name,
277 total_size: offset,
278 align: struct_align,
279 fields,
280 source_file: None,
281 source_line: Some(source_line),
282 arch,
283 is_packed: false,
284 is_union: false,
285 is_repr_rust: false,
286 suppressed_findings: super::suppress::suppressed_from_preceding_source(
287 source,
288 decl_start_byte,
289 ),
290 uncertain_fields,
291 })
292}
293
294pub fn extract_guard_from_go_comment(comment: &str) -> Option<String> {
301 let c = comment.trim();
302 let body = c.strip_prefix("//").map(str::trim)?;
304
305 if let Some(rest) = body.strip_prefix("padlock:guard=") {
307 let guard = rest.trim();
308 if !guard.is_empty() {
309 return Some(guard.to_string());
310 }
311 }
312 if let Some(rest) = body
314 .strip_prefix("guarded_by:")
315 .or_else(|| body.strip_prefix("guarded_by ="))
316 {
317 let guard = rest.trim();
318 if !guard.is_empty() {
319 return Some(guard.to_string());
320 }
321 }
322 if let Some(rest) = body.strip_prefix("+checklocksprotects:") {
324 let guard = rest.trim();
325 if !guard.is_empty() {
326 return Some(guard.to_string());
327 }
328 }
329 None
330}
331
332fn trailing_comment_on_line(source: &str, node: Node<'_>) -> Option<String> {
334 let end = node.end_byte();
337 if end >= source.len() {
338 return None;
339 }
340 let rest = &source[end..];
341 let line = rest.lines().next().unwrap_or("");
343 line.find("//").map(|pos| line[pos..].to_string())
345}
346
347fn collect_field_declarations(
348 source: &str,
349 node: Node<'_>,
350 out: &mut Vec<(String, String, Option<String>, u32)>,
351) {
352 let mut field_names: Vec<String> = Vec::new();
355 let mut ty_text: Option<String> = None;
356 let field_line = node.start_position().row as u32 + 1;
357
358 for i in 0..node.child_count() {
359 if let Some(child) = node.child(i) {
360 match child.kind() {
361 "field_identifier" => field_names.push(source[child.byte_range()].to_string()),
362 "type_identifier" | "pointer_type" | "qualified_type" | "slice_type"
363 | "map_type" | "channel_type" | "array_type" | "interface_type" => {
364 ty_text = Some(source[child.byte_range()].trim().to_string());
365 }
366 _ => {}
367 }
368 }
369 }
370
371 let guard =
372 trailing_comment_on_line(source, node).and_then(|c| extract_guard_from_go_comment(&c));
373
374 if !field_names.is_empty() {
375 if let Some(ty) = ty_text {
376 for name in field_names {
378 out.push((name, ty.clone(), guard.clone(), field_line));
379 }
380 }
381 } else if let Some(ty) = ty_text {
382 let simple_name = ty.split('.').next_back().unwrap_or(&ty).to_string();
387 out.push((simple_name, ty, guard, field_line));
388 }
389}
390
391pub fn parse_go(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
394 let mut parser = Parser::new();
395 parser.set_language(&tree_sitter_go::LANGUAGE.into())?;
396 let tree = parser
397 .parse(source, None)
398 .ok_or_else(|| anyhow::anyhow!("tree-sitter-go parse failed"))?;
399 Ok(extract_structs(source, tree.root_node(), arch))
400}
401
402#[cfg(test)]
405mod tests {
406 use super::*;
407 use padlock_core::arch::X86_64_SYSV;
408
409 #[test]
410 fn parse_simple_go_struct() {
411 let src = r#"
412package main
413type Point struct {
414 X int32
415 Y int32
416}
417"#;
418 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
419 assert_eq!(layouts.len(), 1);
420 assert_eq!(layouts[0].name, "Point");
421 assert_eq!(layouts[0].fields.len(), 2);
422 }
423
424 #[test]
425 fn go_layout_with_padding() {
426 let src = "package p\ntype T struct { A bool; B int64 }";
427 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
428 assert_eq!(layouts.len(), 1);
429 let l = &layouts[0];
430 assert_eq!(l.fields[0].offset, 0);
431 assert_eq!(l.fields[1].offset, 8); }
433
434 #[test]
435 fn go_string_is_two_words() {
436 let src = "package p\ntype S struct { Name string }";
437 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
438 assert_eq!(layouts[0].fields[0].size, 16); }
440
441 #[test]
444 fn extract_guard_padlock_form() {
445 assert_eq!(
446 extract_guard_from_go_comment("// padlock:guard=mu"),
447 Some("mu".to_string())
448 );
449 }
450
451 #[test]
452 fn extract_guard_guarded_by_form() {
453 assert_eq!(
454 extract_guard_from_go_comment("// guarded_by: counter_lock"),
455 Some("counter_lock".to_string())
456 );
457 }
458
459 #[test]
460 fn extract_guard_checklocksprotects_form() {
461 assert_eq!(
462 extract_guard_from_go_comment("// +checklocksprotects:mu"),
463 Some("mu".to_string())
464 );
465 }
466
467 #[test]
468 fn extract_guard_no_match_returns_none() {
469 assert!(extract_guard_from_go_comment("// just a comment").is_none());
470 assert!(extract_guard_from_go_comment("// TODO: fix this").is_none());
471 }
472
473 #[test]
474 fn go_struct_padlock_guard_annotation_sets_concurrent() {
475 let src = r#"package p
476type Cache struct {
477 Readers int64 // padlock:guard=mu
478 Writers int64 // padlock:guard=other_mu
479 Mu sync.Mutex
480}
481"#;
482 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
483 let l = &layouts[0];
484 if let AccessPattern::Concurrent { guard, .. } = &l.fields[0].access {
486 assert_eq!(guard.as_deref(), Some("mu"));
487 } else {
488 panic!(
489 "expected Concurrent for Readers, got {:?}",
490 l.fields[0].access
491 );
492 }
493 if let AccessPattern::Concurrent { guard, .. } = &l.fields[1].access {
494 assert_eq!(guard.as_deref(), Some("other_mu"));
495 } else {
496 panic!(
497 "expected Concurrent for Writers, got {:?}",
498 l.fields[1].access
499 );
500 }
501 }
502
503 #[test]
504 fn go_struct_different_guards_same_cache_line_is_false_sharing() {
505 let src = r#"package p
506type HotPath struct {
507 Readers int64 // padlock:guard=lock_a
508 Writers int64 // padlock:guard=lock_b
509}
510"#;
511 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
512 assert!(padlock_core::analysis::false_sharing::has_false_sharing(
513 &layouts[0]
514 ));
515 }
516
517 #[test]
518 fn go_struct_same_guard_is_not_false_sharing() {
519 let src = r#"package p
520type Safe struct {
521 A int64 // padlock:guard=mu
522 B int64 // padlock:guard=mu
523}
524"#;
525 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
526 assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
527 &layouts[0]
528 ));
529 }
530
531 #[test]
534 fn interface_field_is_two_words() {
535 let src = "package p\ntype S struct { V interface{} }";
537 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
538 assert_eq!(layouts[0].fields[0].size, 16); assert_eq!(layouts[0].fields[0].align, 8);
540 }
541
542 #[test]
543 fn any_field_is_two_words() {
544 let src = "package p\ntype S struct { V any }";
546 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
547 assert_eq!(layouts[0].fields[0].size, 16); assert_eq!(layouts[0].fields[0].align, 8);
549 }
550
551 #[test]
552 fn interface_field_same_size_as_error() {
553 let src_iface = "package p\ntype S struct { V interface{} }";
555 let src_err = "package p\ntype S struct { V error }";
556 let iface = parse_go(src_iface, &X86_64_SYSV).unwrap();
557 let err = parse_go(src_err, &X86_64_SYSV).unwrap();
558 assert_eq!(iface[0].fields[0].size, err[0].fields[0].size);
559 }
560
561 #[test]
562 fn struct_with_mixed_interface_and_ints_has_correct_layout() {
563 let src = "package p\ntype S struct { V interface{}; N int64 }";
565 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
566 let l = &layouts[0];
567 assert_eq!(l.fields[0].offset, 0);
568 assert_eq!(l.fields[0].size, 16);
569 assert_eq!(l.fields[1].offset, 16);
570 assert_eq!(l.total_size, 24);
571 }
572
573 #[test]
574 fn inline_interface_with_methods_is_two_words() {
575 let src = "package p\ntype S struct { Conn interface{ Close() error } }";
580 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
581 assert_eq!(layouts[0].fields[0].size, 16);
582 assert_eq!(layouts[0].fields[0].align, 8);
583 }
584
585 #[test]
586 fn named_cross_package_interface_falls_back_to_pointer_size() {
587 let src = "package p\ntype DB struct { connector driver.Connector }";
594 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
595 assert_eq!(
597 layouts[0].fields[0].size, 8,
598 "named cross-package interface falls back to pointer_size (known limitation)"
599 );
600 assert!(
602 layouts[0]
603 .uncertain_fields
604 .contains(&"connector".to_string()),
605 "qualified-type field should be in uncertain_fields"
606 );
607 }
608
609 #[test]
612 fn local_interface_field_is_fat_pointer() {
613 let src = r#"package p
616type Reader interface {
617 Read(p []byte) (n int, err error)
618}
619type Buf struct {
620 R Reader
621 N int32
622}
623"#;
624 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
625 let l = layouts.iter().find(|l| l.name == "Buf").expect("Buf");
626 let r = l.fields.iter().find(|f| f.name == "R").expect("R field");
627 assert_eq!(
628 r.size, 16,
629 "local interface must be sized as 16B fat pointer"
630 );
631 assert_eq!(r.align, 8);
632 }
633
634 #[test]
635 fn local_interface_field_not_marked_uncertain() {
636 let src = r#"package p
638type Closer interface { Close() error }
639type File struct { C Closer }
640"#;
641 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
642 let l = layouts.iter().find(|l| l.name == "File").expect("File");
643 assert!(
644 !l.uncertain_fields.contains(&"C".to_string()),
645 "local interface field must not be uncertain"
646 );
647 }
648
649 #[test]
650 fn qualified_type_field_marked_uncertain() {
651 let src = "package p\ntype S struct { R io.Reader; N int32 }";
654 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
655 let l = &layouts[0];
656 assert!(
657 l.uncertain_fields.contains(&"R".to_string()),
658 "qualified-type field must be in uncertain_fields"
659 );
660 assert!(
662 !l.uncertain_fields.contains(&"N".to_string()),
663 "plain int32 field must not be uncertain"
664 );
665 }
666
667 #[test]
668 fn pointer_to_qualified_type_not_uncertain() {
669 let src = "package p\ntype S struct { P *io.Reader }";
673 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
674 let l = &layouts[0];
675 assert!(
676 !l.uncertain_fields.contains(&"P".to_string()),
677 "*qualified.Type pointer must not be uncertain"
678 );
679 }
680
681 #[test]
684 fn embedded_struct_field_uses_type_name_as_field_name() {
685 let src = r#"package p
687type Base struct { X int32 }
688type Derived struct {
689 Base
690 Y int32
691}
692"#;
693 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
694 let derived = layouts
695 .iter()
696 .find(|l| l.name == "Derived")
697 .expect("Derived");
698 assert!(
700 derived.fields.iter().any(|f| f.name == "Base"),
701 "embedded field should be named 'Base'"
702 );
703 }
704
705 #[test]
706 fn embedded_qualified_type_uses_unqualified_name() {
707 let src = r#"package p
709type Safe struct {
710 sync.Mutex
711 Value int64
712}
713"#;
714 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
715 let l = layouts.iter().find(|l| l.name == "Safe").expect("Safe");
716 assert!(
717 l.fields.iter().any(|f| f.name == "Mutex"),
718 "embedded sync.Mutex should produce field named 'Mutex'"
719 );
720 }
721
722 #[test]
723 fn embedded_field_has_non_zero_size_from_resolution() {
724 let src = r#"package p
727type Inner struct { A int64; B int64 }
728type Outer struct {
729 Inner
730 C int32
731}
732"#;
733 use crate::{SourceLanguage, parse_source_str};
734 let layouts = parse_source_str(src, &SourceLanguage::Go, &X86_64_SYSV).unwrap();
735 let outer = layouts.iter().find(|l| l.name == "Outer").expect("Outer");
736 let inner_field = outer
737 .fields
738 .iter()
739 .find(|f| f.name == "Inner")
740 .expect("Inner field");
741 assert_eq!(
743 inner_field.size, 16,
744 "embedded Inner field should be resolved to 16 bytes"
745 );
746 }
747
748 #[test]
749 fn struct_with_no_embedded_fields_unaffected() {
750 let src = "package p\ntype S struct { A int32; B int64 }";
751 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
752 let l = &layouts[0];
753 assert_eq!(l.fields.len(), 2);
754 assert_eq!(l.fields[0].name, "A");
755 assert_eq!(l.fields[1].name, "B");
756 }
757
758 #[test]
761 fn go_generic_struct_is_skipped() {
762 let src = "package p\ntype Pair[T any] struct { First T; Second T }";
764 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
765 assert!(
766 layouts.iter().all(|l| l.name != "Pair"),
767 "generic struct must be skipped"
768 );
769 }
770
771 #[test]
772 fn go_concrete_struct_alongside_generic_is_parsed() {
773 let src = "package p\ntype Pair[T any] struct { First T }\ntype Point struct { X int32; Y int32 }";
775 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
776 assert!(
777 layouts.iter().all(|l| l.name != "Pair"),
778 "Pair must be skipped"
779 );
780 assert!(
781 layouts.iter().any(|l| l.name == "Point"),
782 "Point must be parsed"
783 );
784 }
785
786 #[test]
789 fn embedded_unknown_type_falls_back_to_pointer_size() {
790 let src = "package p\ntype S struct { external.Type\nX int32 }";
792 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
793 let l = layouts.iter().find(|l| l.name == "S").expect("S");
794 let emb = l
795 .fields
796 .iter()
797 .find(|f| f.name == "Type")
798 .expect("Type field");
799 assert_eq!(emb.size, 8);
801 }
802}