ferroni 2.7.2

Oniguruma regex in pure Rust, with a TextMate scanner, portable prepared Eager archives, and no C toolchain
Documentation
//! Public prepared Eager API, compatibility, fallback, and ownership contracts.
#![cfg(feature = "precompiled-eager")]

#[path = "support/precompiled_eager.rs"]
mod support;
use ferroni::prelude::*;
use support::*;

#[test]
fn inefficient_prepared_prefilter_retires_without_changing_results() {
    // Ferriki's inline HTML/SVG/template group (issue #465). Its negative
    // searches generate more portable states than the cache can reuse.
    let patterns = [
        r"(?i)(\s?\/\*\s?(html|svg|template|inline-html|inline-template)\s?\*\/\s?)(`)",
        r"(?i)(\s*(html|svg|template|inline-html|inline-template))(`)",
        r"(?i)(?<=\s|\,|\=|\:|\(|\$\()\s{0,}(((\/\*)|(\/\/))\s?(html|svg|template|inline-html|inline-template)[ ]{0,1000}\*?\/?)[ ]{0,1000}$",
        r"(\$\{)",
        r"(\$\(`)",
    ];
    let config = config().options(ferroni::oniguruma::ONIG_OPTION_CAPTURE_GROUP);
    let mut producer = ScannerPatternCache::new();
    Scanner::with_pattern_cache(&patterns, &config, &mut producer).unwrap();
    let pair = producer.export_precompiled_eager().unwrap();
    let mut cache = ScannerPatternCache::new();
    cache.add_precompiled_eager(pair.clone());
    let mut prepared = Scanner::with_pattern_cache(&patterns, &config, &mut cache).unwrap();
    let mut plain = Scanner::with_config(
        &patterns,
        &config.clone().acceleration(AccelerationMode::Off),
    )
    .unwrap();
    assert_eq!(pair.stats().loaded_groups, 1);
    assert!(prepared.prefilter_stats().built);
    let subjects = [
        OnigString::new("export interface LanguageRegistration {\n"),
        OnigString::new(
            "  /** Automatic regex prefiltering. Disable to avoid construction for one-shot use. */\n",
        ),
        OnigString::new(
            "export declare function getSingletonHighlighter(options?: HighlighterOptions): Promise<Highlighter>;\n",
        ),
        OnigString::new(
            "  /** Includes TextMate scopes or token types in HTML transformer callback tokens. */\n",
        ),
        OnigString::new("export interface HtmlWithCss {\n"),
        OnigString::new("  html: string;\n"),
        OnigString::new("  htmlAttrs?: Readonly<Record<string, string>>;\n"),
        OnigString::new("  htmlStyle?: Readonly<Record<string, string>>;\n"),
        OnigString::new(
            "  /** Callback-only metadata from `HighlightOptions.data`; omitted from HTML. */\n",
        ),
        OnigString::new("  /** Unavailable during span/line hooks and for inline structure. */\n"),
    ];
    // Separate identities mirror the standalone replay: subject memoization
    // must not hide repeated transition generation.
    let queries: Vec<_> = subjects
        .iter()
        .flat_map(|subject| {
            (0..=subject.utf16_len()).map(|start| (OnigString::new(subject.content()), start))
        })
        .collect();
    for _ in 0..32 {
        for (subject, start) in &queries {
            assert_eq!(
                prepared.find_next_match_utf16(subject, *start, ScannerFindOptions::NONE),
                plain.find_next_match_utf16(subject, *start, ScannerFindOptions::NONE)
            );
        }
        if prepared.prefilter_stats().retired {
            break;
        }
    }
    assert!(
        prepared.prefilter_stats().retired,
        "{:?}",
        prepared.prefilter_stats()
    );
    assert!(!prepared.prefilter_stats().built);
    // Retirement is permanent, and subsequent positive searches still return
    // the same pattern, UTF-16 offsets, and captures as the ordinary scanner.
    let positive = OnigString::new("é💻 html`<div>${value}</div>`");
    for start in 0..=positive.utf16_len() {
        assert_eq!(
            prepared.find_next_match_utf16(&positive, start, ScannerFindOptions::NONE),
            plain.find_next_match_utf16(&positive, start, ScannerFindOptions::NONE)
        );
    }
    assert!(prepared.prefilter_stats().retired);
}

