use std::collections::{BTreeMap, HashSet};
use std::io::BufRead;
use crate::error::{Error, Result};
use crate::etree::{Command, ParseOps, TextNode, TextTree, parse_error};
use crate::utils;
enum Frame {
BeginEnd {
keyw: String,
outer: TextTree,
},
Encrypted {
keyw: String,
outer: TextTree,
extfields: BTreeMap<String, String>,
},
Conflict {
keyw: String,
outer: TextTree,
ours: TextTree,
mode: ConflictMode,
},
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum ConflictMode {
Ours,
Theirs,
}
pub fn parse<R>(buf_in: R, paops: &mut ParseOps) -> Result<TextTree>
where
R: BufRead,
{
if paops.max_depth != 0 && paops.runtime.level > paops.max_depth {
return Err(Error::Msg("Maximum recursion depth!".into()));
}
let mut text = Vec::new();
let mut lineno = 0;
let mut pstack: Vec<Frame> = Vec::new();
for line_in in buf_in.lines() {
let line = line_in?;
lineno += 1;
if !line.trim_start().starts_with(&paops.separators.left) {
if let Some(TextNode::Plain(last)) = text.last_mut() {
last.push('\n');
last.push_str(&line);
continue;
}
text.push(TextNode::Plain(line.clone()));
continue;
}
let trimmed = line.trim();
let after_left = trimmed
.strip_prefix(&paops.separators.left)
.unwrap_or(trimmed);
let inner = match after_left.strip_suffix(&paops.separators.right) {
Some(s) => s,
None => {
return Err(parse_error(
paops,
lineno,
&line,
format!("Right separator '{}' missing.", paops.separators.right),
));
}
};
let mut parts = inner.split_whitespace();
let kw = match parts.next() {
Some(k) => k,
None => continue,
};
let parsed = match Command::from_keyword(kw) {
Some(c) => c,
None => {
return Err(parse_error(
paops,
lineno,
&line,
format!("Unknown section '{}'.", kw),
));
}
};
let rest: Vec<&str> = parts.collect();
match parsed {
Command::Data => parse_data(&rest, &line, lineno, paops, &mut text)?,
Command::Begin => parse_begin(&rest, &line, lineno, paops, &mut pstack, &mut text)?,
Command::Encrypted => {
parse_encrypted(&rest, &line, lineno, paops, &mut pstack, &mut text)?
}
Command::End => parse_end(&rest, &line, lineno, paops, &mut pstack, &mut text)?,
Command::Stored => parse_stored(&rest, &line, lineno, paops, &mut text)?,
Command::Chain => parse_chain(&rest, &line, lineno, paops, &mut text)?,
Command::Include => parse_include(&rest, &line, lineno, paops, &mut text)?,
Command::Conflict => {
parse_conflict(&rest, &line, lineno, paops, &mut pstack, &mut text)?
}
Command::Ours => parse_ours(&line, lineno, paops, &mut pstack, &mut text)?,
Command::Theirs => parse_theirs(&line, lineno, paops, &mut pstack, &mut text)?,
}
}
if !pstack.is_empty() {
for top in pstack.into_iter().rev() {
match top {
Frame::BeginEnd { keyw, .. } => {
eprintln!("Parse: BEGIN {} without END.", keyw);
}
Frame::Encrypted { keyw, .. } => {
eprintln!("Parse: ENCRYPTED {} without END.", keyw);
}
Frame::Conflict { keyw, .. } => {
eprintln!("Parse: CONFLICT {} without END.", keyw);
}
}
}
return Err(Error::Parse {
file: paops.runtime.fname.clone(),
lineno: 0,
msg: "Unclosed section".into(),
});
}
Ok(text)
}
fn parse_data(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &ParseOps,
text: &mut Vec<TextNode>,
) -> Result<()> {
for tok in cmd {
let mut data = match utils::base64_decode(tok) {
Ok(d) => d,
Err(e) => {
return Err(parse_error(
paops,
lineno,
line,
format!("Error decoding base64 in '{}': {}", tok, e),
));
}
};
if let Some(TextNode::Data(last)) = text.last_mut() {
last.append(&mut data);
} else {
text.push(TextNode::Data(data));
}
}
Ok(())
}
fn parse_begin(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &mut ParseOps,
pstack: &mut Vec<Frame>,
text: &mut Vec<TextNode>,
) -> Result<()> {
if cmd.len() != 1 {
return Err(parse_error(
paops,
lineno,
line,
"BEGIN needs a single keyword.",
));
}
paops.runtime.level += 1;
pstack.push(Frame::BeginEnd {
keyw: cmd[0].to_owned(),
outer: std::mem::take(text),
});
Ok(())
}
pub(crate) fn parse_encrypted_extfields(
cmd: &[&str],
paops: &ParseOps,
lineno: i32,
line: &str,
) -> Result<BTreeMap<String, String>> {
let mut extfields: BTreeMap<String, String> = BTreeMap::new();
for field in cmd.iter().rev() {
if field.find(':').is_none() {
break;
}
let (key, value) = field.split_once(':').unwrap();
if extfields.contains_key(key) {
return Err(parse_error(
paops,
lineno,
line,
format!("Duplicate extended field '{}'", key),
));
}
extfields.insert(key.to_string(), value.to_string());
}
Ok(extfields)
}
fn parse_encrypted(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &mut ParseOps,
pstack: &mut Vec<Frame>,
text: &mut Vec<TextNode>,
) -> Result<()> {
let extfields = parse_encrypted_extfields(cmd, paops, lineno, line)?;
let param_count = cmd.len() - extfields.len();
let extfield_keys: HashSet<String> = extfields.keys().cloned().collect();
let known_extfields: HashSet<String> = ["pbkdf".to_string(), "cipher".to_string()]
.into_iter()
.collect();
if extfield_keys.difference(&known_extfields).next().is_some() {
eprintln!("Warning: Unrecognized extended field(s) present");
}
match param_count {
1 => {
paops.runtime.level += 1;
pstack.push(Frame::Encrypted {
keyw: cmd[0].to_owned(),
outer: std::mem::take(text),
extfields,
});
Ok(())
}
2 => {
if cmd[1].len() != 64 {
return Err(parse_error(paops, lineno, line, "Invalid CAS identifier"));
}
let node = vec![TextNode::Stored {
keyw: "ct".to_string(),
cas: cmd[1].to_string(),
}];
text.push(TextNode::Encrypted {
keyw: cmd[0].to_string(),
txt: node,
extfields,
});
Ok(())
}
_ => Err(parse_error(
paops,
lineno,
line,
format!(
"ENCRYPTED has wrong number of parameters ({}).",
param_count
),
)),
}
}
fn parse_end(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &mut ParseOps,
pstack: &mut Vec<Frame>,
text: &mut Vec<TextNode>,
) -> Result<()> {
if cmd.len() > 1 {
return Err(parse_error(paops, lineno, line, "Unknown padding in END."));
}
match pstack.pop() {
Some(Frame::BeginEnd { keyw, outer }) => {
if !cmd.is_empty() && keyw != cmd[0] {
return Err(parse_error(
paops,
lineno,
line,
format!("END mismatch (expected '{}').", keyw),
));
}
let node = TextNode::BeginEnd {
keyw,
txt: std::mem::take(text),
};
*text = outer;
text.push(node);
paops.runtime.level -= 1;
Ok(())
}
Some(Frame::Encrypted {
keyw,
outer,
extfields,
}) => {
if keyw != cmd[0] {
return Err(parse_error(
paops,
lineno,
line,
format!("END mismatch (expected '{}').", keyw),
));
}
if text.len() != 1 {
return Err(parse_error(
paops,
lineno,
line,
format!(
"{} elements in encrypted {} (must be a single DATA or STORED).",
text.len(),
keyw
),
));
}
match text[0] {
TextNode::Data(_) | TextNode::Stored { .. } => {
let node = TextNode::Encrypted {
keyw,
txt: std::mem::take(text),
extfields,
};
*text = outer;
text.push(node);
paops.runtime.level -= 1;
Ok(())
}
_ => Err(parse_error(
paops,
lineno,
line,
format!("Not DATA or STORED element in encrypted {}.", keyw),
)),
}
}
Some(Frame::Conflict {
keyw,
outer,
ours,
mode,
}) => {
if !cmd.is_empty() && keyw != cmd[0] {
return Err(parse_error(
paops,
lineno,
line,
format!("END mismatch (expected '{}').", keyw),
));
}
let (ours, theirs) = match mode {
ConflictMode::Ours => (std::mem::take(text), ours),
ConflictMode::Theirs => (ours, std::mem::take(text)),
};
let node = TextNode::Conflict { keyw, ours, theirs };
*text = outer;
text.push(node);
paops.runtime.level -= 1;
Ok(())
}
None => Err(parse_error(
paops,
lineno,
line,
"END without a start clause.",
)),
}
}
fn parse_stored(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &ParseOps,
text: &mut Vec<TextNode>,
) -> Result<()> {
if cmd.len() != 2 {
return Err(parse_error(
paops,
lineno,
line,
"STORED needs two parameters.",
));
}
text.push(TextNode::Stored {
keyw: cmd[0].to_owned(),
cas: cmd[1].to_owned(),
});
Ok(())
}
fn parse_chain(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &ParseOps,
text: &mut Vec<TextNode>,
) -> Result<()> {
let extfields = parse_encrypted_extfields(cmd, paops, lineno, line)?;
if extfields.is_empty() {
return Err(parse_error(
paops,
lineno,
line,
"CHAIN needs at least one key:value field (parents / signer / payload / sig).",
));
}
text.push(TextNode::Chain { extfields });
Ok(())
}
fn parse_include(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &ParseOps,
text: &mut Vec<TextNode>,
) -> Result<()> {
if cmd.len() != 1 {
return Err(parse_error(
paops,
lineno,
line,
"INCLUDE needs exactly one hash parameter.",
));
}
text.push(TextNode::Include {
hash: cmd[0].to_owned(),
});
Ok(())
}
fn parse_conflict(
cmd: &[&str],
line: &str,
lineno: i32,
paops: &mut ParseOps,
pstack: &mut Vec<Frame>,
text: &mut Vec<TextNode>,
) -> Result<()> {
if cmd.len() != 1 {
return Err(parse_error(
paops,
lineno,
line,
"CONFLICT needs a single keyword.",
));
}
paops.runtime.level += 1;
pstack.push(Frame::Conflict {
keyw: cmd[0].to_owned(),
outer: std::mem::take(text),
ours: Vec::new(),
mode: ConflictMode::Ours,
});
Ok(())
}
fn parse_ours(
line: &str,
lineno: i32,
paops: &mut ParseOps,
pstack: &mut [Frame],
text: &mut Vec<TextNode>,
) -> Result<()> {
let Some(last) = pstack.last_mut() else {
return Err(parse_error(
paops,
lineno,
line,
"OURS outside of CONFLICT block.",
));
};
let Frame::Conflict { ours, mode, .. } = last else {
return Err(parse_error(
paops,
lineno,
line,
"OURS inside non-CONFLICT block.",
));
};
if *mode == ConflictMode::Theirs {
return Err(parse_error(
paops,
lineno,
line,
"OURS after THEIRS in CONFLICT block.",
));
}
ours.append(text);
*mode = ConflictMode::Ours;
Ok(())
}
fn parse_theirs(
line: &str,
lineno: i32,
paops: &mut ParseOps,
pstack: &mut [Frame],
text: &mut Vec<TextNode>,
) -> Result<()> {
let Some(last) = pstack.last_mut() else {
return Err(parse_error(
paops,
lineno,
line,
"THEIRS outside of CONFLICT block.",
));
};
let Frame::Conflict { ours, mode, .. } = last else {
return Err(parse_error(
paops,
lineno,
line,
"THEIRS inside non-CONFLICT block.",
));
};
if *mode == ConflictMode::Theirs {
return Err(parse_error(
paops,
lineno,
line,
"THEIRS after THEIRS in CONFLICT block.",
));
}
ours.append(text);
*mode = ConflictMode::Theirs;
Ok(())
}