fix: accept non-DNS Common Crawl graph rows
Some checks failed
Standalone registry checks / check (push) Has been cancelled

This commit is contained in:
Nic Weyand 2026-09-22 09:01:39 -04:00
commit 3b6966c81a
Signed by: nicweyand
SSH key fingerprint: SHA256:2te+ycJIQON/Wo/dH6+ZkFSQ4HnHWpetV2azx9E65dQ
7 changed files with 60 additions and 21 deletions

View file

@ -1,5 +1,13 @@
# Changelog # Changelog
## 0.6.1 - 2026-09-22
- Accept complete Common Crawl domain-rank releases containing provider rows
that are not valid DNS hostnames. Such rows remain authenticated and counted
in source coordinates but cannot match or enter the official-domain catalog.
- Keep malformed graph schemas and numeric fields fail-closed, with a regression
derived from the real `com.your_domain` provider row.
## 0.6.0 - 2026-09-22 ## 0.6.0 - 2026-09-22
- Add a streaming Common Crawl domain Web Graph adapter for harmonic-centrality, - Add a streaming Common Crawl domain Web Graph adapter for harmonic-centrality,

4
Cargo.lock generated
View file

@ -78,14 +78,14 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]] [[package]]
name = "argand-atomic" name = "argand-atomic"
version = "0.6.0" version = "0.6.1"
dependencies = [ dependencies = [
"tempfile", "tempfile",
] ]
[[package]] [[package]]
name = "argand-site-registry" name = "argand-site-registry"
version = "0.6.0" version = "0.6.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argand-atomic", "argand-atomic",

View file

@ -4,7 +4,7 @@ resolver = "3"
members = ["crates/argand-atomic", "crates/argand-site-registry"] members = ["crates/argand-atomic", "crates/argand-site-registry"]
[workspace.package] [workspace.package]
version = "0.6.0" version = "0.6.1"
authors = ["Nic Weyand"] authors = ["Nic Weyand"]
edition = "2024" edition = "2024"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"

View file

