cellrune 0.1.18

Bounded XLSX/XLSM reading, deterministic calculation, editing, and writing for Rust
Documentation
use super::super::ast::Expr;
use super::super::coerce::to_number;
use super::super::eval::{Engine, EvalContext};
use super::super::runtime::Array;
use super::super::value::{ErrorKind, Value};
use super::modern_text::push_bounded;
use super::regex_common::{CompiledRegex, RegexCaptureSet};
use super::util::required_text;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExtractMode {
    First,
    All,
    Captures,
}

#[derive(Debug, Clone, Copy)]
struct NamedCaptureLookup<'a> {
    subject: &'a str,
    captures: &'a RegexCaptureSet,
    name_indexes: &'a std::collections::BTreeMap<String, Vec<usize>>,
}

pub(super) fn extract_scalar(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    args: &[Expr],
) -> Value {
    match extract_array(engine, context, args) {
        Ok(array) => array
            .data
            .into_iter()
            .next()
            .unwrap_or(Value::Error(ErrorKind::Calc)),
        Err(kind) => Value::Error(kind),
    }
}

pub(super) fn extract_array(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    args: &[Expr],
) -> Result<Array, ErrorKind> {
    if args.len() < 2 || args.len() > 4 {
        return Err(ErrorKind::Value);
    }
    let subject = required_text(engine, context, &args[0])?;
    let pattern = required_text(engine, context, &args[1])?;
    let mode = match integer_option(engine, context, args.get(2), 0)? {
        0 => ExtractMode::First,
        1 => ExtractMode::All,
        2 => ExtractMode::Captures,
        _ => return Err(ErrorKind::Value),
    };
    let case_insensitive = case_option(engine, context, args.get(3))?;
    let mut regex = CompiledRegex::compile(engine, context, &pattern, case_insensitive)?;
    let maximum = match mode {
        ExtractMode::All => usize::try_from(engine.max_array_cells())
            .ok()
            .and_then(|maximum| maximum.checked_add(1)),
        ExtractMode::First | ExtractMode::Captures => Some(1),
    };
    let matches = regex.captures(engine, context, &subject, maximum)?;
    let Some(first) = matches.first() else {
        return Err(ErrorKind::NA);
    };

    match mode {
        ExtractMode::First => array_from_spans(engine, context, &subject, 1, 1, [first.span(0)]),
        ExtractMode::All => {
            let rows = u32::try_from(matches.len()).map_err(|_| ErrorKind::Num)?;
            array_from_spans(
                engine,
                context,
                &subject,
                rows,
                1,
                matches.iter().map(|captures| captures.span(0)),
            )
        }
        ExtractMode::Captures => {
            let groups = first.len().checked_sub(1).ok_or(ErrorKind::Calc)?;
            if groups == 0 {
                return Err(ErrorKind::Calc);
            }
            let columns = u32::try_from(groups).map_err(|_| ErrorKind::Num)?;
            array_from_spans(
                engine,
                context,
                &subject,
                1,
                columns,
                (1..first.len()).map(|index| first.span(index)),
            )
        }
    }
}

fn array_from_spans(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    subject: &str,
    rows: u32,
    cols: u32,
    spans: impl IntoIterator<Item = Option<(usize, usize)>>,
) -> Result<Array, ErrorKind> {
    let cells = u64::from(rows)
        .checked_mul(u64::from(cols))
        .ok_or(ErrorKind::Num)?;
    engine.ensure_array_cells(cells)?;
    let spans: Vec<Option<(usize, usize)>> = spans.into_iter().collect();
    if u64::try_from(spans.len()).map_err(|_| ErrorKind::Num)? != cells {
        return Err(ErrorKind::Calc);
    }
    let copied_bytes = spans.iter().try_fold(0_u64, |bytes, span| {
        let length = match span {
            Some((start, end)) => subject.get(*start..*end).ok_or(ErrorKind::Value)?.len(),
            None => 0,
        };
        bytes
            .checked_add(u64::try_from(length).map_err(|_| ErrorKind::Num)?)
            .ok_or(ErrorKind::Num)
    })?;
    engine.charge_function_iterations(context, copied_bytes)?;
    let mut data = Vec::with_capacity(usize::try_from(cells).map_err(|_| ErrorKind::Num)?);
    for span in spans {
        let text = match span {
            Some((start, end)) => subject.get(start..end).ok_or(ErrorKind::Value)?.to_owned(),
            None => String::new(),
        };
        engine.ensure_text_bytes(text.len())?;
        data.push(Value::Text(text));
    }
    Ok(Array { rows, cols, data })
}

