1use crate::config::Classification;
12use serde::Deserialize;
13use std::collections::{HashMap, VecDeque};
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone)]
18pub struct CrateInfo {
19 pub name: String,
21 pub version: String,
23 pub source_dir: PathBuf,
25 pub is_dependency: bool,
28 pub classification: Option<Classification>,
31 pub package_id: Option<String>,
34}
35
36#[derive(Deserialize)]
37struct CargoMetadata {
38 packages: Vec<Package>,
39 workspace_root: String,
40 resolve: Option<Resolve>,
43}
44
45#[derive(Deserialize)]
46struct Package {
47 name: String,
48 version: String,
49 id: String,
50 manifest_path: String,
51 source: Option<String>,
52 #[serde(default)]
53 metadata: Option<serde_json::Value>,
54 #[serde(default)]
55 targets: Vec<Target>,
56}
57
58#[derive(Deserialize)]
59struct Target {
60 kind: Vec<String>,
61 #[allow(dead_code)]
62 name: String,
63 #[allow(dead_code)]
64 src_path: String,
65}
66
67#[derive(Deserialize)]
69struct Resolve {
70 nodes: Vec<ResolveNode>,
71}
72
73#[derive(Deserialize)]
75struct ResolveNode {
76 id: String,
78 #[serde(default)]
80 deps: Vec<NodeDep>,
81}
82
83#[derive(Deserialize)]
85struct NodeDep {
86 name: String,
88 pkg: String,
90 #[serde(default)]
92 dep_kinds: Vec<DepKindInfo>,
93}
94
95#[derive(Deserialize)]
97struct DepKindInfo {
98 kind: Option<String>,
100}
101
102fn extract_classification(metadata: &Option<serde_json::Value>) -> Option<Classification> {
104 let capsec = metadata.as_ref()?.get("capsec")?;
105 let class_str = capsec.get("classification")?.as_str()?;
106 match class_str {
107 "pure" => Some(Classification::Pure),
108 "resource" => Some(Classification::Resource),
109 other => {
110 eprintln!(
111 "Warning: unknown classification '{other}' in [package.metadata.capsec], ignoring (valid: pure, resource)"
112 );
113 None
114 }
115 }
116}
117
118#[must_use]
122pub fn normalize_crate_name(name: &str) -> String {
123 name.replace('-', "_")
124}
125
126fn is_proc_macro(pkg: &Package) -> bool {
128 pkg.targets
129 .iter()
130 .any(|t| t.kind.contains(&"proc-macro".to_string()))
131}
132
133#[derive(Debug, Clone)]
135pub struct DepEdge {
136 #[allow(dead_code)]
138 pub extern_name: String,
139 pub pkg_id: String,
141}
142
143pub fn topological_order(resolve: &[(String, Vec<DepEdge>)]) -> Result<Vec<String>, String> {
149 let num_nodes = resolve.len();
150
151 let id_to_idx: HashMap<&str, usize> = resolve
153 .iter()
154 .enumerate()
155 .map(|(i, (id, _))| (id.as_str(), i))
156 .collect();
157
158 let mut in_degree = vec![0usize; num_nodes];
162 let mut dependents: Vec<Vec<usize>> = vec![vec![]; num_nodes];
163
164 for (idx, (_id, deps)) in resolve.iter().enumerate() {
165 for dep in deps {
166 if let Some(&dep_idx) = id_to_idx.get(dep.pkg_id.as_str()) {
167 dependents[dep_idx].push(idx);
171 in_degree[idx] += 1;
172 }
173 }
175 }
176
177 let mut queue: VecDeque<usize> = in_degree
179 .iter()
180 .enumerate()
181 .filter(|&(_, &d)| d == 0)
182 .map(|(i, _)| i)
183 .collect();
184
185 let mut order = Vec::with_capacity(num_nodes);
186
187 while let Some(node) = queue.pop_front() {
188 order.push(resolve[node].0.clone());
189 for &dependent in &dependents[node] {
190 in_degree[dependent] -= 1;
191 if in_degree[dependent] == 0 {
192 queue.push_back(dependent);
193 }
194 }
195 }
196
197 if order.len() == num_nodes {
198 Ok(order)
199 } else {
200 Err(format!(
201 "Cycle detected in dependency graph ({} of {} nodes processed)",
202 order.len(),
203 num_nodes
204 ))
205 }
206}
207
208pub type DepGraphResult = (Vec<(String, Vec<DepEdge>)>, HashMap<String, String>);
210
211pub fn extract_dep_graph(
216 metadata_json: &[u8],
217 exclude_proc_macros: bool,
218) -> Result<DepGraphResult, String> {
219 let metadata: CargoMetadata = serde_json::from_slice(metadata_json)
220 .map_err(|e| format!("Failed to parse cargo metadata: {e}"))?;
221
222 let resolve = metadata
223 .resolve
224 .ok_or("No resolve field in cargo metadata (was --no-deps used?)")?;
225
226 let proc_macro_ids: std::collections::HashSet<&str> = if exclude_proc_macros {
228 metadata
229 .packages
230 .iter()
231 .filter(|p| is_proc_macro(p))
232 .map(|p| p.id.as_str())
233 .collect()
234 } else {
235 std::collections::HashSet::new()
236 };
237
238 let id_to_name: HashMap<String, String> = metadata
240 .packages
241 .iter()
242 .map(|p| (p.id.clone(), normalize_crate_name(&p.name)))
243 .collect();
244
245 let mut graph = Vec::new();
246
247 for node in &resolve.nodes {
248 if proc_macro_ids.contains(node.id.as_str()) {
249 continue;
250 }
251
252 let deps: Vec<DepEdge> = node
253 .deps
254 .iter()
255 .filter(|d| {
256 !d.dep_kinds
258 .iter()
259 .all(|dk| dk.kind.as_deref() == Some("dev"))
260 })
261 .filter(|d| !proc_macro_ids.contains(d.pkg.as_str()))
262 .map(|d| DepEdge {
263 extern_name: normalize_crate_name(&d.name),
264 pkg_id: d.pkg.clone(),
265 })
266 .collect();
267
268 graph.push((node.id.clone(), deps));
269 }
270
271 Ok((graph, id_to_name))
272}
273
274pub fn workspace_topological_order(
280 workspace_crates: &[CrateInfo],
281 resolve_graph: &[(String, Vec<DepEdge>)],
282) -> Option<Vec<String>> {
283 let ws_pkg_ids: std::collections::HashSet<String> = workspace_crates
284 .iter()
285 .filter_map(|c| c.package_id.clone())
286 .collect();
287
288 if ws_pkg_ids.is_empty() {
289 return None;
290 }
291
292 let ws_graph: Vec<(String, Vec<DepEdge>)> = resolve_graph
294 .iter()
295 .filter(|(id, _)| ws_pkg_ids.contains(id))
296 .map(|(id, deps)| {
297 let ws_deps: Vec<DepEdge> = deps
298 .iter()
299 .filter(|d| ws_pkg_ids.contains(&d.pkg_id))
300 .cloned()
301 .collect();
302 (id.clone(), ws_deps)
303 })
304 .collect();
305
306 topological_order(&ws_graph).ok()
307}
308
309pub struct DiscoveryResult {
311 pub crates: Vec<CrateInfo>,
313 pub workspace_root: PathBuf,
315 pub resolve_graph: Option<Vec<(String, Vec<DepEdge>)>>,
317}
318
319pub fn discover_crates(
327 workspace_root: &Path,
328 include_deps: bool,
329 spawn_cap: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::Spawn>,
330 _fs_cap: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::FsRead>,
331) -> Result<DiscoveryResult, String> {
332 let mut args = vec!["metadata", "--format-version=1"];
335 if !include_deps {
336 args.push("--no-deps");
337 }
338
339 let output = capsec_std::process::command("cargo", spawn_cap)
340 .map_err(|e| format!("Failed to create command: {e}"))?
341 .args(&args)
342 .current_dir(workspace_root)
343 .output()
344 .map_err(|e| format!("Failed to run cargo metadata: {e}"))?;
345
346 if !output.status.success() {
347 let stderr = String::from_utf8_lossy(&output.stderr);
348 return Err(format!("cargo metadata failed: {stderr}"));
349 }
350
351 let metadata: CargoMetadata = serde_json::from_slice(&output.stdout)
352 .map_err(|e| format!("Failed to parse cargo metadata: {e}"))?;
353
354 let resolved_root = PathBuf::from(&metadata.workspace_root);
355
356 let mut crates = Vec::new();
357
358 for package in &metadata.packages {
359 let manifest_dir = Path::new(&package.manifest_path)
360 .parent()
361 .unwrap_or(Path::new("."))
362 .to_path_buf();
363
364 let src_dir = manifest_dir.join("src");
365
366 if src_dir.exists() {
367 crates.push(CrateInfo {
368 name: package.name.clone(),
369 version: package.version.clone(),
370 source_dir: src_dir,
371 is_dependency: package.source.is_some(),
372 classification: extract_classification(&package.metadata),
373 package_id: if include_deps {
374 Some(package.id.clone())
375 } else {
376 None
377 },
378 });
379 }
380 }
381
382 let resolve_graph = if include_deps {
384 extract_dep_graph(&output.stdout, true)
385 .ok()
386 .map(|(graph, _)| graph)
387 } else {
388 None
389 };
390
391 Ok(DiscoveryResult {
392 crates,
393 workspace_root: resolved_root,
394 resolve_graph,
395 })
396}
397
398pub fn discover_source_files(
403 dir: &Path,
404 cap: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::FsRead>,
405) -> Vec<PathBuf> {
406 let mut files = Vec::new();
407 discover_recursive(dir, &mut files, cap);
408
409 if let Some(crate_root) = dir.parent() {
411 let build_rs = crate_root.join("build.rs");
412 if build_rs.exists() {
413 files.push(build_rs);
414 }
415 }
416
417 files
418}
419
420fn discover_recursive(
421 dir: &Path,
422 files: &mut Vec<PathBuf>,
423 cap: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::FsRead>,
424) {
425 let entries = match capsec_std::fs::read_dir(dir, cap) {
426 Ok(e) => e,
427 Err(_) => return,
428 };
429
430 for entry in entries.flatten() {
431 let path = entry.path();
432 if path.is_dir() {
433 let name = path.file_name().unwrap_or_default().to_str().unwrap_or("");
434 if name != "target" && !name.starts_with('.') {
435 discover_recursive(&path, files, cap);
436 }
437 } else if path.extension().is_some_and(|e| e == "rs") {
438 files.push(path);
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn discover_source_files_finds_rs_files() {
449 let root = capsec_core::root::test_root();
450 let cap = root.grant::<capsec_core::permission::FsRead>();
451 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
452 let files = discover_source_files(&dir, &cap);
453 assert!(!files.is_empty());
454 assert!(
455 files
456 .iter()
457 .all(|f| f.extension().unwrap_or_default() == "rs")
458 );
459 }
460
461 #[test]
462 fn normalize_crate_name_replaces_hyphens() {
463 assert_eq!(normalize_crate_name("serde-json"), "serde_json");
464 assert_eq!(normalize_crate_name("serde_json"), "serde_json");
465 assert_eq!(normalize_crate_name("my-cool-crate"), "my_cool_crate");
466 assert_eq!(normalize_crate_name("plain"), "plain");
467 }
468
469 fn make_graph(edges: &[(&str, &[&str])]) -> Vec<(String, Vec<DepEdge>)> {
470 edges
471 .iter()
472 .map(|(id, deps)| {
473 let dep_edges = deps
474 .iter()
475 .map(|d| DepEdge {
476 extern_name: d.to_string(),
477 pkg_id: d.to_string(),
478 })
479 .collect();
480 (id.to_string(), dep_edges)
481 })
482 .collect()
483 }
484
485 #[test]
486 fn topo_sort_single_node() {
487 let graph = make_graph(&[("a", &[])]);
488 let order = topological_order(&graph).unwrap();
489 assert_eq!(order, vec!["a"]);
490 }
491
492 #[test]
493 fn topo_sort_linear_chain() {
494 let graph = make_graph(&[("a", &["b"]), ("b", &["c"]), ("c", &[])]);
496 let order = topological_order(&graph).unwrap();
497 let pos = |id: &str| order.iter().position(|x| x == id).unwrap();
499 assert!(pos("c") < pos("b"));
500 assert!(pos("b") < pos("a"));
501 }
502
503 #[test]
504 fn topo_sort_diamond() {
505 let graph = make_graph(&[("a", &["b", "c"]), ("b", &["d"]), ("c", &["d"]), ("d", &[])]);
511 let order = topological_order(&graph).unwrap();
512 let pos = |id: &str| order.iter().position(|x| x == id).unwrap();
513 assert!(pos("d") < pos("b"));
514 assert!(pos("d") < pos("c"));
515 assert!(pos("b") < pos("a"));
516 assert!(pos("c") < pos("a"));
517 }
518
519 #[test]
520 fn topo_sort_cycle_detected() {
521 let graph = make_graph(&[("a", &["b"]), ("b", &["a"])]);
523 let result = topological_order(&graph);
524 assert!(result.is_err());
525 assert!(result.unwrap_err().contains("Cycle detected"));
526 }
527
528 #[test]
529 fn topo_sort_ignores_unknown_deps() {
530 let graph = make_graph(&[("a", &["missing"]), ("b", &[])]);
532 let order = topological_order(&graph).unwrap();
533 assert_eq!(order.len(), 2);
534 }
535
536 #[test]
537 fn extract_dep_graph_filters_dev_deps() {
538 let metadata_json = serde_json::json!({
539 "packages": [
540 {
541 "name": "app",
542 "version": "0.1.0",
543 "id": "app 0.1.0",
544 "manifest_path": "/fake/app/Cargo.toml",
545 "source": null,
546 "targets": [{"kind": ["lib"], "name": "app", "src_path": "/fake/app/src/lib.rs"}]
547 },
548 {
549 "name": "helper",
550 "version": "1.0.0",
551 "id": "helper 1.0.0",
552 "manifest_path": "/fake/helper/Cargo.toml",
553 "source": "registry+https://github.com/rust-lang/crates.io-index",
554 "targets": [{"kind": ["lib"], "name": "helper", "src_path": "/fake/helper/src/lib.rs"}]
555 },
556 {
557 "name": "test-util",
558 "version": "0.1.0",
559 "id": "test-util 0.1.0",
560 "manifest_path": "/fake/test-util/Cargo.toml",
561 "source": "registry+https://github.com/rust-lang/crates.io-index",
562 "targets": [{"kind": ["lib"], "name": "test_util", "src_path": "/fake/test-util/src/lib.rs"}]
563 }
564 ],
565 "workspace_root": "/fake",
566 "workspace_members": ["app 0.1.0"],
567 "resolve": {
568 "nodes": [
569 {
570 "id": "app 0.1.0",
571 "deps": [
572 {
573 "name": "helper",
574 "pkg": "helper 1.0.0",
575 "dep_kinds": [{"kind": null, "target": null}]
576 },
577 {
578 "name": "test_util",
579 "pkg": "test-util 0.1.0",
580 "dep_kinds": [{"kind": "dev", "target": null}]
581 }
582 ]
583 },
584 {
585 "id": "helper 1.0.0",
586 "deps": []
587 },
588 {
589 "id": "test-util 0.1.0",
590 "deps": []
591 }
592 ],
593 "root": "app 0.1.0"
594 }
595 });
596
597 let json_bytes = serde_json::to_vec(&metadata_json).unwrap();
598 let (graph, id_to_name) = extract_dep_graph(&json_bytes, false).unwrap();
599
600 let app_node = graph.iter().find(|(id, _)| id == "app 0.1.0").unwrap();
602 assert_eq!(app_node.1.len(), 1);
603 assert_eq!(app_node.1[0].extern_name, "helper");
604
605 assert_eq!(id_to_name.get("test-util 0.1.0").unwrap(), "test_util");
607
608 let order = topological_order(&graph).unwrap();
610 assert_eq!(order.len(), 3);
611 }
612
613 #[test]
614 fn extract_dep_graph_excludes_proc_macros() {
615 let metadata_json = serde_json::json!({
616 "packages": [
617 {
618 "name": "app",
619 "version": "0.1.0",
620 "id": "app 0.1.0",
621 "manifest_path": "/fake/app/Cargo.toml",
622 "source": null,
623 "targets": [{"kind": ["lib"], "name": "app", "src_path": "/fake/app/src/lib.rs"}]
624 },
625 {
626 "name": "my-derive",
627 "version": "1.0.0",
628 "id": "my-derive 1.0.0",
629 "manifest_path": "/fake/my-derive/Cargo.toml",
630 "source": "registry+https://github.com/rust-lang/crates.io-index",
631 "targets": [{"kind": ["proc-macro"], "name": "my_derive", "src_path": "/fake/my-derive/src/lib.rs"}]
632 }
633 ],
634 "workspace_root": "/fake",
635 "workspace_members": ["app 0.1.0"],
636 "resolve": {
637 "nodes": [
638 {
639 "id": "app 0.1.0",
640 "deps": [
641 {
642 "name": "my_derive",
643 "pkg": "my-derive 1.0.0",
644 "dep_kinds": [{"kind": null, "target": null}]
645 }
646 ]
647 },
648 {
649 "id": "my-derive 1.0.0",
650 "deps": []
651 }
652 ],
653 "root": "app 0.1.0"
654 }
655 });
656
657 let json_bytes = serde_json::to_vec(&metadata_json).unwrap();
658 let (graph, _) = extract_dep_graph(&json_bytes, true).unwrap();
659
660 assert_eq!(graph.len(), 1); let app_node = &graph[0];
663 assert_eq!(app_node.0, "app 0.1.0");
664 assert!(app_node.1.is_empty()); }
666
667 #[test]
668 fn workspace_topo_order_basic() {
669 let ws_crates = vec![
670 CrateInfo {
671 name: "app".to_string(),
672 version: "0.1.0".to_string(),
673 source_dir: PathBuf::from("/fake/app/src"),
674 is_dependency: false,
675 classification: None,
676 package_id: Some("app 0.1.0".to_string()),
677 },
678 CrateInfo {
679 name: "core-lib".to_string(),
680 version: "0.1.0".to_string(),
681 source_dir: PathBuf::from("/fake/core-lib/src"),
682 is_dependency: false,
683 classification: None,
684 package_id: Some("core-lib 0.1.0".to_string()),
685 },
686 ];
687 let graph = vec![
688 (
689 "app 0.1.0".to_string(),
690 vec![DepEdge {
691 extern_name: "core_lib".to_string(),
692 pkg_id: "core-lib 0.1.0".to_string(),
693 }],
694 ),
695 ("core-lib 0.1.0".to_string(), vec![]),
696 ];
697 let order = workspace_topological_order(&ws_crates, &graph).unwrap();
698 let pos = |id: &str| order.iter().position(|x| x == id).unwrap();
699 assert!(
700 pos("core-lib 0.1.0") < pos("app 0.1.0"),
701 "core-lib should come before app"
702 );
703 }
704
705 #[test]
706 fn workspace_topo_order_independent() {
707 let ws_crates = vec![
708 CrateInfo {
709 name: "a".to_string(),
710 version: "0.1.0".to_string(),
711 source_dir: PathBuf::from("/fake/a/src"),
712 is_dependency: false,
713 classification: None,
714 package_id: Some("a 0.1.0".to_string()),
715 },
716 CrateInfo {
717 name: "b".to_string(),
718 version: "0.1.0".to_string(),
719 source_dir: PathBuf::from("/fake/b/src"),
720 is_dependency: false,
721 classification: None,
722 package_id: Some("b 0.1.0".to_string()),
723 },
724 ];
725 let graph = vec![
726 ("a 0.1.0".to_string(), vec![]),
727 ("b 0.1.0".to_string(), vec![]),
728 ];
729 let order = workspace_topological_order(&ws_crates, &graph).unwrap();
730 assert_eq!(order.len(), 2);
731 }
732}