@ -43,9 +43,15 @@ impl SourceAdapter for DomainRanks {
let harmonic_value = nonnegative_finite(fields[1], "harmonic value")?; let harmonic_value = nonnegative_finite(fields[1], "harmonic value")?;
let pagerank_rank = positive_integer(fields[2], "PageRank rank")?; let pagerank_rank = positive_integer(fields[2], "PageRank rank")?;
let pagerank_value = nonnegative_finite(fields[3], "PageRank value")?; let pagerank_value = nonnegative_finite(fields[3], "PageRank value")?;
let target = reverse_domain(fields[4])?;
let member_hosts = positive_integer(fields[5], "member host count")?; let member_hosts = positive_integer(fields[5], "member host count")?;
source_row += 1; source_row += 1;
let Some(target) = reverse_domain(fields[4]) else {
// The provider graph contains a small amount of underscore and
// otherwise non-DNS host material. It cannot match the registry's
// normalized public identity domains, but it remains part of the
// authenticated input stream and source-row coordinate space.
continue;
};
if !self.targets.contains(&target) { if !self.targets.contains(&target) {
continue; continue;
} }
@ -118,18 +124,13 @@ fn nonnegative_finite(value: &str, field: &str) -> anyhow::Result<f64> {
Ok(parsed) Ok(parsed)
} }
fn reverse_domain(value: &str) -> anyhow::Result<String> { fn reverse_domain(value: &str) -> Option<String> {
ensure!( if value.is_empty() || value.len() > 253 || value != value.trim() {
!value.is_empty() && value.len() <= 253 && value == value.trim(), return None;
"invalid reversed domain" }
);
let labels = value.split('.').collect::<Vec<_>>(); let labels = value.split('.').collect::<Vec<_>>();
ensure!( if labels.len() < 2
labels.len() >= 2, || !labels.iter().all(|label| {
"reversed domain needs at least two labels"
);
ensure!(
labels.iter().all(|label| {
!label.is_empty() !label.is_empty()
&& label.len() <= 63 && label.len() <= 63
&& label && label
@ -143,8 +144,9 @@ fn reverse_domain(value: &str) -> anyhow::Result<String> {
.as_bytes() .as_bytes()
.last() .last()
.is_some_and(u8::is_ascii_alphanumeric) .is_some_and(u8::is_ascii_alphanumeric)
}), })
"invalid reversed domain label" {
); return None;
Ok(labels.into_iter().rev().collect::<Vec<_>>().join(".")) }
Some(labels.into_iter().rev().collect::<Vec<_>>().join("."))
} }

View file

@ -153,7 +153,6 @@ fn domain_rank_parser_rejects_schema_and_value_drift() -> anyhow::Result<()> {
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n\n", "#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n\n",
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n0\t1\t1\t1\tcom.example\t1\n", "#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n0\t1\t1\t1\tcom.example\t1\n",
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\tNaN\t1\t1\tcom.example\t1\n", "#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\tNaN\t1\t1\tcom.example\t1\n",
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\t1\t1\t1\tcom..example\t1\n",
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\t1\t1\t1\tcom.example\t0\n", "#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\t1\t1\t1\tcom.example\t0\n",
] { ] {
let root = tempfile::tempdir()?; let root = tempfile::tempdir()?;
@ -167,6 +166,30 @@ fn domain_rank_parser_rejects_schema_and_value_drift() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[test]
fn non_dns_provider_rows_are_skipped_without_losing_source_coordinates() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let mut db = store::open(&root.path().join("store.sqlite"))?;
import_identity_candidates(&mut db, root.path())?;
let input = "#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n\
1\t1\t1\t1\tcom.your_domain\t15\n\
2\t1\t2\t1\tcom.facebook\t18795\n";
import_graph(&mut db, root.path(), input.as_bytes())?;
let retained: Vec<(String, String)> = {
let mut statement = db.prepare(
"SELECT r.native_id,f.value FROM records r JOIN facts f ON f.source_id=r.source_id AND f.ordinal=r.ordinal WHERE f.predicate='popularity' ORDER BY r.native_id",
)?;
statement
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<Result<_, _>>()?
};
assert_eq!(retained.len(), 1);
assert_eq!(retained[0].0, "row:2");
assert!(retained[0].1.contains("facebook.com"));
Ok(())
}
#[test] #[test]
fn domain_rank_source_url_is_exactly_allowlisted() -> anyhow::Result<()> { fn domain_rank_source_url_is_exactly_allowlisted() -> anyhow::Result<()> {
let good = "https://data.commoncrawl.org/projects/hyperlinkgraph/cc-main-2022-may-jun-aug/domain/cc-main-2022-may-jun-aug-domain-ranks.txt.gz"; let good = "https://data.commoncrawl.org/projects/hyperlinkgraph/cc-main-2022-may-jun-aug/domain/cc-main-2022-may-jun-aug-domain-ranks.txt.gz";

View file

@ -34,7 +34,7 @@ source attribution and application-specific malware/content policy.
## Current contracts ## Current contracts
Code version 0.6.0 uses writer schema 5 and `argand.site-rules/v4`. Code version 0.6.1 uses writer schema 5 and `argand.site-rules/v4`.
New compact `COMPLETE.json` files use `argand.site-registry/v3` and bind: New compact `COMPLETE.json` files use `argand.site-registry/v3` and bind:
- authenticated `registry.sqlite` bytes; - authenticated `registry.sqlite` bytes;

View file

@ -2,6 +2,12 @@
# Version 0.6.0 Web Graph and updater validation, 2026-09-22 # Version 0.6.0 Web Graph and updater validation, 2026-09-22
Patch release 0.6.1 additionally replays the real provider-shaped
`com.your_domain` case: the row remains in authenticated input/coordinate
accounting but is not retained as a DNS target. The following valid
`com.facebook` row is retained at its original `row:2` coordinate. Schema and
numeric corruption continue to fail closed.
The evaluation contract was written before implementation. The new Common Crawl The evaluation contract was written before implementation. The new Common Crawl
domain-rank integration passed five default integration tests; its sixth test is domain-rank integration passed five default integration tests; its sixth test is
an explicit resource benchmark. The benchmark parsed 100,000 valid provider-shaped an explicit resource benchmark. The benchmark parsed 100,000 valid provider-shaped