pub(super) fn test(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    if args.len() < 2 || args.len() > 3 {
        return Value::Error(ErrorKind::Value);
    }
    let subject = match required_text(engine, context, &args[0]) {
        Ok(text) => text,
        Err(kind) => return Value::Error(kind),
    };
    let pattern = match required_text(engine, context, &args[1]) {
        Ok(text) => text,
        Err(kind) => return Value::Error(kind),
    };
    let case_insensitive = match case_option(engine, context, args.get(2)) {
        Ok(value) => value,
        Err(kind) => return Value::Error(kind),
    };
    let mut regex = match CompiledRegex::compile(engine, context, &pattern, case_insensitive) {
        Ok(regex) => regex,
        Err(kind) => return Value::Error(kind),
    };
    match regex.captures(engine, context, &subject, Some(1)) {
        Ok(matches) => Value::Logical(!matches.is_empty()),
        Err(kind) => Value::Error(kind),
    }
}

pub(super) fn replace(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    match replace_result(engine, context, args) {
        Ok(output) => Value::Text(output),
        Err(kind) => Value::Error(kind),
    }
}

fn replace_result(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    args: &[Expr],
) -> Result<String, ErrorKind> {
    if args.len() < 3 || args.len() > 5 {
        return Err(ErrorKind::Value);
    }
    let subject = required_text(engine, context, &args[0])?;
    let pattern = required_text(engine, context, &args[1])?;
    let replacement = required_text(engine, context, &args[2])?;
    engine.ensure_text_bytes(replacement.len())?;
    let occurrence = integer_option(engine, context, args.get(3), 0)?;
    let case_insensitive = case_option(engine, context, args.get(4))?;
    let mut regex = CompiledRegex::compile(engine, context, &pattern, case_insensitive)?;
    let maximum_matches = (occurrence > 0)
        .then(|| usize::try_from(occurrence).map_err(|_| ErrorKind::Value))
        .transpose()?;
    let matches = regex.captures(engine, context, &subject, maximum_matches)?;
    let selected = selected_match(occurrence, matches.len());
    let mut output = String::new();
    let mut cursor = 0;
    for (index, captures) in matches.iter().enumerate() {
        if selected.is_some_and(|selected| selected != index) {
            continue;
        }
        let (start, end) = captures.span(0).ok_or(ErrorKind::Value)?;
        push_bounded(
            engine,
            &mut output,
            subject.get(cursor..start).ok_or(ErrorKind::Value)?,
        )?;
        expand_replacement(
            engine,
            context,
            &mut output,
            &replacement,
            &subject,
            captures,
            regex.capture_name_indexes(),
        )?;
        cursor = end;
    }
    push_bounded(
        engine,
        &mut output,
        subject.get(cursor..).ok_or(ErrorKind::Value)?,
    )?;
    Ok(output)
}

fn selected_match(occurrence: i64, count: usize) -> Option<usize> {
    if occurrence == 0 {
        return None;
    }
    if occurrence > 0 {
        usize::try_from(occurrence - 1)
            .ok()
            .filter(|index| *index < count)
            .or(Some(count))
    } else {
        usize::try_from(occurrence.unsigned_abs())
            .ok()
            .and_then(|offset| count.checked_sub(offset))
            .or(Some(count))
    }
}

