1use padlock_core::ir::{StructLayout, optimal_order};
12use similar::{ChangeTag, TextDiff};
13
14pub fn generate_c_fix(layout: &StructLayout) -> String {
19 let optimal = optimal_order(layout);
20 let mut out = format!("struct {} {{\n", layout.name);
21 for field in &optimal {
22 let ty = field_type_name(field);
23 out.push_str(&format!(" {ty} {};\n", field.name));
24 }
25 out.push_str("};\n");
26 out
27}
28
29pub fn generate_rust_fix(layout: &StructLayout) -> String {
31 let optimal = optimal_order(layout);
32 let is_tuple = optimal
34 .iter()
35 .all(|f| f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit()));
36 if is_tuple {
37 let types: Vec<String> = optimal
38 .iter()
39 .map(|f| field_type_name(f).to_string())
40 .collect();
41 return format!("struct {}({});\n", layout.name, types.join(", "));
42 }
43 let mut out = format!("struct {} {{\n", layout.name);
44 for field in &optimal {
45 let ty = field_type_name(field);
46 out.push_str(&format!(" {}: {ty},\n", field.name));
47 }
48 out.push_str("}\n");
49 out
50}
51
52pub fn generate_go_fix(layout: &StructLayout) -> String {
54 let optimal = optimal_order(layout);
55 let mut out = format!("type {} struct {{\n", layout.name);
56 for field in &optimal {
57 let ty = field_type_name(field);
58 out.push_str(&format!("\t{}\t{ty}\n", field.name));
59 }
60 out.push_str("}\n");
61 out
62}
63
64pub fn unified_diff(original: &str, fixed: &str, context_lines: usize) -> String {
66 if original == fixed {
67 return String::from("(no changes)\n");
68 }
69 let diff = TextDiff::from_lines(original, fixed);
70 let mut out = String::new();
71 for (idx, group) in diff.grouped_ops(context_lines).iter().enumerate() {
72 if idx > 0 {
73 out.push_str("...\n");
74 }
75 for op in group {
76 for change in diff.iter_changes(op) {
77 let prefix = match change.tag() {
78 ChangeTag::Delete => "-",
79 ChangeTag::Insert => "+",
80 ChangeTag::Equal => " ",
81 };
82 out.push_str(&format!("{prefix} {}", change.value()));
83 if !change.value().ends_with('\n') {
84 out.push('\n');
85 }
86 }
87 }
88 }
89 out
90}
91
92pub fn extract_rust_field_chunks(body: &str) -> Vec<(String, String)> {
112 let mut result: Vec<(String, String)> = Vec::new();
113 let mut depth: i32 = 0; let mut chunk_start = 0usize;
115 let bytes = body.as_bytes();
116 let mut i = 0usize;
117
118 while i < bytes.len() {
119 match bytes[i] {
120 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
122 while i < bytes.len() && bytes[i] != b'\n' {
123 i += 1;
124 }
125 }
126 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
128 i += 2;
129 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
130 i += 1;
131 }
132 i += 2;
133 }
134 b'"' => {
136 i += 1;
137 while i < bytes.len() {
138 if bytes[i] == b'\\' {
139 i += 2;
140 continue;
141 }
142 if bytes[i] == b'"' {
143 i += 1;
144 break;
145 }
146 i += 1;
147 }
148 }
149 b'<' | b'(' | b'[' => {
150 depth += 1;
151 i += 1;
152 }
153 b'>' | b')' | b']' => {
154 depth = (depth - 1).max(0);
155 i += 1;
156 }
157 b'{' | b'}' => {
160 i += 1;
161 }
162 b',' if depth == 0 => {
163 i += 1; let chunk = &body[chunk_start..i];
165 if let Some(name) = rust_field_name_from_chunk(chunk) {
166 result.push((name, chunk.to_string()));
167 }
168 chunk_start = i;
169 }
170 _ => {
171 i += 1;
172 }
173 }
174 }
175
176 let tail = body[chunk_start..].trim();
178 if !tail.is_empty() {
179 let chunk = &body[chunk_start..];
181 if let Some(name) = rust_field_name_from_chunk(chunk) {
182 result.push((name, chunk.to_string()));
183 }
184 }
185
186 result
187}
188
189fn rust_field_name_from_chunk(chunk: &str) -> Option<String> {
193 for line in chunk.lines() {
194 let s = line.trim();
195 if s.is_empty() || s.starts_with("//") || s.starts_with("#[") || s.starts_with("#![") {
196 continue;
197 }
198 return rust_field_name_from_decl_line(s);
199 }
200 None
201}
202
203fn rust_field_name_from_decl_line(line: &str) -> Option<String> {
205 let mut s = line.trim();
206
207 if let Some(rest) = s.strip_prefix("pub") {
209 let rest = rest.trim_start();
210 if rest.starts_with('(') {
211 let end = rest.find(')')?;
213 s = rest[end + 1..].trim_start();
214 } else {
215 s = rest;
216 }
217 }
218
219 let mut depth: i32 = 0;
221 for (idx, c) in s.char_indices() {
222 match c {
223 '<' | '(' | '[' => depth += 1,
224 '>' | ')' | ']' => depth = (depth - 1).max(0),
225 ':' if depth == 0 => {
226 if s[idx + 1..].starts_with(':') {
228 continue; }
230 let name = s[..idx].trim().to_string();
231 if !name.is_empty()
232 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
233 && !name.starts_with(|c: char| c.is_ascii_digit())
234 {
235 return Some(name);
236 }
237 return None;
238 }
239 _ => {}
240 }
241 }
242 None
243}
244
245pub fn extract_c_field_chunks(body: &str) -> Vec<(String, String)> {
250 let mut result: Vec<(String, String)> = Vec::new();
251 let mut depth: i32 = 0;
252 let mut chunk_start = 0usize;
253 let bytes = body.as_bytes();
254 let mut i = 0usize;
255
256 while i < bytes.len() {
257 match bytes[i] {
258 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
259 while i < bytes.len() && bytes[i] != b'\n' {
260 i += 1;
261 }
262 }
263 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
264 i += 2;
265 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
266 i += 1;
267 }
268 i += 2;
269 }
270 b'"' => {
271 i += 1;
272 while i < bytes.len() {
273 if bytes[i] == b'\\' {
274 i += 2;
275 continue;
276 }
277 if bytes[i] == b'"' {
278 i += 1;
279 break;
280 }
281 i += 1;
282 }
283 }
284 b'<' | b'(' | b'[' | b'{' => {
285 depth += 1;
286 i += 1;
287 }
288 b'>' | b')' | b']' | b'}' => {
289 depth = (depth - 1).max(0);
290 i += 1;
291 }
292 b';' if depth == 0 => {
293 i += 1;
294 let chunk = &body[chunk_start..i];
295 if !chunk.trim().is_empty()
296 && let Some(name) = c_field_name_from_chunk(chunk)
297 {
298 result.push((name, chunk.to_string()));
299 }
300 chunk_start = i;
301 }
302 _ => {
303 i += 1;
304 }
305 }
306 }
307 result
308}
309
310fn c_field_name_from_chunk(chunk: &str) -> Option<String> {
314 let code: String = chunk
316 .lines()
317 .filter(|l| !l.trim().starts_with("//"))
318 .collect::<Vec<_>>()
319 .join(" ");
320
321 let stripped = code.trim_end_matches(';').trim();
324 let stripped = if let Some(bracket) = stripped.rfind('[') {
326 stripped[..bracket].trim()
327 } else {
328 stripped
329 };
330 let stripped = stripped
332 .trim_start_matches('*')
333 .trim_end_matches('*')
334 .trim();
335
336 let last = stripped.split_whitespace().next_back()?;
338 let last = last.trim_start_matches('*').trim_end_matches('*');
340
341 if last.chars().all(|c| c.is_alphanumeric() || c == '_')
342 && !last.is_empty()
343 && !last.starts_with(|c: char| c.is_ascii_digit())
344 && !is_c_keyword(last)
345 {
346 Some(last.to_string())
347 } else {
348 None
349 }
350}
351
352fn is_c_keyword(s: &str) -> bool {
353 matches!(
354 s,
355 "const"
356 | "volatile"
357 | "restrict"
358 | "unsigned"
359 | "signed"
360 | "short"
361 | "long"
362 | "int"
363 | "char"
364 | "float"
365 | "double"
366 | "void"
367 | "struct"
368 | "union"
369 | "enum"
370 | "typedef"
371 | "extern"
372 | "static"
373 | "inline"
374 | "auto"
375 | "register"
376 | "bool"
377 | "_Bool"
378 | "uint8_t"
379 | "uint16_t"
380 | "uint32_t"
381 | "uint64_t"
382 | "int8_t"
383 | "int16_t"
384 | "int32_t"
385 | "int64_t"
386 | "size_t"
387 | "ssize_t"
388 | "ptrdiff_t"
389 | "uintptr_t"
390 | "intptr_t"
391 )
392}
393
394pub fn extract_go_field_chunks(body: &str) -> Vec<(String, String)> {
397 let mut result: Vec<(String, String)> = Vec::new();
398 for line in body.lines() {
399 let s = line.trim();
400 if s.is_empty() || s.starts_with("//") {
401 continue;
402 }
403 if let Some(name) = go_field_name_from_line(s) {
404 result.push((name, format!("{line}\n")));
405 }
406 }
407 result
408}
409
410fn go_field_name_from_line(line: &str) -> Option<String> {
411 let code = if let Some(pos) = line.find("//") {
414 line[..pos].trim()
415 } else {
416 line.trim()
417 };
418 let first = code.split_whitespace().next()?;
419 let name = first.trim_end_matches(',');
420 if name
421 .chars()
422 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
423 && !name.is_empty()
424 {
425 let simple = name.split('.').next_back().unwrap_or(name);
427 Some(simple.to_string())
428 } else {
429 None
430 }
431}
432
433pub fn extract_zig_field_chunks(body: &str) -> Vec<(String, String)> {
436 let mut result: Vec<(String, String)> = Vec::new();
438 let mut depth: i32 = 0;
439 let mut chunk_start = 0usize;
440 let bytes = body.as_bytes();
441 let mut i = 0usize;
442
443 while i < bytes.len() {
444 match bytes[i] {
445 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
446 while i < bytes.len() && bytes[i] != b'\n' {
447 i += 1;
448 }
449 }
450 b'"' => {
451 i += 1;
452 while i < bytes.len() {
453 if bytes[i] == b'\\' {
454 i += 2;
455 continue;
456 }
457 if bytes[i] == b'"' {
458 i += 1;
459 break;
460 }
461 i += 1;
462 }
463 }
464 b'<' | b'(' | b'[' => {
465 depth += 1;
466 i += 1;
467 }
468 b'>' | b')' | b']' => {
469 depth = (depth - 1).max(0);
470 i += 1;
471 }
472 b'{' | b'}' => {
473 i += 1;
474 }
475 b',' if depth == 0 => {
476 i += 1;
477 let chunk = &body[chunk_start..i];
478 if let Some(name) = zig_field_name_from_chunk(chunk) {
479 result.push((name, chunk.to_string()));
480 }
481 chunk_start = i;
482 }
483 _ => {
484 i += 1;
485 }
486 }
487 }
488 let tail = body[chunk_start..].trim();
489 if !tail.is_empty() {
490 let chunk = &body[chunk_start..];
491 if let Some(name) = zig_field_name_from_chunk(chunk) {
492 result.push((name, chunk.to_string()));
493 }
494 }
495 result
496}
497
498fn zig_field_name_from_chunk(chunk: &str) -> Option<String> {
499 for line in chunk.lines() {
500 let s = line.trim();
501 if s.is_empty() || s.starts_with("//") {
502 continue;
503 }
504 let colon = s.find(':')?;
506 let name = s[..colon].trim().to_string();
507 if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
508 return Some(name);
509 }
510 return None;
511 }
512 None
513}
514
515pub fn generate_rust_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
528 if let Some(result) = try_source_aware_rust(layout, struct_source) {
529 return result;
530 }
531 generate_rust_fix(layout)
532}
533
534fn try_source_aware_rust(layout: &StructLayout, struct_source: &str) -> Option<String> {
535 let is_tuple = layout
537 .fields
538 .iter()
539 .all(|f| f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit()));
540
541 if is_tuple {
542 return try_source_aware_rust_tuple(layout, struct_source);
543 }
544
545 let brace_open = struct_source.find('{')?;
546 let body_with_close = &struct_source[brace_open..];
548 let body_len = match_braces(body_with_close)?;
549 let body = &body_with_close[1..body_len - 1]; let chunks = extract_rust_field_chunks(body);
552 if chunks.is_empty() {
553 return None;
554 }
555
556 let chunk_map: std::collections::HashMap<&str, &str> = chunks
557 .iter()
558 .map(|(n, c)| (n.as_str(), c.as_str()))
559 .collect();
560
561 let optimal = optimal_order(layout);
562 if optimal
564 .iter()
565 .any(|f| !chunk_map.contains_key(f.name.as_str()))
566 {
567 return None;
568 }
569
570 let header = &struct_source[..=brace_open];
571 let mut result = header.to_string();
572 result.push('\n');
573 for field in &optimal {
574 result.push_str(chunk_map[field.name.as_str()]);
575 }
576 if !result.ends_with('\n') {
578 result.push('\n');
579 }
580 result.push('}');
581 let after = &struct_source[brace_open + body_len..];
583 result.push_str(after);
584 Some(result)
585}
586
587fn try_source_aware_rust_tuple(layout: &StructLayout, struct_source: &str) -> Option<String> {
590 let paren_open = struct_source.find('(')?;
591 let body_with_close = &struct_source[paren_open..];
592 let paren_len = match_parens(body_with_close)?;
594 let body = &body_with_close[1..paren_len - 1]; let type_chunks = extract_tuple_type_chunks(body);
598 if type_chunks.is_empty() {
599 return None;
600 }
601
602 let chunk_map: std::collections::HashMap<String, &str> = type_chunks
605 .iter()
606 .enumerate()
607 .map(|(i, c)| (format!("_{i}"), c.as_str()))
608 .collect();
609
610 let optimal = optimal_order(layout);
611 if optimal.iter().any(|f| !chunk_map.contains_key(&f.name)) {
612 return None;
613 }
614
615 let header = &struct_source[..=paren_open];
617 let mut result = header.to_string();
618 let reordered: Vec<&str> = optimal.iter().map(|f| chunk_map[&f.name]).collect();
619 result.push_str(&reordered.join(", "));
620 result.push(')');
621 let after = &struct_source[paren_open + paren_len..];
623 result.push_str(after);
624 Some(result)
625}
626
627fn extract_tuple_type_chunks(body: &str) -> Vec<String> {
629 let mut result = Vec::new();
630 let mut depth: i32 = 0;
631 let mut chunk_start = 0usize;
632 let bytes = body.as_bytes();
633 let mut i = 0usize;
634
635 while i < bytes.len() {
636 match bytes[i] {
637 b'<' | b'[' => {
638 depth += 1;
639 i += 1;
640 }
641 b'>' | b']' => {
642 depth = (depth - 1).max(0);
643 i += 1;
644 }
645 b'(' => {
646 depth += 1;
647 i += 1;
648 }
649 b')' => {
650 depth = (depth - 1).max(0);
651 i += 1;
652 }
653 b',' if depth == 0 => {
654 let chunk = body[chunk_start..i].trim().to_string();
655 if !chunk.is_empty() {
656 result.push(chunk);
657 }
658 i += 1;
659 chunk_start = i;
660 }
661 _ => {
662 i += 1;
663 }
664 }
665 }
666 let tail = body[chunk_start..].trim().to_string();
667 if !tail.is_empty() {
668 result.push(tail);
669 }
670 result
671}
672
673fn match_parens(s: &str) -> Option<usize> {
676 let mut depth = 0usize;
677 for (i, c) in s.char_indices() {
678 match c {
679 '(' => depth += 1,
680 ')' => {
681 depth -= 1;
682 if depth == 0 {
683 return Some(i + 1);
684 }
685 }
686 _ => {}
687 }
688 }
689 None
690}
691
692pub fn generate_c_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
694 if let Some(result) = try_source_aware_c(layout, struct_source) {
695 return result;
696 }
697 generate_c_fix(layout)
698}
699
700fn try_source_aware_c(layout: &StructLayout, struct_source: &str) -> Option<String> {
701 let brace_open = struct_source.find('{')?;
702 let body_with_close = &struct_source[brace_open..];
703 let body_len = match_braces(body_with_close)?;
704 let body = &body_with_close[1..body_len - 1];
705
706 let chunks = extract_c_field_chunks(body);
707 if chunks.is_empty() {
708 return None;
709 }
710
711 let chunk_map: std::collections::HashMap<&str, &str> = chunks
712 .iter()
713 .map(|(n, c)| (n.as_str(), c.as_str()))
714 .collect();
715
716 let optimal = optimal_order(layout);
717 if optimal
718 .iter()
719 .any(|f| !chunk_map.contains_key(f.name.as_str()))
720 {
721 return None;
722 }
723
724 let header = &struct_source[..=brace_open];
725 let mut result = header.to_string();
726 result.push('\n');
727 for field in &optimal {
728 result.push_str(chunk_map[field.name.as_str()]);
729 }
730 if !result.ends_with('\n') {
731 result.push('\n');
732 }
733 result.push('}');
734 let close_end = brace_open + body_len;
735 let after = &struct_source[close_end..];
736 result.push_str(after);
737 Some(result)
738}
739
740pub fn generate_go_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
742 if let Some(result) = try_source_aware_go(layout, struct_source) {
743 return result;
744 }
745 generate_go_fix(layout)
746}
747
748fn try_source_aware_go(layout: &StructLayout, struct_source: &str) -> Option<String> {
749 let brace_open = struct_source.find('{')?;
750 let body_with_close = &struct_source[brace_open..];
751 let body_len = match_braces(body_with_close)?;
752 let body = &body_with_close[1..body_len - 1];
753
754 let chunks = extract_go_field_chunks(body);
755 if chunks.is_empty() {
756 return None;
757 }
758
759 let chunk_map: std::collections::HashMap<&str, &str> = chunks
760 .iter()
761 .map(|(n, c)| (n.as_str(), c.as_str()))
762 .collect();
763
764 let optimal = optimal_order(layout);
765 if optimal
766 .iter()
767 .any(|f| !chunk_map.contains_key(f.name.as_str()))
768 {
769 return None;
770 }
771
772 let header = &struct_source[..=brace_open];
773 let mut result = header.to_string();
774 result.push('\n');
775 for field in &optimal {
776 result.push_str(chunk_map[field.name.as_str()]);
777 }
778 if !result.ends_with('\n') {
779 result.push('\n');
780 }
781 result.push('}');
782 let close_end = brace_open + body_len;
783 let after = &struct_source[close_end..];
784 result.push_str(after);
785 Some(result)
786}
787
788pub fn generate_zig_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
790 if let Some(result) = try_source_aware_zig(layout, struct_source) {
791 return result;
792 }
793 generate_zig_fix(layout)
794}
795
796fn try_source_aware_zig(layout: &StructLayout, struct_source: &str) -> Option<String> {
797 let brace_open = struct_source.find('{')?;
798 let body_with_close = &struct_source[brace_open..];
799 let body_len = match_braces(body_with_close)?;
800 let body = &body_with_close[1..body_len - 1];
801
802 let chunks = extract_zig_field_chunks(body);
803 if chunks.is_empty() {
804 return None;
805 }
806
807 let chunk_map: std::collections::HashMap<&str, &str> = chunks
808 .iter()
809 .map(|(n, c)| (n.as_str(), c.as_str()))
810 .collect();
811
812 let optimal = optimal_order(layout);
813 if optimal
814 .iter()
815 .any(|f| !chunk_map.contains_key(f.name.as_str()))
816 {
817 return None;
818 }
819
820 let header = &struct_source[..=brace_open];
821 let mut result = header.to_string();
822 result.push('\n');
823 for field in &optimal {
824 result.push_str(chunk_map[field.name.as_str()]);
825 }
826 if !result.ends_with('\n') {
827 result.push('\n');
828 }
829 result.push('}');
830 let close_end = brace_open + body_len;
831 let after = &struct_source[close_end..];
832 result.push_str(after);
833 Some(result)
834}
835
836fn match_braces(s: &str) -> Option<usize> {
841 let mut depth = 0usize;
842 for (i, c) in s.char_indices() {
843 match c {
844 '{' => depth += 1,
845 '}' => {
846 depth -= 1;
847 if depth == 0 {
848 return Some(i + 1);
849 }
850 }
851 _ => {}
852 }
853 }
854 None
855}
856
857fn consume_semicolon(source: &str, pos: usize) -> usize {
859 let rest = &source[pos..];
860 let ws = rest.len()
861 - rest
862 .trim_start_matches(|c: char| c.is_whitespace() && c != '\n')
863 .len();
864 let after_ws = &rest[ws..];
865 if after_ws.starts_with(';') {
866 pos + ws + 1
867 } else {
868 pos
869 }
870}
871
872pub fn find_c_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
875 for kw in &["struct", "union"] {
876 let needle = format!("{kw} {struct_name}");
877 let mut search_from = 0usize;
878 while let Some(rel) = source[search_from..].find(&needle) {
879 let start = search_from + rel;
880 let after_name = start + needle.len();
881 let boundary = source[after_name..].chars().next();
883 if matches!(
884 boundary,
885 Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
886 ) {
887 if let Some(brace_rel) = source[after_name..].find('{') {
889 let brace_start = after_name + brace_rel;
890 if source[after_name..brace_start]
892 .chars()
893 .all(|c| c.is_whitespace())
894 && let Some(body_len) = match_braces(&source[brace_start..])
895 {
896 let end = consume_semicolon(source, brace_start + body_len);
897 return Some(start..end);
898 }
899 }
900 }
901 search_from = start + 1;
902 }
903 }
904 None
905}
906
907pub fn find_rust_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
909 let needle = format!("struct {struct_name}");
910 let mut search_from = 0usize;
911 while let Some(rel) = source[search_from..].find(&needle) {
912 let start = search_from + rel;
913 let after_name = start + needle.len();
914 let boundary = source[after_name..].chars().next();
915 if matches!(
916 boundary,
917 Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
918 ) && let Some(brace_rel) = source[after_name..].find('{')
919 {
920 let brace_start = after_name + brace_rel;
921 if source[after_name..brace_start]
922 .chars()
923 .all(|c| c.is_whitespace())
924 && let Some(body_len) = match_braces(&source[brace_start..])
925 {
926 return Some(start..brace_start + body_len);
928 }
929 }
930 search_from = start + 1;
931 }
932 None
933}
934
935pub fn find_go_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
937 let needle = format!("type {struct_name} struct");
938 let mut search_from = 0usize;
939 while let Some(rel) = source[search_from..].find(&needle) {
940 let start = search_from + rel;
941 let after_kw = start + needle.len();
942 if let Some(brace_rel) = source[after_kw..].find('{') {
943 let brace_start = after_kw + brace_rel;
944 if source[after_kw..brace_start]
945 .chars()
946 .all(|c| c.is_whitespace())
947 && let Some(body_len) = match_braces(&source[brace_start..])
948 {
949 return Some(start..brace_start + body_len);
950 }
951 }
952 search_from = start + 1;
953 }
954 None
955}
956
957pub fn apply_fixes_c(source: &str, layouts: &[&StructLayout]) -> String {
966 apply_fixes_with_source(
967 source,
968 layouts,
969 find_c_struct_span,
970 generate_c_fix_from_source,
971 )
972}
973
974pub fn apply_fixes_rust(source: &str, layouts: &[&StructLayout]) -> String {
978 apply_fixes_with_source(
979 source,
980 layouts,
981 find_rust_struct_span,
982 generate_rust_fix_from_source,
983 )
984}
985
986pub fn apply_fixes_go(source: &str, layouts: &[&StructLayout]) -> String {
989 apply_fixes_with_source(
990 source,
991 layouts,
992 find_go_struct_span,
993 generate_go_fix_from_source,
994 )
995}
996
997pub fn generate_zig_fix(layout: &StructLayout) -> String {
1001 let optimal = optimal_order(layout);
1002 let qualifier = if layout.is_packed { "packed " } else { "" };
1003 let mut out = format!("const {} = {}struct {{\n", layout.name, qualifier);
1004 for field in &optimal {
1005 let ty = field_type_name(field);
1006 out.push_str(&format!(" {}: {ty},\n", field.name));
1007 }
1008 out.push_str("};\n");
1009 out
1010}
1011
1012pub fn find_zig_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
1015 let needle = format!("const {struct_name}");
1017 let mut search_from = 0usize;
1018 while let Some(rel) = source[search_from..].find(&needle) {
1019 let start = search_from + rel;
1020 let after_name = start + needle.len();
1021 let rest = source[after_name..].trim_start();
1023 if !rest.starts_with('=') {
1024 search_from = start + 1;
1025 continue;
1026 }
1027 let after_eq = after_name + source[after_name..].find('=')? + 1;
1029 let after_eq_rest = &source[after_eq..];
1030 if let Some(struct_rel) = after_eq_rest.find("struct") {
1032 let prefix = &after_eq_rest[..struct_rel];
1035 let prefix_clean = prefix.trim();
1036 if prefix_clean.is_empty() || prefix_clean == "packed" || prefix_clean == "extern" {
1037 let struct_kw_end = after_eq + struct_rel + "struct".len();
1038 if let Some(brace_rel) = source[struct_kw_end..].find('{') {
1039 let brace_start = struct_kw_end + brace_rel;
1040 if source[struct_kw_end..brace_start]
1041 .chars()
1042 .all(|c| c.is_whitespace())
1043 && let Some(body_len) = match_braces(&source[brace_start..])
1044 {
1045 let end = consume_semicolon(source, brace_start + body_len);
1046 return Some(start..end);
1047 }
1048 }
1049 }
1050 }
1051 search_from = start + 1;
1052 }
1053 None
1054}
1055
1056pub fn apply_fixes_zig(source: &str, layouts: &[&StructLayout]) -> String {
1059 apply_fixes_with_source(
1060 source,
1061 layouts,
1062 find_zig_struct_span,
1063 generate_zig_fix_from_source,
1064 )
1065}
1066
1067fn apply_fixes_with_source(
1070 source: &str,
1071 layouts: &[&StructLayout],
1072 find_span: fn(&str, &str) -> Option<std::ops::Range<usize>>,
1073 generate: fn(&StructLayout, &str) -> String,
1074) -> String {
1075 let mut replacements: Vec<(usize, usize, String)> = layouts
1077 .iter()
1078 .filter_map(|layout| {
1079 let span = find_span(source, &layout.name)?;
1080 let struct_source = &source[span.clone()];
1081 let fixed = generate(layout, struct_source);
1082 Some((span.start, span.end, fixed))
1083 })
1084 .collect();
1085
1086 replacements.sort_by_key(|(start, _, _)| *start);
1088
1089 let mut result = source.to_string();
1090 for (start, end, fixed) in replacements.into_iter().rev() {
1091 result.replace_range(start..end, &fixed);
1092 }
1093 result
1094}
1095
1096fn field_type_name(field: &padlock_core::ir::Field) -> &str {
1097 match &field.ty {
1098 padlock_core::ir::TypeInfo::Primitive { name, .. }
1099 | padlock_core::ir::TypeInfo::Opaque { name, .. } => name.as_str(),
1100 padlock_core::ir::TypeInfo::Pointer { .. } => "void*",
1101 padlock_core::ir::TypeInfo::Array { .. } => "/* array */",
1102 padlock_core::ir::TypeInfo::Struct(l) => l.name.as_str(),
1103 }
1104}
1105
1106#[cfg(test)]
1109mod tests {
1110 use super::*;
1111 use padlock_core::ir::test_fixtures::connection_layout;
1112
1113 #[test]
1114 fn c_fix_starts_with_struct() {
1115 let out = generate_c_fix(&connection_layout());
1116 assert!(out.starts_with("struct Connection {"));
1117 }
1118
1119 #[test]
1120 fn c_fix_contains_all_fields() {
1121 let out = generate_c_fix(&connection_layout());
1122 assert!(out.contains("timeout"));
1123 assert!(out.contains("port"));
1124 assert!(out.contains("is_active"));
1125 assert!(out.contains("is_tls"));
1126 }
1127
1128 #[test]
1129 fn c_fix_puts_largest_align_first() {
1130 let out = generate_c_fix(&connection_layout());
1131 let timeout_pos = out.find("timeout").unwrap();
1132 let is_active_pos = out.find("is_active").unwrap();
1133 assert!(timeout_pos < is_active_pos);
1134 }
1135
1136 #[test]
1137 fn rust_fix_uses_colon_syntax() {
1138 let out = generate_rust_fix(&connection_layout());
1139 assert!(out.contains(": f64"));
1140 }
1141
1142 #[test]
1143 fn unified_diff_marks_changes() {
1144 let orig = "struct T { char a; double b; };\n";
1145 let fixed = "struct T { double b; char a; };\n";
1146 let diff = unified_diff(orig, fixed, 1);
1147 assert!(diff.contains('-') || diff.contains('+'));
1148 }
1149
1150 #[test]
1151 fn unified_diff_identical_is_no_changes() {
1152 assert_eq!(unified_diff("x\n", "x\n", 3), "(no changes)\n");
1153 }
1154
1155 #[test]
1158 fn find_c_struct_span_basic() {
1159 let src = "struct Foo { int x; char y; };\nstruct Bar { double z; };\n";
1160 let span = find_c_struct_span(src, "Foo").unwrap();
1161 let text = &src[span];
1162 assert!(text.starts_with("struct Foo"));
1163 assert!(!text.contains("Bar"));
1164 }
1165
1166 #[test]
1167 fn find_c_struct_span_missing_returns_none() {
1168 let src = "struct Other { int x; };";
1169 assert!(find_c_struct_span(src, "Missing").is_none());
1170 }
1171
1172 #[test]
1173 fn find_rust_struct_span_basic() {
1174 let src = "struct Foo {\n x: u32,\n y: u8,\n}\n";
1175 let span = find_rust_struct_span(src, "Foo").unwrap();
1176 assert!(src[span].starts_with("struct Foo"));
1177 }
1178
1179 #[test]
1180 fn find_go_struct_span_basic() {
1181 let src = "type Foo struct {\n\tX int32\n\tY bool\n}\n";
1182 let span = find_go_struct_span(src, "Foo").unwrap();
1183 assert!(src[span].starts_with("type Foo struct"));
1184 }
1185
1186 #[test]
1189 fn apply_fixes_c_reorders_in_place() {
1190 let src = "struct Connection { bool is_active; double timeout; bool is_tls; int port; };\n";
1192 let layout = connection_layout();
1193 let fixed = apply_fixes_c(src, &[&layout]);
1194 let timeout_pos = fixed.find("timeout").unwrap();
1195 let is_active_pos = fixed.find("is_active").unwrap();
1196 assert!(
1197 timeout_pos < is_active_pos,
1198 "double should appear before bool after reorder"
1199 );
1200 }
1201
1202 #[test]
1203 fn apply_fixes_rust_reorders_in_place() {
1204 let src = "struct Connection {\n is_active: bool,\n timeout: f64,\n is_tls: bool,\n port: i32,\n}\n";
1205 let layout = connection_layout();
1206 let fixed = apply_fixes_rust(src, &[&layout]);
1207 let timeout_pos = fixed.find("timeout").unwrap();
1208 let is_active_pos = fixed.find("is_active").unwrap();
1209 assert!(timeout_pos < is_active_pos);
1210 }
1211
1212 #[test]
1213 fn go_fix_uses_tab_syntax() {
1214 let layout = connection_layout();
1215 let out = generate_go_fix(&layout);
1216 assert!(out.starts_with("type Connection struct"));
1217 assert!(out.contains('\t'));
1218 }
1219
1220 #[test]
1221 fn zig_fix_uses_const_struct_syntax() {
1222 let out = generate_zig_fix(&connection_layout());
1223 assert!(out.starts_with("const Connection = struct {"));
1224 assert!(out.ends_with("};\n"));
1225 }
1226
1227 #[test]
1228 fn find_zig_struct_span_basic() {
1229 let src = "const S = struct {\n x: u32,\n y: u8,\n};\n";
1230 let span = find_zig_struct_span(src, "S").unwrap();
1231 assert!(src[span].starts_with("const S = struct"));
1232 }
1233
1234 #[test]
1235 fn find_zig_struct_span_packed() {
1236 let src = "const S = packed struct {\n x: u32,\n y: u8,\n};\n";
1237 let span = find_zig_struct_span(src, "S").unwrap();
1238 assert!(src[span].contains("packed struct"));
1239 }
1240
1241 #[test]
1242 fn find_zig_struct_span_missing_returns_none() {
1243 let src = "const Other = struct { x: u8 };\n";
1244 assert!(find_zig_struct_span(src, "Missing").is_none());
1245 }
1246
1247 #[test]
1248 fn apply_fixes_zig_reorders_in_place() {
1249 use crate::parse_source_str;
1250 use padlock_core::arch::X86_64_SYSV;
1251 let src = "const S = struct {\n a: u8,\n b: u64,\n};\n";
1252 let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1253 let layout = &layouts[0];
1254 let fixed = apply_fixes_zig(src, &[layout]);
1255 let b_pos = fixed.find("b:").unwrap();
1257 let a_pos = fixed.find("a:").unwrap();
1258 assert!(
1259 b_pos < a_pos,
1260 "u64 field should come before u8 after reorder"
1261 );
1262 }
1263
1264 #[test]
1267 fn rust_fix_preserves_pub_visibility() {
1268 let src = "struct S {\n pub a: u8,\n pub b: u64,\n}\n";
1269 use crate::parse_source_str;
1270 use padlock_core::arch::X86_64_SYSV;
1271 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1272 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1273 assert!(fixed.contains("pub b: u64"), "pub on b must be preserved");
1275 assert!(fixed.contains("pub a: u8"), "pub on a must be preserved");
1276 assert!(fixed.find("pub b").unwrap() < fixed.find("pub a").unwrap());
1278 }
1279
1280 #[test]
1281 fn rust_fix_preserves_doc_comments() {
1282 let src = concat!(
1283 "struct S {\n",
1284 " /// small field\n",
1285 " a: u8,\n",
1286 " /// large field\n",
1287 " b: u64,\n",
1288 "}\n"
1289 );
1290 use crate::parse_source_str;
1291 use padlock_core::arch::X86_64_SYSV;
1292 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1293 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1294 assert!(
1295 fixed.contains("/// large field"),
1296 "doc comment for b must survive"
1297 );
1298 assert!(
1299 fixed.contains("/// small field"),
1300 "doc comment for a must survive"
1301 );
1302 assert!(
1304 fixed.find("large field").unwrap() < fixed.find("small field").unwrap(),
1305 "doc comment ordering must follow field ordering"
1306 );
1307 }
1308
1309 #[test]
1310 fn rust_fix_preserves_serde_attributes() {
1311 let src = concat!(
1312 "struct S {\n",
1313 " #[serde(skip)]\n",
1314 " a: u8,\n",
1315 " #[serde(rename = \"big\")]\n",
1316 " b: u64,\n",
1317 "}\n"
1318 );
1319 use crate::parse_source_str;
1320 use padlock_core::arch::X86_64_SYSV;
1321 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1322 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1323 assert!(
1324 fixed.contains("#[serde(skip)]"),
1325 "serde attribute on a must survive"
1326 );
1327 assert!(
1328 fixed.contains("#[serde(rename = \"big\")]"),
1329 "serde attribute on b must survive"
1330 );
1331 }
1332
1333 #[test]
1334 fn rust_fix_preserves_pub_crate_visibility() {
1335 let src = "struct S {\n pub(crate) a: u8,\n pub(crate) b: u64,\n}\n";
1336 use crate::parse_source_str;
1337 use padlock_core::arch::X86_64_SYSV;
1338 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1339 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1340 assert!(
1341 fixed.contains("pub(crate) b: u64"),
1342 "pub(crate) on b must be preserved"
1343 );
1344 assert!(
1345 fixed.contains("pub(crate) a: u8"),
1346 "pub(crate) on a must be preserved"
1347 );
1348 }
1349
1350 #[test]
1351 fn c_fix_preserves_guarded_by_comments() {
1352 let src = concat!(
1353 "struct S {\n",
1354 " char a; // GUARDED_BY(mu)\n",
1355 " double b; // large field\n",
1356 "};\n"
1357 );
1358 use crate::parse_source_str;
1359 use padlock_core::arch::X86_64_SYSV;
1360 let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1361 let fixed = apply_fixes_c(src, &[&layouts[0]]);
1362 assert!(
1363 fixed.contains("GUARDED_BY(mu)"),
1364 "guard annotation comment must survive reorder"
1365 );
1366 assert!(fixed.find("double b").unwrap() < fixed.find("char a").unwrap());
1368 }
1369
1370 #[test]
1371 fn go_fix_preserves_field_tags() {
1372 let src = concat!("type S struct {\n", "\ta uint8\n", "\tb uint64\n", "}\n");
1373 use crate::parse_source_str;
1374 use padlock_core::arch::X86_64_SYSV;
1375 let layouts = parse_source_str(src, &crate::SourceLanguage::Go, &X86_64_SYSV).unwrap();
1376 let fixed = apply_fixes_go(src, &[&layouts[0]]);
1377 assert!(fixed.find("\tb uint64").unwrap() < fixed.find("\ta uint8").unwrap());
1379 }
1380
1381 #[test]
1382 fn zig_fix_preserves_field_comments() {
1383 let src = concat!(
1384 "const S = struct {\n",
1385 " // small\n",
1386 " a: u8,\n",
1387 " // large\n",
1388 " b: u64,\n",
1389 "};\n"
1390 );
1391 use crate::parse_source_str;
1392 use padlock_core::arch::X86_64_SYSV;
1393 let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1394 let fixed = apply_fixes_zig(src, &[&layouts[0]]);
1395 assert!(fixed.contains("// large"), "comment for b must survive");
1396 assert!(fixed.contains("// small"), "comment for a must survive");
1397 assert!(fixed.find("// large").unwrap() < fixed.find("// small").unwrap());
1399 }
1400
1401 #[test]
1404 fn rust_fix_from_source_falls_back_when_no_open_brace() {
1405 let layout = connection_layout();
1407 let out = generate_rust_fix_from_source(&layout, "struct Connection");
1408 assert!(out.starts_with("struct Connection {"));
1410 }
1411
1412 #[test]
1413 fn c_fix_from_source_falls_back_when_chunks_empty() {
1414 let layout = connection_layout();
1417 let out = generate_c_fix_from_source(&layout, "struct Connection { /* no fields */ };");
1418 assert!(out.starts_with("struct Connection {"));
1419 assert!(out.contains("timeout"));
1420 }
1421
1422 #[test]
1423 fn zig_fix_from_source_falls_back_on_missing_field_name() {
1424 let layout = connection_layout();
1426 let out =
1427 generate_zig_fix_from_source(&layout, "const Connection = struct { x: u8, y: u64, };");
1428 assert!(out.contains("timeout"));
1430 }
1431
1432 #[test]
1435 fn go_fix_reorders_fields() {
1436 let layout = connection_layout();
1437 let out = generate_go_fix(&layout);
1438 let pos_timeout = out.find("timeout").unwrap();
1440 let pos_port = out.find("port").unwrap();
1441 let pos_bool = out.find("is_active").unwrap();
1442 assert!(pos_timeout < pos_bool, "timeout must precede booleans");
1443 assert!(pos_port < pos_bool, "port must precede booleans");
1444 }
1445
1446 #[test]
1447 fn go_fix_from_source_preserves_verbatim_field_lines() {
1448 let layout = connection_layout();
1449 let src = r#"type Connection struct {
1450 is_active bool
1451 timeout f64
1452 is_tls bool
1453 port i32
1454}"#;
1455 let out = generate_go_fix_from_source(&layout, src);
1456 assert!(out.contains("timeout f64"), "verbatim timeout line");
1458 assert!(out.contains("port i32"), "verbatim port line");
1459 let pos_timeout = out.find("timeout").unwrap();
1461 let pos_is_active = out.find("is_active").unwrap();
1462 assert!(
1463 pos_timeout < pos_is_active,
1464 "timeout must come before is_active"
1465 );
1466 }
1467
1468 #[test]
1469 fn apply_fixes_go_rewrites_struct_in_file() {
1470 let src = "package p\n\ntype Point struct {\n\tFlag bool\n\tX int64\n\tY int32\n}\n";
1471 use padlock_core::arch::X86_64_SYSV;
1474 use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
1475 let layout = StructLayout {
1476 name: "Point".into(),
1477 total_size: 16,
1478 align: 8,
1479 fields: vec![
1480 Field {
1481 name: "Flag".into(),
1482 ty: TypeInfo::Primitive {
1483 name: "bool".into(),
1484 size: 1,
1485 align: 1,
1486 },
1487 offset: 0,
1488 size: 1,
1489 align: 1,
1490 source_file: None,
1491 source_line: None,
1492 access: AccessPattern::Unknown,
1493 },
1494 Field {
1495 name: "X".into(),
1496 ty: TypeInfo::Primitive {
1497 name: "int64".into(),
1498 size: 8,
1499 align: 8,
1500 },
1501 offset: 8,
1502 size: 8,
1503 align: 8,
1504 source_file: None,
1505 source_line: None,
1506 access: AccessPattern::Unknown,
1507 },
1508 Field {
1509 name: "Y".into(),
1510 ty: TypeInfo::Primitive {
1511 name: "int32".into(),
1512 size: 4,
1513 align: 4,
1514 },
1515 offset: 16,
1516 size: 4,
1517 align: 4,
1518 source_file: None,
1519 source_line: None,
1520 access: AccessPattern::Unknown,
1521 },
1522 ],
1523 source_file: None,
1524 source_line: None,
1525 arch: &X86_64_SYSV,
1526 is_packed: false,
1527 is_union: false,
1528 is_repr_rust: false,
1529 suppressed_findings: vec![],
1530 };
1531 let fixed = apply_fixes_go(src, &[&layout]);
1532 let pos_x = fixed.find("\tX ").unwrap();
1534 let pos_flag = fixed.find("\tFlag").unwrap();
1535 assert!(pos_x < pos_flag, "X must precede Flag after reorder");
1536 assert!(fixed.starts_with("package p\n"), "package line preserved");
1538 }
1539}