droidsaw 2.0.0

DROIDSAW — unified Android reverse engineering CLI. Hermes, DEX, APK signing. JSON output, MCP server. Bytecode is not a security layer.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
//! Detector-local FP suppressor + real-key anchor for trufflehog Honeycomb hits.
//!
//! Trufflehog's Honeycomb detector substring-matches "Honeycomb" inside any
//! string and treats the surrounding window as a candidate credential. This
//! produces a saturated FP stream on Android apps, because the AOSP
//! framework class FQN family `android.graphics.drawable.HoneycombBitmapFactory`
//! / `android.support.v4.app.HoneycombCompat` (and similar Pascal-case
//! identifier shapes that begin with the literal word `Honeycomb`) appear in
//! every app that touches the bitmap pipeline or Android 3.0/Honeycomb-era
//! compatibility shims. A representative audit flagged
//! `HoneycombBitmapFactory` as a Critical Honeycomb credential — it is a
//! public AOSP class name with no key material whatsoever.
//!
//! The calibration layer ships a blanket downgrade: every unverified Honeycomb
//! hit emits at Low rather than Critical. That keeps FP volume bounded but
//! surfaces real Honeycomb keys at Low too — a precision regression. This
//! module is the precision anchor that supersedes the blanket downgrade for
//! the two cases it cannot disambiguate:
//!
//! 1. **Class-name shape** — `Honeycomb[A-Z][A-Za-z0-9_$]*` with all bytes
//!    in the ASCII identifier set. Downgraded Critical → Info with
//!    `id_tag = CREDENTIAL_HONEYCOMB_FP_ANDROID_CLASS` and
//!    `extra.fp_shape = "ANDROID_CLASS"`. Mirrors the Circle `_FP_<SHAPE>`
//!    convention from `credentials_fp.rs` — preserves the audit trail
//!    rather than dropping the line silently.
//! 2. **Documented key anchor** — Honeycomb publishes two key shapes:
//!    classic API key (32 lowercase hex characters, `[0-9a-f]{32}`
//!    exactly) and environment/ingest key (`hc[abc]i[ack]_` prefix + 28
//!    `[A-Za-z0-9]` characters; the prefix is structured —
//!    `hc<type><i><class>_`, where `type ∈ {a,b,c}` is region/tier and
//!    `class ∈ {a,c,k}` is auth class). A raw matching either anchor is
//!    re-promoted to Critical, overriding the row-1a Low downgrade.
//!    `id_tag = CREDENTIAL_HONEYCOMB`, `extra.anchor = "CLASSIC_HEX"`
//!    or `"INGEST_KEY"`.
//!
//! This module delivers the precision re-promotion that the calibration layer
//! defers: class-name shapes are downgraded to Info, while documented key
//! anchors are re-promoted to Critical.
//!
//! **Detector-local, not provenance-aware.** Like the Circle module, this
//! is content-shape suppression: it keys on the `Raw` text bytes of the
//! credential hit, not on the originating SDK class FQN. Orthogonal to the
//! umbrella `provenance-aware-suppression-layer` per R5.
//!
//! **Safe by construction.** Real Honeycomb keys cannot be Java
//! identifiers (lowercase hex / `hc..._` prefixed; Java identifiers don't
//! contain the underscore-after-prefix shape AND have a Pascal first byte
//! the anchor regex doesn't tolerate). The class-name shape gate cannot
//! match a real key (real keys fail the all-identifier-chars + Pascal-case
//! gate via the `_` in ingest keys or because they are pure-lowercase
//! hex, which fails the Pascal-case check).
//!

use std::sync::LazyLock;

use regex::Regex;

/// Detector names this module operates on. Trufflehog emits the canonical
/// `"Honeycomb"` for the Honeycomb observability service detector;
/// case-insensitive match folds future variants.
const HONEYCOMB_DETECTORS: &[&str] = &["honeycomb"];

