Skip to main content

keyhog_scanner/decode/pipeline/
registry.rs

1use crate::decode::base64::{Base64Decoder, Z85Decoder};
2use crate::decode::caesar::CaesarDecoder;
3use crate::decode::hex::HexDecoder;
4#[cfg(feature = "decode")]
5use crate::decode::javascript_static::JavaScriptStaticDecoder;
6use crate::decode::json::JsonDecoder;
7use crate::decode::reverse::ReverseDecoder;
8use crate::decode::url::{
9    HtmlNamedEntityDecoder, HtmlNumericEntityDecoder, MimeEncodedWordDecoder, OctalEscapeDecoder,
10    QuotedPrintableDecoder, UnicodeEscapeDecoder, UrlDecoder,
11};
12#[cfg(feature = "decode")]
13use crate::decode::DecodeAdmission;
14#[cfg(any(feature = "decode", test))]
15use crate::decode::DecodeAdmissionSketch;
16use crate::decode::Decoder;
17use aho_corasick::AhoCorasick;
18use parking_lot::RwLock;
19#[cfg(test)]
20use std::cell::RefCell;
21use std::sync::Arc;
22
23// The active decoder set is stored behind one shared `Arc<Vec<..>>`. Scanner
24// construction captures this Arc in its immutable execution plan. Standalone
25// compatibility helpers clone one Arc per call. Registration is copy-on-write,
26// so existing scanners and in-flight compatibility calls keep their snapshot.
27static DECODERS: std::sync::OnceLock<RwLock<DecoderRegistryState>> = std::sync::OnceLock::new();
28
29struct DecoderRegistryState {
30    decoders: Arc<Vec<RegisteredDecoder>>,
31    compatibility_failure: Option<DecoderRegistrationError>,
32}
33
34#[derive(Clone)]
35pub(crate) enum RegisteredDecoder {
36    Shared(Arc<dyn Decoder>),
37    Reverse,
38    Caesar,
39}
40
41impl RegisteredDecoder {
42    pub(crate) fn name(&self) -> &'static str {
43        match self {
44            Self::Shared(decoder) => decoder.name(),
45            Self::Reverse => "reverse",
46            Self::Caesar => "caesar",
47        }
48    }
49
50    fn version(&self) -> &'static str {
51        match self {
52            Self::Shared(decoder) => decoder.version(),
53            Self::Reverse | Self::Caesar => "detector-policy-1",
54        }
55    }
56
57    #[cfg(any(feature = "decode", test))]
58    fn admission(
59        &self,
60        chunk: &keyhog_core::Chunk,
61        policy: &super::super::policy::CompiledDecodeTransformPolicy,
62    ) -> DecodeAdmission {
63        match self {
64            Self::Shared(decoder) => decoder.admission(chunk),
65            Self::Reverse => ReverseDecoder
66                .admission_sketch_with_policy(chunk, policy)
67                .admission(),
68            Self::Caesar => CaesarDecoder
69                .admission_sketch_with_policy(chunk, policy)
70                .admission(),
71        }
72    }
73
74    #[cfg(any(feature = "decode", test))]
75    fn admission_sketch(
76        &self,
77        chunk: &keyhog_core::Chunk,
78        policy: &super::super::policy::CompiledDecodeTransformPolicy,
79    ) -> DecodeAdmissionSketch {
80        match self {
81            Self::Shared(decoder) => decoder.admission_sketch(chunk),
82            Self::Reverse => ReverseDecoder.admission_sketch_with_policy(chunk, policy),
83            Self::Caesar => CaesarDecoder.admission_sketch_with_policy(chunk, policy),
84        }
85    }
86
87    pub(super) fn decode_chunk_into(
88        &self,
89        chunk: &keyhog_core::Chunk,
90        policy: &super::super::policy::CompiledDecodeTransformPolicy,
91        sink: &mut dyn crate::decode::DecodeOutputSink,
92    ) {
93        match self {
94            Self::Shared(decoder) => decoder.decode_chunk_into(chunk, sink),
95            Self::Reverse => ReverseDecoder.decode_chunk_with_policy_into(chunk, policy, sink),
96            Self::Caesar => CaesarDecoder.decode_chunk_with_policy_into(chunk, policy, sink),
97        }
98    }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
102pub enum DecoderRegistrationError {
103    #[error("decoder name must be non-empty ASCII without whitespace")]
104    InvalidName,
105    #[error("decoder {name:?} version must be non-empty ASCII without whitespace")]
106    InvalidVersion { name: &'static str },
107    #[error("decoder name {0:?} is already registered")]
108    DuplicateName(&'static str),
109    #[error("could not compile the all-decoder trigger automaton: {0}")]
110    TriggerBuild(String),
111}
112
113#[derive(Clone)]
114pub(crate) struct CompiledDecoderPlan {
115    decoders: Arc<Vec<RegisteredDecoder>>,
116    all_decoder_trigger: Option<AhoCorasick>,
117    identity: u64,
118}
119
120impl std::fmt::Debug for CompiledDecoderPlan {
121    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        formatter
123            .debug_struct("CompiledDecoderPlan")
124            .field("decoder_count", &self.decoders.len())
125            .field(
126                "has_all_decoder_trigger",
127                &self.all_decoder_trigger.is_some(),
128            )
129            .field("identity", &self.identity)
130            .finish()
131    }
132}
133
134impl CompiledDecoderPlan {
135    pub(crate) fn snapshot() -> Result<Self, DecoderRegistrationError> {
136        let decoders = snapshot_decoders()?;
137        let mut names = std::collections::HashSet::with_capacity(decoders.len());
138        let mut hasher = blake3::Hasher::new();
139        hasher.update(b"keyhog-compiled-decoder-plan-v1\0");
140        for decoder in decoders.iter() {
141            let name = decoder.name();
142            let version = decoder.version();
143            validate_descriptor(name, version)?;
144            if !names.insert(name) {
145                return Err(DecoderRegistrationError::DuplicateName(name));
146            }
147            hash_descriptor(&mut hasher, name, version);
148        }
149        let mut bytes = [0u8; 8];
150        bytes.copy_from_slice(&hasher.finalize().as_bytes()[..8]);
151        let all_decoder_trigger = compile_all_decoder_trigger(&decoders)?;
152        Ok(Self {
153            decoders,
154            all_decoder_trigger,
155            identity: u64::from_le_bytes(bytes),
156        })
157    }
158
159    pub(crate) fn identity(&self) -> u64 {
160        self.identity
161    }
162
163    #[cfg(any(feature = "decode", test))]
164    pub(crate) fn decoders(&self) -> &[RegisteredDecoder] {
165        &self.decoders
166    }
167
168    pub(crate) fn all_decoder_may_match(&self, data: &str) -> bool {
169        self.all_decoder_trigger
170            .as_ref()
171            .is_none_or(|trigger| trigger.is_match(data.as_bytes()))
172    }
173}
174
175fn validate_descriptor(
176    name: &'static str,
177    version: &'static str,
178) -> Result<(), DecoderRegistrationError> {
179    if name.is_empty() || !name.is_ascii() || name.bytes().any(|byte| byte.is_ascii_whitespace()) {
180        return Err(DecoderRegistrationError::InvalidName);
181    }
182    if version.is_empty()
183        || !version.is_ascii()
184        || version.bytes().any(|byte| byte.is_ascii_whitespace())
185    {
186        return Err(DecoderRegistrationError::InvalidVersion { name });
187    }
188    Ok(())
189}
190
191fn hash_descriptor(hasher: &mut blake3::Hasher, name: &str, version: &str) {
192    for value in [name.as_bytes(), version.as_bytes()] {
193        hasher.update(&(value.len() as u64).to_le_bytes());
194        hasher.update(value);
195    }
196}
197
198fn is_default_decoder_name(name: &str) -> bool {
199    matches!(
200        name,
201        "base64"
202            | "hex"
203            | "url"
204            | "quoted-printable"
205            | "html-named-entity"
206            | "html-numeric-entity"
207            | "octal-escape"
208            | "mime-encoded-word"
209            | "json"
210            | "unicode-escape"
211            | "z85"
212            | "javascript-static"
213            | "reverse"
214            | "caesar"
215    )
216}
217
218fn compile_all_decoder_trigger(
219    decoders: &[RegisteredDecoder],
220) -> Result<Option<AhoCorasick>, DecoderRegistrationError> {
221    if decoders
222        .iter()
223        .any(|decoder| !is_default_decoder_name(decoder.name()))
224    {
225        return Ok(None);
226    }
227    let patterns = (b'!'..=b'~').map(|byte| [byte]);
228    AhoCorasick::builder()
229        .kind(Some(aho_corasick::AhoCorasickKind::ContiguousNFA))
230        .build(patterns)
231        .map(Some)
232        .map_err(|error| DecoderRegistrationError::TriggerBuild(error.to_string()))
233}
234
235#[cfg(test)]
236thread_local! {
237    static THREAD_DECODERS: RefCell<Vec<Arc<dyn Decoder>>> = RefCell::new(Vec::new());
238}
239
240/// Per-decoder wall-time profiler (measurement only). Enabled by the single
241/// scanner profiler switch (`keyhog scan --profile`) so profiling has one runtime
242/// owner instead of one env knob per pass. Records which decoder dominates
243/// decode generation. Zero-cost unset.
244pub(super) fn profile_enabled() -> bool {
245    crate::scan_profile::enabled()
246}
247
248/// Fixed number of per-decoder profiler slots. The `DECODER_NS` / `DECODER_PRODUCED`
249/// accumulators and every index/clamp into them share this one capacity. There
250/// are 14 default decoders today, so the cap carries headroom; a decoder past
251/// slot `MAX_PROFILED_DECODERS` is simply not profiled (`record_decoder_run`
252/// drops it), the `decoder_registry_within_profiler_capacity` gap test guards
253/// the default set against silently outgrowing this.
254const MAX_PROFILED_DECODERS: usize = 16;
255
256static DECODER_NS: [std::sync::atomic::AtomicU64; MAX_PROFILED_DECODERS] = {
257    const Z: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
258    [Z; MAX_PROFILED_DECODERS]
259};
260
261/// Sub-chunks EMITTED per decoder (pre-dedup/screen). The sub-chunk COUNT - not
262/// gen time - is what drives the dominant decode-rescan + per-sub-chunk fixed
263/// phase-1 cost, so this isolates which decoders to gate with a sound prune.
264static DECODER_PRODUCED: [std::sync::atomic::AtomicU64; MAX_PROFILED_DECODERS] = {
265    const Z: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
266    [Z; MAX_PROFILED_DECODERS]
267};
268
269pub(super) fn record_decoder_run(
270    decoder_index: usize,
271    elapsed: std::time::Duration,
272    produced: usize,
273) {
274    if decoder_index >= MAX_PROFILED_DECODERS {
275        return;
276    }
277    use std::sync::atomic::Ordering::Relaxed;
278    DECODER_NS[decoder_index].fetch_add(elapsed.as_nanos() as u64, Relaxed);
279    DECODER_PRODUCED[decoder_index].fetch_add(produced as u64, Relaxed);
280}
281
282/// Print and reset the accumulated per-decoder times (paired with registry
283/// names). Folded into the unified scanner profile dump.
284pub(crate) fn decoder_profile_dump() {
285    use std::sync::atomic::Ordering::Relaxed;
286    let decoders = active_decoders();
287    let names: Vec<&str> = decoders.iter().map(|d| d.name()).collect();
288    let mut rows: Vec<(String, f64)> = (0..names.len().min(MAX_PROFILED_DECODERS))
289        .map(|i| {
290            (
291                names[i].to_string(),
292                DECODER_NS[i].swap(0, Relaxed) as f64 / 1e6,
293            )
294        })
295        .collect();
296    rows.sort_by(|a, b| b.1.total_cmp(&a.1));
297    let total: f64 = rows.iter().map(|r| r.1).sum();
298    let mut prod: Vec<(String, u64)> = (0..names.len().min(MAX_PROFILED_DECODERS))
299        .map(|i| (names[i].to_string(), DECODER_PRODUCED[i].swap(0, Relaxed)))
300        .collect();
301    prod.sort_by(|a, b| b.1.cmp(&a.1));
302    let prod_total: u64 = prod.iter().map(|r| r.1).sum();
303    if total == 0.0 && prod_total == 0 {
304        return;
305    }
306    eprintln!("=== per-decoder decode_chunk time ===");
307    for (name, ms) in &rows {
308        let pct = if total > 0.0 { 100.0 * ms / total } else { 0.0 };
309        eprintln!("  {name:<18}: {ms:>8.1} ms ({pct:>5.1}%)");
310    }
311    eprintln!("  {:<18}: {total:>8.1} ms", "TOTAL");
312    eprintln!("=== per-decoder sub-chunks EMITTED (pre-dedup/screen) ===");
313    for (name, n) in &prod {
314        let pct = if prod_total > 0 {
315            100.0 * *n as f64 / prod_total as f64
316        } else {
317            0.0
318        };
319        eprintln!("  {name:<18}: {n:>8} ({pct:>5.1}%)");
320    }
321    eprintln!("  {:<18}: {prod_total:>8}", "TOTAL");
322}
323
324pub(crate) fn decoder_profile_reset() {
325    use std::sync::atomic::Ordering::Relaxed;
326    for slot in &DECODER_NS {
327        slot.store(0, Relaxed);
328    }
329    for slot in &DECODER_PRODUCED {
330        slot.store(0, Relaxed);
331    }
332}
333
334fn default_decoders() -> Vec<RegisteredDecoder> {
335    vec![
336        RegisteredDecoder::Shared(Arc::new(Base64Decoder)),
337        RegisteredDecoder::Shared(Arc::new(HexDecoder)),
338        RegisteredDecoder::Shared(Arc::new(UrlDecoder)),
339        RegisteredDecoder::Shared(Arc::new(QuotedPrintableDecoder)),
340        RegisteredDecoder::Shared(Arc::new(HtmlNamedEntityDecoder)),
341        RegisteredDecoder::Shared(Arc::new(HtmlNumericEntityDecoder)),
342        RegisteredDecoder::Shared(Arc::new(OctalEscapeDecoder)),
343        RegisteredDecoder::Shared(Arc::new(MimeEncodedWordDecoder)),
344        // JSON unescape - strips `\"` / `\\` / `\n` style escapes inside JSON
345        // string values so credentials stored as JSON-encoded fields survive
346        // into the scanner.
347        RegisteredDecoder::Shared(Arc::new(JsonDecoder)),
348        RegisteredDecoder::Shared(Arc::new(UnicodeEscapeDecoder)),
349        RegisteredDecoder::Shared(Arc::new(Z85Decoder)),
350        // Bounded, side-effect-free JavaScript constant recovery. Keep it after
351        // representation decoders and before the asymmetric evasion decoders.
352        #[cfg(feature = "decode")]
353        RegisteredDecoder::Shared(Arc::new(JavaScriptStaticDecoder)),
354        RegisteredDecoder::Reverse,
355        RegisteredDecoder::Caesar,
356    ]
357}
358
359/// The `name()` of each default decoder, in registration order. This is the
360/// canonical decode-pipeline composition, the order is load-bearing (the
361/// `reverse` and `caesar` decoders deliberately run last, after the structural
362/// decoders), and is pinned by `decoder_registry_default_order` so a reorder
363/// or addition can't silently shift the pipeline.
364#[cfg(feature = "decode")]
365pub(crate) fn default_decoder_names() -> Vec<&'static str> {
366    default_decoders().iter().map(|d| d.name()).collect()
367}
368
369/// Aggregate decoder-owned admission proofs for one root chunk.
370///
371/// Candidate extraction is primed once so built-in predicates that use the
372/// shared extractor do not each allocate and rescan independently. Any custom
373/// decoder that keeps the trait default returns `Unknown`, which is preserved
374/// unless another decoder already proves the chunk is `Possible`.
375#[cfg(feature = "decode")]
376pub(crate) fn decoder_admission(
377    chunk: &keyhog_core::Chunk,
378    policy: &super::super::policy::CompiledDecodeTransformPolicy,
379    plan: &CompiledDecoderPlan,
380) -> DecodeAdmission {
381    super::extractor::clear_shared_candidates();
382    super::extractor::prime_shared_candidates(&chunk.data);
383
384    let mut aggregate = DecodeAdmission::Impossible;
385    for decoder in plan.decoders() {
386        match decoder.admission(chunk, policy) {
387            DecodeAdmission::Possible => {
388                aggregate = DecodeAdmission::Possible;
389                break;
390            }
391            DecodeAdmission::Unknown => aggregate = DecodeAdmission::Unknown,
392            DecodeAdmission::Impossible => {}
393        }
394    }
395
396    super::extractor::clear_shared_candidates();
397    aggregate
398}
399
400#[cfg(any(feature = "decode", test))]
401pub(crate) fn decoder_admission_sketch(
402    chunk: &keyhog_core::Chunk,
403    policy: &super::super::policy::CompiledDecodeTransformPolicy,
404    plan: &CompiledDecoderPlan,
405) -> DecodeAdmissionSketch {
406    decoder_admission_sketch_with_decoders(chunk, policy, plan.decoders())
407}
408
409#[cfg(any(feature = "decode", test))]
410pub(crate) fn active_decoder_admission_sketch(
411    chunk: &keyhog_core::Chunk,
412    policy: &super::super::policy::CompiledDecodeTransformPolicy,
413) -> DecodeAdmissionSketch {
414    let decoders = active_decoders();
415    decoder_admission_sketch_with_decoders(chunk, policy, &decoders)
416}
417
418#[cfg(any(feature = "decode", test))]
419fn decoder_admission_sketch_with_decoders(
420    chunk: &keyhog_core::Chunk,
421    policy: &super::super::policy::CompiledDecodeTransformPolicy,
422    decoders: &[RegisteredDecoder],
423) -> DecodeAdmissionSketch {
424    super::extractor::clear_shared_candidates();
425    super::extractor::prime_shared_candidates(&chunk.data);
426
427    let mut aggregate = DecodeAdmissionSketch::NONE;
428    for decoder in decoders {
429        aggregate.merge(decoder.admission_sketch(chunk, policy));
430    }
431
432    super::extractor::clear_shared_candidates();
433    aggregate
434}
435
436fn decoder_registry() -> &'static RwLock<DecoderRegistryState> {
437    DECODERS.get_or_init(|| {
438        RwLock::new(DecoderRegistryState {
439            decoders: Arc::new(default_decoders()),
440            compatibility_failure: None,
441        })
442    })
443}
444
445#[cfg(not(test))]
446pub(super) fn active_decoders() -> Arc<Vec<RegisteredDecoder>> {
447    // One `Arc` clone (a single atomic increment) instead of deep-cloning the
448    // decoder Vec on every `decode_chunk`. Callers only iterate, so the shared
449    // snapshot suffices.
450    Arc::clone(&decoder_registry().read().decoders)
451}
452
453#[cfg(test)]
454pub(super) fn active_decoders() -> Arc<Vec<RegisteredDecoder>> {
455    let base = Arc::clone(&decoder_registry().read().decoders);
456    THREAD_DECODERS.with(|thread_decoders| {
457        let thread = thread_decoders.borrow();
458        if thread.is_empty() {
459            // Common case: no per-test decoder registered, hand back the shared
460            // snapshot with no allocation, matching the non-test fast path.
461            base
462        } else {
463            let mut combined = (*base).clone();
464            combined.extend(thread.iter().cloned().map(RegisteredDecoder::Shared));
465            Arc::new(combined)
466        }
467    })
468}
469
470#[cfg(not(test))]
471fn snapshot_decoders() -> Result<Arc<Vec<RegisteredDecoder>>, DecoderRegistrationError> {
472    let registry = decoder_registry().read();
473    if let Some(error) = registry.compatibility_failure.clone() {
474        Err(error)
475    } else {
476        Ok(Arc::clone(&registry.decoders))
477    }
478}
479
480#[cfg(test)]
481fn snapshot_decoders() -> Result<Arc<Vec<RegisteredDecoder>>, DecoderRegistrationError> {
482    let base = {
483        let registry = decoder_registry().read();
484        if let Some(error) = registry.compatibility_failure.clone() {
485            return Err(error);
486        }
487        Arc::clone(&registry.decoders)
488    };
489    THREAD_DECODERS.with(|thread_decoders| {
490        let thread = thread_decoders.borrow();
491        if thread.is_empty() {
492            Ok(base)
493        } else {
494            let mut combined = (*base).clone();
495            combined.extend(thread.iter().cloned().map(RegisteredDecoder::Shared));
496            Ok(Arc::new(combined))
497        }
498    })
499}
500
501/// Register a custom decoder for scanners compiled afterward.
502///
503/// Use [`try_register_decoder`] when the caller can handle a registration
504/// error. This compatibility entry point records any error, and later scanner
505/// compilation returns it. Existing compiled scanners retain their immutable
506/// decoder plan.
507pub fn register_decoder(decoder: Box<dyn Decoder>) {
508    if let Err(error) = register_decoder_inner(decoder, true) {
509        tracing::error!(%error, "decoder registration failed; later scanner compilation will fail");
510    }
511}
512
513/// Register a custom decoder and return descriptor or collision errors.
514///
515/// The name and version must be non-empty ASCII without whitespace. A name can
516/// be registered only once. Existing compiled scanners retain their immutable
517/// decoder plan.
518pub fn try_register_decoder(decoder: Box<dyn Decoder>) -> Result<(), DecoderRegistrationError> {
519    register_decoder_inner(decoder, false)
520}
521
522fn register_decoder_inner(
523    decoder: Box<dyn Decoder>,
524    record_failure: bool,
525) -> Result<(), DecoderRegistrationError> {
526    let decoder_name = decoder.name();
527    let mut guard = decoder_registry().write();
528    if let Some(error) = guard.compatibility_failure.clone() {
529        return Err(error);
530    }
531    let result = validate_descriptor(decoder_name, decoder.version()).and_then(|()| {
532        if guard
533            .decoders
534            .iter()
535            .any(|existing| existing.name() == decoder_name)
536        {
537            Err(DecoderRegistrationError::DuplicateName(decoder_name))
538        } else {
539            Ok(())
540        }
541    });
542    if let Err(error) = result {
543        if record_failure && guard.compatibility_failure.is_none() {
544            guard.compatibility_failure = Some(error.clone());
545        }
546        return Err(error);
547    }
548    // Copy-on-write: publish a fresh snapshot so any `active_decoders()` Arc
549    // already handed out keeps its consistent view. Registration happens at
550    // startup / test setup, never on the decode hot path, so this one-time Vec
551    // clone is not a concern.
552    let mut next = (*guard.decoders).clone();
553    next.push(RegisteredDecoder::Shared(Arc::from(decoder)));
554    guard.decoders = Arc::new(next);
555    Ok(())
556}
557
558#[cfg(test)]
559pub(crate) struct ScopedDecoderRegistration {
560    name: &'static str,
561    active: bool,
562}
563
564#[cfg(test)]
565impl Drop for ScopedDecoderRegistration {
566    fn drop(&mut self) {
567        if !self.active {
568            return;
569        }
570        THREAD_DECODERS.with(|thread_decoders| {
571            let mut decoders = thread_decoders.borrow_mut();
572            if let Some(index) = decoders
573                .iter()
574                .rposition(|decoder| decoder.name() == self.name)
575            {
576                decoders.remove(index);
577            }
578        });
579    }
580}
581
582#[cfg(test)]
583pub(crate) fn register_thread_decoder(decoder: Box<dyn Decoder>) -> ScopedDecoderRegistration {
584    let decoder_name = decoder.name();
585    let has_global_duplicate = decoder_registry()
586        .read()
587        .decoders
588        .iter()
589        .any(|existing| existing.name() == decoder_name);
590    if has_global_duplicate {
591        tracing::warn!(
592            decoder = decoder_name,
593            "register_thread_decoder called with a duplicate global decoder name; decoder ignored"
594        );
595        return ScopedDecoderRegistration {
596            name: decoder_name,
597            active: false,
598        };
599    }
600
601    let mut inserted = false;
602    let decoder = Arc::from(decoder);
603    THREAD_DECODERS.with(|thread_decoders| {
604        let mut decoders = thread_decoders.borrow_mut();
605        if decoders.iter().any(|existing| existing.name() == decoder_name) {
606            tracing::warn!(
607                decoder = decoder_name,
608                "register_thread_decoder called with a duplicate thread decoder name; decoder ignored"
609            );
610            return;
611        }
612        decoders.push(decoder);
613        inserted = true;
614    });
615    ScopedDecoderRegistration {
616        name: decoder_name,
617        active: inserted,
618    }
619}
620
621#[cfg(test)]
622#[path = "../../../tests/unit/decode_admission_soundness.rs"]
623mod admission_soundness_tests;