use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryKind {
File,
Symlink,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Entry {
pub kind: EntryKind,
pub hash: String,
pub size: u64,
pub executable: bool,
}
impl Entry {
pub fn read(path: &Path) -> Result<Option<Self>, String> {
let meta = match std::fs::symlink_metadata(path) {
Ok(meta) => meta,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("cannot look at {}: {e}", path.display())),
};
if meta.is_symlink() {
let target = std::fs::read_link(path)
.map_err(|e| format!("cannot read the link {}: {e}", path.display()))?;
let text = super::scan::to_slash(&target);
return Ok(Some(Self {
kind: EntryKind::Symlink,
hash: hash_bytes(text.as_bytes()),
size: text.len() as u64,
executable: false,
}));
}
if meta.is_dir() {
return Ok(None);
}
let bytes = std::fs::read(path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
Ok(Some(Self {
kind: EntryKind::File,
hash: hash_bytes(&bytes),
size: bytes.len() as u64,
executable: is_executable(&meta),
}))
}
pub fn same_as(&self, other: &Self) -> bool {
self.kind == other.kind && self.hash == other.hash && self.executable == other.executable
}
}
pub fn hash_bytes(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(bytes);
format!("{:x}", hasher.finalize())
}
#[cfg(unix)]
fn is_executable(meta: &std::fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt;
meta.permissions().mode() & 0o100 != 0
}
#[cfg(windows)]
fn is_executable(_meta: &std::fs::Metadata) -> bool {
false
}
pub type Baseline = BTreeMap<String, Entry>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Action {
Send { path: String, entry: Entry },
Receive { path: String, entry: Entry },
SendDeletion { path: String },
ReceiveDeletion { path: String },
Conflict {
path: String,
here: Option<Entry>,
there: Option<Entry>,
base: Option<Entry>,
},
Agree { path: String, entry: Option<Entry> },
}
impl Action {
pub fn path(&self) -> &str {
match self {
Self::Send { path, .. }
| Self::Receive { path, .. }
| Self::SendDeletion { path }
| Self::ReceiveDeletion { path }
| Self::Conflict { path, .. }
| Self::Agree { path, .. } => path,
}
}
pub fn kind(&self) -> &'static str {
match self {
Self::Send { .. } => "send",
Self::Receive { .. } => "receive",
Self::SendDeletion { .. } => "send_deletion",
Self::ReceiveDeletion { .. } => "receive_deletion",
Self::Conflict { .. } => "conflict",
Self::Agree { .. } => "agree",
}
}
pub fn is_conflict(&self) -> bool {
matches!(self, Self::Conflict { .. })
}
pub fn moves_data(&self) -> bool {
matches!(
self,
Self::Send { .. }
| Self::Receive { .. }
| Self::SendDeletion { .. }
| Self::ReceiveDeletion { .. }
)
}
pub fn arrives_here(&self) -> bool {
matches!(self, Self::Receive { .. } | Self::ReceiveDeletion { .. })
}
pub fn leaves_here(&self) -> bool {
matches!(self, Self::Send { .. } | Self::SendDeletion { .. })
}
}
pub fn crosses(action: &Action, continuity: crate::peer::model::Continuity, decided: bool) -> bool {
if matches!(action, Action::Agree { .. }) {
return true;
}
if !continuity.carries_decided() {
return false;
}
if decided {
return true;
}
action.arrives_here() && continuity.takes_undecided()
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Convergence {
pub actions: Vec<Action>,
}
impl Convergence {
pub fn conflicts(&self) -> impl Iterator<Item = &Action> {
self.actions.iter().filter(|action| action.is_conflict())
}
pub fn pending(&self) -> usize {
self.actions.iter().filter(|a| a.moves_data()).count()
}
pub fn is_converged(&self) -> bool {
!self.actions.iter().any(|action| action.moves_data() || action.is_conflict())
}
}
pub fn plan(here: &BTreeMap<String, Entry>, there: &BTreeMap<String, Entry>, base: &Baseline) -> Convergence {
let paths: BTreeSet<&String> = here.keys().chain(there.keys()).chain(base.keys()).collect();
let mut actions = Vec::new();
for path in paths {
let mine = here.get(path);
let theirs = there.get(path);
let based = base.get(path);
let changed_here = !same(mine, based);
let changed_there = !same(theirs, based);
let action = match (changed_here, changed_there) {
(false, false) => continue,
(true, false) => match mine {
Some(entry) => Action::Send {
path: path.clone(),
entry: entry.clone(),
},
None => Action::SendDeletion { path: path.clone() },
},
(false, true) => match theirs {
Some(entry) => Action::Receive {
path: path.clone(),
entry: entry.clone(),
},
None => Action::ReceiveDeletion { path: path.clone() },
},
(true, true) => {
if same(mine, theirs) {
Action::Agree {
path: path.clone(),
entry: mine.cloned(),
}
} else {
Action::Conflict {
path: path.clone(),
here: mine.cloned(),
there: theirs.cloned(),
base: based.cloned(),
}
}
}
};
actions.push(action);
}
Convergence { actions }
}
fn same(left: Option<&Entry>, right: Option<&Entry>) -> bool {
match (left, right) {
(None, None) => true,
(Some(a), Some(b)) => a.same_as(b),
_ => false,
}
}
pub fn advance(base: &Baseline, convergence: &Convergence, applied: &BTreeSet<String>) -> Baseline {
let mut next = base.clone();
for action in &convergence.actions {
if action.is_conflict() {
continue;
}
if action.moves_data() && !applied.contains(action.path()) {
continue;
}
match action {
Action::Send { path, entry } | Action::Receive { path, entry } => {
next.insert(path.clone(), entry.clone());
}
Action::SendDeletion { path } | Action::ReceiveDeletion { path } => {
next.remove(path);
}
Action::Agree { path, entry } => match entry {
Some(entry) => {
next.insert(path.clone(), entry.clone());
}
None => {
next.remove(path);
}
},
Action::Conflict { .. } => unreachable!("conflicts are skipped above"),
}
}
next
}
#[cfg(test)]
#[path = "converge_tests.rs"]
mod tests;