Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/third_party/rust/mapped_hyph/src/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 26 kB image not shown  

Quelle  lib.rs   Sprache: unbekannt

 
// Copyright 2019 Mozilla Foundation. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

#[macro_use]
extern crate arrayref;
extern crate memmap2;
#[macro_use]
extern crate log;

use std::slice;
use std::str;
use std::cmp::max;
use std::fs::File;
use std::mem;

use memmap2::Mmap;

// Make submodules available publicly.
pub mod builder;
pub mod ffi;

// 4-byte identification expected at beginning of a compiled dictionary file.
// (This will be updated if an incompatible change to the format is made in
// some future revision.)
const MAGIC_NUMBER: [u8; 4] = [b'H', b'y', b'f', b'0'];

const INVALID_STRING_OFFSET: u16 = 0xffff;
const INVALID_STATE_OFFSET: u32 = 0x00ff_ffff;

const FILE_HEADER_SIZE: usize = 8; // 4-byte magic number, 4-byte count of levels
const LEVEL_HEADER_SIZE: usize = 16;

// Transition actually holds a 24-bit new state offset and an 8-bit input byte
// to match. We will be interpreting byte ranges as Transition arrays (in the
// State::transitions() method below), so use repr(C) to ensure we have the
// memory layout we expect.
// Transition records do not depend on any specific alignment.
#[repr(C)]
#[derive(Debug,Copy,Clone)]
struct Transition(u8, u8, u8, u8);

impl Transition {
    fn new_state_offset(&self) -> usize {
        // Read a 24-bit little-endian number from three bytes.
        self.0 as usize + ((self.1 as usize) << 8) + ((self.2 as usize) << 16)
    }
    fn match_byte(&self) -> u8 {
        self.3
    }
}

// State is an area of the Level's data block that begins with a fixed header,
// followed by an array of transitions. The total size of each State's data
// depends on the number of transitions in the state. Only the basic header
// is defined by the struct here; the rest of the state is accessed via
// pointer magic.
// There are two versions of State, a basic version that supports only simple
// hyphenation (no associated spelling change), and an extended version that
// adds the replacement-string fields to support spelling changes at the
// hyphenation point. Check is_extended() to know which version is present.
// State records are NOT necessarily 4-byte aligned, so multi-byte fields
// should be read with care.
#[derive(Debug,Copy,Clone)]
#[repr(C)]
struct State {
    fallback_state: [u8; 4],
    match_string_offset: [u8; 2],
    num_transitions: u8,
    is_extended: u8,
}

#[repr(C)]
struct StateExtended {
    state: State,
    repl_string_offset: [u8; 2],
    repl_index: i8,
    repl_cut: i8,
}

