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 mut out = format!("struct {} {{\n", layout.name);
33 for field in &optimal {
34 let ty = field_type_name(field);
35 out.push_str(&format!(" {}: {ty},\n", field.name));
36 }
37 out.push_str("}\n");
38 out
39}
40
41pub fn generate_go_fix(layout: &StructLayout) -> String {
43 let optimal = optimal_order(layout);
44 let mut out = format!("type {} struct {{\n", layout.name);
45 for field in &optimal {
46 let ty = field_type_name(field);
47 out.push_str(&format!("\t{}\t{ty}\n", field.name));
48 }
49 out.push_str("}\n");
50 out
51}
52
53pub fn unified_diff(original: &str, fixed: &str, context_lines: usize) -> String {
55 if original == fixed {
56 return String::from("(no changes)\n");
57 }
58 let diff = TextDiff::from_lines(original, fixed);
59 let mut out = String::new();
60 for (idx, group) in diff.grouped_ops(context_lines).iter().enumerate() {
61 if idx > 0 {
62 out.push_str("...\n");
63 }
64 for op in group {
65 for change in diff.iter_changes(op) {
66 let prefix = match change.tag() {
67 ChangeTag::Delete => "-",
68 ChangeTag::Insert => "+",
69 ChangeTag::Equal => " ",
70 };
71 out.push_str(&format!("{prefix} {}", change.value()));
72 if !change.value().ends_with('\n') {
73 out.push('\n');
74 }
75 }
76 }
77 }
78 out
79}
80
81pub fn extract_rust_field_chunks(body: &str) -> Vec<(String, String)> {
101 let mut result: Vec<(String, String)> = Vec::new();
102 let mut depth: i32 = 0; let mut chunk_start = 0usize;
104 let bytes = body.as_bytes();
105 let mut i = 0usize;
106
107 while i < bytes.len() {
108 match bytes[i] {
109 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
111 while i < bytes.len() && bytes[i] != b'\n' {
112 i += 1;
113 }
114 }
115 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
117 i += 2;
118 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
119 i += 1;
120 }
121 i += 2;
122 }
123 b'"' => {
125 i += 1;
126 while i < bytes.len() {
127 if bytes[i] == b'\\' {
128 i += 2;
129 continue;
130 }
131 if bytes[i] == b'"' {
132 i += 1;
133 break;
134 }
135 i += 1;
136 }
137 }
138 b'<' | b'(' | b'[' => {
139 depth += 1;
140 i += 1;
141 }
142 b'>' | b')' | b']' => {
143 depth = (depth - 1).max(0);
144 i += 1;
145 }
146 b'{' | b'}' => {
149 i += 1;
150 }
151 b',' if depth == 0 => {
152 i += 1; let chunk = &body[chunk_start..i];
154 if let Some(name) = rust_field_name_from_chunk(chunk) {
155 result.push((name, chunk.to_string()));
156 }
157 chunk_start = i;
158 }
159 _ => {
160 i += 1;
161 }
162 }
163 }
164
165 let tail = body[chunk_start..].trim();
167 if !tail.is_empty() {
168 let chunk = &body[chunk_start..];
170 if let Some(name) = rust_field_name_from_chunk(chunk) {
171 result.push((name, chunk.to_string()));
172 }
173 }
174
175 result
176}
177
178fn rust_field_name_from_chunk(chunk: &str) -> Option<String> {
182 for line in chunk.lines() {
183 let s = line.trim();
184 if s.is_empty() || s.starts_with("//") || s.starts_with("#[") || s.starts_with("#![") {
185 continue;
186 }
187 return rust_field_name_from_decl_line(s);
188 }
189 None
190}
191
192fn rust_field_name_from_decl_line(line: &str) -> Option<String> {
194 let mut s = line.trim();
195
196 if let Some(rest) = s.strip_prefix("pub") {
198 let rest = rest.trim_start();
199 if rest.starts_with('(') {
200 let end = rest.find(')')?;
202 s = rest[end + 1..].trim_start();
203 } else {
204 s = rest;
205 }
206 }
207
208 let mut depth: i32 = 0;
210 for (idx, c) in s.char_indices() {
211 match c {
212 '<' | '(' | '[' => depth += 1,
213 '>' | ')' | ']' => depth = (depth - 1).max(0),
214 ':' if depth == 0 => {
215 if s[idx + 1..].starts_with(':') {
217 continue; }
219 let name = s[..idx].trim().to_string();
220 if !name.is_empty()
221 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
222 && !name.starts_with(|c: char| c.is_ascii_digit())
223 {
224 return Some(name);
225 }
226 return None;
227 }
228 _ => {}
229 }
230 }
231 None
232}
233
234pub fn extract_c_field_chunks(body: &str) -> Vec<(String, String)> {
239 let mut result: Vec<(String, String)> = Vec::new();
240 let mut depth: i32 = 0;
241 let mut chunk_start = 0usize;
242 let bytes = body.as_bytes();
243 let mut i = 0usize;
244
245 while i < bytes.len() {
246 match bytes[i] {
247 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
248 while i < bytes.len() && bytes[i] != b'\n' {
249 i += 1;
250 }
251 }
252 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
253 i += 2;
254 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
255 i += 1;
256 }
257 i += 2;
258 }
259 b'"' => {
260 i += 1;
261 while i < bytes.len() {
262 if bytes[i] == b'\\' {
263 i += 2;
264 continue;
265 }
266 if bytes[i] == b'"' {
267 i += 1;
268 break;
269 }
270 i += 1;
271 }
272 }
273 b'<' | b'(' | b'[' | b'{' => {
274 depth += 1;
275 i += 1;
276 }
277 b'>' | b')' | b']' | b'}' => {
278 depth = (depth - 1).max(0);
279 i += 1;
280 }
281 b';' if depth == 0 => {
282 i += 1;
283 let chunk = &body[chunk_start..i];
284 if !chunk.trim().is_empty()
285 && let Some(name) = c_field_name_from_chunk(chunk)
286 {
287 result.push((name, chunk.to_string()));
288 }
289 chunk_start = i;
290 }
291 _ => {
292 i += 1;
293 }
294 }
295 }
296 result
297}
298
299fn c_field_name_from_chunk(chunk: &str) -> Option<String> {
303 let code: String = chunk
305 .lines()
306 .filter(|l| !l.trim().starts_with("//"))
307 .collect::<Vec<_>>()
308 .join(" ");
309
310 let stripped = code.trim_end_matches(';').trim();
313 let stripped = if let Some(bracket) = stripped.rfind('[') {
315 stripped[..bracket].trim()
316 } else {
317 stripped
318 };
319 let stripped = stripped
321 .trim_start_matches('*')
322 .trim_end_matches('*')
323 .trim();
324
325 let last = stripped.split_whitespace().next_back()?;
327 let last = last.trim_start_matches('*').trim_end_matches('*');
329
330 if last.chars().all(|c| c.is_alphanumeric() || c == '_')
331 && !last.is_empty()
332 && !last.starts_with(|c: char| c.is_ascii_digit())
333 && !is_c_keyword(last)
334 {
335 Some(last.to_string())
336 } else {
337 None
338 }
339}
340
341fn is_c_keyword(s: &str) -> bool {
342 matches!(
343 s,
344 "const"
345 | "volatile"
346 | "restrict"
347 | "unsigned"
348 | "signed"
349 | "short"
350 | "long"
351 | "int"
352 | "char"
353 | "float"
354 | "double"
355 | "void"
356 | "struct"
357 | "union"
358 | "enum"
359 | "typedef"
360 | "extern"
361 | "static"
362 | "inline"
363 | "auto"
364 | "register"
365 | "bool"
366 | "_Bool"
367 | "uint8_t"
368 | "uint16_t"
369 | "uint32_t"
370 | "uint64_t"
371 | "int8_t"
372 | "int16_t"
373 | "int32_t"
374 | "int64_t"
375 | "size_t"
376 | "ssize_t"
377 | "ptrdiff_t"
378 | "uintptr_t"
379 | "intptr_t"
380 )
381}
382
383pub fn extract_go_field_chunks(body: &str) -> Vec<(String, String)> {
386 let mut result: Vec<(String, String)> = Vec::new();
387 for line in body.lines() {
388 let s = line.trim();
389 if s.is_empty() || s.starts_with("//") {
390 continue;
391 }
392 if let Some(name) = go_field_name_from_line(s) {
393 result.push((name, format!("{line}\n")));
394 }
395 }
396 result
397}
398
399fn go_field_name_from_line(line: &str) -> Option<String> {
400 let code = if let Some(pos) = line.find("//") {
403 line[..pos].trim()
404 } else {
405 line.trim()
406 };
407 let first = code.split_whitespace().next()?;
408 let name = first.trim_end_matches(',');
409 if name
410 .chars()
411 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
412 && !name.is_empty()
413 {
414 let simple = name.split('.').next_back().unwrap_or(name);
416 Some(simple.to_string())
417 } else {
418 None
419 }
420}
421
422pub fn extract_zig_field_chunks(body: &str) -> Vec<(String, String)> {
425 let mut result: Vec<(String, String)> = Vec::new();
427 let mut depth: i32 = 0;
428 let mut chunk_start = 0usize;
429 let bytes = body.as_bytes();
430 let mut i = 0usize;
431
432 while i < bytes.len() {
433 match bytes[i] {
434 b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'/' => {
435 while i < bytes.len() && bytes[i] != b'\n' {
436 i += 1;
437 }
438 }
439 b'"' => {
440 i += 1;
441 while i < bytes.len() {
442 if bytes[i] == b'\\' {
443 i += 2;
444 continue;
445 }
446 if bytes[i] == b'"' {
447 i += 1;
448 break;
449 }
450 i += 1;
451 }
452 }
453 b'<' | b'(' | b'[' => {
454 depth += 1;
455 i += 1;
456 }
457 b'>' | b')' | b']' => {
458 depth = (depth - 1).max(0);
459 i += 1;
460 }
461 b'{' | b'}' => {
462 i += 1;
463 }
464 b',' if depth == 0 => {
465 i += 1;
466 let chunk = &body[chunk_start..i];
467 if let Some(name) = zig_field_name_from_chunk(chunk) {
468 result.push((name, chunk.to_string()));
469 }
470 chunk_start = i;
471 }
472 _ => {
473 i += 1;
474 }
475 }
476 }
477 let tail = body[chunk_start..].trim();
478 if !tail.is_empty() {
479 let chunk = &body[chunk_start..];
480 if let Some(name) = zig_field_name_from_chunk(chunk) {
481 result.push((name, chunk.to_string()));
482 }
483 }
484 result
485}
486
487fn zig_field_name_from_chunk(chunk: &str) -> Option<String> {
488 for line in chunk.lines() {
489 let s = line.trim();
490 if s.is_empty() || s.starts_with("//") {
491 continue;
492 }
493 let colon = s.find(':')?;
495 let name = s[..colon].trim().to_string();
496 if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
497 return Some(name);
498 }
499 return None;
500 }
501 None
502}
503
504pub fn generate_rust_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
517 if let Some(result) = try_source_aware_rust(layout, struct_source) {
518 return result;
519 }
520 generate_rust_fix(layout)
521}
522
523fn try_source_aware_rust(layout: &StructLayout, struct_source: &str) -> Option<String> {
524 let brace_open = struct_source.find('{')?;
525 let body_with_close = &struct_source[brace_open..];
527 let body_len = match_braces(body_with_close)?;
528 let body = &body_with_close[1..body_len - 1]; let chunks = extract_rust_field_chunks(body);
531 if chunks.is_empty() {
532 return None;
533 }
534
535 let chunk_map: std::collections::HashMap<&str, &str> = chunks
536 .iter()
537 .map(|(n, c)| (n.as_str(), c.as_str()))
538 .collect();
539
540 let optimal = optimal_order(layout);
541 if optimal
543 .iter()
544 .any(|f| !chunk_map.contains_key(f.name.as_str()))
545 {
546 return None;
547 }
548
549 let header = &struct_source[..=brace_open];
550 let mut result = header.to_string();
551 result.push('\n');
552 for field in &optimal {
553 result.push_str(chunk_map[field.name.as_str()]);
554 }
555 if !result.ends_with('\n') {
557 result.push('\n');
558 }
559 result.push('}');
560 let after = &struct_source[brace_open + body_len..];
562 result.push_str(after);
563 Some(result)
564}
565
566pub fn generate_c_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
568 if let Some(result) = try_source_aware_c(layout, struct_source) {
569 return result;
570 }
571 generate_c_fix(layout)
572}
573
574fn try_source_aware_c(layout: &StructLayout, struct_source: &str) -> Option<String> {
575 let brace_open = struct_source.find('{')?;
576 let body_with_close = &struct_source[brace_open..];
577 let body_len = match_braces(body_with_close)?;
578 let body = &body_with_close[1..body_len - 1];
579
580 let chunks = extract_c_field_chunks(body);
581 if chunks.is_empty() {
582 return None;
583 }
584
585 let chunk_map: std::collections::HashMap<&str, &str> = chunks
586 .iter()
587 .map(|(n, c)| (n.as_str(), c.as_str()))
588 .collect();
589
590 let optimal = optimal_order(layout);
591 if optimal
592 .iter()
593 .any(|f| !chunk_map.contains_key(f.name.as_str()))
594 {
595 return None;
596 }
597
598 let header = &struct_source[..=brace_open];
599 let mut result = header.to_string();
600 result.push('\n');
601 for field in &optimal {
602 result.push_str(chunk_map[field.name.as_str()]);
603 }
604 if !result.ends_with('\n') {
605 result.push('\n');
606 }
607 result.push('}');
608 let close_end = brace_open + body_len;
609 let after = &struct_source[close_end..];
610 result.push_str(after);
611 Some(result)
612}
613
614pub fn generate_go_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
616 if let Some(result) = try_source_aware_go(layout, struct_source) {
617 return result;
618 }
619 generate_go_fix(layout)
620}
621
622fn try_source_aware_go(layout: &StructLayout, struct_source: &str) -> Option<String> {
623 let brace_open = struct_source.find('{')?;
624 let body_with_close = &struct_source[brace_open..];
625 let body_len = match_braces(body_with_close)?;
626 let body = &body_with_close[1..body_len - 1];
627
628 let chunks = extract_go_field_chunks(body);
629 if chunks.is_empty() {
630 return None;
631 }
632
633 let chunk_map: std::collections::HashMap<&str, &str> = chunks
634 .iter()
635 .map(|(n, c)| (n.as_str(), c.as_str()))
636 .collect();
637
638 let optimal = optimal_order(layout);
639 if optimal
640 .iter()
641 .any(|f| !chunk_map.contains_key(f.name.as_str()))
642 {
643 return None;
644 }
645
646 let header = &struct_source[..=brace_open];
647 let mut result = header.to_string();
648 result.push('\n');
649 for field in &optimal {
650 result.push_str(chunk_map[field.name.as_str()]);
651 }
652 if !result.ends_with('\n') {
653 result.push('\n');
654 }
655 result.push('}');
656 let close_end = brace_open + body_len;
657 let after = &struct_source[close_end..];
658 result.push_str(after);
659 Some(result)
660}
661
662pub fn generate_zig_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
664 if let Some(result) = try_source_aware_zig(layout, struct_source) {
665 return result;
666 }
667 generate_zig_fix(layout)
668}
669
670fn try_source_aware_zig(layout: &StructLayout, struct_source: &str) -> Option<String> {
671 let brace_open = struct_source.find('{')?;
672 let body_with_close = &struct_source[brace_open..];
673 let body_len = match_braces(body_with_close)?;
674 let body = &body_with_close[1..body_len - 1];
675
676 let chunks = extract_zig_field_chunks(body);
677 if chunks.is_empty() {
678 return None;
679 }
680
681 let chunk_map: std::collections::HashMap<&str, &str> = chunks
682 .iter()
683 .map(|(n, c)| (n.as_str(), c.as_str()))
684 .collect();
685
686 let optimal = optimal_order(layout);
687 if optimal
688 .iter()
689 .any(|f| !chunk_map.contains_key(f.name.as_str()))
690 {
691 return None;
692 }
693
694 let header = &struct_source[..=brace_open];
695 let mut result = header.to_string();
696 result.push('\n');
697 for field in &optimal {
698 result.push_str(chunk_map[field.name.as_str()]);
699 }
700 if !result.ends_with('\n') {
701 result.push('\n');
702 }
703 result.push('}');
704 let close_end = brace_open + body_len;
705 let after = &struct_source[close_end..];
706 result.push_str(after);
707 Some(result)
708}
709
710fn match_braces(s: &str) -> Option<usize> {
715 let mut depth = 0usize;
716 for (i, c) in s.char_indices() {
717 match c {
718 '{' => depth += 1,
719 '}' => {
720 depth -= 1;
721 if depth == 0 {
722 return Some(i + 1);
723 }
724 }
725 _ => {}
726 }
727 }
728 None
729}
730
731fn consume_semicolon(source: &str, pos: usize) -> usize {
733 let rest = &source[pos..];
734 let ws = rest.len()
735 - rest
736 .trim_start_matches(|c: char| c.is_whitespace() && c != '\n')
737 .len();
738 let after_ws = &rest[ws..];
739 if after_ws.starts_with(';') {
740 pos + ws + 1
741 } else {
742 pos
743 }
744}
745
746pub fn find_c_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
749 for kw in &["struct", "union"] {
750 let needle = format!("{kw} {struct_name}");
751 let mut search_from = 0usize;
752 while let Some(rel) = source[search_from..].find(&needle) {
753 let start = search_from + rel;
754 let after_name = start + needle.len();
755 let boundary = source[after_name..].chars().next();
757 if matches!(
758 boundary,
759 Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
760 ) {
761 if let Some(brace_rel) = source[after_name..].find('{') {
763 let brace_start = after_name + brace_rel;
764 if source[after_name..brace_start]
766 .chars()
767 .all(|c| c.is_whitespace())
768 && let Some(body_len) = match_braces(&source[brace_start..])
769 {
770 let end = consume_semicolon(source, brace_start + body_len);
771 return Some(start..end);
772 }
773 }
774 }
775 search_from = start + 1;
776 }
777 }
778 None
779}
780
781pub fn find_rust_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
783 let needle = format!("struct {struct_name}");
784 let mut search_from = 0usize;
785 while let Some(rel) = source[search_from..].find(&needle) {
786 let start = search_from + rel;
787 let after_name = start + needle.len();
788 let boundary = source[after_name..].chars().next();
789 if matches!(
790 boundary,
791 Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
792 ) && let Some(brace_rel) = source[after_name..].find('{')
793 {
794 let brace_start = after_name + brace_rel;
795 if source[after_name..brace_start]
796 .chars()
797 .all(|c| c.is_whitespace())
798 && let Some(body_len) = match_braces(&source[brace_start..])
799 {
800 return Some(start..brace_start + body_len);
802 }
803 }
804 search_from = start + 1;
805 }
806 None
807}
808
809pub fn find_go_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
811 let needle = format!("type {struct_name} struct");
812 let mut search_from = 0usize;
813 while let Some(rel) = source[search_from..].find(&needle) {
814 let start = search_from + rel;
815 let after_kw = start + needle.len();
816 if let Some(brace_rel) = source[after_kw..].find('{') {
817 let brace_start = after_kw + brace_rel;
818 if source[after_kw..brace_start]
819 .chars()
820 .all(|c| c.is_whitespace())
821 && let Some(body_len) = match_braces(&source[brace_start..])
822 {
823 return Some(start..brace_start + body_len);
824 }
825 }
826 search_from = start + 1;
827 }
828 None
829}
830
831pub fn apply_fixes_c(source: &str, layouts: &[&StructLayout]) -> String {
840 apply_fixes_with_source(
841 source,
842 layouts,
843 find_c_struct_span,
844 generate_c_fix_from_source,
845 )
846}
847
848pub fn apply_fixes_rust(source: &str, layouts: &[&StructLayout]) -> String {
852 apply_fixes_with_source(
853 source,
854 layouts,
855 find_rust_struct_span,
856 generate_rust_fix_from_source,
857 )
858}
859
860pub fn apply_fixes_go(source: &str, layouts: &[&StructLayout]) -> String {
863 apply_fixes_with_source(
864 source,
865 layouts,
866 find_go_struct_span,
867 generate_go_fix_from_source,
868 )
869}
870
871pub fn generate_zig_fix(layout: &StructLayout) -> String {
875 let optimal = optimal_order(layout);
876 let qualifier = if layout.is_packed { "packed " } else { "" };
877 let mut out = format!("const {} = {}struct {{\n", layout.name, qualifier);
878 for field in &optimal {
879 let ty = field_type_name(field);
880 out.push_str(&format!(" {}: {ty},\n", field.name));
881 }
882 out.push_str("};\n");
883 out
884}
885
886pub fn find_zig_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
889 let needle = format!("const {struct_name}");
891 let mut search_from = 0usize;
892 while let Some(rel) = source[search_from..].find(&needle) {
893 let start = search_from + rel;
894 let after_name = start + needle.len();
895 let rest = source[after_name..].trim_start();
897 if !rest.starts_with('=') {
898 search_from = start + 1;
899 continue;
900 }
901 let after_eq = after_name + source[after_name..].find('=')? + 1;
903 let after_eq_rest = &source[after_eq..];
904 if let Some(struct_rel) = after_eq_rest.find("struct") {
906 let prefix = &after_eq_rest[..struct_rel];
909 let prefix_clean = prefix.trim();
910 if prefix_clean.is_empty() || prefix_clean == "packed" || prefix_clean == "extern" {
911 let struct_kw_end = after_eq + struct_rel + "struct".len();
912 if let Some(brace_rel) = source[struct_kw_end..].find('{') {
913 let brace_start = struct_kw_end + brace_rel;
914 if source[struct_kw_end..brace_start]
915 .chars()
916 .all(|c| c.is_whitespace())
917 && let Some(body_len) = match_braces(&source[brace_start..])
918 {
919 let end = consume_semicolon(source, brace_start + body_len);
920 return Some(start..end);
921 }
922 }
923 }
924 }
925 search_from = start + 1;
926 }
927 None
928}
929
930pub fn apply_fixes_zig(source: &str, layouts: &[&StructLayout]) -> String {
933 apply_fixes_with_source(
934 source,
935 layouts,
936 find_zig_struct_span,
937 generate_zig_fix_from_source,
938 )
939}
940
941fn apply_fixes_with_source(
944 source: &str,
945 layouts: &[&StructLayout],
946 find_span: fn(&str, &str) -> Option<std::ops::Range<usize>>,
947 generate: fn(&StructLayout, &str) -> String,
948) -> String {
949 let mut replacements: Vec<(usize, usize, String)> = layouts
951 .iter()
952 .filter_map(|layout| {
953 let span = find_span(source, &layout.name)?;
954 let struct_source = &source[span.clone()];
955 let fixed = generate(layout, struct_source);
956 Some((span.start, span.end, fixed))
957 })
958 .collect();
959
960 replacements.sort_by_key(|(start, _, _)| *start);
962
963 let mut result = source.to_string();
964 for (start, end, fixed) in replacements.into_iter().rev() {
965 result.replace_range(start..end, &fixed);
966 }
967 result
968}
969
970fn field_type_name(field: &padlock_core::ir::Field) -> &str {
971 match &field.ty {
972 padlock_core::ir::TypeInfo::Primitive { name, .. }
973 | padlock_core::ir::TypeInfo::Opaque { name, .. } => name.as_str(),
974 padlock_core::ir::TypeInfo::Pointer { .. } => "void*",
975 padlock_core::ir::TypeInfo::Array { .. } => "/* array */",
976 padlock_core::ir::TypeInfo::Struct(l) => l.name.as_str(),
977 }
978}
979
980#[cfg(test)]
983mod tests {
984 use super::*;
985 use padlock_core::ir::test_fixtures::connection_layout;
986
987 #[test]
988 fn c_fix_starts_with_struct() {
989 let out = generate_c_fix(&connection_layout());
990 assert!(out.starts_with("struct Connection {"));
991 }
992
993 #[test]
994 fn c_fix_contains_all_fields() {
995 let out = generate_c_fix(&connection_layout());
996 assert!(out.contains("timeout"));
997 assert!(out.contains("port"));
998 assert!(out.contains("is_active"));
999 assert!(out.contains("is_tls"));
1000 }
1001
1002 #[test]
1003 fn c_fix_puts_largest_align_first() {
1004 let out = generate_c_fix(&connection_layout());
1005 let timeout_pos = out.find("timeout").unwrap();
1006 let is_active_pos = out.find("is_active").unwrap();
1007 assert!(timeout_pos < is_active_pos);
1008 }
1009
1010 #[test]
1011 fn rust_fix_uses_colon_syntax() {
1012 let out = generate_rust_fix(&connection_layout());
1013 assert!(out.contains(": f64"));
1014 }
1015
1016 #[test]
1017 fn unified_diff_marks_changes() {
1018 let orig = "struct T { char a; double b; };\n";
1019 let fixed = "struct T { double b; char a; };\n";
1020 let diff = unified_diff(orig, fixed, 1);
1021 assert!(diff.contains('-') || diff.contains('+'));
1022 }
1023
1024 #[test]
1025 fn unified_diff_identical_is_no_changes() {
1026 assert_eq!(unified_diff("x\n", "x\n", 3), "(no changes)\n");
1027 }
1028
1029 #[test]
1032 fn find_c_struct_span_basic() {
1033 let src = "struct Foo { int x; char y; };\nstruct Bar { double z; };\n";
1034 let span = find_c_struct_span(src, "Foo").unwrap();
1035 let text = &src[span];
1036 assert!(text.starts_with("struct Foo"));
1037 assert!(!text.contains("Bar"));
1038 }
1039
1040 #[test]
1041 fn find_c_struct_span_missing_returns_none() {
1042 let src = "struct Other { int x; };";
1043 assert!(find_c_struct_span(src, "Missing").is_none());
1044 }
1045
1046 #[test]
1047 fn find_rust_struct_span_basic() {
1048 let src = "struct Foo {\n x: u32,\n y: u8,\n}\n";
1049 let span = find_rust_struct_span(src, "Foo").unwrap();
1050 assert!(src[span].starts_with("struct Foo"));
1051 }
1052
1053 #[test]
1054 fn find_go_struct_span_basic() {
1055 let src = "type Foo struct {\n\tX int32\n\tY bool\n}\n";
1056 let span = find_go_struct_span(src, "Foo").unwrap();
1057 assert!(src[span].starts_with("type Foo struct"));
1058 }
1059
1060 #[test]
1063 fn apply_fixes_c_reorders_in_place() {
1064 let src = "struct Connection { bool is_active; double timeout; bool is_tls; int port; };\n";
1066 let layout = connection_layout();
1067 let fixed = apply_fixes_c(src, &[&layout]);
1068 let timeout_pos = fixed.find("timeout").unwrap();
1069 let is_active_pos = fixed.find("is_active").unwrap();
1070 assert!(
1071 timeout_pos < is_active_pos,
1072 "double should appear before bool after reorder"
1073 );
1074 }
1075
1076 #[test]
1077 fn apply_fixes_rust_reorders_in_place() {
1078 let src = "struct Connection {\n is_active: bool,\n timeout: f64,\n is_tls: bool,\n port: i32,\n}\n";
1079 let layout = connection_layout();
1080 let fixed = apply_fixes_rust(src, &[&layout]);
1081 let timeout_pos = fixed.find("timeout").unwrap();
1082 let is_active_pos = fixed.find("is_active").unwrap();
1083 assert!(timeout_pos < is_active_pos);
1084 }
1085
1086 #[test]
1087 fn go_fix_uses_tab_syntax() {
1088 let layout = connection_layout();
1089 let out = generate_go_fix(&layout);
1090 assert!(out.starts_with("type Connection struct"));
1091 assert!(out.contains('\t'));
1092 }
1093
1094 #[test]
1095 fn zig_fix_uses_const_struct_syntax() {
1096 let out = generate_zig_fix(&connection_layout());
1097 assert!(out.starts_with("const Connection = struct {"));
1098 assert!(out.ends_with("};\n"));
1099 }
1100
1101 #[test]
1102 fn find_zig_struct_span_basic() {
1103 let src = "const S = struct {\n x: u32,\n y: u8,\n};\n";
1104 let span = find_zig_struct_span(src, "S").unwrap();
1105 assert!(src[span].starts_with("const S = struct"));
1106 }
1107
1108 #[test]
1109 fn find_zig_struct_span_packed() {
1110 let src = "const S = packed struct {\n x: u32,\n y: u8,\n};\n";
1111 let span = find_zig_struct_span(src, "S").unwrap();
1112 assert!(src[span].contains("packed struct"));
1113 }
1114
1115 #[test]
1116 fn find_zig_struct_span_missing_returns_none() {
1117 let src = "const Other = struct { x: u8 };\n";
1118 assert!(find_zig_struct_span(src, "Missing").is_none());
1119 }
1120
1121 #[test]
1122 fn apply_fixes_zig_reorders_in_place() {
1123 use crate::parse_source_str;
1124 use padlock_core::arch::X86_64_SYSV;
1125 let src = "const S = struct {\n a: u8,\n b: u64,\n};\n";
1126 let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1127 let layout = &layouts[0];
1128 let fixed = apply_fixes_zig(src, &[layout]);
1129 let b_pos = fixed.find("b:").unwrap();
1131 let a_pos = fixed.find("a:").unwrap();
1132 assert!(
1133 b_pos < a_pos,
1134 "u64 field should come before u8 after reorder"
1135 );
1136 }
1137
1138 #[test]
1141 fn rust_fix_preserves_pub_visibility() {
1142 let src = "struct S {\n pub a: u8,\n pub b: u64,\n}\n";
1143 use crate::parse_source_str;
1144 use padlock_core::arch::X86_64_SYSV;
1145 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1146 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1147 assert!(fixed.contains("pub b: u64"), "pub on b must be preserved");
1149 assert!(fixed.contains("pub a: u8"), "pub on a must be preserved");
1150 assert!(fixed.find("pub b").unwrap() < fixed.find("pub a").unwrap());
1152 }
1153
1154 #[test]
1155 fn rust_fix_preserves_doc_comments() {
1156 let src = concat!(
1157 "struct S {\n",
1158 " /// small field\n",
1159 " a: u8,\n",
1160 " /// large field\n",
1161 " b: u64,\n",
1162 "}\n"
1163 );
1164 use crate::parse_source_str;
1165 use padlock_core::arch::X86_64_SYSV;
1166 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1167 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1168 assert!(
1169 fixed.contains("/// large field"),
1170 "doc comment for b must survive"
1171 );
1172 assert!(
1173 fixed.contains("/// small field"),
1174 "doc comment for a must survive"
1175 );
1176 assert!(
1178 fixed.find("large field").unwrap() < fixed.find("small field").unwrap(),
1179 "doc comment ordering must follow field ordering"
1180 );
1181 }
1182
1183 #[test]
1184 fn rust_fix_preserves_serde_attributes() {
1185 let src = concat!(
1186 "struct S {\n",
1187 " #[serde(skip)]\n",
1188 " a: u8,\n",
1189 " #[serde(rename = \"big\")]\n",
1190 " b: u64,\n",
1191 "}\n"
1192 );
1193 use crate::parse_source_str;
1194 use padlock_core::arch::X86_64_SYSV;
1195 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1196 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1197 assert!(
1198 fixed.contains("#[serde(skip)]"),
1199 "serde attribute on a must survive"
1200 );
1201 assert!(
1202 fixed.contains("#[serde(rename = \"big\")]"),
1203 "serde attribute on b must survive"
1204 );
1205 }
1206
1207 #[test]
1208 fn rust_fix_preserves_pub_crate_visibility() {
1209 let src = "struct S {\n pub(crate) a: u8,\n pub(crate) b: u64,\n}\n";
1210 use crate::parse_source_str;
1211 use padlock_core::arch::X86_64_SYSV;
1212 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1213 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1214 assert!(
1215 fixed.contains("pub(crate) b: u64"),
1216 "pub(crate) on b must be preserved"
1217 );
1218 assert!(
1219 fixed.contains("pub(crate) a: u8"),
1220 "pub(crate) on a must be preserved"
1221 );
1222 }
1223
1224 #[test]
1225 fn c_fix_preserves_guarded_by_comments() {
1226 let src = concat!(
1227 "struct S {\n",
1228 " char a; // GUARDED_BY(mu)\n",
1229 " double b; // large field\n",
1230 "};\n"
1231 );
1232 use crate::parse_source_str;
1233 use padlock_core::arch::X86_64_SYSV;
1234 let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1235 let fixed = apply_fixes_c(src, &[&layouts[0]]);
1236 assert!(
1237 fixed.contains("GUARDED_BY(mu)"),
1238 "guard annotation comment must survive reorder"
1239 );
1240 assert!(fixed.find("double b").unwrap() < fixed.find("char a").unwrap());
1242 }
1243
1244 #[test]
1245 fn go_fix_preserves_field_tags() {
1246 let src = concat!("type S struct {\n", "\ta uint8\n", "\tb uint64\n", "}\n");
1247 use crate::parse_source_str;
1248 use padlock_core::arch::X86_64_SYSV;
1249 let layouts = parse_source_str(src, &crate::SourceLanguage::Go, &X86_64_SYSV).unwrap();
1250 let fixed = apply_fixes_go(src, &[&layouts[0]]);
1251 assert!(fixed.find("\tb uint64").unwrap() < fixed.find("\ta uint8").unwrap());
1253 }
1254
1255 #[test]
1256 fn zig_fix_preserves_field_comments() {
1257 let src = concat!(
1258 "const S = struct {\n",
1259 " // small\n",
1260 " a: u8,\n",
1261 " // large\n",
1262 " b: u64,\n",
1263 "};\n"
1264 );
1265 use crate::parse_source_str;
1266 use padlock_core::arch::X86_64_SYSV;
1267 let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1268 let fixed = apply_fixes_zig(src, &[&layouts[0]]);
1269 assert!(fixed.contains("// large"), "comment for b must survive");
1270 assert!(fixed.contains("// small"), "comment for a must survive");
1271 assert!(fixed.find("// large").unwrap() < fixed.find("// small").unwrap());
1273 }
1274
1275 #[test]
1278 fn rust_fix_from_source_falls_back_when_no_open_brace() {
1279 let layout = connection_layout();
1281 let out = generate_rust_fix_from_source(&layout, "struct Connection");
1282 assert!(out.starts_with("struct Connection {"));
1284 }
1285
1286 #[test]
1287 fn c_fix_from_source_falls_back_when_chunks_empty() {
1288 let layout = connection_layout();
1291 let out = generate_c_fix_from_source(&layout, "struct Connection { /* no fields */ };");
1292 assert!(out.starts_with("struct Connection {"));
1293 assert!(out.contains("timeout"));
1294 }
1295
1296 #[test]
1297 fn zig_fix_from_source_falls_back_on_missing_field_name() {
1298 let layout = connection_layout();
1300 let out =
1301 generate_zig_fix_from_source(&layout, "const Connection = struct { x: u8, y: u64, };");
1302 assert!(out.contains("timeout"));
1304 }
1305}