use super::policy::{Clock, Id, Kind, Reason, Refusal, Verdict, is_hex, names_an_id};
use super::time::iso8601_utc;
const LENGTH: usize = 24;
const TIME_CHARS: usize = 8;
pub(crate) fn classify(token: &str, key: Option<&str>, clock: Clock) -> Verdict {
if token.len() != LENGTH || !is_hex(token) {
return Verdict::Ignored;
}
let seconds =
i64::from_str_radix(&token[..TIME_CHARS], 16).expect("eight checked hex digits fit i64");
let milliseconds = seconds * 1_000;
let timestamp = iso8601_utc(milliseconds);
let refusal = |detail: &str| {
Verdict::Refused(Refusal {
timestamp: Some(timestamp.clone()),
..Refusal::new(None, Reason::AmbiguousKind, detail.to_string())
})
};
if !names_an_id(key) {
return refusal(
"24 hex digits are an ObjectId, a truncated hash and a hex dump in equal measure, \
and nothing in this document names this field an identifier",
);
}
if !clock.is_plausible(milliseconds) {
return refusal(
"the field is named like an identifier, and 24 hex digits are an ObjectId only if \
the leading four bytes are also a time; these are not, and a truncated hash fits \
the same shape",
);
}
Verdict::Named(Id::at(Kind::ObjectId, timestamp))
}
#[cfg(test)]
mod tests {
use super::*;
const CLOCK: Clock = Clock::at(1_786_492_800_000);
const OBJECT_ID: &str = "6a7bb780a1b2c3d4e5f60718";
fn named(token: &str, key: &str) -> Id {
match classify(token, Some(key), CLOCK) {
Verdict::Named(id) => id,
other => panic!("{token} was {other:?}, not a named ObjectId"),
}
}
fn refused(token: &str, key: Option<&str>) -> Refusal {
match classify(token, key, CLOCK) {
Verdict::Refused(refusal) => refusal,
other => panic!("{token} was {other:?}, not a refusal"),
}
}
#[test]
fn an_objectid_under_a_key_naming_an_id_is_named_with_its_timestamp() {
assert_eq!(
named(OBJECT_ID, "_id").timestamp,
Some("2026-08-12T00:00:00.000Z".to_string())
);
}
#[test]
fn every_way_a_document_names_an_id_field_is_evidence() {
for key in [
"_id",
"id",
"objectId",
"userId",
"user_id",
"documents.[0]._id",
"record.$oid",
] {
assert_eq!(
named(OBJECT_ID, key).kind,
Kind::ObjectId,
"{key} did not settle it"
);
}
}
#[test]
fn an_objectid_reads_in_either_case() {
assert_eq!(
named("6A7BB780A1B2C3D4E5F60718", "_id").timestamp,
named(OBJECT_ID, "_id").timestamp
);
}
#[test]
fn the_same_run_is_named_under_an_id_key_and_refused_without_one() {
assert_eq!(named(OBJECT_ID, "_id").kind, Kind::ObjectId);
for key in [Some("checksum"), Some("commit"), Some("digest"), None] {
let refusal = refused(OBJECT_ID, key);
assert_eq!(refusal.reason, Reason::AmbiguousKind, "{key:?}");
assert_eq!(refusal.kind, None, "{key:?}: a kind was named");
assert_eq!(
refusal.timestamp,
Some("2026-08-12T00:00:00.000Z".to_string()),
"{key:?}: the decode is carried into the refusal"
);
assert!(
refusal.detail.contains("names this field an identifier"),
"{key:?}: {}",
refusal.detail
);
}
}
#[test]
fn a_run_in_a_document_with_no_keys_is_never_an_objectid() {
assert_eq!(refused(OBJECT_ID, None).reason, Reason::AmbiguousKind);
}
#[test]
fn a_hex_run_whose_leading_bytes_are_not_a_time_is_ambiguous() {
for token in ["ffffffffa1b2c3d4e5f60718", "00000001a1b2c3d4e5f60718"] {
let refusal = refused(token, Some("_id"));
assert_eq!(refusal.reason, Reason::AmbiguousKind, "{token}");
assert_eq!(refusal.kind, None, "no kind was named");
assert!(refusal.timestamp.is_some(), "the decode is still carried");
assert!(
refusal.detail.contains("also a time"),
"the refusal does not say which signal was missing: {}",
refusal.detail
);
}
}
#[test]
fn a_truncated_hash_is_refused_rather_than_named() {
assert_eq!(
refused("e83c5163316f89bfbde7d9ab", Some("commitId")).reason,
Reason::AmbiguousKind
);
}
#[test]
fn a_run_that_is_not_hex_is_not_an_objectid() {
assert_eq!(
classify("6a7bb780a1b2c3d4e5f6071g", Some("_id"), CLOCK),
Verdict::Ignored
);
assert_eq!(
classify("6a7bb780-a1b2-c3d4-e5f6", Some("_id"), CLOCK),
Verdict::Ignored
);
}
}