use std::sync::LazyLock;
use regex::Regex;
const HONEYCOMB_DETECTORS: &[&str] = &["honeycomb"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoneycombFpShape {
AndroidClass,
}
impl HoneycombFpShape {
pub fn as_tag(self) -> &'static str {
match self {
HoneycombFpShape::AndroidClass => "ANDROID_CLASS",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoneycombKeyAnchor {
ClassicHex,
IngestKey,
}
impl HoneycombKeyAnchor {
pub fn as_tag(self) -> &'static str {
match self {
HoneycombKeyAnchor::ClassicHex => "CLASSIC_HEX",
HoneycombKeyAnchor::IngestKey => "INGEST_KEY",
}
}
}
pub fn is_honeycomb_detector(detector: &str) -> bool {
let lower = detector.to_ascii_lowercase();
HONEYCOMB_DETECTORS.iter().any(|n| lower == *n)
}
static CLASSIC_HEX_RE: LazyLock<Result<Regex, regex::Error>> =
LazyLock::new(|| Regex::new(r"^[0-9a-f]{32}$"));
static INGEST_KEY_RE: LazyLock<Result<Regex, regex::Error>> =
LazyLock::new(|| Regex::new(r"^hc[abc]i[ack]_[A-Za-z0-9]{28}$"));
pub fn classify_honeycomb_raw_shape(raw: &str) -> Option<HoneycombFpShape> {
if !raw.starts_with("Honeycomb") {
return None;
}
let next = raw.as_bytes().get(9)?;
if !next.is_ascii_uppercase() {
return None;
}
if !raw
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'$')
{
return None;
}
Some(HoneycombFpShape::AndroidClass)
}
pub fn classify_honeycomb_raw_anchor(raw: &str) -> Option<HoneycombKeyAnchor> {
let ingest_ok = match &*INGEST_KEY_RE {
Ok(re) => re.is_match(raw),
Err(_) => false,
};
if ingest_ok {
return Some(HoneycombKeyAnchor::IngestKey);
}
let classic_ok = match &*CLASSIC_HEX_RE {
Ok(re) => re.is_match(raw),
Err(_) => false,
};
if classic_ok {
return Some(HoneycombKeyAnchor::ClassicHex);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn honeycomb_detector_case_insensitive() {
assert!(is_honeycomb_detector("Honeycomb"));
assert!(is_honeycomb_detector("HONEYCOMB"));
assert!(is_honeycomb_detector("honeycomb"));
assert!(is_honeycomb_detector("HoneyComb"));
}
#[test]
fn honeycomb_detector_rejects_non_honeycomb() {
assert!(!is_honeycomb_detector("Box"));
assert!(!is_honeycomb_detector("Honeycombs")); assert!(!is_honeycomb_detector("Honey"));
assert!(!is_honeycomb_detector(""));
}
#[test]
fn classify_aosp_honeycomb_bitmap_factory() {
assert_eq!(
classify_honeycomb_raw_shape("HoneycombBitmapFactory"),
Some(HoneycombFpShape::AndroidClass),
);
}
#[test]
fn classify_aosp_honeycomb_compat() {
assert_eq!(
classify_honeycomb_raw_shape("HoneycombCompat"),
Some(HoneycombFpShape::AndroidClass),
);
}
#[test]
fn classify_extended_honeycomb_class_names() {
let documented = [
"HoneycombMR1NotificationHelper",
"HoneycombGalleryActivity",
"HoneycombV11Compat",
"HoneycombBackport",
"HoneycombA", ];
for raw in documented {
assert_eq!(
classify_honeycomb_raw_shape(raw),
Some(HoneycombFpShape::AndroidClass),
"expected AndroidClass for {raw}",
);
}
}
#[test]
fn classify_dollar_inner_class_admitted() {
assert_eq!(
classify_honeycomb_raw_shape("HoneycombFactory$Inner"),
Some(HoneycombFpShape::AndroidClass),
);
}
#[test]
fn classify_lowercase_honeycomb_rejected() {
assert_eq!(classify_honeycomb_raw_shape("honeycombbitmapfactory"), None);
}
#[test]
fn classify_bare_honeycomb_word_rejected() {
assert_eq!(classify_honeycomb_raw_shape("Honeycomb"), None);
}
#[test]
fn classify_honeycomb_with_lowercase_suffix_rejected() {
assert_eq!(
classify_honeycomb_raw_shape("Honeycombbitmapfactory"),
None,
);
}
#[test]
fn classify_honeycomb_with_punctuation_rejected() {
assert_eq!(classify_honeycomb_raw_shape("HoneycombFactory=foo"), None);
assert_eq!(classify_honeycomb_raw_shape("Honeycomb.BitmapFactory"), None);
assert_eq!(
classify_honeycomb_raw_shape("Honeycomb BitmapFactory"),
None,
);
}
#[test]
fn classify_honeycomb_with_slash_rejected() {
assert_eq!(
classify_honeycomb_raw_shape("Honeycomb/BitmapFactory"),
None,
);
}
#[test]
fn classify_empty_string_is_not_class_name() {
assert_eq!(classify_honeycomb_raw_shape(""), None);
}
#[test]
fn classify_classic_hex_anchor() {
let raw = "0123456789abcdef0123456789abcdef";
assert_eq!(raw.len(), 32);
assert_eq!(
classify_honeycomb_raw_anchor(raw),
Some(HoneycombKeyAnchor::ClassicHex),
);
}
#[test]
fn classify_classic_hex_uppercase_rejected() {
let raw = "0123456789ABCDEF0123456789ABCDEF";
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn classify_classic_hex_short_rejected() {
let raw = "0123456789abcdef0123456789abcde";
assert_eq!(raw.len(), 31);
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn classify_classic_hex_long_rejected() {
let raw = "0123456789abcdef0123456789abcdef0";
assert_eq!(raw.len(), 33);
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn classify_ingest_key_anchor() {
let documented = [
"hcaia_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hcaic_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hcaik_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hcbia_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hcbic_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hcbik_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hccia_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hccic_AbCdEfGhIjKlMnOpQrStUvWxYz01",
"hccik_AbCdEfGhIjKlMnOpQrStUvWxYz01",
];
for raw in documented {
assert_eq!(raw.len(), 6 + 28);
assert_eq!(
classify_honeycomb_raw_anchor(raw),
Some(HoneycombKeyAnchor::IngestKey),
"expected IngestKey for {raw}",
);
}
}
#[test]
fn classify_ingest_key_with_wrong_type_letter_rejected() {
let raw = "hcdia_AbCdEfGhIjKlMnOpQrStUvWxYz01";
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn classify_ingest_key_with_wrong_class_letter_rejected() {
let raw = "hcaid_AbCdEfGhIjKlMnOpQrStUvWxYz01";
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn classify_ingest_key_with_dash_in_body_rejected() {
let raw = "hcaik_AbCd-fGhIjKlMnOpQrStUvWxYz01";
assert_eq!(raw.len(), 6 + 28);
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn classify_ingest_key_short_body_rejected() {
let raw = "hcaik_AbCdEfGhIjKlMnOpQrStUvWxY";
assert_eq!(raw.len(), 6 + 25);
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn class_name_and_anchor_are_disjoint_classic() {
let classic = "0123456789abcdef0123456789abcdef";
assert_eq!(classify_honeycomb_raw_shape(classic), None);
assert_eq!(
classify_honeycomb_raw_anchor(classic),
Some(HoneycombKeyAnchor::ClassicHex),
);
}
#[test]
fn class_name_and_anchor_are_disjoint_ingest() {
let ingest = "hcaik_AbCdEfGhIjKlMnOpQrStUvWxYz01";
assert_eq!(classify_honeycomb_raw_shape(ingest), None);
assert_eq!(
classify_honeycomb_raw_anchor(ingest),
Some(HoneycombKeyAnchor::IngestKey),
);
}
#[test]
fn aosp_class_is_not_an_anchor() {
let raw = "HoneycombBitmapFactory";
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
assert_eq!(
classify_honeycomb_raw_shape(raw),
Some(HoneycombFpShape::AndroidClass),
);
}
#[test]
fn fp_shape_tag_is_stable() {
assert_eq!(HoneycombFpShape::AndroidClass.as_tag(), "ANDROID_CLASS");
}
#[test]
fn key_anchor_tags_are_stable() {
assert_eq!(HoneycombKeyAnchor::ClassicHex.as_tag(), "CLASSIC_HEX");
assert_eq!(HoneycombKeyAnchor::IngestKey.as_tag(), "INGEST_KEY");
}
#[test]
fn unicode_raw_does_not_panic_shape() {
let raw = "Honeycomb\u{1F600}Factory";
assert_eq!(classify_honeycomb_raw_shape(raw), None);
}
#[test]
fn unicode_raw_does_not_panic_anchor() {
let raw = "hcaik_\u{1F600}_28_chars_xxxxxxxxxxxxxx";
assert_eq!(classify_honeycomb_raw_anchor(raw), None);
}
#[test]
fn bare_word_then_punctuation_rejected() {
assert_eq!(classify_honeycomb_raw_shape("Honeycomb!"), None);
assert_eq!(classify_honeycomb_raw_shape("Honeycomb_Foo"), None);
}
}