#[test]
fn frozen_native_pair_preserves_results_and_wire_format() {
    check_fixture();
    assert_eq!(
        &GROUPS[..8],
        format!("FPRT{PRECOMPILED_EAGER_FORMAT_VERSION:03}\0").as_bytes()
    );
    fn send_sync<T: Send + Sync>() {}
    send_sync::<PrecompiledEager>();
    let mut producer = ScannerPatternCache::new();
    for patterns in SETS.iter().rev() {
        Scanner::with_pattern_cache(patterns, &config(), &mut producer).unwrap();
    }
    let exported = producer.export_precompiled_eager().unwrap();
    assert_eq!(exported.program_bytes(), PROGRAMS);
    assert_eq!(exported.group_bytes(), GROUPS);
}

#[test]
fn missing_groups_and_different_settings_compile_normally() {
    let source = fixture();
    let mut cache = ScannerPatternCache::new();
    cache.add_precompiled_eager(source.clone());
    compare(&["missing", "[0-9]+extra"], &config(), &mut cache);
    let changed = config().options(ferroni::oniguruma::ONIG_OPTION_IGNORECASE);
    compare(SETS[0], &changed, &mut cache);
    for mode in [AccelerationMode::Auto, AccelerationMode::Off] {
        compare(SETS[0], &config().acceleration(mode), &mut cache);
    }
    assert_eq!(source.stats(), PrecompiledEagerStats::default());
}

#[test]
fn clones_share_observations_and_clearing_sources_preserves_live_scanners() {
    let source = fixture();
    let mut cache = ScannerPatternCache::new();
    cache.add_precompiled_eager(source.clone());
    let mut scanner = Scanner::with_pattern_cache(SETS[0], &config(), &mut cache).unwrap();
    assert_eq!(source.stats().loaded_groups, 1);
    cache.clear();
    compare(SETS[0], &config(), &mut cache);
    assert_eq!(source.stats().loaded_groups, 2);
    cache.clear_precompiled();
    cache.clear();
    compare(SETS[0], &config(), &mut cache);
    assert_eq!(source.stats().loaded_groups, 2);
    assert_eq!(
        scanner
            .find_next_match("42", 0, ScannerFindOptions::NONE)
            .unwrap()
            .index,
        1
    );
    let mut other = ScannerPatternCache::new();
    other.add_precompiled_eager(source.clone());
    compare(SETS[1], &config(), &mut other);
    assert_eq!(source.stats().loaded_groups, 3);
}

#[test]
fn mismatched_pairs_versions_and_bounded_indexes_are_rejected() {
    let mut other = ScannerPatternCache::new();
    Scanner::with_pattern_cache(&["different"], &config(), &mut other).unwrap();
    assert!(PrecompiledEager::from_bytes(other.export_precompiled().unwrap(), GROUPS).is_err());
    for end in [0, 8, GROUPS.len() / 2, GROUPS.len() - 1] {
        assert!(PrecompiledEager::from_bytes(PROGRAMS, &GROUPS[..end]).is_err());
    }
    let mut unknown = GROUPS.to_vec();
    unknown[..8].copy_from_slice(b"FPRT999\0");
    assert!(matches!(
        PrecompiledEager::from_bytes(PROGRAMS, unknown),
        Err(PrecompiledError::UnsupportedFormat)
    ));
    let mut oversized = GROUPS.to_vec();
    oversized[13..17].copy_from_slice(&u32::MAX.to_le_bytes());
    assert!(matches!(
        PrecompiledEager::from_bytes(PROGRAMS, oversized),
        Err(PrecompiledError::ResourceLimit)
    ));
}

#[test]
fn damaged_records_fail_preflight_and_fall_back_during_loading() {
    let mut damaged = GROUPS.to_vec();
    *damaged.last_mut().unwrap() ^= 0xff;
    let source = PrecompiledEager::from_bytes(PROGRAMS, damaged).unwrap();
    assert!(source.validate_groups().is_err());
    assert_eq!(source.stats(), PrecompiledEagerStats::default());
    let mut cache = ScannerPatternCache::new();
    cache.add_precompiled_eager(source.clone());
    for patterns in SETS {
        compare(patterns, &config(), &mut cache);
    }
    assert_eq!(source.stats().invalid_groups, 1);
    assert_eq!(source.stats().loaded_groups, SETS.len() - 1);
}

