Skip to content

Commit 18497fa

Browse files
committed
cargo-rail: push lockfile (this is annoying and needs to be addressed); fixing the 'target' fuzzy matching w/ word boundaries - this prevents the false positives seen in Quiche testing
1 parent 706d1c2 commit 18497fa

File tree

3 files changed

+195
-3
lines changed

3 files changed

+195
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ Keep your workspace lean. Per target triple:
121121
```bash
122122
cargo rail unify --check # Preview (CI-safe, exits 1 if changes needed)
123123
cargo rail unify # Apply changes
124-
cargo rail unify sync # Re-detect targets after adding new platforms
125124
cargo rail unify undo # Restore from backup
125+
cargo rail config sync # Add missing config fields + sync targets
126126
```
127127

128128
What it does:

src/targets.rs

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub fn detect_targets_excluding(workspace_root: &Path, exclude: &[&Path]) -> Rai
6868

6969
if let Ok(content) = std::fs::read_to_string(&file_path) {
7070
for target in &canonical_targets {
71-
if content.contains(target) {
71+
if contains_target_match(&content, target) {
7272
found.insert(target.clone());
7373
}
7474
}
@@ -151,6 +151,59 @@ pub fn get_rust_target_list() -> RailResult<Vec<String>> {
151151
.ok_or_else(|| RailError::message("Failed to get target list from rustc. Ensure rustc is installed and in PATH."))
152152
}
153153

154+
/// Check if content contains a target triple as a complete word/token
155+
///
156+
/// This avoids false positives like matching "thumbv7em-none-eabi" when the
157+
/// file only contains "thumbv7em-none-eabihf". Target triples in config files
158+
/// are typically surrounded by:
159+
/// - Whitespace
160+
/// - Quotes (single or double)
161+
/// - Brackets
162+
/// - Commas
163+
/// - Newlines
164+
/// - Start/end of string
165+
fn contains_target_match(content: &str, target: &str) -> bool {
166+
// Find all occurrences of the target string
167+
let mut start = 0;
168+
while let Some(pos) = content[start..].find(target) {
169+
let absolute_pos = start + pos;
170+
let end_pos = absolute_pos + target.len();
171+
172+
// Check character before (if any)
173+
let char_before = if absolute_pos > 0 {
174+
content[..absolute_pos].chars().last()
175+
} else {
176+
None
177+
};
178+
179+
// Check character after (if any)
180+
let char_after = content[end_pos..].chars().next();
181+
182+
// A target triple character is: alphanumeric, hyphen, or underscore
183+
// Note: Some targets have dots (e.g., "thumbv8m.main-none-eabi") but treating
184+
// dots as target chars breaks TOML table detection like [target.x86_64-linux-gnu].
185+
// Since dotted targets are rare (only thumbv8m.* variants) and must be quoted
186+
// in TOML anyway, we treat dots as boundaries. This works because:
187+
// - In arrays: "thumbv8m.main-none-eabi" - the target is quoted, boundaries are quotes
188+
// - In tables: [target."thumbv8m.main-none-eabi"] - must be quoted due to the dot
189+
let is_target_char = |c: char| c.is_alphanumeric() || c == '-' || c == '_';
190+
191+
// Valid boundary: start of string, or non-target character
192+
let valid_before = char_before.is_none() || !is_target_char(char_before.unwrap());
193+
// Valid boundary: end of string, or non-target character
194+
let valid_after = char_after.is_none() || !is_target_char(char_after.unwrap());
195+
196+
if valid_before && valid_after {
197+
return true;
198+
}
199+
200+
// Move past this match to find next occurrence
201+
start = absolute_pos + 1;
202+
}
203+
204+
false
205+
}
206+
154207
/// Recursively find all .toml files in workspace
155208
///
156209
/// - Searches up to depth 3 (avoids excessive traversal)
@@ -621,4 +674,143 @@ jobs:
621674

622675
assert!(!has_github_workflows(temp.path()));
623676
}
677+
678+
// ============================================================================
679+
// Word Boundary Matching Tests
680+
// ============================================================================
681+
682+
#[test]
683+
fn test_contains_target_match_exact() {
684+
assert!(contains_target_match("thumbv7em-none-eabihf", "thumbv7em-none-eabihf"));
685+
}
686+
687+
#[test]
688+
fn test_contains_target_match_quoted() {
689+
assert!(contains_target_match(r#""thumbv7em-none-eabihf""#, "thumbv7em-none-eabihf"));
690+
assert!(contains_target_match(r#"'thumbv7em-none-eabihf'"#, "thumbv7em-none-eabihf"));
691+
}
692+
693+
#[test]
694+
fn test_contains_target_match_in_array() {
695+
let content = r#"targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]"#;
696+
assert!(contains_target_match(content, "x86_64-unknown-linux-gnu"));
697+
assert!(contains_target_match(content, "aarch64-apple-darwin"));
698+
}
699+
700+
#[test]
701+
fn test_contains_target_match_in_table_key() {
702+
let content = r#"[target.thumbv7em-none-eabihf]
703+
linker = "arm-none-eabi-gcc""#;
704+
assert!(contains_target_match(content, "thumbv7em-none-eabihf"));
705+
}
706+
707+
#[test]
708+
fn test_contains_target_match_rejects_substring() {
709+
// This is the critical test - should NOT match shorter target when only longer exists
710+
let content = r#"targets = ["thumbv7em-none-eabihf"]"#;
711+
712+
// Should match the actual target
713+
assert!(contains_target_match(content, "thumbv7em-none-eabihf"));
714+
715+
// Should NOT match the shorter substring target
716+
assert!(
717+
!contains_target_match(content, "thumbv7em-none-eabi"),
718+
"Should not match 'thumbv7em-none-eabi' when file only contains 'thumbv7em-none-eabihf'"
719+
);
720+
}
721+
722+
#[test]
723+
fn test_contains_target_match_rejects_all_substring_cases() {
724+
// Test several known substring cases from rustc target list
725+
let test_cases = [
726+
("aarch64-apple-ios-sim", "aarch64-apple-ios"),
727+
("arm-unknown-linux-gnueabihf", "arm-unknown-linux-gnueabi"),
728+
("x86_64-unknown-linux-gnux32", "x86_64-unknown-linux-gnu"),
729+
("wasm32-wasip1-threads", "wasm32-wasip1"),
730+
("x86_64-pc-windows-gnullvm", "x86_64-pc-windows-gnu"),
731+
];
732+
733+
for (full_target, substring_target) in test_cases {
734+
let content = format!(r#"targets = ["{}"]"#, full_target);
735+
assert!(
736+
contains_target_match(&content, full_target),
737+
"Should match full target: {}",
738+
full_target
739+
);
740+
assert!(
741+
!contains_target_match(&content, substring_target),
742+
"Should NOT match substring '{}' when file only contains '{}'",
743+
substring_target,
744+
full_target
745+
);
746+
}
747+
}
748+
749+
#[test]
750+
fn test_contains_target_match_allows_both_when_both_present() {
751+
// When both the short and long target are in the file, both should match
752+
let content = r#"targets = ["thumbv7em-none-eabi", "thumbv7em-none-eabihf"]"#;
753+
754+
assert!(contains_target_match(content, "thumbv7em-none-eabi"));
755+
assert!(contains_target_match(content, "thumbv7em-none-eabihf"));
756+
}
757+
758+
#[test]
759+
fn test_contains_target_match_yaml_format() {
760+
let content = r#"
761+
matrix:
762+
target:
763+
- x86_64-unknown-linux-gnu
764+
- aarch64-apple-darwin
765+
"#;
766+
assert!(contains_target_match(content, "x86_64-unknown-linux-gnu"));
767+
assert!(contains_target_match(content, "aarch64-apple-darwin"));
768+
// Should not false positive on similar targets
769+
assert!(!contains_target_match(content, "x86_64-unknown-linux-gnux32"));
770+
}
771+
772+
#[test]
773+
fn test_contains_target_match_with_dots() {
774+
// Targets like thumbv8m.main-none-eabi have dots in them.
775+
// In TOML, these MUST be quoted because dots are path separators.
776+
// So we test the quoted form which is what users would actually write.
777+
let content = r#"[target."thumbv8m.main-none-eabihf"]"#;
778+
assert!(contains_target_match(content, "thumbv8m.main-none-eabihf"));
779+
assert!(!contains_target_match(content, "thumbv8m.main-none-eabi"));
780+
781+
// Also test in array form
782+
let content2 = r#"targets = ["thumbv8m.main-none-eabihf"]"#;
783+
assert!(contains_target_match(content2, "thumbv8m.main-none-eabihf"));
784+
assert!(!contains_target_match(content2, "thumbv8m.main-none-eabi"));
785+
}
786+
787+
#[test]
788+
fn test_detect_targets_no_false_positives() {
789+
let temp = TempDir::new().unwrap();
790+
791+
// Create a file with ONLY the longer target
792+
fs::write(
793+
temp.path().join("rust-toolchain.toml"),
794+
r#"
795+
[toolchain]
796+
channel = "stable"
797+
targets = ["thumbv7em-none-eabihf"]
798+
"#,
799+
)
800+
.unwrap();
801+
802+
let targets = detect_targets(temp.path()).unwrap();
803+
804+
// Should find the actual target
805+
assert!(
806+
targets.contains(&"thumbv7em-none-eabihf".to_string()),
807+
"Should detect thumbv7em-none-eabihf"
808+
);
809+
810+
// Should NOT find the substring target
811+
assert!(
812+
!targets.contains(&"thumbv7em-none-eabi".to_string()),
813+
"Should NOT detect thumbv7em-none-eabi (false positive)"
814+
);
815+
}
624816
}

0 commit comments

Comments
 (0)