use core::fmt;
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::path::Path;
use std::rc::Rc;
use std::str::FromStr;
use anyhow::{anyhow, Error, Result};
use chrono::{DateTime, Local};
use grep::regex::RegexMatcher;
use lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize;
use serde::export::Formatter;
use thiserror::Error;
use crate::fs::FsOcflStore;
mod fs;
const OBJECT_MARKER: &str = "0=ocfl_object_1.0";
const ROOT_INVENTORY_FILE: &str = "inventory.json";
const MUTABLE_HEAD_INVENTORY_FILE: &str = "extensions/0004-mutable-head/head/inventory.json";
lazy_static! {
static ref VERSION_REGEX: Regex = Regex::new(r#"^v\d+$"#).unwrap();
static ref OBJECT_ID_MATCHER: RegexMatcher = RegexMatcher::new(r#""id"\s*:\s*"([^"]+)""#).unwrap();
}
pub struct OcflRepo {
store: Box<dyn OcflStore>
}
#[derive(Deserialize, Debug)]
#[serde(try_from = "&str")]
pub struct VersionNum {
pub number: u32,
pub width: usize,
}
#[derive(Debug, Eq, PartialEq)]
pub struct ObjectVersion {
pub id: String,
pub object_root: String,
pub digest_algorithm: String,
pub version_details: VersionDetails,
pub state: HashMap<String, FileDetails>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct FileDetails {
pub digest: Rc<String>,
pub digest_algorithm: Rc<String>,
pub content_path: String,
pub storage_path: String,
pub last_update: Rc<VersionDetails>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct VersionDetails {
pub version_num: VersionNum,
pub created: DateTime<Local>,
pub user_name: Option<String>,
pub user_address: Option<String>,
pub message: Option<String>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct ObjectVersionDetails {
pub id: String,
pub object_root: String,
pub digest_algorithm: String,
pub version_details: VersionDetails,
}
#[derive(Debug, Eq, PartialEq)]
pub struct Diff {
pub diff_type: DiffType,
pub path: String,
}
#[derive(Debug, Eq, PartialEq)]
pub enum DiffType {
Added,
Modified,
Deleted,
}
#[derive(Error, Debug)]
pub enum RocflError {
#[error("Object {object_id} is corrupt: {message}")]
CorruptObject {
object_id: String,
message: String,
},
#[error("Not found: {0}")]
NotFound(String),
#[error("Illegal argument: {0}")]
IllegalArgument(String)
}
impl OcflRepo {
pub fn new_fs_repo<P: AsRef<Path>>(storage_root: P) -> Result<Self> {
Ok(Self {
store: Box::new(FsOcflStore::new(storage_root)?)
})
}
pub fn list_objects(&self, filter_glob: Option<&str>) -> Result<Box<dyn Iterator<Item=Result<ObjectVersionDetails>>>> {
let inv_iter = self.store.iter_inventories(filter_glob)?;
Ok(Box::new(InventoryAdapterIter::new(inv_iter, |inventory| {
ObjectVersionDetails::from_inventory(inventory, None)
})))
}
pub fn get_object(&self, object_id: &str, version_num: Option<&VersionNum>) -> Result<ObjectVersion> {
let inventory = self.store.get_inventory(object_id)?;
Ok(ObjectVersion::from_inventory(inventory, version_num)?)
}
pub fn get_object_details(&self, object_id: &str, version_num: Option<&VersionNum>) -> Result<ObjectVersionDetails> {
let inventory = self.store.get_inventory(object_id)?;
Ok(ObjectVersionDetails::from_inventory(inventory, version_num)?)
}
pub fn list_object_versions(&self, object_id: &str) -> Result<Vec<VersionDetails>> {
let inventory = self.store.get_inventory(object_id)?;
let mut versions = Vec::with_capacity(inventory.versions.len());
for (id, version) in inventory.versions {
versions.push(VersionDetails::from_version(id, version))
}
Ok(versions)
}
pub fn list_file_versions(&self, object_id: &str, path: &str) -> Result<Vec<VersionDetails>> {
let inventory = self.store.get_inventory(object_id)?;
let mut versions = Vec::new();
let path = path.to_string();
let mut current_digest: Option<String> = None;
for (id, version) in inventory.versions {
match version.lookup_digest(&path) {
Some(digest) => {
if current_digest.is_none() || current_digest.as_ref().unwrap().ne(digest) {
current_digest = Some(digest.clone());
versions.push(VersionDetails::from_version(id, version));
}
}
None => {
if current_digest.is_some() {
current_digest = None;
versions.push(VersionDetails::from_version(id, version));
}
}
}
}
if versions.is_empty() {
return Err(RocflError::NotFound(format!("Path {} not found in object {}", path, object_id)).into());
}
Ok(versions)
}
pub fn diff(&self, object_id: &str, left_version: Option<&VersionNum>, right_version: &VersionNum) -> Result<Vec<Diff>> {
if left_version.is_some() && right_version.eq(left_version.unwrap()) {
return Ok(vec![])
}
let mut inventory = self.store.get_inventory(object_id)?;
let right = inventory.remove_version(&right_version)?;
let left = match left_version {
Some(version) => Some(inventory.remove_version(version)?),
None => {
if right_version.number > 1 {
Some(inventory.remove_version(&right_version.previous().unwrap())?)
} else {
None
}
}
};
let mut right_state = invert_path_map(right.state);
let mut diffs = Vec::new();
if left.is_none() {
for (path, _digest) in right_state {
diffs.push(Diff::added(path));
}
} else {
let left_state = invert_path_map(left.unwrap().state);
for (path, left_digest) in left_state {
match right_state.remove(&path) {
None => diffs.push(Diff::deleted(path)),
Some(right_digest) => {
if left_digest.deref().ne(right_digest.deref()) {
diffs.push(Diff::modified(path))
}
}
}
}
for (path, _digest) in right_state {
diffs.push(Diff::added(path))
}
}
Ok(diffs)
}
}
impl VersionNum {
pub fn previous(&self) -> Result<VersionNum> {
if self.number - 1 < 1 {
return Err(anyhow!("Versions cannot be less than 1"));
}
Ok(Self {
number: self.number - 1,
width: self.width,
})
}
pub fn next(&self) -> Result<VersionNum> {
let max = match self.width {
0 => usize::MAX,
_ => (10 * (self.width - 1)) - 1
};
if self.number + 1 > max as u32 {
return Err(anyhow!("Version cannot be greater than {}", max));
}
Ok(Self {
number: self.number + 1,
width: self.width,
})
}
}
impl TryFrom<&str> for VersionNum {
type Error = RocflError;
fn try_from(version: &str) -> Result<Self, Self::Error> {
if !VERSION_REGEX.is_match(version) {
return Err(RocflError::IllegalArgument(format!("Invalid version {}", version)));
}
match version[1..].parse::<u32>() {
Ok(num) => {
if num < 1 {
return Err(RocflError::IllegalArgument(format!("Invalid version {}", version)));
}
let width = match version.starts_with("v0") {
true => version.len() - 1,
false => 0
};
Ok(Self {
number: num,
width,
})
}
Err(_) => return Err(RocflError::IllegalArgument(format!("Invalid version {}", version)))
}
}
}
impl TryFrom<u32> for VersionNum {
type Error = RocflError;
fn try_from(version: u32) -> Result<Self, Self::Error> {
if version < 1 {
return Err(RocflError::IllegalArgument(format!("Invalid version number {}", version)));
}
Ok(Self {
number: version,
width: 0,
})
}
}
impl FromStr for VersionNum {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match VersionNum::try_from(s) {
Ok(v) => Ok(v),
Err(_) => Ok(VersionNum::try_from(u32::from_str(s)?)?),
}
}
}
impl Clone for VersionNum {
fn clone(&self) -> Self {
Self {
number: self.number,
width: self.width,
}
}
}
impl fmt::Display for VersionNum {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "v{:0width$}", self.number, width = self.width)
}
}
impl PartialEq for VersionNum {
fn eq(&self, other: &Self) -> bool {
self.number == other.number
}
}
impl Eq for VersionNum {}
impl Hash for VersionNum {
fn hash<H: Hasher>(&self, state: &mut H) {
self.number.hash(state)
}
}
impl PartialOrd for VersionNum {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for VersionNum {
fn cmp(&self, other: &Self) -> Ordering {
self.number.cmp(&other.number)
}
}
impl ObjectVersion {
fn from_inventory(mut inventory: Inventory, version_num: Option<&VersionNum>) -> Result<Self> {
let version_num = match version_num {
Some(version) => version.clone(),
None => inventory.head.clone(),
};
let version = inventory.get_version(&version_num)?;
let version_details = VersionDetails::new(&version_num, version);
let state = ObjectVersion::construct_state(&version_num, &mut inventory)?;
Ok(Self {
id: inventory.id,
object_root: inventory.object_root,
digest_algorithm: inventory.digest_algorithm,
version_details,
state
})
}
fn construct_state(target: &VersionNum, inventory: &mut Inventory) -> Result<HashMap<String, FileDetails>> {
let mut state = HashMap::new();
let digest_algorithm = Rc::new(inventory.digest_algorithm.clone());
let mut current_version_num = (*target).clone();
let mut current_version = inventory.remove_version(target)?;
let mut target_path_map = invert_path_map(current_version.state);
current_version.state = HashMap::new();
while !target_path_map.is_empty() {
let mut not_found = HashMap::new();
let version_details = Rc::new(VersionDetails::from_version(current_version_num, current_version));
if version_details.version_num.number == 1 {
for (target_path, target_digest) in target_path_map.into_iter() {
let content_path = inventory.lookup_content_path(&target_digest)?.to_string();
state.insert(target_path, FileDetails::new(content_path,
target_digest,
Rc::clone(&digest_algorithm),
&inventory.object_root,
Rc::clone(&version_details)));
}
break;
}
let previous_version_num = version_details.version_num.previous()?;
let mut previous_version = inventory.remove_version(&previous_version_num)?;
let mut previous_path_map = invert_path_map(previous_version.state);
previous_version.state = HashMap::new();
for (target_path, target_digest) in target_path_map.into_iter() {
let entry = previous_path_map.remove_entry(&target_path);
if entry.is_none() || entry.unwrap().1 != target_digest {
let content_path = inventory.lookup_content_path(&target_digest)?.to_string();
state.insert(target_path, FileDetails::new(content_path,
target_digest,
Rc::clone(&digest_algorithm),
&inventory.object_root,
Rc::clone(&version_details)));
} else {
not_found.insert(target_path, target_digest);
}
}
current_version_num = previous_version_num;
current_version = previous_version;
target_path_map = not_found;
}
Ok(state)
}
}
impl FileDetails {
fn new(content_path: String, digest: Rc<String>, digest_algorithm: Rc<String>,
object_root: &str, version_details: Rc<VersionDetails>) -> Self {
Self {
storage_path: format!("{}/{}", object_root, content_path),
content_path,
digest,
digest_algorithm,
last_update: version_details,
}
}
}
impl VersionDetails {
fn new(version_num: &VersionNum, version: &Version) -> Self {
let (user, address) = match &version.user {
Some(user) => (user.name.clone(), user.address.clone()),
None => (None, None)
};
Self {
version_num: version_num.clone(),
created: version.created.clone(),
user_name: user,
user_address: address,
message: version.message.clone()
}
}
fn from_version(version_num: VersionNum, version: Version) -> Self {
let (user, address) = match version.user {
Some(user) => (user.name, user.address),
None => (None, None)
};
Self {
version_num,
created: version.created,
user_name: user,
user_address: address,
message: version.message,
}
}
}
impl ObjectVersionDetails {
fn from_inventory(mut inventory: Inventory, version_num: Option<&VersionNum>) -> Result<Self> {
let version_num = match version_num {
Some(version) => version.clone(),
None => inventory.head.clone(),
};
let version = inventory.remove_version(&version_num)?;
let version_details = VersionDetails::from_version(version_num, version);
Ok(Self {
id: inventory.id,
object_root: inventory.object_root,
digest_algorithm: inventory.digest_algorithm,
version_details,
})
}
}
impl Diff {
fn added(path: String) -> Self {
Self {
diff_type: DiffType::Added,
path
}
}
fn modified(path: String) -> Self {
Self {
diff_type: DiffType::Modified,
path
}
}
fn deleted(path: String) -> Self {
Self {
diff_type: DiffType::Deleted,
path
}
}
}
trait OcflStore {
fn get_inventory(&self, object_id: &str) -> Result<Inventory>;
fn iter_inventories(&self, filter_glob: Option<&str>) -> Result<Box<dyn Iterator<Item=Result<Inventory>>>>;
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Inventory {
id: String,
#[serde(rename = "type")]
type_declaration: String,
digest_algorithm: String,
head: VersionNum,
content_directory: Option<String>,
manifest: HashMap<String, Vec<String>>,
versions: BTreeMap<VersionNum, Version>,
fixity: Option<HashMap<String, HashMap<String, Vec<String>>>>,
#[serde(skip)]
object_root: String,
}
#[derive(Deserialize, Debug)]
struct Version {
created: DateTime<Local>,
state: HashMap<String, Vec<String>>,
message: Option<String>,
user: Option<User>
}
#[derive(Deserialize, Debug)]
struct User {
name: Option<String>,
address: Option<String>
}
struct InventoryAdapterIter<T> {
iter: Box<dyn Iterator<Item=Result<Inventory>>>,
adapter: Box<dyn Fn(Inventory) -> Result<T>>
}
impl Inventory {
pub fn validate(&self) -> Result<()> {
if !self.versions.contains_key(&self.head) {
return Err(RocflError::CorruptObject {
object_id: self.id.clone(),
message: format!("HEAD version {} was not found", self.head),
}.into())
}
Ok(())
}
fn get_version(&self, version_num: &VersionNum) -> Result<&Version> {
match self.versions.get(version_num) {
Some(v) => Ok(v),
None => Err(not_found(&self.id, Some(version_num)).into())
}
}
fn remove_version(&mut self, version_num: &VersionNum) -> Result<Version> {
match self.versions.remove(version_num) {
Some(v) => Ok(v),
None => Err(not_found(&self.id, Some(version_num)).into())
}
}
fn lookup_content_path(&self, digest: &str) -> Result<&str> {
match self.manifest.get(digest) {
Some(paths) => {
match paths.first() {
Some(path) => Ok(path.as_str()),
None => Err(RocflError::CorruptObject {
object_id: self.id.clone(),
message: format!("Digest {} is not mapped to any content paths", digest)
}.into())
}
}
None => Err(RocflError::CorruptObject {
object_id: self.id.clone(),
message: format!("Digest {} not found in manifest", digest)
}.into())
}
}
}
impl Version {
fn lookup_digest(&self, logical_path: &String) -> Option<&String> {
for (digest, paths) in &self.state {
if paths.contains(logical_path) {
return Some(digest);
}
}
None
}
}
impl<T> InventoryAdapterIter<T> {
fn new(iter: Box<dyn Iterator<Item=Result<Inventory>>>, adapter: impl Fn(Inventory) -> Result<T> + 'static) -> Self {
Self {
iter,
adapter: Box::new(adapter)
}
}
}
impl<T> Iterator for InventoryAdapterIter<T> {
type Item = Result<T>;
fn next(&mut self) -> Option<Self::Item> {
match self.iter.next() {
None => None,
Some(Err(e)) => Some(Err(e)),
Some(Ok(inventory)) => {
Some(self.adapter.deref()(inventory))
}
}
}
}
fn invert_path_map(map: HashMap<String, Vec<String>>) -> HashMap<String, Rc<String>> {
let mut inverted = HashMap::new();
for (digest, paths) in map.into_iter() {
let digest = Rc::new(digest);
for path in paths.into_iter() {
inverted.insert(path, Rc::clone(&digest));
}
}
inverted
}
fn not_found(object_id: &str, version_num: Option<&VersionNum>) -> RocflError {
match version_num {
Some(version) => RocflError::NotFound(format!("Object {} version {}", object_id, version)),
None => RocflError::NotFound(format!("Object {}", object_id))
}
}