impl State {
    // Accessors for the various State header fields; see file format description.
    fn fallback_state(&self) -> usize {
        u32::from_le_bytes(self.fallback_state) as usize
    }
    fn match_string_offset(&self) -> usize {
        u16::from_le_bytes(self.match_string_offset) as usize
    }
    fn num_transitions(&self) -> u8 {
        self.num_transitions
    }
    fn is_extended(&self) -> bool {
        self.is_extended != 0
    }
    // Accessors that are only valid if is_extended() is true.
    // These use `unsafe` to dereference a pointer to the relevant field;
    // this is OK because Level::get_state always validates the total state size
    // before returning a state reference, so these pointers will be valid for
    // any extended state it returns.
    #[allow(dead_code)]
    fn as_extended(&self) -> &StateExtended {
        debug_assert!(self.is_extended());
        unsafe { mem::transmute(self) }
    }
    #[allow(dead_code)]
    fn repl_string_offset(&self) -> usize {
        u16::from_le_bytes(self.as_extended().repl_string_offset) as usize
    }
    #[allow(dead_code)]
    fn repl_index(&self) -> i8 {
        self.as_extended().repl_index
    }
    #[allow(dead_code)]
    fn repl_cut(&self) -> i8 {
        self.as_extended().repl_cut
    }
    // Return the state's Transitions as a slice reference.
    fn transitions(&self) -> &[Transition] {
        let count = self.num_transitions() as usize;
        if count == 0 {
            return &[];
        }
        let transition_offset = if self.is_extended() { mem::size_of::<StateExtended>() } else { mem::size_of::<State>() } as isize;
        // We know the `offset` here will not look beyond the valid range of memory
        // because Level::get_state() checks the state length (accounting for the
        // number of transitions) before returning a State reference.
        let trans_ptr = unsafe { (self as *const State as *const u8).offset(transition_offset) as *const Transition };
        // Again, because Level::get_state() already checked the state length, we know
        // this slice address and count will be valid.
        unsafe { slice::from_raw_parts(trans_ptr, count) }
    }
    // Look up the Transition for a given input byte, or None.
    fn transition_for(&self, b: u8) -> Option<Transition> {
        // The transitions array is sorted by match_byte() value, but there are
        // usually very few entries; benchmarking showed that using binary_search_by
        // here gave no benefit (possibly slightly slower).
        self.transitions().iter().copied().find(|t| t.match_byte() == b)
    }
    // Just for debugging use...
    #[allow(dead_code)]
    fn deep_show(&self, prefix: &str, dic: &Level) {
        if self.match_string_offset() != INVALID_STRING_OFFSET as usize {
            let match_string = dic.string_at_offset(self.match_string_offset());
            println!("{}match: {}", prefix, str::from_utf8(match_string).unwrap());
        }
        for t in self.transitions() {
            println!("{}{} ->", prefix, t.match_byte() as char);
            let next_prefix = format!("{}  ", prefix);
            dic.get_state(t.new_state_offset()).unwrap().deep_show(&next_prefix, &dic);
        }
    }
}

// We count the presentation-form ligature characters U+FB00..FB06 as multiple
// chars for the purposes of lefthyphenmin/righthyphenmin. In UTF-8, all these
// ligature characters are 3-byte sequences beginning with <0xEF, 0xAC>; this
// helper returns the "decomposed length" of the ligature given its trailing
// byte.
fn lig_length(trail_byte: u8) -> usize {
    // This is only called on valid UTF-8 where we already know trail_byte
    // must be >= 0x80.
    // Ligature lengths:       ff   fi   fl   ffi  ffl  long-st  st
    const LENGTHS: [u8; 7] = [ 2u8, 2u8, 2u8, 3u8, 3u8, 2u8,     2u8 ];
    if trail_byte > 0x86 {
        return 1;
    }
    LENGTHS[trail_byte as usize - 0x80] as usize
}

fn is_utf8_trail_byte(byte: u8) -> bool {
    (byte & 0xC0) == 0x80
}

fn is_ascii_digit(byte: u8) -> bool {
    byte <= b'9' && byte >= b'0'
}

fn is_odd(byte: u8) -> bool {
    (byte & 0x01) == 0x01
}

// A hyphenation Level has a header followed by State records and packed string
// data. The total size of the slice depends on the number and size of the
// States and Strings it contains.
// Note that the data of the Level may not have any specific alignment!
#[derive(Debug,Copy,Clone)]
struct Level<'a> {
    data: &'a [u8],
    // Header fields cached by the constructor for faster access:
    state_data_base_: usize,
    string_data_base_: usize,
}

