use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
const SECRET: &str = "s3cr3t-hunter2-do-not-print";
fn secret_url() -> url::Url {
url::Url::parse(&format!("https://alice:{SECRET}@wpad.corp:8080/proxy.pac"))
.expect("the fixture URL parses")
}
fn secret_script() -> String {
format!("function FindProxyForURL(u, h) {{ return \"PROXY alice:{SECRET}@p:8080\"; }}")
}
#[derive(Debug, Clone, Copy)]
enum Exposure {
Secret,
Delegates(&'static str),
Inert(&'static str),
}
#[derive(Debug)]
struct Registered {
file: &'static str,
ty: &'static str,
exposure: Exposure,
available: bool,
}
const REGISTRY: &[Registered] = &[
Registered {
file: "auth.rs",
ty: "ProxyAuth",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "env.rs",
ty: "ProxyEnv",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "error.rs",
ty: "Error",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "mode.rs",
ty: "ProxyMode",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "pac/dispatch.rs",
ty: "PacResolver",
exposure: Exposure::Inert(
"the `PacPolicy` (flags, numbers, an address and a time), `attached` in place of the evaluator, and the native resolver's session handle and timeout",
),
available: cfg!(feature = "pac"),
},
Registered {
file: "pac/mod.rs",
ty: "PacRequirement",
exposure: Exposure::Secret,
available: cfg!(feature = "pac"),
},
Registered {
file: "pac/mod.rs",
ty: "PacScript",
exposure: Exposure::Secret,
available: cfg!(feature = "pac"),
},
Registered {
file: "pac/winhttp.rs",
ty: "WinHttpPacSource",
exposure: Exposure::Secret,
available: cfg!(all(
windows,
feature = "pac",
feature = "pac-windows-native"
)),
},
Registered {
file: "sys/linux/gnome.rs",
ty: "Handle",
exposure: Exposure::Inert("a stop flag and whether the thread is joined, both bool"),
available: cfg!(all(target_os = "linux", feature = "linux-gnome")),
},
Registered {
file: "sys/linux/gsettings_map.rs",
ty: "GValue",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "sys/linux/kde.rs",
ty: "FileWatch",
exposure: Exposure::Inert("nothing at all — the struct name and `..`"),
available: cfg!(all(target_os = "linux", feature = "linux-kde")),
},
Registered {
file: "sys/linux/kioslaverc.rs",
ty: "KconfigEntry",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "sys/linux/kioslaverc.rs",
ty: "KioslavercSettings",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "sys/linux/watcher.rs",
ty: "Watch",
exposure: Exposure::Inert(
"whether the portal route was forced, whether it polls, whether it runs — all bool",
),
available: cfg!(target_os = "linux"),
},
Registered {
file: "sys/mac/notify.rs",
ty: "Watch",
exposure: Exposure::Inert("a stop flag, whether the thread runs, and `degraded`"),
available: cfg!(target_os = "macos"),
},
Registered {
file: "sys/proxy_dict.rs",
ty: "ProxyDict",
exposure: Exposure::Secret,
available: true,
},
Registered {
file: "watch.rs",
ty: "CapturedEnv",
exposure: Exposure::Inert("nothing at all — the struct name and `..`"),
available: true,
},
Registered {
file: "watch.rs",
ty: "ProxyWatcher",
exposure: Exposure::Delegates("ProxyMode"),
available: true,
},
];
enum Reason {
ClosedAtFill(&'static str),
ViaRegistered { ty: &'static str, why: &'static str },
CrateText(&'static str),
}
struct Derived {
file: &'static str,
ty: &'static str,
reason: Reason,
available: bool,
}
static DERIVED: &[Derived] = &[
Derived {
file: "bypass.rs",
ty: "HostPattern",
reason: Reason::ClosedAtFill(
"`HostPattern::parse` refuses any entry containing `@` before it parses a port, \
and phrases the refusal through `redact_offending_token`, so a proxy URL \
pasted into a bypass list never becomes a pattern in the first place. The \
variants' fields are `pub` and `#[non_exhaustive]` on an enum does not seal \
its existing variants, so a caller can also hand-build one — `tests/bypass.rs` \
does, deliberately — and print back whatever it wrote there. That is the \
caller's own string; `parse` stays the only way a value this crate read gets in",
),
available: true,
},
Derived {
file: "debug_masking.rs",
ty: "Exposure",
reason: Reason::CrateText("the `&'static str` verdicts are the ones written in this file"),
available: true,
},
Derived {
file: "debug_masking.rs",
ty: "Registered",
reason: Reason::CrateText("`file` and `ty` are the literals in [`REGISTRY`]"),
available: true,
},
Derived {
file: "diagnostic.rs",
ty: "RejectedValue",
reason: Reason::ClosedAtFill(
"`RejectedValue::new` is the only constructor taking raw input and it stores \
`redact_offending_token(input)`, never the input",
),
available: true,
},
Derived {
file: "diagnostic.rs",
ty: "RejectionSource",
reason: Reason::CrateText(
"each payload names *which* setting was read, not what it held: \
`EnvironmentVariable` gets a name out of `SCHEME_VARS`, `Kioslaverc` a \
`SlotKeys::key`, `GSettings` a GNOME schema key, `SystemConfiguration` a \
`SCHEMES` or `UNROUTABLE` host key — all crate constants. A KDE key read \
from the file would \
not be one, which is why this row names the source and not just the type",
),
available: true,
},
Derived {
file: "endpoint.rs",
ty: "ProxyEndpoint",
reason: Reason::ClosedAtFill(
"`parse` and `new` are the constructors, and `parse` splits `user:password@` \
off into `auth` before the rest becomes the `host` — so the `url::Host` here \
holds an address and the credential prints through `ProxyAuth`'s registered \
impl. The fields are `pub`, so a caller holding one can overwrite `host` \
afterwards; what it writes is its own string, not a value this crate read",
),
available: true,
},
Derived {
file: "pac/subprocess.rs",
ty: "SubprocessEvaluator",
reason: Reason::ClosedAtFill(
"`new` is the only constructor and the fields are private, so `worker` holds the \
path the caller chose and nothing this crate read. The rest is a `PacPolicy`: \
flags, numbers, an address and a time. The evaluator's own `allow_unsandboxed` \
is a flag",
),
available: cfg!(feature = "pac-subprocess"),
},
Derived {
file: "sys/linux/gsettings_map.rs",
ty: "GnomeSettings",
reason: Reason::ViaRegistered {
ty: "GValue",
why: "it holds the values, and every one of them prints through `GValue`'s own \
impl. Its keys are `String` but are built by `read_key` from `ROOT_KEYS` / \
`CHILD_KEYS` / `HTTP_AUTH_KEYS` and a child name, so unlike KDE's they \
cannot come from the file",
},
available: true,
},
Derived {
file: "sys/proxy_dict.rs",
ty: "DictValue",
reason: Reason::ViaRegistered {
ty: "ProxyDict",
why: "the containment runs the other way: this is the leaf that holds the text, \
and it is safe only for as long as `ProxyDict`'s hand-written impl is the \
only thing that prints one. A `{:?}` on a bare `DictValue` would bypass \
every mask, which is what a catch-all arm here would give it",
},
available: true,
},
Derived {
file: "sys/win/ffi.rs",
ty: "RegKey",
reason: Reason::CrateText(
"`root` is one of the names `root_name` returns and `path` is the `&'static \
str` the caller opened it with — always one of this backend's own key-path \
constants. Both name *where* a value lives; neither can hold the value, \
which is the side a proxy password would be on",
),
available: cfg!(target_os = "windows"),
},
Derived {
file: "sys/win/notify.rs",
ty: "WatchedKey",
reason: Reason::CrateText(
"`path` is the `&'static str` registry path this backend opened, and the rest \
of the struct is two handles, a `bool` and a `ProxyConfigSource`",
),
available: cfg!(windows),
},
];
fn impl_debug_target(line: &str) -> Option<&str> {
let line = line.trim_start();
if !(line.starts_with("impl ") || line.starts_with("impl<")) {
return None;
}
let rest = line.split_once("Debug for ")?.1;
let end = rest
.find(|c: char| c == '<' || c == '{' || c.is_whitespace())
.unwrap_or(rest.len());
Some(&rest[..end])
}
fn hides_a_debug_impl_header(line: &str) -> bool {
let line = line.trim();
let block_comment = line.contains("/*") || line.contains("*/");
if block_comment && line.contains("Debug") {
return true;
}
if line.starts_with("use ") && line.contains("Debug") && line.contains(" as ") {
return true;
}
if !(line.starts_with("impl ") || line.starts_with("impl<")) {
return false;
}
line.ends_with("Debug") || line.split_whitespace().next_back() == Some("for")
}
fn read_src_files() -> Vec<(String, String)> {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
let mut pending = vec![src.clone()];
while let Some(dir) = pending.pop() {
for entry in fs::read_dir(&dir).expect("a directory under src/ is readable") {
let path = entry
.expect("a directory entry under src/ is readable")
.path();
if path.is_dir() {
pending.push(path);
continue;
}
if path.extension().is_none_or(|ext| ext != "rs") {
continue;
}
let text = fs::read_to_string(&path).expect("a source file under src/ is UTF-8");
let rel = path
.strip_prefix(&src)
.expect("the file was found under src/")
.to_string_lossy()
.replace('\\', "/");
files.push((rel, text));
}
}
files
}
fn scan_src() -> BTreeSet<(String, String)> {
let mut found = BTreeSet::new();
for (rel, text) in read_src_files() {
for line in text.lines() {
assert!(
!hides_a_debug_impl_header(line),
"{rel} writes a line this scanner reads past without seeing the impl it \
covers — a header broken across lines at `for`, or interrupted by a block \
comment, or a `use` renaming the trait so that no header names it — so a \
hand-written `Debug` there would never reach REGISTRY and the gate would \
pass without checking it. Keep the whole `impl … Debug for Type` on one \
line, with no `/* … */` in it (`#[rustfmt::skip]` if it is too long to \
fit, and on one line even then), and import the trait under its own name."
);
if let Some(ty) = impl_debug_target(line) {
found.insert((rel.clone(), ty.to_string()));
}
}
}
found
}
fn code_only(line: &str) -> &str {
line.split("//").next().unwrap_or(line)
}
fn derives_debug(line: &str) -> bool {
let line = line.trim_start();
line.starts_with("#[derive(")
&& line
.split(|c: char| !is_name_byte(c))
.any(|word| word == "Debug")
}
fn splits_a_derive_attribute(line: &str) -> bool {
let line = line.trim_start();
line.starts_with("#[derive(") && !line.contains(")]")
}
fn stands_between_a_derive_and_its_item(line: &str) -> bool {
let line = line.trim();
line.is_empty() || line.starts_with("#[") || line.starts_with("//")
}
fn derived_item_name(line: &str) -> Option<&str> {
let mut words = line.split_whitespace();
let mut word = words.next()?;
if word.starts_with("pub") {
word = words.next()?;
}
if !matches!(word, "struct" | "enum" | "union") {
return None;
}
let name = words.next()?;
let end = name.find(|c: char| !is_name_byte(c)).unwrap_or(name.len());
(end > 0).then(|| &name[..end])
}
fn names_a_text_type(line: &str, aliases: &BTreeSet<String>) -> bool {
code_only(line)
.split(|c: char| !is_name_byte(c))
.any(|word| {
matches!(
word,
"String"
| "str"
| "OsString"
| "OsStr"
| "PathBuf"
| "Path"
| "Cow"
| "Host"
| "Url"
) || aliases.contains(word)
})
}
fn text_carrier_aliases(files: &[(String, String)]) -> BTreeSet<String> {
let mut aliases = BTreeSet::new();
loop {
let known = aliases.len();
for (_, text) in files {
for line in text.lines() {
let Some((head, definition)) = code_only(line).trim_start().split_once('=') else {
continue;
};
let mut words = head.split_whitespace();
let mut word = words.next();
if word.is_some_and(|word| word.starts_with("pub")) {
word = words.next();
}
if word != Some("type") {
continue;
}
let Some(name) = words.next() else { continue };
let end = name.find(|c: char| !is_name_byte(c)).unwrap_or(name.len());
if end > 0 && names_a_text_type(definition, &aliases) {
aliases.insert(name[..end].to_string());
}
}
}
if aliases.len() == known {
return aliases;
}
}
}
fn item_body<'a>(lines: &[&'a str]) -> Vec<&'a str> {
let mut body = Vec::new();
let mut depth = 0i32;
let mut opened = false;
for line in lines {
body.push(*line);
for c in code_only(line).chars() {
match c {
'{' | '(' => {
depth += 1;
opened = true;
}
'}' | ')' => depth -= 1,
_ => {}
}
}
if opened && depth <= 0 {
break;
}
if !opened && code_only(line).trim_end().ends_with(';') {
break;
}
}
body
}
fn scan_derived_text_holders() -> BTreeSet<(String, String)> {
let files = read_src_files();
let aliases = text_carrier_aliases(&files);
let mut found = BTreeSet::new();
for (rel, text) in &files {
let lines: Vec<&str> = text.lines().collect();
for (index, line) in lines.iter().enumerate() {
assert!(
!splits_a_derive_attribute(line),
"{rel}:{} splits a `#[derive(…)]` across lines, which this scanner reads \
one line at a time — a `Debug` below the break would never reach DERIVED \
and the gate would pass without checking it. Keep the attribute on one \
line (`#[rustfmt::skip]` if it is too long to fit).",
index + 1
);
if !derives_debug(line) {
continue;
}
let mut at = index + 1;
while lines
.get(at)
.is_some_and(|line| stands_between_a_derive_and_its_item(line))
{
at += 1;
}
let head = lines.get(at).copied().unwrap_or_default();
let ty = derived_item_name(head).unwrap_or_else(|| {
panic!(
"{rel}:{} derives `Debug` for something this scan cannot name: {head:?}",
index + 1
)
});
if item_body(&lines[at..])
.iter()
.any(|l| names_a_text_type(l, &aliases))
{
found.insert((rel.clone(), ty.to_string()));
}
}
}
found
}
fn keeps_a_rejected_token(line: &str) -> bool {
let line = line.trim_start();
let line = line.strip_prefix("self.").unwrap_or(line);
[
"rejected.push(",
"skipped.push(",
"rejected.extend(",
"skipped.extend(",
]
.iter()
.any(|call| line.starts_with(call))
}
fn rejected_list_binding(line: &str) -> Option<&str> {
if line.trim_start().starts_with("//") {
return None;
}
let (head, _) = [": Vec<RejectedValue>", ": &mut Vec<RejectedValue>"]
.iter()
.find_map(|separator| line.split_once(separator))?;
let name = head.rsplit(|c: char| !is_name_byte(c)).next()?;
(!name.is_empty()).then_some(name)
}
fn is_name_byte(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
struct Case {
file: &'static str,
ty: &'static str,
rendered: String,
must_contain: &'static [&'static str],
}
fn cases() -> Vec<Case> {
let mut cases = vec![
Case {
file: "auth.rs",
ty: "ProxyAuth",
rendered: format!("{:?}", crate::auth::ProxyAuth::new("alice", Some(SECRET))),
must_contain: &["alice", "***"],
},
Case {
file: "auth.rs",
ty: "ProxyAuth",
rendered: {
let endpoint = crate::endpoint::ProxyEndpoint::parse(
&format!("http://alice%3A{SECRET}%3Atail@proxy.corp:8080"),
80,
)
.expect("percent-encoded userinfo must parse");
format!("{:?}", endpoint.auth.expect("auth present"))
},
must_contain: &["alice", "***"],
},
Case {
file: "auth.rs",
ty: "ProxyAuth",
rendered: format!(
"{:?}",
crate::auth::ProxyAuth::from_username(format!("alice:{SECRET}"))
),
must_contain: &["alice", "***"],
},
Case {
file: "error.rs",
ty: "Error",
rendered: format!(
"{:?}",
crate::error::Error::PacFetchRequired { url: secret_url() }
),
must_contain: &["wpad.corp", "***"],
},
Case {
file: "mode.rs",
ty: "ProxyMode",
rendered: format!("{:?}", crate::mode::ProxyMode::pac(secret_url())),
must_contain: &["wpad.corp", "***"],
},
Case {
file: "mode.rs",
ty: "ProxyMode",
rendered: format!(
"{:?}",
crate::mode::ProxyMode::pac(
url::Url::parse(&format!(
"https://alice%3A{SECRET}@wpad.corp:8080/proxy.pac"
))
.expect("percent-encoded userinfo must parse")
)
),
must_contain: &["wpad.corp", "***"],
},
Case {
file: "mode.rs",
ty: "ProxyMode",
rendered: format!("{:?}", crate::mode::ProxyMode::pac_inline(secret_script())),
must_contain: &["len", "fnv1a"],
},
];
{
let env = crate::env::ProxyEnv::from_vars([
(
"http_proxy",
format!("http://alice:{SECRET}@proxy.corp:99999"),
),
("no_proxy", format!("[alice:{SECRET}]")),
])
.expect("malformed values are rejected entries, not a snapshot failure");
cases.push(Case {
file: "mode.rs",
ty: "ProxyMode",
rendered: format!("{:?}", env.to_mode()),
must_contain: &["proxy.corp", "***", "withheld"],
});
cases.push(Case {
file: "env.rs",
ty: "ProxyEnv",
rendered: format!("{env:?}"),
must_contain: &["proxy.corp", "***", "withheld"],
});
}
{
let mut settings = crate::sys::linux::kioslaverc::KioslavercSettings::new();
settings.insert("httpProxy", secret_url().to_string());
cases.push(Case {
file: "sys/linux/kioslaverc.rs",
ty: "KconfigEntry",
rendered: format!("{settings:?}"),
must_contain: &["wpad.corp", "***"],
});
let mut settings = crate::sys::linux::kioslaverc::KioslavercSettings::new();
settings.insert(
&format!("https://alice:{SECRET}@wpad.corp/proxy.pac?a"),
"1",
);
settings.insert("httpProxy", "http://proxy.corp:8080");
cases.push(Case {
file: "sys/linux/kioslaverc.rs",
ty: "KioslavercSettings",
rendered: format!("{settings:?}"),
must_contain: &["wpad.corp", "***", "httpProxy", "proxy.corp"],
});
}
{
let mut settings = crate::sys::linux::gsettings_map::GnomeSettings::new();
settings.insert(
crate::sys::linux::gsettings_map::KEY_AUTOCONFIG_URL,
crate::sys::linux::gsettings_map::GValue::Text(secret_url().to_string()),
);
cases.push(Case {
file: "sys/linux/gsettings_map.rs",
ty: "GValue",
rendered: format!("{settings:?}"),
must_contain: &["wpad.corp", "***"],
});
}
{
let mut dict = crate::sys::proxy_dict::ProxyDict::new();
dict.insert(
"ProxyAutoConfigURLString",
crate::sys::proxy_dict::DictValue::Text(secret_url().to_string()),
);
dict.insert(
"ProxyAutoConfigJavaScript",
crate::sys::proxy_dict::DictValue::Text(secret_script()),
);
cases.push(Case {
file: "sys/proxy_dict.rs",
ty: "ProxyDict",
rendered: format!("{dict:?}"),
must_contain: &["wpad.corp", "***", "fnv1a"],
});
}
{
let mut dict = crate::sys::proxy_dict::ProxyDict::new();
dict.insert(
"ProxyAutoConfigURLString",
crate::sys::proxy_dict::DictValue::Text(format!(
"http://alice:my {SECRET}@wpad.corp:8080/proxy.pac"
)),
);
cases.push(Case {
file: "sys/proxy_dict.rs",
ty: "ProxyDict",
rendered: format!("{dict:?}"),
must_contain: &["withheld"],
});
}
{
let mut dict = crate::sys::proxy_dict::ProxyDict::new();
dict.insert(
"HTTPProxy",
crate::sys::proxy_dict::DictValue::Text(format!("alice:{SECRET}@proxy.corp:8080")),
);
dict.insert(
"ExceptionsList",
crate::sys::proxy_dict::DictValue::List {
items: vec!["example.com".to_owned(), format!("[bob:{SECRET}]")],
unreadable: 0,
},
);
cases.push(Case {
file: "sys/proxy_dict.rs",
ty: "ProxyDict",
rendered: format!("{dict:?}"),
must_contain: &["proxy.corp", "***", "example.com", "withheld"],
});
}
{
let mut dict = crate::sys::proxy_dict::ProxyDict::new();
dict.insert(
"ProxyAutoConfigURLString",
crate::sys::proxy_dict::DictValue::List {
items: vec![secret_url().to_string()],
unreadable: 0,
},
);
dict.insert(
"ProxyAutoConfigJavaScript",
crate::sys::proxy_dict::DictValue::List {
items: vec![secret_script()],
unreadable: 0,
},
);
cases.push(Case {
file: "sys/proxy_dict.rs",
ty: "ProxyDict",
rendered: format!("{dict:?}"),
must_contain: &["withheld"],
});
}
#[cfg(feature = "pac")]
{
let url = secret_url();
let script = secret_script();
cases.push(Case {
file: "pac/mod.rs",
ty: "PacRequirement",
rendered: format!("{:?}", crate::pac::PacRequirement::Fetch(&url)),
must_contain: &["wpad.corp", "***"],
});
cases.push(Case {
file: "pac/mod.rs",
ty: "PacRequirement",
rendered: format!("{:?}", crate::pac::PacRequirement::Inline(&script)),
must_contain: &["len", "fnv1a"],
});
cases.push(Case {
file: "pac/mod.rs",
ty: "PacScript",
rendered: format!("{:?}", crate::pac::PacScript::new(script)),
must_contain: &["len", "fnv1a"],
});
}
#[cfg(all(windows, feature = "pac", feature = "pac-windows-native"))]
{
let url = secret_url();
cases.push(Case {
file: "pac/winhttp.rs",
ty: "WinHttpPacSource",
rendered: format!("{:?}", crate::pac::WinHttpPacSource::Url(url.clone())),
must_contain: &["wpad.corp", "***"],
});
cases.push(Case {
file: "pac/winhttp.rs",
ty: "WinHttpPacSource",
rendered: format!("{:?}", crate::pac::WinHttpPacSource::AutoDetectThenUrl(url)),
must_contain: &["wpad.corp", "***"],
});
}
cases
}
fn derived_cases() -> Vec<Case> {
let mut cases = Vec::new();
{
let refused = crate::bypass::HostPattern::parse(&format!("alice:{SECRET}@proxy.corp"));
let kept = crate::bypass::HostPattern::parse(".example.com");
cases.push(Case {
file: "bypass.rs",
ty: "HostPattern",
rendered: format!("{refused:?} {kept:?}"),
must_contain: &["proxy.corp", "***", "example.com"],
});
}
{
let endpoint = crate::endpoint::ProxyEndpoint::parse(
&format!("http://alice:{SECRET}@proxy.corp:8080"),
80,
)
.expect("a credential-bearing proxy URL must parse");
cases.push(Case {
file: "endpoint.rs",
ty: "ProxyEndpoint",
rendered: format!("{endpoint:?}"),
must_contain: &["alice", "***", "proxy.corp", "8080"],
});
}
{
let rejected = crate::diagnostic::RejectedValue::new(
crate::diagnostic::RejectionKind::InvalidProxyEndpoint,
crate::diagnostic::RejectionSource::BypassList,
secret_url(),
);
cases.push(Case {
file: "diagnostic.rs",
ty: "RejectedValue",
rendered: format!("{rejected:?}"),
must_contain: &["wpad.corp", "***", "BypassList"],
});
}
#[cfg(feature = "pac-subprocess")]
{
let evaluator = crate::pac::SubprocessEvaluator::new(
"bin/proxy-watch-pac-worker",
crate::pac::PacPolicy::new(),
);
cases.push(Case {
file: "pac/subprocess.rs",
ty: "SubprocessEvaluator",
rendered: format!("{evaluator:?}"),
must_contain: &["proxy-watch-pac-worker", "PacPolicy"],
});
}
cases
}
#[cfg(test)]
mod tests {
use super::*;
fn hides_the_secret_without_erasing_the_rest(case: &Case) {
assert!(
!case.rendered.contains(SECRET),
"{}'s `Debug` ({}) printed the password: {}",
case.ty,
case.file,
case.rendered
);
for fragment in case.must_contain {
assert!(
case.rendered.contains(fragment),
"{}'s `Debug` ({}) dropped {fragment:?} — that is erasure, not masking: {}",
case.ty,
case.file,
case.rendered
);
}
}
#[test]
fn every_hand_written_debug_is_registered() {
let found: BTreeSet<(String, String)> = scan_src();
let registered: BTreeSet<(String, String)> = REGISTRY
.iter()
.map(|r| (r.file.to_string(), r.ty.to_string()))
.collect();
let unregistered: Vec<_> = found.difference(®istered).collect();
assert!(
unregistered.is_empty(),
"hand-written `Debug` impls missing from REGISTRY in src/debug_masking.rs: \
{unregistered:?}. Add a row saying whether the type can reach a URL, a \
script body or a password; if it can, mark it Exposure::Secret and add a \
case to `cases`."
);
let stale: Vec<_> = registered.difference(&found).collect();
assert!(
stale.is_empty(),
"REGISTRY rows with no matching impl in src/: {stale:?}. The impl was \
renamed, moved or deleted — update the row rather than leaving it."
);
}
#[test]
fn every_verdict_that_skips_a_value_case_justifies_itself() {
for entry in REGISTRY {
match entry.exposure {
Exposure::Secret => {}
Exposure::Delegates(target) => {
let delegate = REGISTRY.iter().find(|r| r.ty == target).unwrap_or_else(|| {
panic!("{}'s delegate {target} is not registered", entry.ty)
});
assert!(
matches!(delegate.exposure, Exposure::Secret),
"{} delegates to {target}, which is not itself checked as a \
secret bearer — the delegation proves nothing",
entry.ty
);
}
Exposure::Inert(reason) => assert!(
!reason.trim().is_empty(),
"{} ({}) is registered inert with no account of what it does print",
entry.ty,
entry.file
),
}
}
}
#[test]
fn every_secret_bearing_debug_hides_it() {
let cases = cases();
for case in &cases {
hides_the_secret_without_erasing_the_rest(case);
}
let covered: BTreeSet<(&str, &str)> = cases.iter().map(|c| (c.file, c.ty)).collect();
let expected: BTreeSet<(&str, &str)> = REGISTRY
.iter()
.filter(|r| matches!(r.exposure, Exposure::Secret) && r.available)
.map(|r| (r.file, r.ty))
.collect();
assert_eq!(
covered, expected,
"the secret-bearing impls available on this target and the ones `cases` \
actually renders have drifted apart"
);
}
#[test]
fn every_derived_debug_that_can_hold_text_is_declared() {
let found: BTreeSet<(String, String)> = scan_derived_text_holders();
let declared: BTreeSet<(String, String)> = DERIVED
.iter()
.map(|d| (d.file.to_string(), d.ty.to_string()))
.collect();
let undeclared: Vec<_> = found.difference(&declared).collect();
assert!(
undeclared.is_empty(),
"these derive `Debug` over a field naming a text type, with no row in DERIVED \
(src/debug_masking.rs): {undeclared:?}. Say why the text cannot be a secret — \
and if it can be, hand-write the impl instead and register it in REGISTRY."
);
let stale: Vec<_> = declared.difference(&found).collect();
assert!(
stale.is_empty(),
"DERIVED rows with no matching derived item in src/: {stale:?}. The item was \
renamed, lost its text field, or now writes its `Debug` by hand — in the last \
case the row belongs in REGISTRY."
);
}
#[test]
fn every_reason_a_derive_is_safe_says_what_it_rests_on() {
for entry in DERIVED {
let why = match entry.reason {
Reason::ClosedAtFill(why) | Reason::CrateText(why) => why,
Reason::ViaRegistered { ty, why } => {
let target = REGISTRY.iter().find(|r| r.ty == ty).unwrap_or_else(|| {
panic!("{}'s masked type {ty} is not in REGISTRY", entry.ty)
});
assert!(
matches!(target.exposure, Exposure::Secret),
"{} rests on {ty}, which is not itself checked as a secret bearer — \
the reason proves nothing",
entry.ty
);
why
}
};
assert!(
!why.trim().is_empty(),
"{} ({}) is declared safe with no account of why",
entry.ty,
entry.file
);
}
}
#[test]
fn every_value_closed_at_fill_is_shown_to_be() {
let cases = derived_cases();
for case in &cases {
hides_the_secret_without_erasing_the_rest(case);
}
let covered: BTreeSet<(&str, &str)> = cases.iter().map(|c| (c.file, c.ty)).collect();
let expected: BTreeSet<(&str, &str)> = DERIVED
.iter()
.filter(|d| matches!(d.reason, Reason::ClosedAtFill(_)) && d.available)
.map(|d| (d.file, d.ty))
.collect();
assert_eq!(
covered, expected,
"the fill paths declared closed on this target and the ones `derived_cases` \
actually renders have drifted apart"
);
}
#[test]
fn every_kept_token_is_masked_where_it_is_kept() {
let mut unmasked = Vec::new();
for (rel, text) in read_src_files() {
let lines: Vec<&str> = text.lines().collect();
for (number, line) in lines.iter().enumerate() {
if !keeps_a_rejected_token(line) {
continue;
}
let trimmed = line.trim();
if matches!(
trimmed,
"rejected.extend(skipped);"
| "rejected.push(value.for_scheme(Some(Scheme::Https)));"
) {
continue;
}
let masked = lines[number..lines.len().min(number + 3)].iter().any(|l| {
l.contains("redact_offending_token") || l.contains("RejectedValue::new")
});
if !masked {
unmasked.push(format!("{rel}:{}: {trimmed}", number + 1));
}
}
}
assert!(
unmasked.is_empty(),
"these keep a token that failed to parse without masking it: {unmasked:#?}. \
Wrap it in `RejectedValue::new` or `util::redact_offending_token`; \
`redact_userinfo` is not enough, \
because a token that failed to parse may never have reached its `@`. If the \
append genuinely carries no untrusted text, say so where it is written and \
exempt it here."
);
}
#[test]
fn every_declared_list_of_rejected_values_is_named_something_the_scan_looks_for() {
let mut unseen = Vec::new();
for (rel, text) in read_src_files() {
for (number, line) in text.lines().enumerate() {
let Some(name) = rejected_list_binding(line) else {
continue;
};
if !keeps_a_rejected_token(&format!("{name}.push(value);")) {
unseen.push(format!("{rel}:{}: {}", number + 1, line.trim()));
}
}
}
assert!(
unseen.is_empty(),
"these hold rejected tokens under a name `keeps_a_rejected_token` does not \
look for, so appends to them are never checked for masking: {unseen:#?}. \
Either rename the binding or add its spelling to that scan."
);
}
#[test]
fn the_scanner_reads_the_shapes_this_tree_uses() {
assert_eq!(
impl_debug_target("impl fmt::Debug for ProxyAuth {"),
Some("ProxyAuth")
);
assert_eq!(
impl_debug_target("impl std::fmt::Debug for Error {"),
Some("Error")
);
assert_eq!(
impl_debug_target("impl fmt::Debug for PacRequirement<'_> {"),
Some("PacRequirement")
);
assert_eq!(
impl_debug_target("impl<T> fmt::Debug for Wrapper<T> {"),
Some("Wrapper")
);
assert_eq!(
impl_debug_target(" impl Debug for Indented {"),
Some("Indented")
);
assert_eq!(
impl_debug_target("impl fmt::Debug for Tight{"),
Some("Tight")
);
assert_eq!(
impl_debug_target("/// impl fmt::Debug for Mentioned {"),
None
);
assert_eq!(impl_debug_target("#[derive(Debug)]"), None);
assert_eq!(impl_debug_target("impl fmt::Display for ProxyAuth {"), None);
let before = "impl<'a, T: Bound> fmt::Debug";
assert_eq!(impl_debug_target(before), None);
assert!(hides_a_debug_impl_header(before));
assert!(hides_a_debug_impl_header("impl std::fmt::Debug"));
let after = "impl fmt::Debug for";
assert_eq!(impl_debug_target(after), None);
assert!(hides_a_debug_impl_header(after));
assert!(hides_a_debug_impl_header("impl<T: fmt::Debug> Show for"));
let comment = "/* audited later */";
for hidden in [
format!("impl fmt::Debug {comment} for NewType {{"),
format!("{comment} impl fmt::Debug for NewType {{"),
] {
assert_ne!(impl_debug_target(&hidden), Some("NewType"), "{hidden}");
assert!(hides_a_debug_impl_header(&hidden), "{hidden}");
}
assert!(!hides_a_debug_impl_header(" \"*/people/*\""));
let closer = "*/";
assert!(hides_a_debug_impl_header(&format!(
"{closer} impl fmt::Debug for NewType {{"
)));
assert!(!hides_a_debug_impl_header(
"impl<T: fmt::Debug> Show for T {"
));
assert!(!hides_a_debug_impl_header(
"impl fmt::Debug for ProxyAuth {"
));
assert!(!hides_a_debug_impl_header("/// impl fmt::Debug"));
let renamed = format!("use std::fmt::Debug {} Shown;", "as");
assert!(hides_a_debug_impl_header(&renamed));
assert_eq!(impl_debug_target("impl Shown for ProxyAuth {"), None);
assert!(!hides_a_debug_impl_header("use std::fmt::Write as _;"));
assert!(!hides_a_debug_impl_header("use std::fmt::Debug;"));
assert!(keeps_a_rejected_token(" rejected.push(x);"));
assert!(keeps_a_rejected_token("self.rejected.push(x);"));
assert!(keeps_a_rejected_token(
"skipped.push(format!(\"{k}={v}\"));"
));
assert!(keeps_a_rejected_token("rejected.extend(skipped);"));
assert!(keeps_a_rejected_token("skipped.extend(more);"));
assert!(keeps_a_rejected_token("rejected.push(RejectedValue::new("));
assert!(!keeps_a_rejected_token("patterns.push(pattern);"));
let owned = ": Vec<RejectedValue>,";
let borrowed = ": &mut Vec<RejectedValue>,";
assert_eq!(
rejected_list_binding(&format!(" pub rejected{owned}")),
Some("rejected")
);
assert_eq!(
rejected_list_binding(&format!(" rejected{borrowed}")),
Some("rejected")
);
assert_eq!(
rejected_list_binding(&format!(" pub refused{owned}")),
Some("refused")
);
assert!(!keeps_a_rejected_token("refused.push(value);"));
assert_eq!(
rejected_list_binding("fn parse() -> (Config, Vec<RejectedValue>) {"),
None
);
assert_eq!(
rejected_list_binding(&format!(" // rejected{owned} is what this holds")),
None
);
}
#[test]
fn the_derive_scanner_reads_the_shapes_this_tree_uses() {
let attribute = |list: &str| format!("#[derive({list})]");
assert!(derives_debug(&attribute("Debug")));
assert!(derives_debug(&format!(" {}", attribute("Clone, Debug"))));
assert!(derives_debug(&attribute("Debug, Clone, Copy")));
assert!(!derives_debug(&attribute("Clone, PartialEq, Eq")));
assert!(!derives_debug(&format!("/// {}", attribute("Debug"))));
assert!(!derives_debug(&attribute("DebugCustom")));
let split = "#[derive(";
assert!(!derives_debug(split));
assert!(splits_a_derive_attribute(split));
assert!(splits_a_derive_attribute("#[derive(Clone,"));
assert!(!splits_a_derive_attribute(&attribute("Debug")));
assert!(!splits_a_derive_attribute("#[non_exhaustive]"));
assert_eq!(
derived_item_name("pub(crate) enum DictValue {"),
Some("DictValue")
);
assert_eq!(derived_item_name("struct WatchedKey {"), Some("WatchedKey"));
assert_eq!(
derived_item_name("pub struct Wrapper(String);"),
Some("Wrapper")
);
assert_eq!(derived_item_name("pub enum Kind<'a> {"), Some("Kind"));
assert_eq!(derived_item_name("pub struct Unit;"), Some("Unit"));
assert_eq!(derived_item_name("impl Debug for Thing {"), None);
assert_eq!(derived_item_name("enum {"), None);
let none = BTreeSet::new();
assert!(names_a_text_type(" value: Option<String>,", &none));
assert!(names_a_text_type(
" entries: HashMap<String, GValue>,",
&none
));
assert!(names_a_text_type(" path: &'static str,", &none));
assert!(names_a_text_type(" url: Url,", &none));
assert!(names_a_text_type(" host: Host,", &none));
assert!(names_a_text_type(" a: OsString,", &none));
assert!(names_a_text_type(" b: &OsStr,", &none));
assert!(names_a_text_type(" c: PathBuf,", &none));
assert!(names_a_text_type(" d: &Path,", &none));
assert!(names_a_text_type(" e: Cow<'a, T>,", &none));
assert!(!names_a_text_type(" port: Option<u16>,", &none));
assert!(!names_a_text_type(" /// The host as a String.", &none));
let field = " credential: CredentialText,";
assert!(!names_a_text_type(field, &none));
let declaration = "type CredentialText = String;";
let aliases = text_carrier_aliases(&[(String::from("new.rs"), declaration.to_string())]);
assert!(names_a_text_type(field, &aliases));
assert!(
text_carrier_aliases(&[(
String::from("new.rs"),
format!("type Port = u16;\n// {declaration}\n")
)])
.is_empty()
);
let chain = text_carrier_aliases(&[
(
String::from("outer.rs"),
String::from("pub type Outer = Inner;"),
),
(
String::from("inner.rs"),
String::from("pub(crate) type Inner = String;"),
),
]);
assert!(names_a_text_type(" a: Inner,", &chain));
assert!(names_a_text_type(" b: Outer,", &chain));
assert_eq!(
item_body(&[
"struct A {",
" /// takes a `{`",
" a: u8,",
"}",
"next"
])
.len(),
4
);
assert_eq!(item_body(&["struct B(String);", "next"]).len(), 1);
assert_eq!(item_body(&["struct C;", "next"]).len(), 1);
assert_eq!(
item_body(&["struct D(", " String,", ");", "next"]).len(),
3
);
assert!(stands_between_a_derive_and_its_item("#[non_exhaustive]"));
assert!(stands_between_a_derive_and_its_item("// a note"));
assert!(stands_between_a_derive_and_its_item(" "));
assert!(!stands_between_a_derive_and_its_item("pub struct E {"));
}
}