/// FP-shape classification for trufflehog Honeycomb hits whose `Raw` matches
/// a Java/Kotlin class identifier rather than a documented Honeycomb key
/// shape. Stable string representation used as the audit-DB
/// `extra.fp_shape` value and as the `id_tag` suffix.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoneycombFpShape {
    /// Pascal-case Java class identifier starting with the literal word
    /// `Honeycomb` and containing only ASCII identifier chars
    /// (`A-Za-z0-9_$`). Examples: `HoneycombBitmapFactory`,
    /// `HoneycombCompat`. AOSP framework + 3rd-party SDK class names; not
    /// credentials.
    AndroidClass,
}

impl HoneycombFpShape {
    /// Stable tag used in the audit-DB `extra.fp_shape` JSON field and as
    /// the `_FP_<SHAPE>` suffix on `id_tag`.
    pub fn as_tag(self) -> &'static str {
        match self {
            HoneycombFpShape::AndroidClass => "ANDROID_CLASS",
        }
    }
}

/// Real-Honeycomb-key anchor classification. Returned by
/// [`classify_honeycomb_raw_anchor`] when the `Raw` matches one of the two
/// documented Honeycomb key shapes — used by the ingestion path to
/// re-promote the finding to Critical even when sibling `#5`'s row-1a
/// blanket downgrade would otherwise route Honeycomb hits to Low.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoneycombKeyAnchor {
    /// Classic API key: 32 lowercase hex characters.
    ClassicHex,
    /// Environment / Ingest key: `hc[abc]i[ack]_` prefix + 28
    /// `[A-Za-z0-9]` characters.
    IngestKey,
}

impl HoneycombKeyAnchor {
    /// Stable tag used in the audit-DB `extra.anchor` JSON field.
    pub fn as_tag(self) -> &'static str {
        match self {
            HoneycombKeyAnchor::ClassicHex => "CLASSIC_HEX",
            HoneycombKeyAnchor::IngestKey => "INGEST_KEY",
        }
    }
}

/// Returns true when `detector` (the trufflehog `DetectorName` field) is
/// the Honeycomb observability detector. Case-insensitive.
pub fn is_honeycomb_detector(detector: &str) -> bool {
    let lower = detector.to_ascii_lowercase();
    HONEYCOMB_DETECTORS.iter().any(|n| lower == *n)
}

/// Lazy-compiled regex for the documented Honeycomb classic API key shape.
/// 32 lowercase hex characters; full-anchor (start-of-string + end-of-string).
///
/// Source: Honeycomb classic API key documentation. Real keys are exactly
/// 32 chars of `[0-9a-f]`. Mixed-case or longer/shorter strings are not
/// classic Honeycomb keys.
static CLASSIC_HEX_RE: LazyLock<Result<Regex, regex::Error>> =
    LazyLock::new(|| Regex::new(r"^[0-9a-f]{32}$"));

/// Lazy-compiled regex for the documented Honeycomb environment/ingest
/// key shape: `hc[abc]i[ack]_` prefix + 28 `[A-Za-z0-9]` characters.
///
/// The prefix is structured: `hc` literal + region/tier letter + `i`
/// literal + auth-class letter + `_` literal. Length 6 prefix + 28 body
/// = 34 chars total. Full-anchor.
static INGEST_KEY_RE: LazyLock<Result<Regex, regex::Error>> =
    LazyLock::new(|| Regex::new(r"^hc[abc]i[ack]_[A-Za-z0-9]{28}$"));

