use alloc::{
borrow::{Cow, ToOwned},
format,
string::{String, ToString},
vec::Vec,
};
use crate::{
component::IcalComponentKind,
param::IcalParam,
prop::{IcalPropKind, IcalPropName},
tree::{
cst::{IcalCst, IcalItem},
line::IcalLine,
value::IcalValueCursor,
},
value::IcalValue,
version::IcalVersion,
};
pub struct IcalMerge<'m, 'a> {
pub base: &'m IcalCst<'a>,
pub left: &'m IcalCst<'a>,
pub right: &'m IcalCst<'a>,
pub right_speaks_for: Option<Cow<'a, str>>,
}
impl<'a> IcalMerge<'_, 'a> {
pub fn merge(self) -> IcalMergeReport<'a> {
let version = self.base.version();
let base = nodes(self.base);
let left = nodes(self.left);
let right = nodes(self.right);
let left_ops = diff(&base, &left, version);
let right_ops = diff(&base, &right, version);
let mut merged = self.left.clone();
let mut conflicts = Vec::new();
for op in &right_ops {
let verdict = self.judge(op, &left_ops, &base, &left);
if verdict.applies {
apply(&mut merged, op, self.right);
}
if let Some(reason) = verdict.reason {
conflicts.push(IcalMergeConflict {
right: op.action.clone(),
reason,
});
}
}
IcalMergeReport {
merged,
left: left_ops.into_iter().map(|op| op.action).collect(),
right: right_ops.into_iter().map(|op| op.action).collect(),
conflicts,
}
}
fn judge(
&self,
op: &Op<'a>,
left_ops: &[Op<'a>],
base: &[Node<'_, 'a>],
left: &[Node<'_, 'a>],
) -> Verdict<'a> {
if let Some(speaker) = &self.right_speaks_for
&& op.organiser_owned
&& organiser_of(op.path(), base, left).is_some_and(|held| held != *speaker)
{
return Verdict {
applies: false,
reason: Some(IcalMergeReason::Authority),
};
}
if let Some(collision) = left_ops.iter().find(|left| collides(left, op)) {
let applies = collision.action.is_removal() && !op.action.is_removal();
return Verdict {
applies,
reason: Some(IcalMergeReason::Divergent(collision.action.clone())),
};
}
Verdict {
applies: true,
reason: left_ops
.iter()
.find(|left| across_the_series(left, op))
.map(|left| IcalMergeReason::Recurrence(left.action.clone())),
}
}
}
struct Verdict<'a> {
applies: bool,
reason: Option<IcalMergeReason<'a>>,
}
#[derive(Clone, Debug)]
pub struct IcalMergeReport<'a> {
pub merged: IcalCst<'a>,
pub left: Vec<IcalMergeAction<'a>>,
pub right: Vec<IcalMergeAction<'a>>,
pub conflicts: Vec<IcalMergeConflict<'a>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IcalMergeConflict<'a> {
pub right: IcalMergeAction<'a>,
pub reason: IcalMergeReason<'a>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IcalMergeReason<'a> {
Divergent(IcalMergeAction<'a>),
Recurrence(IcalMergeAction<'a>),
Authority,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IcalComponentPath<'a>(pub Vec<IcalComponentStep<'a>>);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IcalComponentStep<'a> {
pub name: Cow<'a, str>,
pub key: Cow<'a, str>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IcalPropPath<'a> {
pub component: IcalComponentPath<'a>,
pub name: Cow<'a, str>,
pub index: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IcalMergeAction<'a> {
ComponentAdded {
at: IcalComponentPath<'a>,
},
ComponentRemoved {
at: IcalComponentPath<'a>,
},
PropAdded {
at: IcalPropPath<'a>,
value: IcalValue<'a>,
},
PropRemoved {
at: IcalPropPath<'a>,
value: IcalValue<'a>,
},
ValueChanged {
at: IcalPropPath<'a>,
old: IcalValue<'a>,
new: IcalValue<'a>,
},
ValueItemAdded {
at: IcalPropPath<'a>,
item: Cow<'a, str>,
},
ValueItemRemoved {
at: IcalPropPath<'a>,
item: Cow<'a, str>,
},
ParamAdded {
at: IcalPropPath<'a>,
param: IcalParam<'a>,
},
ParamRemoved {
at: IcalPropPath<'a>,
param: IcalParam<'a>,
},
ParamChanged {
at: IcalPropPath<'a>,
old: IcalParam<'a>,
new: IcalParam<'a>,
},
}
impl IcalMergeAction<'_> {
fn is_removal(&self) -> bool {
matches!(
self,
Self::ComponentRemoved { .. }
| Self::PropRemoved { .. }
| Self::ValueItemRemoved { .. }
| Self::ParamRemoved { .. }
)
}
}
struct Op<'a> {
action: IcalMergeAction<'a>,
slot: Slot,
organiser_owned: bool,
}
impl<'a> Op<'a> {
fn path(&self) -> &IcalComponentPath<'a> {
match &self.action {
IcalMergeAction::ComponentAdded { at } | IcalMergeAction::ComponentRemoved { at } => at,
IcalMergeAction::PropAdded { at, .. }
| IcalMergeAction::PropRemoved { at, .. }
| IcalMergeAction::ValueChanged { at, .. }
| IcalMergeAction::ValueItemAdded { at, .. }
| IcalMergeAction::ValueItemRemoved { at, .. }
| IcalMergeAction::ParamAdded { at, .. }
| IcalMergeAction::ParamRemoved { at, .. }
| IcalMergeAction::ParamChanged { at, .. } => &at.component,
}
}
fn prop(&self) -> Option<&IcalPropPath<'a>> {
match &self.action {
IcalMergeAction::ComponentAdded { .. } | IcalMergeAction::ComponentRemoved { .. } => {
None
}
IcalMergeAction::PropAdded { at, .. }
| IcalMergeAction::PropRemoved { at, .. }
| IcalMergeAction::ValueChanged { at, .. }
| IcalMergeAction::ValueItemAdded { at, .. }
| IcalMergeAction::ValueItemRemoved { at, .. }
| IcalMergeAction::ParamAdded { at, .. }
| IcalMergeAction::ParamRemoved { at, .. }
| IcalMergeAction::ParamChanged { at, .. } => Some(at),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Slot {
Component,
Prop,
Value,
Items,
Param(String),
}
fn collides(left: &Op<'_>, right: &Op<'_>) -> bool {
if left.path() != right.path() {
return false;
}
match (&left.slot, &right.slot) {
(Slot::Component, Slot::Component) => true,
(Slot::Component, _) | (_, Slot::Component) => true,
_ if left.prop() != right.prop() => false,
(Slot::Items, _) | (_, Slot::Items) => false,
(Slot::Param(left), Slot::Param(right)) => left == right,
(Slot::Param(_), _) | (_, Slot::Param(_)) => false,
_ => true,
}
}
fn across_the_series(left: &Op<'_>, right: &Op<'_>) -> bool {
let (Some(left), Some(right)) = (left.path().0.last(), right.path().0.last()) else {
return false;
};
let (Some(left_uid), Some(right_uid)) =
(left.key.split('/').next(), right.key.split('/').next())
else {
return false;
};
left.name == right.name
&& left_uid == right_uid
&& left.key.contains('/') != right.key.contains('/')
}
struct Node<'c, 'a> {
path: IcalComponentPath<'a>,
cst: &'c IcalCst<'a>,
}
fn nodes<'c, 'a>(cst: &'c IcalCst<'a>) -> Vec<Node<'c, 'a>> {
let mut out = Vec::new();
walk(cst, IcalComponentPath::default(), &mut out);
out
}
fn walk<'c, 'a>(cst: &'c IcalCst<'a>, path: IcalComponentPath<'a>, out: &mut Vec<Node<'c, 'a>>) {
out.push(Node {
path: path.clone(),
cst,
});
let mut seen: Vec<(String, usize)> = Vec::new();
for child in components(cst) {
let name = component_name(child);
let ordinal = match seen.iter_mut().find(|(held, _)| *held == name) {
Some((_, count)) => {
*count += 1;
*count
}
None => {
seen.push((name.clone(), 0));
0
}
};
let mut nested = path.clone();
nested.0.push(IcalComponentStep {
key: Cow::Owned(key(child, ordinal)),
name: Cow::Owned(name),
});
walk(child, nested, out);
}
}
fn components<'c, 'a>(cst: &'c IcalCst<'a>) -> impl Iterator<Item = &'c IcalCst<'a>> {
cst.items.iter().filter_map(|item| match item {
IcalItem::Component(child) => Some(&**child),
_ => None,
})
}
fn component_name(cst: &IcalCst<'_>) -> String {
cst.begin
.as_ref()
.map(|begin| begin.raw_value_str().to_ascii_uppercase())
.unwrap_or_default()
}
fn key(cst: &IcalCst<'_>, ordinal: usize) -> String {
let Some(uid) = raw(cst, IcalPropKind::Uid) else {
return ordinal.to_string();
};
match raw(cst, IcalPropKind::RecurrenceId) {
Some(id) => format!("{uid}/{id}"),
None => uid,
}
}
fn raw(cst: &IcalCst<'_>, kind: IcalPropKind) -> Option<String> {
lines(cst)
.find(|line| line.name.get().eq_ignore_ascii_case(&kind))
.map(|line| line.raw_value_str().into_owned())
}
fn lines<'c, 'a>(cst: &'c IcalCst<'a>) -> impl Iterator<Item = &'c IcalLine<'a>> {
cst.items.iter().filter_map(|item| match item {
IcalItem::Prop(line) => Some(line),
_ => None,
})
}
fn organiser_of<'a>(
path: &IcalComponentPath<'a>,
base: &[Node<'_, 'a>],
left: &[Node<'_, 'a>],
) -> Option<String> {
base.iter()
.chain(left)
.find(|node| node.path == *path)
.and_then(|node| raw(node.cst, IcalPropKind::Organizer))
}
fn whole_component_owned(path: &IcalComponentPath<'_>) -> bool {
!path
.0
.last()
.is_some_and(|step| matches!(step.name.parse(), Ok(IcalComponentKind::VAlarm)))
}
fn organiser_owned(component: &IcalComponentPath<'_>, name: &IcalPropName<'_>) -> bool {
let scheduled = component.0.last().is_some_and(|step| {
matches!(
step.name.parse(),
Ok(IcalComponentKind::VEvent | IcalComponentKind::VTodo | IcalComponentKind::VJournal)
)
});
let IcalPropName::Kind(kind) = name else {
return false;
};
scheduled
&& !matches!(
kind,
IcalPropKind::Attendee | IcalPropKind::Transp | IcalPropKind::DtStamp
)
}
fn diff<'a>(base: &[Node<'_, 'a>], side: &[Node<'_, 'a>], version: IcalVersion) -> Vec<Op<'a>> {
let mut ops = Vec::new();
for node in base {
if !side.iter().any(|held| held.path == node.path) && !removed_above(&node.path, side, base)
{
ops.push(Op {
action: IcalMergeAction::ComponentRemoved {
at: node.path.clone(),
},
slot: Slot::Component,
organiser_owned: whole_component_owned(&node.path),
});
}
}
for node in side {
if !base.iter().any(|held| held.path == node.path) && !added_above(&node.path, side, base) {
ops.push(Op {
action: IcalMergeAction::ComponentAdded {
at: node.path.clone(),
},
slot: Slot::Component,
organiser_owned: whole_component_owned(&node.path),
});
}
}
for node in base {
let Some(held) = side.iter().find(|held| held.path == node.path) else {
continue;
};
diff_component(node, held, version, &mut ops);
}
ops
}
fn removed_above(
path: &IcalComponentPath<'_>,
side: &[Node<'_, '_>],
base: &[Node<'_, '_>],
) -> bool {
ancestors(path).any(|above| {
base.iter().any(|node| node.path == above) && !side.iter().any(|node| node.path == above)
})
}
fn added_above(path: &IcalComponentPath<'_>, side: &[Node<'_, '_>], base: &[Node<'_, '_>]) -> bool {
ancestors(path).any(|above| {
side.iter().any(|node| node.path == above) && !base.iter().any(|node| node.path == above)
})
}
fn ancestors<'p, 'a>(
path: &'p IcalComponentPath<'a>,
) -> impl Iterator<Item = IcalComponentPath<'a>> + 'p {
(1..path.0.len()).map(|depth| IcalComponentPath(path.0[..depth].to_vec()))
}
fn diff_component<'a>(
base: &Node<'_, 'a>,
side: &Node<'_, 'a>,
version: IcalVersion,
ops: &mut Vec<Op<'a>>,
) {
let base_props: Vec<&IcalLine<'a>> = lines(base.cst).collect();
let side_props: Vec<&IcalLine<'a>> = lines(side.cst).collect();
let mut names: Vec<String> = Vec::new();
for line in base_props.iter().chain(&side_props) {
let name = line.name.get().to_ascii_uppercase();
if !names.contains(&name) {
names.push(name);
}
}
for name in names {
let of = |lines: &[&IcalLine<'a>]| -> Vec<usize> {
lines
.iter()
.enumerate()
.filter(|(_, line)| line.name.get().eq_ignore_ascii_case(&name))
.map(|(index, _)| index)
.collect()
};
let mut base_free = of(&base_props);
let mut side_free = of(&side_props);
let mut pairs = Vec::new();
let mut b = 0;
while b < base_free.len() {
let same = side_free.iter().position(|&s| {
base_props[base_free[b]].decode(version) == side_props[s].decode(version)
});
match same {
Some(s) => pairs.push((base_free.remove(b), side_free.remove(s))),
None => b += 1,
}
}
while !base_free.is_empty() && !side_free.is_empty() {
pairs.push((base_free.remove(0), side_free.remove(0)));
}
for index in base_free {
let line = base_props[index];
let at = prop_path(&base.path, &base_props, index);
ops.push(Op {
organiser_owned: organiser_owned(&base.path, &decode_name(line)),
action: IcalMergeAction::PropRemoved {
value: line.decode(version).value.into_owned(),
at,
},
slot: Slot::Prop,
});
}
for index in side_free {
let line = side_props[index];
let at = prop_path(&side.path, &side_props, index);
ops.push(Op {
organiser_owned: organiser_owned(&side.path, &decode_name(line)),
action: IcalMergeAction::PropAdded {
value: line.decode(version).value.into_owned(),
at,
},
slot: Slot::Prop,
});
}
for (b, s) in pairs {
diff_prop(&base.path, &base_props, b, side_props[s], version, ops);
}
}
}
fn decode_name<'a>(line: &IcalLine<'a>) -> IcalPropName<'a> {
IcalPropName::from(Cow::Owned(line.name.get().to_owned()))
}
fn prop_path<'a>(
component: &IcalComponentPath<'a>,
lines: &[&IcalLine<'a>],
at: usize,
) -> IcalPropPath<'a> {
let name = lines[at].name.get();
let index = lines[..at]
.iter()
.filter(|held| held.name.get().eq_ignore_ascii_case(name))
.count();
IcalPropPath {
component: component.clone(),
name: Cow::Owned(name.to_owned()),
index,
}
}
fn diff_prop<'a>(
component: &IcalComponentPath<'a>,
lines: &[&IcalLine<'a>],
at: usize,
side: &IcalLine<'a>,
version: IcalVersion,
ops: &mut Vec<Op<'a>>,
) {
let base = lines[at];
let at = prop_path(component, lines, at);
let owned = organiser_owned(component, &decode_name(base));
let base_prop = base.decode(version);
let side_prop = side.decode(version);
for param in &base_prop.params {
let name = param_name(param);
let held = side_prop
.params
.iter()
.find(|held| param_name(held) == name);
let action = match held {
None => IcalMergeAction::ParamRemoved {
at: at.clone(),
param: param.clone().into_owned(),
},
Some(held) if held != param => IcalMergeAction::ParamChanged {
at: at.clone(),
old: param.clone().into_owned(),
new: held.clone().into_owned(),
},
Some(_) => continue,
};
ops.push(Op {
action,
slot: Slot::Param(name),
organiser_owned: owned,
});
}
for param in &side_prop.params {
let name = param_name(param);
if base_prop.params.iter().any(|held| param_name(held) == name) {
continue;
}
ops.push(Op {
action: IcalMergeAction::ParamAdded {
at: at.clone(),
param: param.clone().into_owned(),
},
slot: Slot::Param(name),
organiser_owned: owned,
});
}
if base_prop.value == side_prop.value {
return;
}
match (&base_prop.value, &side_prop.value) {
(IcalValue::TextList(old), IcalValue::TextList(new)) => {
list_ops(&at, &old.0, &new.0, owned, ops)
}
(IcalValue::DateTimeList(old), IcalValue::DateTimeList(new)) => {
list_ops(&at, &old.0, &new.0, owned, ops)
}
(old, new) => ops.push(Op {
action: IcalMergeAction::ValueChanged {
at,
old: old.clone().into_owned(),
new: new.clone().into_owned(),
},
slot: Slot::Value,
organiser_owned: owned,
}),
}
}
fn list_ops<'a>(
at: &IcalPropPath<'a>,
old: &[Cow<'_, str>],
new: &[Cow<'_, str>],
owned: bool,
ops: &mut Vec<Op<'a>>,
) {
let removed = old.iter().filter(|item| !new.contains(item));
let added = new.iter().filter(|item| !old.contains(item));
for item in removed {
ops.push(Op {
action: IcalMergeAction::ValueItemRemoved {
at: at.clone(),
item: Cow::Owned(item.to_string()),
},
slot: Slot::Items,
organiser_owned: owned,
});
}
for item in added {
ops.push(Op {
action: IcalMergeAction::ValueItemAdded {
at: at.clone(),
item: Cow::Owned(item.to_string()),
},
slot: Slot::Items,
organiser_owned: owned,
});
}
}
fn param_name(param: &IcalParam<'_>) -> String {
match param {
IcalParam::Unknown { name, .. } => name.to_ascii_uppercase(),
known => known
.kind()
.map(|kind| kind.to_ascii_uppercase())
.unwrap_or_default(),
}
}
fn apply<'a>(merged: &mut IcalCst<'a>, op: &Op<'a>, right: &IcalCst<'a>) {
match &op.action {
IcalMergeAction::ComponentAdded { at } => {
let (Some(source), Some(target)) = (find(right, at), find_mut(merged, &parent(at)))
else {
return;
};
target
.items
.push(IcalItem::Component(alloc::boxed::Box::new(source.clone())));
}
IcalMergeAction::ComponentRemoved { at } => {
let (Some(step), Some(target)) = (at.0.last(), find_mut(merged, &parent(at))) else {
return;
};
let step = step.clone();
let mut ordinal = 0;
target.items.retain(|item| {
let IcalItem::Component(child) = item else {
return true;
};
if component_name(child) != step.name {
return true;
}
let held = key(child, ordinal);
ordinal += 1;
held != step.key
});
}
action => apply_to_line(merged, action, right),
}
}
fn apply_to_line<'a>(merged: &mut IcalCst<'a>, action: &IcalMergeAction<'a>, right: &IcalCst<'a>) {
let Some(at) = prop_path_of(action) else {
return;
};
let Some(component) = find_mut(merged, &at.component) else {
return;
};
if let IcalMergeAction::PropAdded { .. } = action {
if let Some(line) = find(right, &at.component).and_then(|cst| nth_line(cst, at)) {
component.items.push(IcalItem::Prop(line.clone()));
}
return;
}
if let IcalMergeAction::PropRemoved { .. } = action {
let mut index = 0;
let name = at.name.clone();
let nth = at.index;
component.items.retain(|item| {
let IcalItem::Prop(line) = item else {
return true;
};
if !line.name.get().eq_ignore_ascii_case(&name) {
return true;
}
let held = index;
index += 1;
held != nth
});
return;
}
let Some(source) = find(right, &at.component).and_then(|cst| nth_line(cst, at)) else {
return;
};
if nth_line_mut(component, at).is_none() {
component.items.push(IcalItem::Prop(source.clone()));
return;
}
let Some(line) = nth_line_mut(component, at) else {
return;
};
match action {
IcalMergeAction::ValueChanged { .. } => line.value = source.value.clone(),
IcalMergeAction::ValueItemAdded { item, .. } => {
let mut items: Vec<String> = list(line);
if !items.iter().any(|held| held == item) {
items.push(item.to_string());
}
set_list(line, &items);
}
IcalMergeAction::ValueItemRemoved { item, .. } => {
let kept: Vec<String> = list(line).into_iter().filter(|held| held != item).collect();
set_list(line, &kept);
}
IcalMergeAction::ParamRemoved { param, .. } => {
let name = param_name(param);
line.params
.retain(|held| held.name.get().to_ascii_uppercase() != name);
}
IcalMergeAction::ParamAdded { param, .. }
| IcalMergeAction::ParamChanged { new: param, .. } => {
let name = param_name(param);
let encoded = param.encode();
match line
.params
.iter_mut()
.find(|held| held.name.get().to_ascii_uppercase() == name)
{
Some(held) => *held = encoded,
None => line.params.push(encoded),
}
}
_ => {}
}
}
fn list(line: &mut IcalLine<'_>) -> Vec<String> {
IcalValueCursor { line }
.list()
.into_iter()
.map(Cow::into_owned)
.collect()
}
fn set_list(line: &mut IcalLine<'_>, items: &[String]) {
IcalValueCursor { line }.set_list(items);
}
fn prop_path_of<'p, 'a>(action: &'p IcalMergeAction<'a>) -> Option<&'p IcalPropPath<'a>> {
match action {
IcalMergeAction::ComponentAdded { .. } | IcalMergeAction::ComponentRemoved { .. } => None,
IcalMergeAction::PropAdded { at, .. }
| IcalMergeAction::PropRemoved { at, .. }
| IcalMergeAction::ValueChanged { at, .. }
| IcalMergeAction::ValueItemAdded { at, .. }
| IcalMergeAction::ValueItemRemoved { at, .. }
| IcalMergeAction::ParamAdded { at, .. }
| IcalMergeAction::ParamRemoved { at, .. }
| IcalMergeAction::ParamChanged { at, .. } => Some(at),
}
}
fn parent<'a>(path: &IcalComponentPath<'a>) -> IcalComponentPath<'a> {
let mut parent = path.clone();
parent.0.pop();
parent
}
fn find<'c, 'a>(cst: &'c IcalCst<'a>, path: &IcalComponentPath<'a>) -> Option<&'c IcalCst<'a>> {
let mut held = cst;
for step in &path.0 {
held = components(held)
.enumerate()
.find(|(ordinal, child)| {
component_name(child) == step.name && key(child, *ordinal) == step.key
})
.map(|(_, child)| child)?;
}
Some(held)
}
fn find_mut<'c, 'a>(
cst: &'c mut IcalCst<'a>,
path: &IcalComponentPath<'a>,
) -> Option<&'c mut IcalCst<'a>> {
let mut held = cst;
for step in &path.0 {
let mut ordinal = 0;
held = held.items.iter_mut().find_map(|item| {
let IcalItem::Component(child) = item else {
return None;
};
if component_name(child) != step.name {
return None;
}
let matched = key(child, ordinal) == step.key;
ordinal += 1;
matched.then_some(&mut **child)
})?;
}
Some(held)
}
fn nth_line<'c, 'a>(cst: &'c IcalCst<'a>, at: &IcalPropPath<'a>) -> Option<&'c IcalLine<'a>> {
lines(cst)
.filter(|line| line.name.get().eq_ignore_ascii_case(&at.name))
.nth(at.index)
}
fn nth_line_mut<'c, 'a>(
cst: &'c mut IcalCst<'a>,
at: &IcalPropPath<'a>,
) -> Option<&'c mut IcalLine<'a>> {
cst.items
.iter_mut()
.filter_map(|item| match item {
IcalItem::Prop(line) => Some(line),
_ => None,
})
.filter(|line| line.name.get().eq_ignore_ascii_case(&at.name))
.nth(at.index)
}