use super::policy::{self, Context, Reading};
use super::position::PositionIndex;
use super::{Found, KeySpan, csv, dotenv, format, ini, json, log, scanner, toml, yaml};
pub(crate) fn find(text: &str, format_key: &str) -> Vec<Found> {
let spans = keys(text, format_key);
let index = PositionIndex::new(text);
scanner::candidates(text)
.into_iter()
.filter_map(|candidate| {
let key = key_at(&spans, candidate.offset);
let context = Context::from_key(key);
let reading = policy::read(candidate.text, context)?;
let position = index.at(candidate.offset);
Some(match reading {
Reading::Address {
kind,
normalized,
class,
block,
} => Found {
kind: Some(kind),
text: candidate.text.to_string(),
line: position.line,
column: position.column,
key: key.map(str::to_string),
normalized: Some(normalized),
class: Some(class),
cidr: block,
refused: None,
},
Reading::Refused { kind, refusal } => Found {
kind,
text: candidate.text.to_string(),
line: position.line,
column: position.column,
key: key.map(str::to_string),
normalized: None,
class: None,
cidr: None,
refused: Some(refusal),
},
})
})
.collect()
}
fn keys(text: &str, format_key: &str) -> Vec<KeySpan> {
match format::canonical(format_key) {
"json" => json::keys(text),
"yaml" => yaml::keys(text),
"toml" => toml::keys(text),
"ini" => ini::keys(text),
"env" => dotenv::keys(text),
"csv" => csv::keys(text, csv::COMMA),
"tsv" => csv::keys(text, csv::TAB),
"log" => log::keys(text),
_ => Vec::new(),
}
}
fn key_at(spans: &[KeySpan], offset: usize) -> Option<&str> {
let candidate = spans
.partition_point(|span| span.start <= offset)
.checked_sub(1)?;
let span = &spans[candidate];
(offset < span.end).then_some(span.path.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::{Class, Kind, policy::Reason};
fn found(text: &str, format: &str) -> Vec<Found> {
find(text, format)
}
fn one(text: &str, format: &str) -> Found {
let mut all = found(text, format);
assert_eq!(all.len(), 1, "{text} produced {all:?}");
all.remove(0)
}
#[test]
fn an_address_carries_its_position_and_its_key() {
let one = one("server:\n bind: 10.0.0.5\n", "yaml");
assert_eq!(one.kind, Some(Kind::Ipv4));
assert_eq!(one.text, "10.0.0.5");
assert_eq!(one.line, 2);
assert_eq!(one.column, 9);
assert_eq!(one.key.as_deref(), Some("server.bind"));
assert_eq!(one.normalized.as_deref(), Some("10.0.0.5"));
assert_eq!(one.class, Some(Class::Private));
}
#[test]
fn findings_come_back_in_document_order() {
let text = "a: 10.0.0.1\nb: 10.0.0.2\nc: 10.0.0.3\n";
let texts: Vec<String> = found(text, "yaml").into_iter().map(|f| f.text).collect();
assert_eq!(texts, ["10.0.0.1", "10.0.0.2", "10.0.0.3"]);
}
#[test]
fn a_dotted_triple_refuses_or_disappears_depending_on_its_key() {
let refused = one("build: 10.0.1\n", "yaml");
assert_eq!(
refused.refused.expect("a refusal").reason,
Reason::AmbiguousVersion
);
assert!(found("version: 10.0.1\n", "yaml").is_empty());
assert!(found("app_version: 10.0.1\n", "yaml").is_empty());
}
#[test]
fn a_version_glued_to_a_name_never_reaches_the_policy_layer() {
assert!(found("released v1.2.3.4 today", "unknown").is_empty());
assert!(found("upgraded to v10.0.1", "unknown").is_empty());
assert_eq!(one("saw 1.2.3.4 once", "unknown").kind, Some(Kind::Ipv4));
}
#[test]
fn an_address_key_reaches_the_policy_layer() {
let one = one("BIND_IP=2130706433\n", "env");
let refusal = one.refused.expect("a refusal");
assert_eq!(refusal.reason, Reason::IntegerForm);
assert_eq!(one.key.as_deref(), Some("BIND_IP"));
assert!(found("TIMEOUT=2130706433\n", "env").is_empty());
}
#[test]
fn an_unknown_format_still_finds_addresses_and_reports_no_key() {
let one = one("allow 2001:db8::1 through", "unknown");
assert_eq!(one.normalized.as_deref(), Some("2001:db8::1"));
assert_eq!(one.key, None);
}
#[test]
fn an_address_outside_a_value_has_no_key() {
let one = one("# fallback 10.0.0.9\n", "yaml");
assert_eq!(one.text, "10.0.0.9");
assert_eq!(one.key, None);
}
#[test]
fn an_address_in_a_trailing_comment_has_no_key() {
for (format, text) in [
("yaml", "bind: 10.0.0.5 # fallback 10.0.0.9\n"),
("toml", "bind = \"10.0.0.5\" # fallback 10.0.0.9\n"),
("ini", "bind = 10.0.0.5 ; fallback 10.0.0.9\n"),
("ini", "bind = 10.0.0.5 # fallback 10.0.0.9\n"),
("env", "BIND=10.0.0.5 # fallback 10.0.0.9\n"),
] {
let all = found(text, format);
assert_eq!(all.len(), 2, "{format}: {all:?}");
assert!(all[0].key.is_some(), "{format}: the value lost its key");
assert_eq!(all[1].key, None, "{format}: the comment kept a key");
}
}
#[test]
fn a_comment_marker_inside_a_value_is_not_a_comment() {
for (format, text) in [
("yaml", "bind: \"10.0.0.5 # 10.0.0.9\"\n"),
("toml", "bind = \"10.0.0.5 # 10.0.0.9\"\n"),
("ini", "bind = \"10.0.0.5 ; 10.0.0.9\"\n"),
("env", "BIND=\"10.0.0.5 # 10.0.0.9\"\n"),
("yaml", "bind: 10.0.0.5#10.0.0.9\n"),
("env", "BIND=10.0.0.5#10.0.0.9\n"),
] {
let all = found(text, format);
assert_eq!(all.len(), 2, "{format}: {all:?}");
assert!(
all[1].key.is_some(),
"{format}: a value was cut at its own marker"
);
}
}
#[test]
fn a_key_lookup_lands_inside_the_span_and_nowhere_else() {
let spans = vec![
KeySpan {
start: 5,
end: 10,
path: "a".into(),
},
KeySpan {
start: 20,
end: 25,
path: "b".into(),
},
];
assert_eq!(key_at(&spans, 0), None);
assert_eq!(key_at(&spans, 5), Some("a"));
assert_eq!(key_at(&spans, 9), Some("a"));
assert_eq!(key_at(&spans, 10), None);
assert_eq!(key_at(&spans, 24), Some("b"));
assert_eq!(key_at(&spans, 99), None);
assert_eq!(key_at(&[], 3), None);
}
#[test]
fn a_log_line_yields_the_address_that_connected() {
let text = "192.168.1.10 - - [12/Aug/2026:10:30:00 +0000] \"GET /x HTTP/1.1\" 200 512\n";
let all = found(text, "log");
assert_eq!(all.len(), 1, "{all:?}");
assert_eq!(all[0].normalized.as_deref(), Some("192.168.1.10"));
assert_eq!(all[0].line, 1);
assert_eq!(all[0].column, 1);
}
}