Skip to content

Commit 79dc15b

Browse files
authored
Merge pull request #59 from auths-dev/dev-hotFixComparisons
fix: handle bare crate names in compare command
2 parents 52b1e7d + 8c1f967 commit 79dc15b

3 files changed

Lines changed: 85 additions & 24 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ Scans Rust source for ambient authority (filesystem, network, env, process, FFI)
3737

3838
```bash
3939
cargo install cargo-capsec
40+
41+
# Or install from source
42+
cargo install --path crates/cargo-capsec
4043
```
4144

4245
### Adopt in 30 seconds

crates/capsec-deep/src/main.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -346,17 +346,23 @@ impl rustc_driver::Callbacks for CapsecCallbacks {
346346
eprintln!("[capsec-deep] Found {} findings in {crate_name}", findings.len());
347347
}
348348

349-
// Write findings as JSONL
349+
// Write findings as JSONL — buffer all lines and write in a single batch
350+
// to avoid interleaving when cargo compiles multiple crates in parallel.
350351
if let Ok(output_path) = std::env::var("CAPSEC_DEEP_OUTPUT") {
351-
if let Ok(mut file) = std::fs::OpenOptions::new()
352-
.create(true)
353-
.append(true)
354-
.open(&output_path)
355-
{
356-
for finding in &findings {
357-
if let Ok(json) = serde_json::to_string(finding) {
358-
let _ = writeln!(file, "{json}");
359-
}
352+
let mut batch = String::new();
353+
for finding in &findings {
354+
if let Ok(json) = serde_json::to_string(finding) {
355+
batch.push_str(&json);
356+
batch.push('\n');
357+
}
358+
}
359+
if !batch.is_empty() {
360+
if let Ok(mut file) = std::fs::OpenOptions::new()
361+
.create(true)
362+
.append(true)
363+
.open(&output_path)
364+
{
365+
let _ = file.write_all(batch.as_bytes());
360366
}
361367
}
362368
} else if debug {

crates/cargo-capsec/src/diff.rs

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,24 @@ pub fn run_compare(opts: CompareOptions) {
9797
let fs_read = cap_root.grant::<capsec_core::permission::FsRead>();
9898
let spawn_cap = cap_root.grant::<capsec_core::permission::Spawn>();
9999

100-
let left = parse_crate_spec_or_latest(&opts.left);
101-
let right = parse_crate_spec_or_latest(&opts.right);
100+
let mut left = parse_crate_spec_or_latest(&opts.left);
101+
let mut right = parse_crate_spec_or_latest(&opts.right);
102102

103-
eprintln!("Fetching {} v{}...", left.name, left.version);
103+
eprintln!("Fetching {}...", left.name);
104104
let left_dir = fetch_crate_source(&left.name, &left.version, &spawn_cap, &fs_read)
105105
.unwrap_or_else(|e| {
106106
eprintln!("Error: {e}");
107107
std::process::exit(1);
108108
});
109+
resolve_version_from_path(&mut left, &left_dir);
109110

110-
eprintln!("Fetching {} v{}...", right.name, right.version);
111+
eprintln!("Fetching {}...", right.name);
111112
let right_dir = fetch_crate_source(&right.name, &right.version, &spawn_cap, &fs_read)
112113
.unwrap_or_else(|e| {
113114
eprintln!("Error: {e}");
114115
std::process::exit(1);
115116
});
117+
resolve_version_from_path(&mut right, &right_dir);
116118

117119
eprintln!("Scanning...\n");
118120
let config = Config::default();
@@ -146,8 +148,13 @@ fn fetch_crate_source(
146148
let temp_dir = std::env::temp_dir().join(format!("capsec-fetch-{crate_name}-{version}"));
147149
let _ = std::fs::create_dir_all(&temp_dir);
148150

151+
let version_spec = if version == "*" {
152+
format!("\"{version}\"")
153+
} else {
154+
format!("\"={version}\"")
155+
};
149156
let cargo_toml = format!(
150-
"[package]\nname = \"capsec-fetch-temp\"\nversion = \"0.0.1\"\nedition = \"2021\"\n\n[dependencies]\n{crate_name} = \"={version}\"\n"
157+
"[package]\nname = \"capsec-fetch-temp\"\nversion = \"0.0.1\"\nedition = \"2021\"\n\n[dependencies]\n{crate_name} = {version_spec}\n"
151158
);
152159
std::fs::write(temp_dir.join("Cargo.toml"), cargo_toml)
153160
.map_err(|e| format!("Failed to write temp Cargo.toml: {e}"))?;
@@ -180,6 +187,7 @@ fn fetch_crate_source(
180187
}
181188

182189
/// Looks for a crate's source in ~/.cargo/registry/src/.
190+
/// When version is "*", finds the latest version available in the cache.
183191
fn find_registry_source(crate_name: &str, version: &str) -> Option<PathBuf> {
184192
let home = std::env::var("CARGO_HOME").unwrap_or_else(|_| {
185193
std::env::var("HOME")
@@ -192,17 +200,38 @@ fn find_registry_source(crate_name: &str, version: &str) -> Option<PathBuf> {
192200
return None;
193201
}
194202

195-
// Iterate index directories (index.crates.io-HASH/)
196203
let entries = std::fs::read_dir(&registry_src).ok()?;
197-
for entry in entries.flatten() {
198-
let crate_dir = entry.path().join(format!("{crate_name}-{version}"));
199-
if crate_dir.exists() {
200-
// Some crates have src/ subdir, some don't
201-
let src_dir = crate_dir.join("src");
202-
if src_dir.exists() {
203-
return Some(src_dir);
204+
for index_dir in entries.flatten() {
205+
if version == "*" {
206+
// Find any version of this crate — pick the latest by name sort
207+
let prefix = format!("{crate_name}-");
208+
if let Ok(crate_dirs) = std::fs::read_dir(index_dir.path()) {
209+
let mut matches: Vec<_> = crate_dirs
210+
.flatten()
211+
.filter(|e| {
212+
e.file_name()
213+
.to_str()
214+
.is_some_and(|n| n.starts_with(&prefix))
215+
})
216+
.collect();
217+
matches.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
218+
if let Some(best) = matches.first() {
219+
let src_dir = best.path().join("src");
220+
if src_dir.exists() {
221+
return Some(src_dir);
222+
}
223+
return Some(best.path());
224+
}
225+
}
226+
} else {
227+
let crate_dir = index_dir.path().join(format!("{crate_name}-{version}"));
228+
if crate_dir.exists() {
229+
let src_dir = crate_dir.join("src");
230+
if src_dir.exists() {
231+
return Some(src_dir);
232+
}
233+
return Some(crate_dir);
204234
}
205-
return Some(crate_dir);
206235
}
207236
}
208237

@@ -250,6 +279,29 @@ fn diff_findings(old: &[Finding], new: &[Finding]) -> DiffResult {
250279

251280
// ── Parsers ──
252281

282+
/// If version is "*", resolves it from the fetched directory path.
283+
/// e.g., path `.cargo/registry/src/.../ureq-2.12.1/src` → version "2.12.1"
284+
fn resolve_version_from_path(spec: &mut CrateSpec, dir: &Path) {
285+
if spec.version != "*" {
286+
return;
287+
}
288+
// Walk up from src/ to the crate dir: ureq-2.12.1
289+
let crate_dir = if dir.ends_with("src") {
290+
dir.parent()
291+
} else {
292+
Some(dir)
293+
};
294+
if let Some(dir_name) = crate_dir
295+
.and_then(|d| d.file_name())
296+
.and_then(|n| n.to_str())
297+
{
298+
let prefix = format!("{}-", spec.name);
299+
if let Some(ver) = dir_name.strip_prefix(&prefix) {
300+
spec.version = ver.to_string();
301+
}
302+
}
303+
}
304+
253305
/// Parses "serde_json@1.0.133" into CrateSpec.
254306
fn parse_crate_spec(spec: &str) -> Result<CrateSpec, String> {
255307
let parts: Vec<&str> = spec.splitn(2, '@').collect();

0 commit comments

Comments
 (0)