use std::collections::{BTreeSet, VecDeque};
use std::process::Command;
const ALWAYS_FORBIDDEN_CRATES: &[&str] = &[
"reqwest",
"hyper",
"hyper-util",
"h2",
"isahc",
"surf",
"attohttpc",
"curl",
"curl-sys",
"tiny_http",
"actix-web",
"axum",
"warp",
"rouille",
"minreq",
"tungstenite",
"tokio-tungstenite",
"websocket",
"ssh2",
"libssh2-sys",
"russh",
"tokio",
"async-std",
"smol",
"async-io",
"openssl",
"openssl-sys",
"native-tls",
"trust-dns-resolver",
"hickory-resolver",
"sentry",
"sentry-core",
"opentelemetry",
"tracing-opentelemetry",
"posthog",
"posthog-rs",
"segment",
"amplitude",
"mixpanel",
"datadog-apm",
"metrics-exporter-prometheus",
];
const CONNECT_GATED_CRATES: &[&str] = &["ureq", "rustls", "keyring"];
const CONNECT_CRATE: &str = "costroid-connect";
const CONNECT_ALLOWED: &[&str] = &[
"aes",
"async-broadcast",
"async-trait",
"block-padding",
"byteorder",
"cbc",
"cc",
"cipher",
"concurrent-queue",
"core-foundation",
"crossbeam-utils",
"dbus",
"dbus-secret-service",
"endi",
"enumflags2",
"enumflags2_derive",
"event-listener",
"event-listener-strategy",
"find-msvc-tools",
"futures-core",
"futures-macro",
"futures-sink",
"futures-task",
"futures-util",
"hkdf",
"hmac",
"http",
"httparse",
"inout",
"keyring",
"libdbus-sys",
"num",
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"openssl-probe",
"ordered-stream",
"parking",
"percent-encoding",
"pin-project-lite",
"pkg-config",
"ring",
"rpassword",
"rtoolbox",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
"rustls-webpki",
"schannel",
"secrecy",
"secret-service",
"security-framework",
"security-framework-sys",
"serde_repr",
"sha1",
"shlex",
"slab",
"tracing",
"tracing-attributes",
"tracing-core",
"untrusted",
"ureq",
"ureq-proto",
"utf8-zero",
"windows-targets",
"windows_x86_64_msvc",
"xdg-home",
"zbus",
"zbus_macros",
"zbus_names",
"zeroize",
"zeroize_derive",
"zvariant",
"zvariant_derive",
"zvariant_utils",
];
const SHIPPED_TARGETS: &[&str] = &[
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
];
fn reachable_crate_names(extra_args: &[&str]) -> BTreeSet<String> {
let mut names = BTreeSet::new();
for target in SHIPPED_TARGETS {
names.extend(reachable_for_target(target, extra_args));
}
names
}
fn reachable_for_target(target: &str, extra_args: &[&str]) -> BTreeSet<String> {
let mut args = vec![
"metadata",
"--format-version",
"1",
"--locked",
"--filter-platform",
target,
];
args.extend_from_slice(extra_args);
let output = match Command::new(env!("CARGO")).args(&args).output() {
Ok(output) => output,
Err(err) => panic!(
"failed to run `cargo metadata --filter-platform {target} {extra_args:?}`: {err}"
),
};
assert!(
output.status.success(),
"`cargo metadata --filter-platform {target} {extra_args:?}` failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let meta: serde_json::Value = match serde_json::from_slice(&output.stdout) {
Ok(value) => value,
Err(err) => panic!("`cargo metadata` emitted invalid JSON: {err}"),
};
let packages = match meta["packages"].as_array() {
Some(packages) => packages,
None => panic!("`cargo metadata` output had no `packages` array"),
};
let id_to_name: std::collections::HashMap<&str, &str> = packages
.iter()
.filter_map(|pkg| Some((pkg["id"].as_str()?, pkg["name"].as_str()?)))
.collect();
let resolve = &meta["resolve"];
let nodes = match resolve["nodes"].as_array() {
Some(nodes) => nodes,
None => panic!("`cargo metadata` output had no `resolve.nodes` array"),
};
let mut edges: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
for node in nodes {
let Some(id) = node["id"].as_str() else {
continue;
};
let deps: Vec<&str> = node["deps"]
.as_array()
.map(|deps| deps.iter().filter_map(|d| d["pkg"].as_str()).collect())
.unwrap_or_default();
edges.insert(id, deps);
}
let members = match meta["workspace_members"].as_array() {
Some(members) => members,
None => panic!("`cargo metadata` output had no `workspace_members` array"),
};
let mut queue: VecDeque<&str> = VecDeque::new();
let mut visited: BTreeSet<&str> = BTreeSet::new();
for member in members {
let Some(id) = member.as_str() else {
continue;
};
if id_to_name.get(id).copied() != Some(CONNECT_CRATE) && visited.insert(id) {
queue.push_back(id);
}
}
while let Some(id) = queue.pop_front() {
if let Some(deps) = edges.get(id) {
for &dep in deps {
if visited.insert(dep) {
queue.push_back(dep);
}
}
}
}
visited
.iter()
.filter_map(|id| id_to_name.get(id).map(|name| name.to_string()))
.collect()
}
#[test]
#[ignore]
fn print_connect_delta() {
let default = reachable_crate_names(&[]);
let connect = reachable_crate_names(&["--features", "connect"]);
let delta: Vec<&String> = connect.difference(&default).collect();
println!("CONNECT_DELTA ({} crates):", delta.len());
for name in &delta {
println!(" {name}");
}
}
#[test]
fn default_build_links_no_network_tls_or_telemetry_crate() {
let names = reachable_crate_names(&[]);
assert!(
!names.contains(CONNECT_CRATE),
"the default build must not link `{CONNECT_CRATE}` — the `connect` feature \
must be off by default so the local-only build links no network/keychain code."
);
let hits: Vec<&str> = ALWAYS_FORBIDDEN_CRATES
.iter()
.chain(CONNECT_GATED_CRATES.iter())
.copied()
.filter(|crate_name| names.contains(*crate_name))
.collect();
assert!(
hits.is_empty(),
"the default/local-only build forbids networking/TLS/telemetry dependencies, \
but the resolved graph contains: {hits:?}.\n\
Costroid must read local logs only and make no network call. Network code \
belongs solely in `costroid-connect`, behind the off-by-default `connect` \
feature — if a crate must move there, update CONNECT_GATED_CRATES, deny.toml, \
and the `connect`-build test together."
);
}
#[test]
fn connect_feature_admits_only_the_sanctioned_trio() {
let names = reachable_crate_names(&["--features", "connect"]);
assert!(
names.contains(CONNECT_CRATE),
"`--features connect` must link `{CONNECT_CRATE}` — otherwise the gate is not \
actually wired to the connections subsystem."
);
let hits: Vec<&str> = ALWAYS_FORBIDDEN_CRATES
.iter()
.copied()
.filter(|crate_name| names.contains(*crate_name))
.collect();
assert!(
hits.is_empty(),
"even with `connect` on, Costroid forbids async runtimes, non-rustls TLS \
(OpenSSL), other HTTP clients, and all telemetry, but the resolved graph \
contains: {hits:?}.\n\
The connections subsystem uses only `ureq` + `rustls` + `keyring`."
);
for gated in CONNECT_GATED_CRATES {
assert!(
names.contains(*gated),
"`--features connect` must link `{gated}` — the connections subsystem \
(T8 credential store, T9a authorized-host HTTP client) depends on the \
full `ureq`/`rustls`/`keyring` trio, so the gate has to pull it in."
);
}
let default = reachable_crate_names(&[]);
let allowed: BTreeSet<&str> = CONNECT_ALLOWED.iter().copied().collect();
let unexpected: Vec<&str> = names
.difference(&default)
.map(String::as_str)
.filter(|name| *name != CONNECT_CRATE && !allowed.contains(name))
.collect();
assert!(
unexpected.is_empty(),
"`--features connect` introduced crate(s) not in the reviewed allowlist: \
{unexpected:?}.\n\
Every crate the connect-on graph adds must be reviewed (is it a network / TLS / \
telemetry path?) and, if legitimate, added to CONNECT_ALLOWED. Regenerate the \
expected delta with: \
`cargo test -p costroid --test offline print_connect_delta -- --ignored --nocapture`."
);
}