impl Level<'_> {
    // Constructor that initializes our cache variables.
    fn new(data: &[u8]) -> Level {
        Level {
            data,
            state_data_base_: u32::from_le_bytes(*array_ref!(data, 0, 4)) as usize,
            string_data_base_: u32::from_le_bytes(*array_ref!(data, 4, 4)) as usize,
        }
    }

    // Accessors for Level header fields; see file format description.
    fn state_data_base(&self) -> usize {
        self.state_data_base_ // cached by constructor
    }
    fn string_data_base(&self) -> usize {
        self.string_data_base_ // cached by constructor
    }
    fn nohyphen_string_offset(&self) -> usize {
        u16::from_le_bytes(*array_ref!(self.data, 8, 2)) as usize
    }
    #[allow(dead_code)]
    fn nohyphen_count(&self) -> u16 {
        u16::from_le_bytes(*array_ref!(self.data, 10, 2))
    }
    fn lh_min(&self) -> usize {
        max(1, self.data[12] as usize)
    }
    fn rh_min(&self) -> usize {
        max(1, self.data[13] as usize)
    }
    fn clh_min(&self) -> usize {
        max(1, self.data[14] as usize)
    }
    fn crh_min(&self) -> usize {
        max(1, self.data[15] as usize)
    }
    fn word_boundary_mins(&self) -> (usize, usize, usize, usize) {
        (self.lh_min(), self.rh_min(), self.clh_min(), self.crh_min())
    }
    // Strings are represented as offsets from the Level's string_data_base.
    // This returns a byte slice referencing the string at a given offset,
    // or an empty slice if invalid.
    fn string_at_offset(&self, offset: usize) -> &'_ [u8] {
        if offset == INVALID_STRING_OFFSET as usize {
            return &[];
        }
        let string_base = self.string_data_base() as usize + offset;
        // TODO: move this to the validation function.
        debug_assert!(string_base < self.data.len());
        if string_base + 1 > self.data.len() {
            return &[];
        }
        let len = self.data[string_base] as usize;
        // TODO: move this to the validation function.
        debug_assert!(string_base + 1 + len <= self.data.len());
        if string_base + 1 + len > self.data.len() {
            return &[];
        }
        self.data.get(string_base + 1 .. string_base + 1 + len).unwrap()
    }
    // The nohyphen field actually contains multiple NUL-separated substrings;
    // return them as a vector of individual byte slices.
    fn nohyphen(&self) -> Vec<&[u8]> {
        let string_offset = self.nohyphen_string_offset();
        let nohyph_str = self.string_at_offset(string_offset as usize);
        if nohyph_str.is_empty() {
            return vec![];
        }
        nohyph_str.split(|&b| b == 0).collect()
    }
    // States are represented as an offset from the Level's state_data_base.
    // This returns a reference to the State at a given offset, or None if invalid.
    fn get_state(&self, offset: usize) -> Option<&State> {
        if offset == INVALID_STATE_OFFSET as usize {
            return None;
        }
        debug_assert_eq!(offset & 3, 0);
        let state_base = self.state_data_base() + offset;
        // TODO: move this to the validation function.
        debug_assert!(state_base + mem::size_of::<State>() <= self.string_data_base());
        if state_base + mem::size_of::<State>() > self.string_data_base() {
            return None;
        }
        let state_ptr = &self.data[state_base] as *const u8 as *const State;
        // This is safe because we just checked against self.string_data_base() above.
        let state = unsafe { state_ptr.as_ref().unwrap() };
        let length = if state.is_extended() { mem::size_of::<StateExtended>() } else { mem::size_of::<State>() }
                + mem::size_of::<Transition>() * state.num_transitions() as usize;
        // TODO: move this to the validation function.
        debug_assert!(state_base + length <= self.string_data_base());
        if state_base + length > self.string_data_base() {
            return None;
        }
        // This is safe because we checked the full state length against self.string_data_base().
        unsafe { state_ptr.as_ref() }
    }
    // Sets hyphenation values (odd = potential break, even = no break) in values[],
    // and returns the change in the number of odd values present, so the caller can
    // keep track of the total number of potential breaks in the word.
    fn find_hyphen_values(&self, word: &str, values: &mut [u8], lh_min: usize, rh_min: usize) -> isize {
        // Bail out immediately if the word is too short to hyphenate.
        if word.len() < lh_min + rh_min {
            return 0;
        }
        let start_state = self.get_state(0);
        let mut st = start_state;
        let mut hyph_count = 0;
        for i in 0 .. word.len() + 2 {
            // Loop over the word by bytes, with a virtual '.' added at each end
            // to match word-boundary patterns.
            let b = if i == 0 || i == word.len() + 1 { b'.' } else { word.as_bytes()[i - 1] };
            loop {
                // Loop to repeatedly fall back if we don't find a matching transition.
                // Note that this could infinite-loop if there is a state whose fallback
                // points to itself (or a cycle of fallbacks), but this would represent
                // a table compilation error.
                // (A potential validation function could check for fallback cycles.)
                if st.is_none() {
                    st = start_state;
                    break;
                }
                let state = st.unwrap();
                if let Some(tr) = state.transition_for(b) {
                    // Found a transition for the current byte. Look up the new state;
                    // if it has a match_string, merge its weights into `values`.
                    st = self.get_state(tr.new_state_offset());
                    if let Some(state) = st {
                        let match_offset = state.match_string_offset();
                        if match_offset != INVALID_STRING_OFFSET as usize {
                            if state.is_extended() {
                                debug_assert!(false, "extended hyphenation not supported by this function");
                            } else {
                                let match_str = self.string_at_offset(match_offset);
                                let offset = i + 1 - match_str.len();
                                assert!(offset + match_str.len() <= word.len() + 2);
                                for (j, ch) in match_str.iter().enumerate() {
                                    let index = offset + j;
                                    if index >= lh_min && index <= word.len() - rh_min {
                                        // lh_min and rh_min are guaranteed to be >= 1,
                                        // so this will not try to access outside values[].
                                        let old_value = values[index - 1];
                                        let value = ch - b'0';
                                        if value > old_value {
                                            if is_odd(old_value) != is_odd(value) {
                                                // Adjust hyph_count for the change we're making
                                                hyph_count += if is_odd(value) { 1 } else { -1 };
                                            }
                                            values[index - 1] = value;
                                        }
                                    }
                                }
                            }
                        }
                    }
                    // We have handled the current input byte; leave the fallback loop
                    // and get next input.
                    break;
                }
                // No transition for the current byte; go to fallback state and try again.
                st = self.get_state(state.fallback_state());
            }
        }

        // If the word was not purely ASCII, or if the word begins/ends with
        // digits, the use of lh_min and rh_min above may not have correctly
        // excluded enough positions, so we need to fix things up here.
        let mut index = 0;
        let mut count = 0;
        let word_bytes = word.as_bytes();
        let mut clear_hyphen_at = |i| { if is_odd(values[i]) { hyph_count -= 1; } values[i] = 0; };
        // Handle lh_min.
        while count < lh_min - 1 && index < word_bytes.len() {
            let byte = word_bytes[index];
            clear_hyphen_at(index);
            if byte < 0x80 {
                index += 1;
                if is_ascii_digit(byte) {
                    continue; // ASCII digits don't count
                }
            } else if byte == 0xEF && word_bytes[index + 1] == 0xAC {
                // Unicode presentation-form ligature characters, which we count as
                // multiple chars for the purpose of lh_min/rh_min, all begin with
                // 0xEF, 0xAC in UTF-8.
                count += lig_length(word_bytes[index + 2]);
                clear_hyphen_at(index + 1);
                clear_hyphen_at(index + 2);
                index += 3;
                continue;
            } else {
                index += 1;
                while index < word_bytes.len() && is_utf8_trail_byte(word_bytes[index])  {
                    clear_hyphen_at(index);
                    index += 1;
                }
            }
            count += 1;
        }

        // Handle rh_min.
        count = 0;
        index = word.len();
        while count < rh_min && index > 0 {
            index -= 1;
            let byte = word_bytes[index];
            if index < word.len() - 1 {
                clear_hyphen_at(index);
            }
            if byte < 0x80 {
                // Only count if not an ASCII digit
                if !is_ascii_digit(byte) {
                    count += 1;
                }
                continue;
            }
            if is_utf8_trail_byte(byte) {
                continue;
            }
            if byte == 0xEF && word_bytes[index + 1] == 0xAC {
                // Presentation-form ligatures count as multiple chars.
                count += lig_length(word_bytes[index + 2]);
                continue;
            }
            count += 1;
        }

        hyph_count
    }
}

