use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use snapdir_core::{Manifest, ManifestEntry};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Added,
Deleted,
Modified,
Unchanged,
}
impl Status {
#[must_use]
pub fn letter(self) -> Option<&'static str> {
match self {
Status::Added => Some("A"),
Status::Deleted => Some("D"),
Status::Modified => Some("M"),
Status::Unchanged => None,
}
}
#[must_use]
pub fn json_token(self) -> &'static str {
match self {
Status::Added => "A",
Status::Deleted => "D",
Status::Modified => "M",
Status::Unchanged => "=",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnConflict {
Error,
LastWins,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffEntry {
pub status: Status,
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Collision {
pub path: String,
}
fn fingerprint(entry: &ManifestEntry) -> (snapdir_core::PathType, &str, Option<&str>, Option<u64>) {
match entry.path_type {
snapdir_core::PathType::File => (
entry.path_type,
entry.permissions.as_str(),
Some(entry.checksum.as_str()),
Some(entry.size),
),
snapdir_core::PathType::Directory => {
(entry.path_type, entry.permissions.as_str(), None, None)
}
}
}
fn same_content(a: &ManifestEntry, b: &ManifestEntry) -> bool {
fingerprint(a) == fingerprint(b)
}
pub fn union_side(
manifests: &[Manifest],
on_conflict: OnConflict,
) -> Result<BTreeMap<String, ManifestEntry>, Collision> {
let mut map: BTreeMap<String, ManifestEntry> = BTreeMap::new();
for manifest in manifests {
for entry in manifest.entries() {
match map.get(&entry.path) {
Some(existing) if same_content(existing, entry) => {
}
Some(_existing) => match on_conflict {
OnConflict::Error => {
return Err(Collision {
path: entry.path.clone(),
});
}
OnConflict::LastWins => {
map.insert(entry.path.clone(), entry.clone());
}
},
None => {
map.insert(entry.path.clone(), entry.clone());
}
}
}
}
Ok(map)
}
#[must_use]
pub fn classify(
from: &BTreeMap<String, ManifestEntry>,
to: &BTreeMap<String, ManifestEntry>,
include_unchanged: bool,
) -> Vec<DiffEntry> {
let mut paths: BTreeSet<&str> = BTreeSet::new();
for p in from.keys() {
paths.insert(p.as_str());
}
for p in to.keys() {
paths.insert(p.as_str());
}
let mut rows = Vec::new();
for path in &paths {
let in_from = from.get(*path);
let in_to = to.get(*path);
let is_dir = |e: &ManifestEntry| e.path_type == snapdir_core::PathType::Directory;
let present_only_dirs = match (in_from, in_to) {
(Some(f), Some(t)) => is_dir(f) && is_dir(t),
(Some(e), None) | (None, Some(e)) => is_dir(e),
(None, None) => false,
};
if present_only_dirs {
continue;
}
let status = match (in_from, in_to) {
(None, Some(_)) => Status::Added,
(Some(_), None) => Status::Deleted,
(Some(f), Some(t)) => {
if same_content(f, t) {
Status::Unchanged
} else {
Status::Modified
}
}
(None, None) => unreachable!("a path in the union must be in at least one side"),
};
if status == Status::Unchanged && !include_unchanged {
continue;
}
rows.push(DiffEntry {
status,
path: (*path).to_owned(),
});
}
rows
}
#[must_use]
pub fn render_porcelain(rows: &[DiffEntry]) -> String {
let mut out = String::new();
for row in rows {
let letter = row.status.letter().unwrap_or("=");
out.push_str(letter);
out.push('\t');
out.push_str(&row.path);
out.push('\n');
}
out
}
#[must_use]
pub fn render_json(rows: &[DiffEntry]) -> String {
let mut out = String::from("[");
for (i, row) in rows.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str("{\"status\":\"");
out.push_str(row.status.json_token());
out.push_str("\",\"path\":\"");
out.push_str(&json_escape(&row.path));
out.push_str("\"}");
}
out.push(']');
out
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out
}