/// Classify the `Raw` field of a Honeycomb trufflehog hit as a Java
/// class-name FP shape, or return `None` if the raw is not the
/// `Honeycomb[A-Z][A-Za-z0-9_$]*` Pascal-case identifier shape.
///
/// **Behavior contract:**
/// - Returns `Some(HoneycombFpShape::AndroidClass)` if `raw` starts with
///   the literal `Honeycomb`, the next byte is ASCII uppercase, AND every
///   byte in `raw` is an ASCII identifier char (`A-Za-z0-9_$`). Examples:
///   `HoneycombBitmapFactory`, `HoneycombCompat`,
///   `HoneycombMR1NotificationHelper`, `HoneycombGalleryActivity`.
/// - Returns `None` for any other shape, including:
///   - The literal word `Honeycomb` alone (no following Pascal-case suffix)
///   - `honeycomb` lowercase (not a Pascal-case class identifier)
///   - Strings containing non-identifier chars (`=`, `:`, `/`, `.`,
///     space) — real Honeycomb keys + JWT-shaped material won't match
///     anyway because of the literal-`Honeycomb` prefix requirement
///   - Real Honeycomb classic-hex keys (lowercase hex; no `Honeycomb`
///     prefix)
///   - Real Honeycomb ingest keys (`hc..._` prefix; no `Honeycomb` prefix)
///
/// **Typed-Err on byte-walk failure** — none possible; the predicate uses
/// only `str::starts_with` + bounds-checked `bytes().nth(...)` + `bytes().all(...)`.
/// No direct indexing, no `unwrap`/`expect`.
pub fn classify_honeycomb_raw_shape(raw: &str) -> Option<HoneycombFpShape> {
    if !raw.starts_with("Honeycomb") {
        return None;
    }
    // Byte 9 (right after the literal `Honeycomb`) must be ASCII uppercase
    // — that is the Pascal-case-suffix gate. Empty suffix (raw == "Honeycomb"
    // alone) is rejected.
    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)
}