#[test]
fn empty_archives_are_valid_optional_sources() {
    let source = ScannerPatternCache::new()
        .export_precompiled_eager()
        .unwrap();
    assert!(source.is_empty());
    source.validate_groups().unwrap();
    let mut cache = ScannerPatternCache::new();
    cache.add_precompiled_eager(source.clone());
    compare(SETS[0], &config(), &mut cache);
    assert_eq!(source.stats(), PrecompiledEagerStats::default());
}

#[path = "support/eager_archive_mutations.rs"]
mod mutations;

#[test]
fn valid_checksums_do_not_bypass_instruction_validation() {
    use mutations::{repair_checksums, word};
    let mut damaged = GROUPS.to_vec();
    // Find a compact group and corrupt its first program's anchored entry.
    let mut at = 17;
    let mut changed = false;
    for _ in 0..word(&damaged, 13).unwrap() {
        let checksum_at = at + 4 + 4 * word(&damaged, at).unwrap();
        let start = checksum_at + 8;
        let end = start + word(&damaged, checksum_at + 4).unwrap();
        if damaged[start] == 6 {
            let mut program = start + 1;
            for _ in 0..3 {
                program += 4 + 4 * word(&damaged, program).unwrap();
            }
            program += 4; // Settle limit, then the program representation tag.
            assert_eq!(damaged[program], 0);
            damaged[program + 1..program + 5].copy_from_slice(&u32::MAX.to_le_bytes());
            changed = true;
            break;
        }
        at = end;
    }
    assert!(changed);
    repair_checksums(&mut damaged);
    let source = PrecompiledEager::from_bytes(PROGRAMS, damaged).unwrap();
    assert!(source.validate_groups().is_err());
    let mut cache = ScannerPatternCache::new();
    cache.add_precompiled_eager(source.clone());
    for patterns in SETS {
        compare(patterns, &config(), &mut cache);
    }
    assert_eq!(source.stats().invalid_groups, 1);
}

#[test]
fn shared_container_views_preserve_ownership_validation_and_results() {
    use ferroni::scanner::PrecompiledBytes;
    use std::sync::Arc;
    let mut bytes = b"host prefix".to_vec();
    let program_start = bytes.len();
    bytes.extend_from_slice(PROGRAMS);
    let group_start = bytes.len();
    bytes.extend_from_slice(GROUPS);
    let group_end = bytes.len();
    bytes.extend_from_slice(b"host suffix");
    let owner = Arc::new(bytes);
    let weak = Arc::downgrade(&owner);
    let image = PrecompiledBytes::from_shared_owner(Arc::clone(&owner));
    let programs = image.slice(program_start..group_start).unwrap();
    let groups = image.slice(group_start..group_end).unwrap();
    let pair = PrecompiledEager::from_shared_bytes(programs.clone(), groups.clone()).unwrap();
    assert_eq!(
        pair.program_bytes().as_ptr(),
        owner[program_start..].as_ptr()
    );
    assert_eq!(pair.group_bytes().as_ptr(), owner[group_start..].as_ptr());
    assert_eq!(pair.program_bytes(), PROGRAMS);
    assert_eq!(pair.group_bytes(), GROUPS);
    pair.validate_groups().unwrap();
    assert!(PrecompiledEager::from_shared_bytes(groups, programs).is_err());
    drop(image);
    drop(owner);
    let mut cache = ScannerPatternCache::new();
    cache.add_precompiled_eager(pair.clone());
    for patterns in SETS {
        compare(patterns, &config(), &mut cache);
    }
    assert!(pair.stats().loaded_groups > 0);
    assert_eq!(pair.stats().invalid_groups, 0);
    assert!(weak.upgrade().is_some());
    drop(pair);
    cache.clear_precompiled();
    assert!(weak.upgrade().is_none());
}