fn expand_replacement(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    output: &mut String,
    replacement: &str,
    subject: &str,
    captures: &RegexCaptureSet,
    name_indexes: &std::collections::BTreeMap<String, Vec<usize>>,
) -> Result<(), ErrorKind> {
    engine.charge_function_iterations(context, replacement.len() as u64)?;
    let named_lookup = NamedCaptureLookup {
        subject,
        captures,
        name_indexes,
    };
    let mut cursor = 0;
    while let Some(relative) = replacement[cursor..].find('$') {
        let dollar = cursor + relative;
        push_bounded(engine, output, &replacement[cursor..dollar])?;
        let tail = replacement.get(dollar + 1..).ok_or(ErrorKind::Value)?;
        let Some(next) = tail.chars().next() else {
            return Err(ErrorKind::Value);
        };
        cursor = dollar + 1 + next.len_utf8();
        match next {
            '$' => push_bounded(engine, output, "$"),
            '&' => append_capture(engine, output, subject, captures, 0),
            '`' => {
                let (start, _) = captures.span(0).ok_or(ErrorKind::Value)?;
                push_bounded(
                    engine,
                    output,
                    subject.get(..start).ok_or(ErrorKind::Value)?,
                )
            }
            '\'' => {
                let (_, end) = captures.span(0).ok_or(ErrorKind::Value)?;
                push_bounded(engine, output, subject.get(end..).ok_or(ErrorKind::Value)?)
            }
            '_' => push_bounded(engine, output, subject),
            '0'..='9' => {
                let end = replacement[cursor..]
                    .find(|character: char| !character.is_ascii_digit())
                    .map_or(replacement.len(), |relative| cursor + relative);
                let start = dollar + 1;
                let index = replacement[start..end]
                    .parse::<usize>()
                    .map_err(|_| ErrorKind::Value)?;
                cursor = end;
                append_capture(engine, output, subject, captures, index)
            }
            '{' => {
                let relative_end = replacement[cursor..].find('}').ok_or(ErrorKind::Value)?;
                let end = cursor + relative_end;
                let name = &replacement[cursor..end];
                cursor = end + 1;
                append_named_capture(engine, context, output, named_lookup, name, true)
            }
            '<' => {
                let relative_end = replacement[cursor..].find('>').ok_or(ErrorKind::Value)?;
                let end = cursor + relative_end;
                let name = &replacement[cursor..end];
                cursor = end + 1;
                append_named_capture(engine, context, output, named_lookup, name, false)
            }
            _ => Err(ErrorKind::Value),
        }?;
    }
    push_bounded(engine, output, &replacement[cursor..])
}

fn append_named_capture(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    output: &mut String,
    lookup: NamedCaptureLookup<'_>,
    name: &str,
    allow_number: bool,
) -> Result<(), ErrorKind> {
    if allow_number && let Ok(index) = name.parse::<usize>() {
        return append_capture(engine, output, lookup.subject, lookup.captures, index);
    }
    if name == "*MARK" {
        return Err(ErrorKind::Value);
    }
    let indexes = lookup.name_indexes.get(name).ok_or(ErrorKind::Value)?;
    engine.charge_function_iterations(
        context,
        u64::try_from(indexes.len()).map_err(|_| ErrorKind::Num)?,
    )?;
    let first = *indexes.first().ok_or(ErrorKind::Value)?;
    let index = indexes
        .iter()
        .copied()
        .find(|index| lookup.captures.span(*index).is_some())
        .unwrap_or(first);
    append_capture(engine, output, lookup.subject, lookup.captures, index)
}

fn append_capture(
    engine: &Engine<'_>,
    output: &mut String,
    subject: &str,
    captures: &RegexCaptureSet,
    index: usize,
) -> Result<(), ErrorKind> {
    let (start, end) = captures.span(index).ok_or(ErrorKind::Value)?;
    push_bounded(
        engine,
        output,
        subject.get(start..end).ok_or(ErrorKind::Value)?,
    )
}

fn case_option(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    expression: Option<&Expr>,
) -> Result<bool, ErrorKind> {
    match integer_option(engine, context, expression, 0)? {
        0 => Ok(false),
        1 => Ok(true),
        _ => Err(ErrorKind::Value),
    }
}

fn integer_option(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    expression: Option<&Expr>,
    default: i64,
) -> Result<i64, ErrorKind> {
    let Some(expression) = expression else {
        return Ok(default);
    };
    if matches!(expression, Expr::Missing) {
        return Ok(default);
    }
    let number = to_number(&engine.eval_scalar(context, expression))?.trunc();
    if number < i64::MIN as f64 || number >= i64::MAX as f64 {
        return Err(ErrorKind::Value);
    }
    Ok(number as i64)
}