/// Classify the `Raw` field of a Honeycomb trufflehog hit as a real
/// Honeycomb-key anchor, or return `None` if no documented anchor matches.
///
/// **Behavior contract:**
/// - Returns `Some(HoneycombKeyAnchor::IngestKey)` if `raw` matches
///   `^hc[abc]i[ack]_[A-Za-z0-9]{28}$` — checked first because the prefix
///   is structurally specific.
/// - Returns `Some(HoneycombKeyAnchor::ClassicHex)` if `raw` is exactly
///   32 lowercase hex characters.
/// - Returns `None` for any other shape.
///
/// **Typed-Err on regex compile failure** — `LazyLock<Result<Regex, _>>`
/// surfaces a regex compile error as `Err`; the `match` arms collapse it
/// to "no anchor matched" rather than panicking. The patterns are
/// compile-time constants and resolve to `Ok` in practice; the wrapper
/// is defensive.
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::*;

    // ── Detector-name classifier ────────────────────────────────────

    #[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")); // not exact match
        assert!(!is_honeycomb_detector("Honey"));
        assert!(!is_honeycomb_detector(""));
    }

    // ── Class-name FP shape ─────────────────────────────────────────

    /// The representative AOSP-class FP: AOSP framework class FQN.
    /// MUST classify as AndroidClass.
    #[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() {
        // Other plausible Pascal-case `Honeycomb`-prefixed class names
        // — empirical FP shape extends to any Pascal-case suffix. The
        // gate requires byte 9 to be ASCII uppercase, so the suffix
        // must start with an uppercase letter (no `_`-leading suffix).
        let documented = [
            "HoneycombMR1NotificationHelper",
            "HoneycombGalleryActivity",
            "HoneycombV11Compat",
            "HoneycombBackport",
            "HoneycombA", // minimal Pascal-case suffix
        ];
        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() {
        // `Honeycomb$Inner` — Java/Kotlin inner-class identifier shape.
        // `$` is a valid identifier char.
        assert_eq!(
            classify_honeycomb_raw_shape("HoneycombFactory$Inner"),
            Some(HoneycombFpShape::AndroidClass),
        );
    }

    #[test]
    fn classify_lowercase_honeycomb_rejected() {
        // `honeycomb` lowercase — not a Pascal-case identifier; doesn't
        // start with the literal `Honeycomb`.
        assert_eq!(classify_honeycomb_raw_shape("honeycombbitmapfactory"), None);
    }

    #[test]
    fn classify_bare_honeycomb_word_rejected() {
        // The bare word `Honeycomb` alone has no Pascal-case suffix and
        // is rejected — it's not unambiguously a class name (could be a
        // marketing string, code comment, etc.). Falls through to `#5`'s
        // row-1a Low downgrade.
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb"), None);
    }

    #[test]
    fn classify_honeycomb_with_lowercase_suffix_rejected() {
        // `Honeycombbitmapfactory` — no uppercase break after the prefix;
        // not Pascal-case.
        assert_eq!(
            classify_honeycomb_raw_shape("Honeycombbitmapfactory"),
            None,
        );
    }

    #[test]
    fn classify_honeycomb_with_punctuation_rejected() {
        // `Honeycomb=foo` contains `=`; not a pure identifier.
        assert_eq!(classify_honeycomb_raw_shape("HoneycombFactory=foo"), None);
        // `Honeycomb.Bar` contains `.`; FQN dot-notation is not pure
        // identifier (FQN ingestion would happen at the class-resolution
        // layer, not the credential-shape gate).
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb.BitmapFactory"), None);
        // Space between `Honeycomb` and the suffix.
        assert_eq!(
            classify_honeycomb_raw_shape("Honeycomb BitmapFactory"),
            None,
        );
    }

    #[test]
    fn classify_honeycomb_with_slash_rejected() {
        // Internal JVM class FQN form uses `/`; not a pure identifier.
        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);
    }

    // ── Real key anchors (must classify so ingest re-promotes) ──────

    #[test]
    fn classify_classic_hex_anchor() {
        // 32 lowercase hex chars = classic Honeycomb API key.
        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() {
        // Honeycomb classic keys are lowercase hex specifically; uppercase
        // hex strings are likely test vectors / SHA digests in code,
        // not Honeycomb keys.
        let raw = "0123456789ABCDEF0123456789ABCDEF";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_classic_hex_short_rejected() {
        // 31 hex chars — too short.
        let raw = "0123456789abcdef0123456789abcde";
        assert_eq!(raw.len(), 31);
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_classic_hex_long_rejected() {
        // 33 hex chars — too long.
        let raw = "0123456789abcdef0123456789abcdef0";
        assert_eq!(raw.len(), 33);
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_ingest_key_anchor() {
        // Ingest key shape: `hc[abc]i[ack]_` + 28 alphanumeric.
        // Cover all 9 documented prefix combinations.
        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() {
        // `d` is not in `[abc]` — not a valid Honeycomb prefix.
        let raw = "hcdia_AbCdEfGhIjKlMnOpQrStUvWxYz01";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_ingest_key_with_wrong_class_letter_rejected() {
        // `d` is not in `[ack]` — not a valid auth-class letter.
        let raw = "hcaid_AbCdEfGhIjKlMnOpQrStUvWxYz01";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_ingest_key_with_dash_in_body_rejected() {
        // `-` is not in `[A-Za-z0-9]` — body must be pure alphanumeric.
        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() {
        // 25-char body — too short.
        let raw = "hcaik_AbCdEfGhIjKlMnOpQrStUvWxY";
        assert_eq!(raw.len(), 6 + 25);
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    // ── Class-name vs anchor disjointness ───────────────────────────

    #[test]
    fn class_name_and_anchor_are_disjoint_classic() {
        // Classic-hex anchor is pure lowercase; cannot match the
        // `Honeycomb`-Pascal-case prefix.
        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() {
        // Ingest key starts with `hc...` lowercase; cannot match the
        // `Honeycomb`-Pascal-case prefix. Conversely, class names contain
        // no `_<28-alphanum>$` shape with the specific prefix bytes.
        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() {
        // `HoneycombBitmapFactory` cannot match either anchor regex.
        let raw = "HoneycombBitmapFactory";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
        assert_eq!(
            classify_honeycomb_raw_shape(raw),
            Some(HoneycombFpShape::AndroidClass),
        );
    }

    // ── Tag stability ───────────────────────────────────────────────

    #[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");
    }

    // ── Defensive edge cases ────────────────────────────────────────

    #[test]
    fn unicode_raw_does_not_panic_shape() {
        // Multi-byte UTF-8 input. The shape gate's prefix check returns
        // false (literal `Honeycomb` not at start in this case), and
        // even if it did, the `bytes().all(...)` walk over multi-byte
        // sequences fails the all-identifier-chars filter.
        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() {
        // `Honeycomb!` — punctuation right after the prefix; falls
        // through to row-1a downgrade rather than being mis-classified
        // as a class name.
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb!"), None);
        // `Honeycomb_` — underscore right after the prefix; the next
        // byte is `_`, not ASCII uppercase, so the Pascal-case gate
        // rejects.
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb_Foo"), None);
    }
}