/// Hyphenation engine encapsulating a language-specific set of patterns (rules)
/// that identify possible break positions within a word.
pub struct Hyphenator<'a>(&'a [u8]);

impl Hyphenator<'_> {
    /// Return a Hyphenator that wraps the given buffer.
    /// This does *not* check that the given buffer is in fact a valid hyphenation table.
    /// Use `is_valid_hyphenator()` to determine whether it is usable.
    /// (Calling hyphenation methods on a Hyphenator that wraps arbitrary,
    /// unvalidated data is not unsafe, but may panic.)
    pub fn new(buffer: &[u8]) -> Hyphenator {
        Hyphenator(buffer)
    }

    // Internal implementation details
    fn magic_number(&self) -> &[u8] {
        &self.0[0 .. 4]
    }
    fn num_levels(&self) -> usize {
        u32::from_le_bytes(*array_ref!(self.0, 4, 4)) as usize
    }
    fn level(&self, i: usize) -> Level {
        let offset = u32::from_le_bytes(*array_ref!(self.0, FILE_HEADER_SIZE + 4 * i, 4)) as usize;
        let limit = if i == self.num_levels() - 1 {
            self.0.len()
        } else {
            u32::from_le_bytes(*array_ref!(self.0, FILE_HEADER_SIZE + 4 * i + 4, 4)) as usize
        };
        debug_assert!(offset + LEVEL_HEADER_SIZE <= limit && limit <= self.0.len());
        debug_assert_eq!(offset & 3, 0);
        debug_assert_eq!(limit & 3, 0);
        Level::new(&self.0[offset .. limit])
    }

    /// Identify acceptable hyphenation positions in the given `word`.
    ///
    /// The caller-supplied `values` must be at least as long as the `word`.
    ///
    /// On return, any elements with an odd value indicate positions in the word
    /// after which a hyphen could be inserted.
    ///
    /// Returns the number of possible hyphenation positions that were found.
    ///
    /// # Panics
    /// If the given `values` slice is too small to hold the results.
    ///
    /// If the block of memory represented by `self.0` is not in fact a valid
    /// hyphenation dictionary, this function may panic with an overflow or
    /// array bounds violation.
    pub fn find_hyphen_values(&self, word: &str, values: &mut [u8]) -> isize {
        assert!(values.len() >= word.len());
        values.iter_mut().for_each(|x| *x = 0);
        let top_level = self.level(0);
        let (lh_min, rh_min, clh_min, crh_min) = top_level.word_boundary_mins();
        if word.len() < lh_min + rh_min {
            return 0;
        }
        let mut hyph_count = top_level.find_hyphen_values(word, values, lh_min, rh_min);
        let compound = hyph_count > 0;
        // Subsequent levels are applied to fragments between potential breaks
        // already found:
        for l in 1 .. self.num_levels() {
            let level = self.level(l);
            if hyph_count > 0 {
                let mut begin = 0;
                let mut lh = lh_min;
                // lh_min and rh_min are both guaranteed to be greater than zero,
                // so this loop will not reach fully to the end of the word.
                for i in lh_min - 1 .. word.len() - rh_min {
                    if is_odd(values[i]) {
                        if i > begin {
                            // We've found a component of a compound;
                            // clear the corresponding values and apply the new level.
                            // (These values must be even, so hyph_count is unchanged.)
                            values[begin .. i].iter_mut().for_each(|x| {
                                *x = 0;
                            });
                            hyph_count += level.find_hyphen_values(&word[begin ..= i],
                                                                   &mut values[begin ..= i],
                                                                   lh, crh_min);
                        }
                        begin = i + 1;
                        lh = clh_min;
                    }
                }
                if begin == 0 {
                    // No compound-word breaks were found, just apply level to the whole word.
                    hyph_count += level.find_hyphen_values(word, values, lh_min, rh_min);
                } else if begin < word.len() {
                    // Handle trailing component of compound.
                    hyph_count += level.find_hyphen_values(&word[begin .. word.len()],
                                                           &mut values[begin .. word.len()],
                                                           clh_min, rh_min);
                }
            } else {
                hyph_count += level.find_hyphen_values(word, values, lh_min, rh_min);
            }
        }

        // Only need to check nohyphen strings if top-level (compound) breaks were found.
        if compound && hyph_count > 0 {
            let nohyph = top_level.nohyphen();
            if !nohyph.is_empty() {
                for i in lh_min ..= word.len() - rh_min {
                    if is_odd(values[i - 1]) {
                        for nh in &nohyph {
                            if i + nh.len() <= word.len() && *nh == &word.as_bytes()[i .. i + nh.len()] {
                                values[i - 1] = 0;
                                hyph_count -= 1;
                                break;
                            }
                            if nh.len() <= i && *nh == &word.as_bytes()[i - nh.len() .. i] {
                                values[i - 1] = 0;
                                hyph_count -= 1;
                                break;
                            }
                        }
                    }
                }
            }
        }

        hyph_count
    }

    /// Generate the hyphenated form of a `word` by inserting the given `hyphen_char`
    /// at each valid break position.
    ///
    /// # Panics
    /// If the block of memory represented by `self` is not in fact a valid
    /// hyphenation dictionary, this function may panic with an overflow or
    /// array bounds violation.
    ///
    /// Also panics if the length of the hyphenated word would overflow `usize`.
    pub fn hyphenate_word(&self, word: &str, hyphchar: char) -> String {
        let mut values = vec![0u8; word.len()];
        let hyph_count = self.find_hyphen_values(word, &mut values);
        if hyph_count <= 0 {
            return word.to_string();
        }
        // We know how long the result will be, so we can preallocate here.
        let result_len = word.len() + hyph_count as usize * hyphchar.len_utf8();
        let mut result = String::with_capacity(result_len);
        let mut n = 0;
        for ch in word.char_indices() {
            if ch.0 > 0 && is_odd(values[ch.0 - 1]) {
                result.push(hyphchar);
                n += 1;
            }
            result.push(ch.1);
        }
        debug_assert_eq!(n, hyph_count);
        debug_assert_eq!(result_len, result.len());
        result
    }

    /// Check if the block of memory looks like it could be a valid hyphenation
    /// table.
    pub fn is_valid_hyphenator(&self) -> bool {
        // Size must be at least 4 bytes for magic_number + 4 bytes num_levels;
        // smaller than this cannot be safely inspected.
        if self.0.len() < FILE_HEADER_SIZE {
            return false;
        }
        if self.magic_number() != MAGIC_NUMBER {
            return false;
        }
        // For each level, there's a 4-byte offset in the header, and the level
        // has its own 16-byte header, so we can check a minimum size again here.
        let num_levels = self.num_levels();
        if self.0.len() < FILE_HEADER_SIZE + LEVEL_HEADER_SIZE * num_levels {
            return false;
        }
        // Check that state_data_base and string_data_base for each hyphenation
        // level are within range.
        for l in 0 .. num_levels {
            let level = self.level(l);
            if level.state_data_base() < LEVEL_HEADER_SIZE ||
                   level.state_data_base() > level.string_data_base() ||
                   level.string_data_base() > level.data.len() {
                return false;
            }
            // TODO: consider doing more extensive validation of states and
            // strings within the level?
        }
        // It's still possible the dic is internally broken, but at least it's
        // worth trying to use it!
        true
    }
}

/// Load the compiled hyphenation file at `dic_path`, if present.
///
/// Returns `None` if the specified file cannot be opened or mapped,
/// otherwise returns a `memmap2::Mmap` mapping the file.
///
/// # Safety
///
/// This is unsafe for the same reason `Mmap::map()` is unsafe:
/// mapped_hyph does not guarantee safety if the mapped file is modified
/// (e.g. by another process) while we're using it.
///
/// This verifies that the file looks superficially like it may be a
/// compiled hyphenation table, but does *not* fully check the validity
/// of the file contents! Calling hyphenation functions with the returned
/// data is not unsafe, but may panic if the data is invalid.
pub unsafe fn load_file(dic_path: &str) -> Option<Mmap> {
    let file = File::open(dic_path).ok()?;
    let dic = Mmap::map(&file).ok()?;
    let hyph = Hyphenator(&*dic);
    if hyph.is_valid_hyphenator() {
        return Some(dic);
    }
    None
}

[ Dauer der Verarbeitung: 0.3 Sekunden  (vorverarbeitet)  ]