use super::api_argument::breaks_a_split;
use super::separate::{find_end_paren, CBuffer, Split};
use super::{untrace, PipelineError, Traced};
use crate::commands::originate::OriginateError;
use crate::commands::variables::{BlockParse, VariablesType};
#[cfg(test)]
pub(super) mod c_oracle;
pub(crate) fn unbalanced(text: &str, (open, close): (char, char)) -> Option<String> {
let mut depth = 0i32;
for ch in text.chars() {
if ch == open {
depth += 1;
} else if ch == close {
depth -= 1;
if depth < 0 {
return Some(format!(
"closes a '{open}' it never opened, ending the block early"
));
}
}
}
(depth != 0).then(|| format!("opens a '{open}' it never closes, swallowing the block's end"))
}
pub(crate) fn check_separator(sep: char, vars_type: VariablesType) -> Result<(), OriginateError> {
let (open, close) = vars_type.delimiters();
if breaks_a_split(sep)
|| sep == open
|| sep == close
|| matches!(sep, '=' | '^' | '$' | '{')
|| (sep == '|' && vars_type == VariablesType::Channel)
{
return Err(OriginateError::ParseError(format!(
"invalid ^^ separator: '{sep}'"
)));
}
Ok(())
}
const BLOCK_PAIRS: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PairEffect {
Set(String),
Ignored,
Cleared,
Unreadable,
Valueless,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Pair {
pub(crate) key: String,
pub(crate) effect: PairEffect,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Block {
pub(crate) open: char,
pub(crate) separator: char,
pub(crate) pairs: Vec<Pair>,
pub(crate) rewrites_following_text: bool,
}
pub(crate) struct Parsed {
pub(crate) block: Block,
pub(crate) next: usize,
splits_past_close_by_byte: bool,
}
impl Parsed {
pub(crate) fn read(self) -> Result<(Block, usize), PipelineError> {
if self.splits_past_close_by_byte {
return Err(PipelineError::SplitSeparatorUnreadable);
}
Ok((self.block, self.next))
}
}
pub(crate) fn parse_block(
buffer: &mut CBuffer,
at: usize,
open: char,
close: char,
comma: char,
block_parse: BlockParse,
) -> Option<Parsed> {
let end = at + find_end_paren(buffer.c_str(at), open, close)?;
let separator = match buffer
.c_str(at)
.get(1..end - at)?
{
[('^', ..), ('^', ..), (picked, ..), ..] if *picked == comma => ',',
[('^', ..), ('^', ..), (picked, ..), ..] => *picked,
[('^', ..), ('^', ..)] => '\0',
_ => ',',
};
let mut block = Block {
open,
separator,
pairs: Vec::new(),
rewrites_following_text: false,
};
let next = end + 1;
if block.separator_unreadable() {
let escaped = escapes_its_end(
buffer
.c_str(at)
.get(4..end - at)
.unwrap_or_default(),
);
let chains_a_block = following_block(buffer, next, open, close).is_some();
return Some(Parsed {
block,
next,
splits_past_close_by_byte: escaped || chains_a_block,
});
}
let following = chars(buffer.c_str(next));
buffer.terminate(end);
let (mut data, mut delim) = (at + 1, comma);
let mut second = following_block(buffer, next, open, close);
let mut reads_past_by_byte = false;
loop {
if buffer.at(data) == '^' && buffer.at(data + 1) == '^' {
delim = buffer.at(data + 2);
data += 3;
}
let escaped = escapes_its_end(buffer.c_str(data));
let split = block_split(buffer, data, delim, BLOCK_PAIRS, block_parse);
if split.unreadable_head {
reads_past_by_byte |= escaped;
block
.pairs
.push(Pair {
key: untrace(buffer.c_str(data)),
effect: PairEffect::Unreadable,
});
} else {
for token in split.tokens {
let escaped = escapes_its_end(buffer.c_str(token.start));
let pair = pair(buffer, token.start, block_parse);
reads_past_by_byte |= escaped && pair.effect == PairEffect::Unreadable;
block
.pairs
.push(pair);
}
}
match second.take() {
Some(close) => data = close,
None => break,
}
}
block.rewrites_following_text = chars(buffer.c_str(next)) != following;
Some(Parsed {
block,
next,
splits_past_close_by_byte: reads_past_by_byte,
})
}
fn escapes_its_end(text: &[Traced]) -> bool {
text.iter()
.rev()
.take_while(|&&(c, ..)| c == '\\')
.count()
% 2
== 1
}
fn chars(text: &[Traced]) -> Vec<char> {
text.iter()
.map(|&(c, ..)| c)
.collect()
}
fn following_block(buffer: &CBuffer, after: usize, open: char, close: char) -> Option<usize> {
let rest = buffer.c_str(after);
let opener = match rest
.iter()
.position(|&(c, ..)| c != ' ')
{
_ if rest.is_empty() => return None,
Some(at) if rest[at].0 == open => after + at,
Some(_) => return None,
None => after + rest.len(),
};
find_end_paren(buffer.c_str(opener + 1), open, close).map(|at| opener + 1 + at)
}
impl Block {
pub(crate) fn separator_unreadable(&self) -> bool {
!self
.separator
.is_ascii()
}
}
fn block_split(
buffer: &mut CBuffer,
index: usize,
delim: char,
limit: usize,
block_parse: BlockParse,
) -> Split {
match block_parse {
BlockParse::PairSplitCleans => buffer.separate(index, delim, limit),
}
}
fn pair(buffer: &mut CBuffer, index: usize, block_parse: BlockParse) -> Pair {
let token = untrace(buffer.c_str(index));
let end = index
+ buffer
.c_str(index)
.len();
let split = block_split(buffer, index, '=', 2, block_parse);
if split.unreadable_head {
return Pair {
key: token,
effect: PairEffect::Unreadable,
};
}
let fields: Vec<usize> = split
.tokens
.iter()
.map(|field| field.start)
.collect();
let key = fields
.first()
.map_or_else(String::new, |&at| untrace(buffer.c_str(at)));
let effect = match fields[..] {
[_, value] => match untrace(buffer.c_str(value)) {
value if value.is_empty() => PairEffect::Cleared,
value => PairEffect::Set(value),
},
[_] if split
.tokens
.first()
.is_some_and(|field| {
field
.raw
.end
< end
}) =>
{
PairEffect::Valueless
}
_ => PairEffect::Ignored,
};
Pair { key, effect }
}
pub(crate) fn same_header(name: &str, other: &str) -> bool {
name.eq_ignore_ascii_case(other)
}
pub(crate) fn install<'a>(blocks: impl IntoIterator<Item = &'a Block>) -> Vec<(&'a str, &'a str)> {
let mut headers: Vec<(&str, &str)> = Vec::new();
for pair in blocks
.into_iter()
.flat_map(|block| &block.pairs)
{
match &pair.effect {
PairEffect::Set(value) => {
headers.retain(|(name, _)| !same_header(name, &pair.key));
headers.push((&pair.key, value))
}
PairEffect::Cleared => headers.retain(|(name, _)| !same_header(name, &pair.key)),
PairEffect::Ignored | PairEffect::Unreadable | PairEffect::Valueless => {}
}
}
headers
}