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 if !body.starts_with('\n') {
573 result.push('\n');
574 }
575 for field in &optimal {
576 result.push_str(chunk_map[field.name.as_str()]);
577 }
578 if !result.ends_with('\n') {
580 result.push('\n');
581 }
582 result.push('}');
583 let after = &struct_source[brace_open + body_len..];
585 result.push_str(after);
586 Some(result)
587}
588
589fn try_source_aware_rust_tuple(layout: &StructLayout, struct_source: &str) -> Option<String> {
592 let paren_open = struct_source.find('(')?;
593 let body_with_close = &struct_source[paren_open..];
594 let paren_len = match_parens(body_with_close)?;
596 let body = &body_with_close[1..paren_len - 1]; let type_chunks = extract_tuple_type_chunks(body);
600 if type_chunks.is_empty() {
601 return None;
602 }
603
604 let chunk_map: std::collections::HashMap<String, &str> = type_chunks
607 .iter()
608 .enumerate()
609 .map(|(i, c)| (format!("_{i}"), c.as_str()))
610 .collect();
611
612 let optimal = optimal_order(layout);
613 if optimal.iter().any(|f| !chunk_map.contains_key(&f.name)) {
614 return None;
615 }
616
617 let header = &struct_source[..=paren_open];
619 let mut result = header.to_string();
620 let reordered: Vec<&str> = optimal.iter().map(|f| chunk_map[&f.name]).collect();
621 result.push_str(&reordered.join(", "));
622 result.push(')');
623 let after = &struct_source[paren_open + paren_len..];
625 result.push_str(after);
626 Some(result)
627}
628
629fn extract_tuple_type_chunks(body: &str) -> Vec<String> {
631 let mut result = Vec::new();
632 let mut depth: i32 = 0;
633 let mut chunk_start = 0usize;
634 let bytes = body.as_bytes();
635 let mut i = 0usize;
636
637 while i < bytes.len() {
638 match bytes[i] {
639 b'<' | b'[' => {
640 depth += 1;
641 i += 1;
642 }
643 b'>' | b']' => {
644 depth = (depth - 1).max(0);
645 i += 1;
646 }
647 b'(' => {
648 depth += 1;
649 i += 1;
650 }
651 b')' => {
652 depth = (depth - 1).max(0);
653 i += 1;
654 }
655 b',' if depth == 0 => {
656 let chunk = body[chunk_start..i].trim().to_string();
657 if !chunk.is_empty() {
658 result.push(chunk);
659 }
660 i += 1;
661 chunk_start = i;
662 }
663 _ => {
664 i += 1;
665 }
666 }
667 }
668 let tail = body[chunk_start..].trim().to_string();
669 if !tail.is_empty() {
670 result.push(tail);
671 }
672 result
673}
674
675fn match_parens(s: &str) -> Option<usize> {
678 let mut depth = 0usize;
679 for (i, c) in s.char_indices() {
680 match c {
681 '(' => depth += 1,
682 ')' => {
683 depth -= 1;
684 if depth == 0 {
685 return Some(i + 1);
686 }
687 }
688 _ => {}
689 }
690 }
691 None
692}
693
694pub fn generate_c_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
696 if let Some(result) = try_source_aware_c(layout, struct_source) {
697 return result;
698 }
699 generate_c_fix(layout)
700}
701
702fn try_source_aware_c(layout: &StructLayout, struct_source: &str) -> Option<String> {
703 let brace_open = struct_source.find('{')?;
704 let body_with_close = &struct_source[brace_open..];
705 let body_len = match_braces(body_with_close)?;
706 let body = &body_with_close[1..body_len - 1];
707
708 let chunks = extract_c_field_chunks(body);
709 if chunks.is_empty() {
710 return None;
711 }
712
713 let chunk_map: std::collections::HashMap<&str, &str> = chunks
714 .iter()
715 .map(|(n, c)| (n.as_str(), c.as_str()))
716 .collect();
717
718 let optimal = optimal_order(layout);
719 if optimal
720 .iter()
721 .any(|f| !chunk_map.contains_key(f.name.as_str()))
722 {
723 return None;
724 }
725
726 let header = &struct_source[..=brace_open];
727 let mut result = header.to_string();
728 if !body.starts_with('\n') {
729 result.push('\n');
730 }
731 for field in &optimal {
732 result.push_str(chunk_map[field.name.as_str()]);
733 }
734 if !result.ends_with('\n') {
735 result.push('\n');
736 }
737 result.push('}');
738 let close_end = brace_open + body_len;
739 let after = &struct_source[close_end..];
740 result.push_str(after);
741 Some(result)
742}
743
744pub fn generate_go_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
746 if let Some(result) = try_source_aware_go(layout, struct_source) {
747 return result;
748 }
749 generate_go_fix(layout)
750}
751
752fn try_source_aware_go(layout: &StructLayout, struct_source: &str) -> Option<String> {
753 let brace_open = struct_source.find('{')?;
754 let body_with_close = &struct_source[brace_open..];
755 let body_len = match_braces(body_with_close)?;
756 let body = &body_with_close[1..body_len - 1];
757
758 let chunks = extract_go_field_chunks(body);
759 if chunks.is_empty() {
760 return None;
761 }
762
763 let chunk_map: std::collections::HashMap<&str, &str> = chunks
764 .iter()
765 .map(|(n, c)| (n.as_str(), c.as_str()))
766 .collect();
767
768 let optimal = optimal_order(layout);
769 if optimal
770 .iter()
771 .any(|f| !chunk_map.contains_key(f.name.as_str()))
772 {
773 return None;
774 }
775
776 let header = &struct_source[..=brace_open];
777 let mut result = header.to_string();
778 if !body.starts_with('\n') {
779 result.push('\n');
780 }
781 for field in &optimal {
782 result.push_str(chunk_map[field.name.as_str()]);
783 }
784 if !result.ends_with('\n') {
785 result.push('\n');
786 }
787 result.push('}');
788 let close_end = brace_open + body_len;
789 let after = &struct_source[close_end..];
790 result.push_str(after);
791 Some(result)
792}
793
794pub fn generate_zig_fix_from_source(layout: &StructLayout, struct_source: &str) -> String {
796 if let Some(result) = try_source_aware_zig(layout, struct_source) {
797 return result;
798 }
799 generate_zig_fix(layout)
800}
801
802fn try_source_aware_zig(layout: &StructLayout, struct_source: &str) -> Option<String> {
803 let brace_open = struct_source.find('{')?;
804 let body_with_close = &struct_source[brace_open..];
805 let body_len = match_braces(body_with_close)?;
806 let body = &body_with_close[1..body_len - 1];
807
808 let chunks = extract_zig_field_chunks(body);
809 if chunks.is_empty() {
810 return None;
811 }
812
813 let chunk_map: std::collections::HashMap<&str, &str> = chunks
814 .iter()
815 .map(|(n, c)| (n.as_str(), c.as_str()))
816 .collect();
817
818 let optimal = optimal_order(layout);
819 if optimal
820 .iter()
821 .any(|f| !chunk_map.contains_key(f.name.as_str()))
822 {
823 return None;
824 }
825
826 let header = &struct_source[..=brace_open];
827 let mut result = header.to_string();
828 if !body.starts_with('\n') {
829 result.push('\n');
830 }
831 for field in &optimal {
832 result.push_str(chunk_map[field.name.as_str()]);
833 }
834 if !result.ends_with('\n') {
835 result.push('\n');
836 }
837 result.push('}');
838 let close_end = brace_open + body_len;
839 let after = &struct_source[close_end..];
840 result.push_str(after);
841 Some(result)
842}
843
844fn match_braces(s: &str) -> Option<usize> {
849 let mut depth = 0usize;
850 for (i, c) in s.char_indices() {
851 match c {
852 '{' => depth += 1,
853 '}' => {
854 depth -= 1;
855 if depth == 0 {
856 return Some(i + 1);
857 }
858 }
859 _ => {}
860 }
861 }
862 None
863}
864
865fn consume_semicolon(source: &str, pos: usize) -> usize {
867 let rest = &source[pos..];
868 let ws = rest.len()
869 - rest
870 .trim_start_matches(|c: char| c.is_whitespace() && c != '\n')
871 .len();
872 let after_ws = &rest[ws..];
873 if after_ws.starts_with(';') {
874 pos + ws + 1
875 } else {
876 pos
877 }
878}
879
880pub fn find_c_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
883 for kw in &["struct", "union"] {
884 let needle = format!("{kw} {struct_name}");
885 let mut search_from = 0usize;
886 while let Some(rel) = source[search_from..].find(&needle) {
887 let start = search_from + rel;
888 let after_name = start + needle.len();
889 let boundary = source[after_name..].chars().next();
891 if matches!(
892 boundary,
893 Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
894 ) {
895 if let Some(brace_rel) = source[after_name..].find('{') {
897 let brace_start = after_name + brace_rel;
898 if source[after_name..brace_start]
900 .chars()
901 .all(|c| c.is_whitespace())
902 && let Some(body_len) = match_braces(&source[brace_start..])
903 {
904 let end = consume_semicolon(source, brace_start + body_len);
905 return Some(start..end);
906 }
907 }
908 }
909 search_from = start + 1;
910 }
911 }
912 None
913}
914
915pub fn find_rust_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
917 let needle = format!("struct {struct_name}");
918 let mut search_from = 0usize;
919 while let Some(rel) = source[search_from..].find(&needle) {
920 let start = search_from + rel;
921 let after_name = start + needle.len();
922 let boundary = source[after_name..].chars().next();
923 if matches!(
924 boundary,
925 Some('{') | Some('\n') | Some('\r') | Some(' ') | Some('\t') | None
926 ) && let Some(brace_rel) = source[after_name..].find('{')
927 {
928 let brace_start = after_name + brace_rel;
929 if source[after_name..brace_start]
930 .chars()
931 .all(|c| c.is_whitespace())
932 && let Some(body_len) = match_braces(&source[brace_start..])
933 {
934 return Some(start..brace_start + body_len);
936 }
937 }
938 search_from = start + 1;
939 }
940 None
941}
942
943pub fn find_go_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
945 let needle = format!("type {struct_name} struct");
946 let mut search_from = 0usize;
947 while let Some(rel) = source[search_from..].find(&needle) {
948 let start = search_from + rel;
949 let after_kw = start + needle.len();
950 if let Some(brace_rel) = source[after_kw..].find('{') {
951 let brace_start = after_kw + brace_rel;
952 if source[after_kw..brace_start]
953 .chars()
954 .all(|c| c.is_whitespace())
955 && let Some(body_len) = match_braces(&source[brace_start..])
956 {
957 return Some(start..brace_start + body_len);
958 }
959 }
960 search_from = start + 1;
961 }
962 None
963}
964
965pub fn apply_fixes_c(source: &str, layouts: &[&StructLayout]) -> String {
974 apply_fixes_with_source(
975 source,
976 layouts,
977 find_c_struct_span,
978 generate_c_fix_from_source,
979 )
980}
981
982pub fn apply_fixes_rust(source: &str, layouts: &[&StructLayout]) -> String {
986 apply_fixes_with_source(
987 source,
988 layouts,
989 find_rust_struct_span,
990 generate_rust_fix_from_source,
991 )
992}
993
994pub fn apply_fixes_go(source: &str, layouts: &[&StructLayout]) -> String {
997 apply_fixes_with_source(
998 source,
999 layouts,
1000 find_go_struct_span,
1001 generate_go_fix_from_source,
1002 )
1003}
1004
1005pub fn generate_zig_fix(layout: &StructLayout) -> String {
1009 let optimal = optimal_order(layout);
1010 let qualifier = if layout.is_packed { "packed " } else { "" };
1011 let mut out = format!("const {} = {}struct {{\n", layout.name, qualifier);
1012 for field in &optimal {
1013 let ty = field_type_name(field);
1014 out.push_str(&format!(" {}: {ty},\n", field.name));
1015 }
1016 out.push_str("};\n");
1017 out
1018}
1019
1020pub fn find_zig_struct_span(source: &str, struct_name: &str) -> Option<std::ops::Range<usize>> {
1023 let needle = format!("const {struct_name}");
1025 let mut search_from = 0usize;
1026 while let Some(rel) = source[search_from..].find(&needle) {
1027 let start = search_from + rel;
1028 let after_name = start + needle.len();
1029 let rest = source[after_name..].trim_start();
1031 if !rest.starts_with('=') {
1032 search_from = start + 1;
1033 continue;
1034 }
1035 let after_eq = after_name + source[after_name..].find('=')? + 1;
1037 let after_eq_rest = &source[after_eq..];
1038 if let Some(struct_rel) = after_eq_rest.find("struct") {
1040 let prefix = &after_eq_rest[..struct_rel];
1043 let prefix_clean = prefix.trim();
1044 if prefix_clean.is_empty() || prefix_clean == "packed" || prefix_clean == "extern" {
1045 let struct_kw_end = after_eq + struct_rel + "struct".len();
1046 if let Some(brace_rel) = source[struct_kw_end..].find('{') {
1047 let brace_start = struct_kw_end + brace_rel;
1048 if source[struct_kw_end..brace_start]
1049 .chars()
1050 .all(|c| c.is_whitespace())
1051 && let Some(body_len) = match_braces(&source[brace_start..])
1052 {
1053 let end = consume_semicolon(source, brace_start + body_len);
1054 return Some(start..end);
1055 }
1056 }
1057 }
1058 }
1059 search_from = start + 1;
1060 }
1061 None
1062}
1063
1064pub fn apply_fixes_zig(source: &str, layouts: &[&StructLayout]) -> String {
1067 apply_fixes_with_source(
1068 source,
1069 layouts,
1070 find_zig_struct_span,
1071 generate_zig_fix_from_source,
1072 )
1073}
1074
1075fn apply_fixes_with_source(
1078 source: &str,
1079 layouts: &[&StructLayout],
1080 find_span: fn(&str, &str) -> Option<std::ops::Range<usize>>,
1081 generate: fn(&StructLayout, &str) -> String,
1082) -> String {
1083 let mut replacements: Vec<(usize, usize, String)> = layouts
1085 .iter()
1086 .filter_map(|layout| {
1087 let span = find_span(source, &layout.name)?;
1088 let struct_source = &source[span.clone()];
1089 let fixed = generate(layout, struct_source);
1090 Some((span.start, span.end, fixed))
1091 })
1092 .collect();
1093
1094 replacements.sort_by_key(|(start, _, _)| *start);
1096
1097 let mut result = source.to_string();
1098 for (start, end, fixed) in replacements.into_iter().rev() {
1099 result.replace_range(start..end, &fixed);
1100 }
1101 result
1102}
1103
1104fn field_type_name(field: &padlock_core::ir::Field) -> &str {
1105 match &field.ty {
1106 padlock_core::ir::TypeInfo::Primitive { name, .. }
1107 | padlock_core::ir::TypeInfo::Opaque { name, .. } => name.as_str(),
1108 padlock_core::ir::TypeInfo::Pointer { .. } => "void*",
1109 padlock_core::ir::TypeInfo::Array { .. } => "/* array */",
1110 padlock_core::ir::TypeInfo::Struct(l) => l.name.as_str(),
1111 }
1112}
1113
1114#[cfg(test)]
1117mod tests {
1118 use super::*;
1119 use padlock_core::ir::test_fixtures::connection_layout;
1120
1121 #[test]
1122 fn c_fix_starts_with_struct() {
1123 let out = generate_c_fix(&connection_layout());
1124 assert!(out.starts_with("struct Connection {"));
1125 }
1126
1127 #[test]
1128 fn c_fix_contains_all_fields() {
1129 let out = generate_c_fix(&connection_layout());
1130 assert!(out.contains("timeout"));
1131 assert!(out.contains("port"));
1132 assert!(out.contains("is_active"));
1133 assert!(out.contains("is_tls"));
1134 }
1135
1136 #[test]
1137 fn c_fix_puts_largest_align_first() {
1138 let out = generate_c_fix(&connection_layout());
1139 let timeout_pos = out.find("timeout").unwrap();
1140 let is_active_pos = out.find("is_active").unwrap();
1141 assert!(timeout_pos < is_active_pos);
1142 }
1143
1144 #[test]
1145 fn rust_fix_uses_colon_syntax() {
1146 let out = generate_rust_fix(&connection_layout());
1147 assert!(out.contains(": f64"));
1148 }
1149
1150 #[test]
1151 fn unified_diff_marks_changes() {
1152 let orig = "struct T { char a; double b; };\n";
1153 let fixed = "struct T { double b; char a; };\n";
1154 let diff = unified_diff(orig, fixed, 1);
1155 assert!(diff.contains('-') || diff.contains('+'));
1156 }
1157
1158 #[test]
1159 fn unified_diff_identical_is_no_changes() {
1160 assert_eq!(unified_diff("x\n", "x\n", 3), "(no changes)\n");
1161 }
1162
1163 #[test]
1166 fn find_c_struct_span_basic() {
1167 let src = "struct Foo { int x; char y; };\nstruct Bar { double z; };\n";
1168 let span = find_c_struct_span(src, "Foo").unwrap();
1169 let text = &src[span];
1170 assert!(text.starts_with("struct Foo"));
1171 assert!(!text.contains("Bar"));
1172 }
1173
1174 #[test]
1175 fn find_c_struct_span_missing_returns_none() {
1176 let src = "struct Other { int x; };";
1177 assert!(find_c_struct_span(src, "Missing").is_none());
1178 }
1179
1180 #[test]
1181 fn find_rust_struct_span_basic() {
1182 let src = "struct Foo {\n x: u32,\n y: u8,\n}\n";
1183 let span = find_rust_struct_span(src, "Foo").unwrap();
1184 assert!(src[span].starts_with("struct Foo"));
1185 }
1186
1187 #[test]
1188 fn find_go_struct_span_basic() {
1189 let src = "type Foo struct {\n\tX int32\n\tY bool\n}\n";
1190 let span = find_go_struct_span(src, "Foo").unwrap();
1191 assert!(src[span].starts_with("type Foo struct"));
1192 }
1193
1194 #[test]
1197 fn apply_fixes_c_reorders_in_place() {
1198 let src = "struct Connection { bool is_active; double timeout; bool is_tls; int port; };\n";
1200 let layout = connection_layout();
1201 let fixed = apply_fixes_c(src, &[&layout]);
1202 let timeout_pos = fixed.find("timeout").unwrap();
1203 let is_active_pos = fixed.find("is_active").unwrap();
1204 assert!(
1205 timeout_pos < is_active_pos,
1206 "double should appear before bool after reorder"
1207 );
1208 }
1209
1210 #[test]
1211 fn apply_fixes_rust_reorders_in_place() {
1212 let src = "struct Connection {\n is_active: bool,\n timeout: f64,\n is_tls: bool,\n port: i32,\n}\n";
1213 let layout = connection_layout();
1214 let fixed = apply_fixes_rust(src, &[&layout]);
1215 let timeout_pos = fixed.find("timeout").unwrap();
1216 let is_active_pos = fixed.find("is_active").unwrap();
1217 assert!(timeout_pos < is_active_pos);
1218 }
1219
1220 #[test]
1221 fn go_fix_uses_tab_syntax() {
1222 let layout = connection_layout();
1223 let out = generate_go_fix(&layout);
1224 assert!(out.starts_with("type Connection struct"));
1225 assert!(out.contains('\t'));
1226 }
1227
1228 #[test]
1229 fn zig_fix_uses_const_struct_syntax() {
1230 let out = generate_zig_fix(&connection_layout());
1231 assert!(out.starts_with("const Connection = struct {"));
1232 assert!(out.ends_with("};\n"));
1233 }
1234
1235 #[test]
1236 fn find_zig_struct_span_basic() {
1237 let src = "const S = struct {\n x: u32,\n y: u8,\n};\n";
1238 let span = find_zig_struct_span(src, "S").unwrap();
1239 assert!(src[span].starts_with("const S = struct"));
1240 }
1241
1242 #[test]
1243 fn find_zig_struct_span_packed() {
1244 let src = "const S = packed struct {\n x: u32,\n y: u8,\n};\n";
1245 let span = find_zig_struct_span(src, "S").unwrap();
1246 assert!(src[span].contains("packed struct"));
1247 }
1248
1249 #[test]
1250 fn find_zig_struct_span_missing_returns_none() {
1251 let src = "const Other = struct { x: u8 };\n";
1252 assert!(find_zig_struct_span(src, "Missing").is_none());
1253 }
1254
1255 #[test]
1256 fn apply_fixes_zig_reorders_in_place() {
1257 use crate::parse_source_str;
1258 use padlock_core::arch::X86_64_SYSV;
1259 let src = "const S = struct {\n a: u8,\n b: u64,\n};\n";
1260 let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1261 let layout = &layouts[0];
1262 let fixed = apply_fixes_zig(src, &[layout]);
1263 let b_pos = fixed.find("b:").unwrap();
1265 let a_pos = fixed.find("a:").unwrap();
1266 assert!(
1267 b_pos < a_pos,
1268 "u64 field should come before u8 after reorder"
1269 );
1270 }
1271
1272 #[test]
1275 fn rust_fix_preserves_pub_visibility() {
1276 let src = "struct S {\n pub a: u8,\n pub b: u64,\n}\n";
1277 use crate::parse_source_str;
1278 use padlock_core::arch::X86_64_SYSV;
1279 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1280 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1281 assert!(fixed.contains("pub b: u64"), "pub on b must be preserved");
1283 assert!(fixed.contains("pub a: u8"), "pub on a must be preserved");
1284 assert!(fixed.find("pub b").unwrap() < fixed.find("pub a").unwrap());
1286 }
1287
1288 #[test]
1289 fn rust_fix_preserves_doc_comments() {
1290 let src = concat!(
1291 "struct S {\n",
1292 " /// small field\n",
1293 " a: u8,\n",
1294 " /// large field\n",
1295 " b: u64,\n",
1296 "}\n"
1297 );
1298 use crate::parse_source_str;
1299 use padlock_core::arch::X86_64_SYSV;
1300 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1301 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1302 assert!(
1303 fixed.contains("/// large field"),
1304 "doc comment for b must survive"
1305 );
1306 assert!(
1307 fixed.contains("/// small field"),
1308 "doc comment for a must survive"
1309 );
1310 assert!(
1312 fixed.find("large field").unwrap() < fixed.find("small field").unwrap(),
1313 "doc comment ordering must follow field ordering"
1314 );
1315 }
1316
1317 #[test]
1318 fn rust_fix_preserves_serde_attributes() {
1319 let src = concat!(
1320 "struct S {\n",
1321 " #[serde(skip)]\n",
1322 " a: u8,\n",
1323 " #[serde(rename = \"big\")]\n",
1324 " b: u64,\n",
1325 "}\n"
1326 );
1327 use crate::parse_source_str;
1328 use padlock_core::arch::X86_64_SYSV;
1329 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1330 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1331 assert!(
1332 fixed.contains("#[serde(skip)]"),
1333 "serde attribute on a must survive"
1334 );
1335 assert!(
1336 fixed.contains("#[serde(rename = \"big\")]"),
1337 "serde attribute on b must survive"
1338 );
1339 }
1340
1341 #[test]
1342 fn rust_fix_preserves_pub_crate_visibility() {
1343 let src = "struct S {\n pub(crate) a: u8,\n pub(crate) b: u64,\n}\n";
1344 use crate::parse_source_str;
1345 use padlock_core::arch::X86_64_SYSV;
1346 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1347 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1348 assert!(
1349 fixed.contains("pub(crate) b: u64"),
1350 "pub(crate) on b must be preserved"
1351 );
1352 assert!(
1353 fixed.contains("pub(crate) a: u8"),
1354 "pub(crate) on a must be preserved"
1355 );
1356 }
1357
1358 #[test]
1359 fn c_fix_preserves_guarded_by_comments() {
1360 let src = concat!(
1361 "struct S {\n",
1362 " char a; // GUARDED_BY(mu)\n",
1363 " double b; // large field\n",
1364 "};\n"
1365 );
1366 use crate::parse_source_str;
1367 use padlock_core::arch::X86_64_SYSV;
1368 let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1369 let fixed = apply_fixes_c(src, &[&layouts[0]]);
1370 assert!(
1371 fixed.contains("GUARDED_BY(mu)"),
1372 "guard annotation comment must survive reorder"
1373 );
1374 assert!(fixed.find("double b").unwrap() < fixed.find("char a").unwrap());
1376 }
1377
1378 #[test]
1379 fn go_fix_preserves_field_tags() {
1380 let src = concat!("type S struct {\n", "\ta uint8\n", "\tb uint64\n", "}\n");
1381 use crate::parse_source_str;
1382 use padlock_core::arch::X86_64_SYSV;
1383 let layouts = parse_source_str(src, &crate::SourceLanguage::Go, &X86_64_SYSV).unwrap();
1384 let fixed = apply_fixes_go(src, &[&layouts[0]]);
1385 assert!(fixed.find("\tb uint64").unwrap() < fixed.find("\ta uint8").unwrap());
1387 }
1388
1389 #[test]
1390 fn zig_fix_preserves_field_comments() {
1391 let src = concat!(
1392 "const S = struct {\n",
1393 " // small\n",
1394 " a: u8,\n",
1395 " // large\n",
1396 " b: u64,\n",
1397 "};\n"
1398 );
1399 use crate::parse_source_str;
1400 use padlock_core::arch::X86_64_SYSV;
1401 let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1402 let fixed = apply_fixes_zig(src, &[&layouts[0]]);
1403 assert!(fixed.contains("// large"), "comment for b must survive");
1404 assert!(fixed.contains("// small"), "comment for a must survive");
1405 assert!(fixed.find("// large").unwrap() < fixed.find("// small").unwrap());
1407 }
1408
1409 #[test]
1412 fn rust_fix_from_source_falls_back_when_no_open_brace() {
1413 let layout = connection_layout();
1415 let out = generate_rust_fix_from_source(&layout, "struct Connection");
1416 assert!(out.starts_with("struct Connection {"));
1418 }
1419
1420 #[test]
1421 fn c_fix_from_source_falls_back_when_chunks_empty() {
1422 let layout = connection_layout();
1425 let out = generate_c_fix_from_source(&layout, "struct Connection { /* no fields */ };");
1426 assert!(out.starts_with("struct Connection {"));
1427 assert!(out.contains("timeout"));
1428 }
1429
1430 #[test]
1431 fn zig_fix_from_source_falls_back_on_missing_field_name() {
1432 let layout = connection_layout();
1434 let out =
1435 generate_zig_fix_from_source(&layout, "const Connection = struct { x: u8, y: u64, };");
1436 assert!(out.contains("timeout"));
1438 }
1439
1440 #[test]
1443 fn go_fix_reorders_fields() {
1444 let layout = connection_layout();
1445 let out = generate_go_fix(&layout);
1446 let pos_timeout = out.find("timeout").unwrap();
1448 let pos_port = out.find("port").unwrap();
1449 let pos_bool = out.find("is_active").unwrap();
1450 assert!(pos_timeout < pos_bool, "timeout must precede booleans");
1451 assert!(pos_port < pos_bool, "port must precede booleans");
1452 }
1453
1454 #[test]
1455 fn go_fix_from_source_preserves_verbatim_field_lines() {
1456 let layout = connection_layout();
1457 let src = r#"type Connection struct {
1458 is_active bool
1459 timeout f64
1460 is_tls bool
1461 port i32
1462}"#;
1463 let out = generate_go_fix_from_source(&layout, src);
1464 assert!(out.contains("timeout f64"), "verbatim timeout line");
1466 assert!(out.contains("port i32"), "verbatim port line");
1467 let pos_timeout = out.find("timeout").unwrap();
1469 let pos_is_active = out.find("is_active").unwrap();
1470 assert!(
1471 pos_timeout < pos_is_active,
1472 "timeout must come before is_active"
1473 );
1474 }
1475
1476 #[test]
1477 fn apply_fixes_go_rewrites_struct_in_file() {
1478 let src = "package p\n\ntype Point struct {\n\tFlag bool\n\tX int64\n\tY int32\n}\n";
1479 use padlock_core::arch::X86_64_SYSV;
1482 use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
1483 let layout = StructLayout {
1484 name: "Point".into(),
1485 total_size: 16,
1486 align: 8,
1487 fields: vec![
1488 Field {
1489 name: "Flag".into(),
1490 ty: TypeInfo::Primitive {
1491 name: "bool".into(),
1492 size: 1,
1493 align: 1,
1494 },
1495 offset: 0,
1496 size: 1,
1497 align: 1,
1498 source_file: None,
1499 source_line: None,
1500 access: AccessPattern::Unknown,
1501 },
1502 Field {
1503 name: "X".into(),
1504 ty: TypeInfo::Primitive {
1505 name: "int64".into(),
1506 size: 8,
1507 align: 8,
1508 },
1509 offset: 8,
1510 size: 8,
1511 align: 8,
1512 source_file: None,
1513 source_line: None,
1514 access: AccessPattern::Unknown,
1515 },
1516 Field {
1517 name: "Y".into(),
1518 ty: TypeInfo::Primitive {
1519 name: "int32".into(),
1520 size: 4,
1521 align: 4,
1522 },
1523 offset: 16,
1524 size: 4,
1525 align: 4,
1526 source_file: None,
1527 source_line: None,
1528 access: AccessPattern::Unknown,
1529 },
1530 ],
1531 source_file: None,
1532 source_line: None,
1533 arch: &X86_64_SYSV,
1534 is_packed: false,
1535 is_union: false,
1536 is_repr_rust: false,
1537 suppressed_findings: vec![],
1538 uncertain_fields: Vec::new(),
1539 };
1540 let fixed = apply_fixes_go(src, &[&layout]);
1541 let pos_x = fixed.find("\tX ").unwrap();
1543 let pos_flag = fixed.find("\tFlag").unwrap();
1544 assert!(pos_x < pos_flag, "X must precede Flag after reorder");
1545 assert!(fixed.starts_with("package p\n"), "package line preserved");
1547 }
1548
1549 #[test]
1552 fn c_fix_no_blank_line_after_opening_brace() {
1553 use crate::parse_source_str;
1554 use padlock_core::arch::X86_64_SYSV;
1555 let src = "struct S {\n char a;\n double b;\n};\n";
1556 let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1557 let fixed = apply_fixes_c(src, &[&layouts[0]]);
1558 let brace = fixed.find('{').unwrap();
1561 let after_brace = &fixed[brace + 1..];
1562 assert!(
1563 !after_brace.starts_with("\n\n"),
1564 "C fix must not insert a blank line after '{{': got {:?}",
1565 &after_brace[..after_brace.len().min(20)]
1566 );
1567 }
1568
1569 #[test]
1570 fn go_fix_no_blank_line_after_opening_brace() {
1571 use crate::parse_source_str;
1572 use padlock_core::arch::X86_64_SYSV;
1573 let src = "type S struct {\n\ta uint8\n\tb uint64\n}\n";
1574 let layouts = parse_source_str(src, &crate::SourceLanguage::Go, &X86_64_SYSV).unwrap();
1575 let fixed = apply_fixes_go(src, &[&layouts[0]]);
1576 let brace = fixed.find('{').unwrap();
1577 let after_brace = &fixed[brace + 1..];
1578 assert!(
1579 !after_brace.starts_with("\n\n"),
1580 "Go fix must not insert a blank line after '{{': got {:?}",
1581 &after_brace[..after_brace.len().min(20)]
1582 );
1583 }
1584
1585 #[test]
1586 fn zig_fix_no_blank_line_after_opening_brace() {
1587 use crate::parse_source_str;
1588 use padlock_core::arch::X86_64_SYSV;
1589 let src = "const S = struct {\n a: u8,\n b: u64,\n};\n";
1590 let layouts = parse_source_str(src, &crate::SourceLanguage::Zig, &X86_64_SYSV).unwrap();
1591 let fixed = apply_fixes_zig(src, &[&layouts[0]]);
1592 let brace = fixed.find('{').unwrap();
1593 let after_brace = &fixed[brace + 1..];
1594 assert!(
1595 !after_brace.starts_with("\n\n"),
1596 "Zig fix must not insert a blank line after '{{': got {:?}",
1597 &after_brace[..after_brace.len().min(20)]
1598 );
1599 }
1600
1601 #[test]
1602 fn rust_fix_no_blank_line_after_opening_brace() {
1603 use crate::parse_source_str;
1604 use padlock_core::arch::X86_64_SYSV;
1605 let src = "struct S {\n a: u8,\n b: u64,\n}\n";
1606 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1607 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1608 let brace = fixed.find('{').unwrap();
1609 let after_brace = &fixed[brace + 1..];
1610 assert!(
1611 !after_brace.starts_with("\n\n"),
1612 "Rust fix must not insert a blank line after '{{': got {:?}",
1613 &after_brace[..after_brace.len().min(20)]
1614 );
1615 }
1616
1617 #[test]
1620 fn rust_fix_single_field_struct_unchanged() {
1621 use crate::parse_source_str;
1622 use padlock_core::arch::X86_64_SYSV;
1623 let src = "struct S {\n x: u64,\n}\n";
1624 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1625 let result = generate_rust_fix_from_source(&layouts[0], src);
1628 assert!(result.contains("x: u64"), "single field must be present");
1629 assert!(
1630 !result.contains("\n\n"),
1631 "no blank line in single-field output"
1632 );
1633 }
1634
1635 #[test]
1636 fn c_fix_single_field_struct_unchanged() {
1637 use crate::parse_source_str;
1638 use padlock_core::arch::X86_64_SYSV;
1639 let src = "struct S {\n double x;\n};\n";
1640 let layouts = parse_source_str(src, &crate::SourceLanguage::C, &X86_64_SYSV).unwrap();
1641 let result = generate_c_fix_from_source(&layouts[0], src);
1642 assert!(result.contains("double x"), "single field must be present");
1643 assert!(
1644 !result.contains("\n\n"),
1645 "no blank line in single-field output"
1646 );
1647 }
1648
1649 #[test]
1652 fn rust_fix_preserves_trailing_comma() {
1653 use crate::parse_source_str;
1654 use padlock_core::arch::X86_64_SYSV;
1655 let src = "struct S {\n a: u8,\n b: u64,\n}\n";
1657 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1658 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1659 assert!(
1661 fixed.contains("b: u64,"),
1662 "trailing comma on reordered-first field must be preserved"
1663 );
1664 assert!(
1665 fixed.contains("a: u8,"),
1666 "trailing comma on last field must be preserved"
1667 );
1668 }
1669
1670 #[test]
1671 fn rust_fix_no_trailing_comma_on_last_field() {
1672 use crate::parse_source_str;
1673 use padlock_core::arch::X86_64_SYSV;
1674 let src = "struct S {\n a: u8,\n b: u64\n}\n";
1676 let layouts = parse_source_str(src, &crate::SourceLanguage::Rust, &X86_64_SYSV).unwrap();
1677 let fixed = apply_fixes_rust(src, &[&layouts[0]]);
1678 assert!(
1680 fixed.contains("b: u64"),
1681 "b field must be present after reorder"
1682 );
1683 assert_eq!(fixed.chars().filter(|&c| c == '}').count(), 1);
1685 }
1686}