use core::fmt;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::fmt::{Display, Formatter, Write};
use std::hash::{Hash, Hasher};
use std::path;
use std::path::Path;
use std::rc::Rc;
use std::str::{FromStr, Split};
use chrono::{DateTime, Local};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use strum_macros::EnumIter;
use VersionRef::Head;
use crate::ocfl::bimap::PathBiMap;
use crate::ocfl::consts::*;
use crate::ocfl::digest::HexDigest;
use crate::ocfl::error::{Result, RocflError};
use crate::ocfl::inventory::{Inventory, Version};
use crate::ocfl::Knowable::{Known, Unknown};
use crate::ocfl::VersionRef::Number;
use crate::ocfl::{util, DigestAlgorithm};
static VERSION_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^v\d+$"#).unwrap());
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Knowable<K, U> {
Known(K),
Unknown(U),
}
#[derive(Deserialize, Serialize, Debug, Copy, Clone)]
#[serde(try_from = "&str")]
#[serde(into = "String")]
pub struct VersionNum {
pub number: u32,
pub width: u32,
}
pub enum VersionRef {
Number(VersionNum),
Head,
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, EnumIter)]
pub enum SpecVersion {
Ocfl1_0,
Ocfl1_1,
}
#[derive(Debug)]
pub struct RepoInfo {
pub spec_version: String,
pub layout: Option<String>,
pub extensions: Vec<String>,
}
#[derive(Debug)]
pub struct ObjectInfo {
pub spec_version: String,
pub digest_algorithm: Option<String>,
pub extensions: Vec<String>,
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct Namaste {
pub(crate) filename: &'static str,
pub(crate) content: &'static str,
}
pub trait InventoryPath {
fn parts(&self) -> Split<char>;
fn parent(&self) -> Self;
fn filename(&self) -> &str;
fn resolve(&self, other: &Self) -> Self;
fn ends_with(&self, suffix: &str) -> bool;
fn starts_with(&self, prefix: &str) -> bool;
fn as_path(&self) -> &Path;
fn as_str(&self) -> &str;
fn is_empty(&self) -> bool;
}
#[derive(Deserialize, Serialize, Debug, Eq, Ord, PartialOrd, PartialEq, Hash, Clone)]
struct InventoryPathInner(String);
#[derive(Serialize, Debug, Eq, Ord, PartialOrd, PartialEq, Hash, Clone)]
#[serde(transparent)]
pub struct LogicalPath {
inner: InventoryPathInner,
}
#[derive(Debug, Eq, Ord, PartialOrd, PartialEq, Hash, Clone)]
pub struct ContentPath {
inner: InventoryPathInner,
pub version: ContentPathVersion,
}
#[derive(Debug, Eq, Ord, PartialOrd, PartialEq, Hash, Copy, Clone)]
pub enum ContentPathVersion {
VersionNum(VersionNum),
MutableHead,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct ObjectVersion {
pub id: String,
pub object_root: String,
pub digest_algorithm: DigestAlgorithm,
pub version_details: VersionDetails,
pub state: HashMap<Rc<LogicalPath>, FileDetails>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct FileDetails {
pub digest: Rc<HexDigest>,
pub digest_algorithm: DigestAlgorithm,
pub content_path: Rc<ContentPath>,
pub storage_path: String,
pub last_update: Rc<VersionDetails>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
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, Clone)]
pub struct ObjectVersionDetails {
pub id: String,
pub object_root: String,
pub digest_algorithm: DigestAlgorithm,
pub version_details: VersionDetails,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct CommitMeta {
pub(super) user_name: Option<String>,
pub(super) user_address: Option<String>,
pub(super) message: Option<String>,
pub(super) created: Option<DateTime<Local>>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum Diff {
Added(Rc<LogicalPath>),
Modified(Rc<LogicalPath>),
Deleted(Rc<LogicalPath>),
Renamed {
original: Vec<Rc<LogicalPath>>,
renamed: Vec<Rc<LogicalPath>>,
},
}
pub(crate) struct PrettyPrintSet<'a, T: Display>(pub(crate) &'a HashSet<T>);
impl<K, U> Knowable<K, U> {
pub fn is_known(&self) -> bool {
match self {
Known(_) => true,
Unknown(_) => false,
}
}
pub fn is_unknown(&self) -> bool {
!self.is_known()
}
pub const fn as_ref(&self) -> Knowable<&K, &U> {
match self {
Known(value) => Known(value),
Unknown(value) => Unknown(value),
}
}
pub fn unwrap_known(self) -> K {
match self {
Known(value) => value,
Unknown(_) => panic!("Expected known value, but was unknown"),
}
}
pub fn unwrap_unknown(self) -> U {
match self {
Known(_) => panic!("Expected unknown value, but was known"),
Unknown(value) => value,
}
}
}
impl VersionNum {
pub fn v1() -> Self {
Self {
number: 1,
width: 0,
}
}
pub fn v1_with_width(width: u32) -> Self {
Self { number: 1, width }
}
pub fn previous(&self) -> Result<VersionNum> {
if self.number - 1 < 1 {
return Err(RocflError::IllegalState(
"Versions cannot be less than 1".to_string(),
));
}
Ok(Self {
number: self.number - 1,
width: self.width,
})
}
pub fn next(&self) -> Result<VersionNum> {
let max = match self.width {
0 => u32::MAX,
_ => u32::pow(10, self.width - 1) - 1,
};
if self.number + 1 > max as u32 {
return Err(RocflError::IllegalState(format!(
"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::InvalidValue(format!(
"Invalid version {}",
version
)));
}
match version[1..].parse::<u32>() {
Ok(num) => {
if num < 1 {
return Err(RocflError::InvalidValue(format!(
"Invalid version {}",
version
)));
}
let width = match version.starts_with("v0") {
true => version.len() - 1,
false => 0,
};
Ok(Self {
number: num,
width: width as u32,
})
}
Err(_) => Err(RocflError::InvalidValue(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::InvalidValue(format!(
"Invalid version number {}",
version
)));
}
Ok(Self {
number: version,
width: 0,
})
}
}
impl FromStr for VersionNum {
type Err = RocflError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match VersionNum::try_from(s) {
Ok(v) => Ok(v),
Err(_) => match u32::from_str(s) {
Ok(parsed) => Ok(VersionNum::try_from(parsed)?),
Err(_) => Err(RocflError::InvalidValue(format!(
"Invalid version number {}",
s
))),
},
}
}
}
impl fmt::Display for VersionNum {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "v{:0width$}", self.number, width = self.width as usize)
}
}
impl From<VersionNum> for String {
fn from(version_num: VersionNum) -> Self {
format!("{}", version_num)
}
}
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 VersionRef {
pub fn resolve(&self, head_num: VersionNum) -> VersionNum {
match self {
Number(num) => *num,
Head => head_num,
}
}
}
impl From<VersionNum> for VersionRef {
fn from(num: VersionNum) -> Self {
Self::Number(num)
}
}
impl From<Option<VersionNum>> for VersionRef {
fn from(num: Option<VersionNum>) -> Self {
num.map_or(Head, Number)
}
}
impl TryFrom<u32> for VersionRef {
type Error = RocflError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
Ok(VersionNum::try_from(value)?.into())
}
}
impl TryFrom<&str> for VersionRef {
type Error = RocflError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(VersionNum::try_from(value)?.into())
}
}
impl SpecVersion {
pub fn try_from_num(version: &str) -> Result<SpecVersion> {
match version {
"1.0" => Ok(SpecVersion::Ocfl1_0),
"1.1" => Ok(SpecVersion::Ocfl1_1),
_ => Err(RocflError::InvalidValue(version.to_string())),
}
}
pub fn try_from_root_namaste_name(name: &str) -> Result<SpecVersion> {
match name {
ROOT_NAMASTE_FILE_1_0 => Ok(SpecVersion::Ocfl1_0),
ROOT_NAMASTE_FILE_1_1 => Ok(SpecVersion::Ocfl1_1),
_ => Err(RocflError::InvalidValue(name.to_string())),
}
}
pub fn try_from_object_namaste_name(name: &str) -> Result<SpecVersion> {
match name {
OBJECT_NAMASTE_FILE_1_0 => Ok(SpecVersion::Ocfl1_0),
OBJECT_NAMASTE_FILE_1_1 => Ok(SpecVersion::Ocfl1_1),
_ => Err(RocflError::InvalidValue(name.to_string())),
}
}
pub fn try_from_inventory_type(name: &str) -> Result<SpecVersion> {
match name {
INVENTORY_TYPE_1_0 => Ok(SpecVersion::Ocfl1_0),
INVENTORY_TYPE_1_1 => Ok(SpecVersion::Ocfl1_1),
_ => Err(RocflError::InvalidValue(name.to_string())),
}
}
pub fn version(self) -> &'static str {
match self {
SpecVersion::Ocfl1_0 => "1.0",
SpecVersion::Ocfl1_1 => "1.1",
}
}
pub(crate) fn root_namaste(self) -> Namaste {
match self {
SpecVersion::Ocfl1_0 => Namaste::new(ROOT_NAMASTE_FILE_1_0, ROOT_NAMASTE_CONTENT_1_0),
SpecVersion::Ocfl1_1 => Namaste::new(ROOT_NAMASTE_FILE_1_1, ROOT_NAMASTE_CONTENT_1_1),
}
}
pub(crate) fn object_namaste(self) -> Namaste {
match self {
SpecVersion::Ocfl1_0 => {
Namaste::new(OBJECT_NAMASTE_FILE_1_0, OBJECT_NAMASTE_CONTENT_1_0)
}
SpecVersion::Ocfl1_1 => {
Namaste::new(OBJECT_NAMASTE_FILE_1_1, OBJECT_NAMASTE_CONTENT_1_1)
}
}
}
pub(crate) fn inventory_type(self) -> &'static str {
match self {
SpecVersion::Ocfl1_0 => INVENTORY_TYPE_1_0,
SpecVersion::Ocfl1_1 => INVENTORY_TYPE_1_1,
}
}
pub(crate) fn spec_filename(self) -> &'static str {
match self {
SpecVersion::Ocfl1_0 => OCFL_SPEC_FILE_1_0,
SpecVersion::Ocfl1_1 => OCFL_SPEC_FILE_1_1,
}
}
}
impl RepoInfo {
pub fn new(spec_version: String, layout: Option<String>, extensions: Vec<String>) -> Self {
Self {
spec_version,
layout,
extensions,
}
}
}
impl ObjectInfo {
pub fn new(
spec_version: String,
digest_algorithm: Option<String>,
extensions: Vec<String>,
) -> Self {
Self {
spec_version,
digest_algorithm,
extensions,
}
}
}
impl Namaste {
fn new(filename: &'static str, content: &'static str) -> Self {
Self { filename, content }
}
}
impl InventoryPath for InventoryPathInner {
fn parts(&self) -> Split<char> {
self.0.split('/')
}
fn parent(&self) -> Self {
match self.0.rfind('/') {
Some(last_slash) => Self(self.0.as_str()[0..last_slash].into()),
None => Self("".to_string()),
}
}
fn filename(&self) -> &str {
match self.0.rfind('/') {
Some(last_slash) => &self.0.as_str()[last_slash + 1..],
None => self.0.as_str(),
}
}
fn resolve(&self, other: &Self) -> Self {
if self.0.is_empty() {
other.clone()
} else {
Self(format!("{}/{}", self.0, other.0))
}
}
fn ends_with(&self, suffix: &str) -> bool {
self.0.ends_with(suffix)
}
fn starts_with(&self, prefix: &str) -> bool {
self.0.starts_with(prefix)
}
fn as_path(&self) -> &Path {
self.as_ref()
}
fn as_str(&self) -> &str {
self.as_ref()
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl InventoryPath for LogicalPath {
fn parts(&self) -> Split<char> {
self.inner.parts()
}
fn parent(&self) -> Self {
Self {
inner: self.inner.parent(),
}
}
fn filename(&self) -> &str {
self.inner.filename()
}
fn resolve(&self, other: &Self) -> Self {
Self {
inner: self.inner.resolve(&other.inner),
}
}
fn ends_with(&self, suffix: &str) -> bool {
self.inner.ends_with(suffix)
}
fn starts_with(&self, prefix: &str) -> bool {
self.inner.starts_with(prefix)
}
fn as_path(&self) -> &Path {
self.as_ref()
}
fn as_str(&self) -> &str {
self.as_ref()
}
fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl InventoryPath for ContentPath {
fn parts(&self) -> Split<char> {
self.inner.parts()
}
fn parent(&self) -> Self {
Self {
inner: self.inner.parent(),
version: self.version,
}
}
fn filename(&self) -> &str {
self.inner.filename()
}
fn resolve(&self, other: &Self) -> Self {
Self {
inner: self.inner.resolve(&other.inner),
version: self.version,
}
}
fn ends_with(&self, suffix: &str) -> bool {
self.inner.ends_with(suffix)
}
fn starts_with(&self, prefix: &str) -> bool {
self.inner.starts_with(prefix)
}
fn as_path(&self) -> &Path {
self.as_ref()
}
fn as_str(&self) -> &str {
self.as_ref()
}
fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl LogicalPath {
pub fn to_content_path(&self, version_num: VersionNum, content_dir: &str) -> ContentPath {
ContentPath::for_logical_path(version_num, content_dir, self)
}
}
impl ContentPath {
pub fn for_logical_path(
version_num: VersionNum,
content_dir: &str,
logical_path: &LogicalPath,
) -> Self {
Self {
inner: InventoryPathInner(format!("{}/{}/{}", version_num, content_dir, logical_path)),
version: ContentPathVersion::VersionNum(version_num),
}
}
}
impl TryFrom<&str> for InventoryPathInner {
type Error = RocflError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let trimmed = value.trim_start_matches('/').trim_end_matches('/');
if !trimmed.is_empty() {
let has_illegal_part = trimmed
.split('/')
.any(|part| part == "." || part == ".." || part.is_empty());
if has_illegal_part {
return Err(RocflError::InvalidValue(format!(
"Paths may not contain '.', '..', or '' parts. Found: {} ",
value
)));
}
}
Ok(Self(trimmed.to_string()))
}
}
impl TryFrom<&str> for LogicalPath {
type Error = RocflError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self {
inner: InventoryPathInner::try_from(value)?,
})
}
}
impl TryFrom<&str> for ContentPath {
type Error = RocflError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let inner = InventoryPathInner::try_from(value)?;
let version = if value.starts_with(MUTABLE_HEAD_EXT_DIR) {
ContentPathVersion::MutableHead
} else {
match value.find('/') {
Some(index) => ContentPathVersion::VersionNum(value[0..index].try_into()?),
None => {
return Err(RocflError::InvalidValue(format!(
"Content paths must begin with a valid version number. Found: {} ",
value
)));
}
}
};
Ok(Self { inner, version })
}
}
impl TryFrom<String> for InventoryPathInner {
type Error = RocflError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.as_str().try_into()
}
}
impl TryFrom<String> for LogicalPath {
type Error = RocflError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(Self {
inner: InventoryPathInner::try_from(value)?,
})
}
}
impl TryFrom<String> for ContentPath {
type Error = RocflError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<&String> for InventoryPathInner {
type Error = RocflError;
fn try_from(value: &String) -> Result<Self, Self::Error> {
value.as_str().try_into()
}
}
impl TryFrom<&String> for LogicalPath {
type Error = RocflError;
fn try_from(value: &String) -> Result<Self, Self::Error> {
Ok(Self {
inner: InventoryPathInner::try_from(value)?,
})
}
}
impl TryFrom<&String> for ContentPath {
type Error = RocflError;
fn try_from(value: &String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl TryFrom<Cow<'_, str>> for InventoryPathInner {
type Error = RocflError;
fn try_from(value: Cow<'_, str>) -> Result<Self, Self::Error> {
value.as_ref().try_into()
}
}
impl TryFrom<Cow<'_, str>> for LogicalPath {
type Error = RocflError;
fn try_from(value: Cow<'_, str>) -> Result<Self, Self::Error> {
Ok(Self {
inner: InventoryPathInner::try_from(value)?,
})
}
}
impl TryFrom<Cow<'_, str>> for ContentPath {
type Error = RocflError;
fn try_from(value: Cow<'_, str>) -> Result<Self, Self::Error> {
Self::try_from(value.as_ref())
}
}
impl From<InventoryPathInner> for String {
fn from(path: InventoryPathInner) -> Self {
path.0
}
}
impl From<LogicalPath> for String {
fn from(path: LogicalPath) -> Self {
path.inner.0
}
}
impl From<ContentPath> for String {
fn from(path: ContentPath) -> Self {
path.inner.0
}
}
impl AsRef<str> for InventoryPathInner {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for LogicalPath {
fn as_ref(&self) -> &str {
self.inner.as_ref()
}
}
impl AsRef<str> for ContentPath {
fn as_ref(&self) -> &str {
self.inner.as_ref()
}
}
impl AsRef<Path> for InventoryPathInner {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl AsRef<Path> for LogicalPath {
fn as_ref(&self) -> &Path {
self.inner.as_ref()
}
}
impl AsRef<Path> for ContentPath {
fn as_ref(&self) -> &Path {
self.inner.as_ref()
}
}
impl Display for InventoryPathInner {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Display for LogicalPath {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl Display for ContentPath {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl Serialize for ContentPath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ContentPath {
fn deserialize<D>(deserializer: D) -> Result<ContentPath, D::Error>
where
D: Deserializer<'de>,
{
struct ContentPathVisitor;
impl<'de> Visitor<'de> for ContentPathVisitor {
type Value = ContentPath;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a path string that is a valid OCFL content path")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v.starts_with('/') || v.ends_with('/') {
return Err(E::custom(format!(
"Invalid value: content paths may not begin or end with a '/'. Found: {}",
v
)));
}
v.try_into()
.map_err(|e: RocflError| E::custom(e.to_string()))
}
}
deserializer.deserialize_str(ContentPathVisitor)
}
}
impl<'de> Deserialize<'de> for LogicalPath {
fn deserialize<D>(deserializer: D) -> Result<LogicalPath, D::Error>
where
D: Deserializer<'de>,
{
struct LogicalPathVisitor;
impl<'de> Visitor<'de> for LogicalPathVisitor {
type Value = LogicalPath;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a path string that is a valid OCFL logical path")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v.starts_with('/') || v.ends_with('/') {
return Err(E::custom(format!(
"Invalid value: logical paths may not begin or end with a '/'. Found: {}",
v
)));
}
v.try_into()
.map_err(|e: RocflError| E::custom(e.to_string()))
}
}
deserializer.deserialize_str(LogicalPathVisitor)
}
}
impl ObjectVersion {
pub fn from_inventory<S: AsRef<str> + Copy>(
mut inventory: Inventory,
version_num: VersionRef,
object_storage_path: S,
object_staging_path: Option<S>,
use_backslashes: bool,
) -> Result<Self> {
let version_num = version_num.resolve(inventory.head);
let version = inventory.get_version(version_num)?;
let version_details = VersionDetails::new(version_num, version);
let state = ObjectVersion::construct_state(
version_num,
&mut inventory,
object_storage_path,
object_staging_path,
use_backslashes,
)?;
Ok(Self {
id: inventory.id,
object_root: inventory.storage_path,
digest_algorithm: inventory.digest_algorithm,
version_details,
state,
})
}
fn construct_state<S: AsRef<str> + Copy>(
target: VersionNum,
inventory: &mut Inventory,
object_storage_path: S,
object_staging_path: Option<S>,
use_backslashes: bool,
) -> Result<HashMap<Rc<LogicalPath>, FileDetails>> {
let mut state = HashMap::new();
let mut current_version_num = target;
let mut current_version = inventory.remove_version(target)?;
let mut target_path_map = current_version.remove_state();
let staging_version_prefix = if object_staging_path.is_some() {
Some(format!("{}/", target))
} else {
None
};
while !target_path_map.is_empty() {
let mut not_found = PathBiMap::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 {
let content_path = inventory.content_path_for_digest(
&target_digest,
current_version_num.into(),
Some(&target_path),
)?;
let storage_path = ObjectVersion::storage_path(
content_path.as_str(),
object_storage_path,
use_backslashes,
&staging_version_prefix,
&object_staging_path,
);
state.insert(
target_path,
FileDetails::new(
content_path.clone(),
storage_path,
target_digest,
inventory.digest_algorithm,
version_details.clone(),
),
);
}
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 = previous_version.remove_state();
for (target_path, target_digest) in target_path_map {
let entry = previous_path_map.remove_path(&target_path);
if entry.is_none() || entry.unwrap().1 != target_digest {
let content_path = inventory.content_path_for_digest(
&target_digest,
current_version_num.into(),
Some(&target_path),
)?;
let storage_path = ObjectVersion::storage_path(
content_path.as_str(),
object_storage_path,
use_backslashes,
&staging_version_prefix,
&object_staging_path,
);
state.insert(
target_path,
FileDetails::new(
content_path.clone(),
storage_path,
target_digest,
inventory.digest_algorithm,
version_details.clone(),
),
);
} else {
not_found.insert_rc(target_digest, target_path);
}
}
current_version_num = previous_version_num;
current_version = previous_version;
target_path_map = not_found;
}
Ok(state)
}
fn storage_path<S: AsRef<str> + Copy>(
content_path: &str,
storage_path: S,
use_backslashes: bool,
staging_version_prefix: &Option<String>,
staging_path: &Option<S>,
) -> String {
if staging_version_prefix.is_some()
&& content_path.starts_with(staging_version_prefix.as_ref().unwrap())
{
convert_path_separator(
util::BACKSLASH_SEPARATOR,
join(
util::BACKSLASH_SEPARATOR,
staging_path.unwrap().as_ref(),
content_path,
),
)
} else {
convert_path_separator(
use_backslashes,
join(use_backslashes, storage_path.as_ref(), content_path),
)
}
}
}
impl FileDetails {
pub fn new(
content_path: Rc<ContentPath>,
storage_path: String,
digest: Rc<HexDigest>,
digest_algorithm: DigestAlgorithm,
version_details: Rc<VersionDetails>,
) -> Self {
Self {
content_path,
storage_path,
digest,
digest_algorithm,
last_update: version_details,
}
}
}
impl VersionDetails {
pub 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,
created: version.created,
user_name: user,
user_address: address,
message: version.message.clone(),
}
}
pub 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 {
pub fn from_inventory(mut inventory: Inventory, version_num: VersionRef) -> Result<Self> {
let version_num = version_num.resolve(inventory.head);
let version = inventory.remove_version(version_num)?;
let version_details = VersionDetails::from_version(version_num, version);
Ok(Self {
id: inventory.id,
object_root: inventory.storage_path,
digest_algorithm: inventory.digest_algorithm,
version_details,
})
}
}
impl Default for CommitMeta {
fn default() -> Self {
Self::new()
}
}
impl CommitMeta {
pub fn new() -> Self {
Self {
user_name: None,
user_address: None,
message: None,
created: None,
}
}
pub fn with_user(mut self, name: Option<String>, address: Option<String>) -> Result<Self> {
if address.is_some() && name.is_none() {
return Err(RocflError::InvalidValue(
"User name must be set when user address is set.".to_string(),
));
}
self.user_name = name;
self.user_address = address;
Ok(self)
}
pub fn with_message(mut self, message: Option<String>) -> Self {
self.message = message;
self
}
pub fn with_created(mut self, created: Option<DateTime<Local>>) -> Self {
self.created = created;
self
}
}
impl Diff {
pub fn path(&self) -> &Rc<LogicalPath> {
match self {
Diff::Added(path) => path,
Diff::Modified(path) => path,
Diff::Deleted(path) => path,
Diff::Renamed { original, .. } => original
.first()
.expect("At least one renamed path should have existed"),
}
}
}
impl<'a, T: Display> Display for PrettyPrintSet<'a, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_char('[')?;
let max = self.0.len() - 1;
for (i, entry) in self.0.iter().enumerate() {
write!(f, "{}", entry)?;
if i < max {
write!(f, ", ")?;
}
}
f.write_char(']')
}
}
fn join(use_backslashes: bool, parent: &str, child: &str) -> String {
if use_backslashes {
format!("{}\\{}", parent, child)
} else {
format!("{}/{}", parent, child)
}
}
fn convert_path_separator(use_backslashes: bool, path: String) -> String {
if use_backslashes && path::MAIN_SEPARATOR == '\\' {
return path.replace('/', "\\");
}
path
}
#[cfg(test)]
mod tests {
use std::convert::{TryFrom, TryInto};
use crate::ocfl::{LogicalPath, VersionNum};
#[test]
fn allow_next_version_when_zero_padded_and_less_than_max() {
let current = VersionNum::try_from("v00039").unwrap();
current.next().unwrap();
}
#[test]
#[should_panic(expected = "Version cannot be greater than 9999")]
fn enforce_max_version_when_padded() {
let current = VersionNum::try_from("v09999").unwrap();
current.next().unwrap();
}
#[test]
fn create_logical_path_when_valid() {
let value = "foo/.bar/baz.txt";
let path: LogicalPath = value.try_into().unwrap();
assert_eq!(value, path.inner.0);
}
#[test]
fn create_logical_path_when_root() {
let path: LogicalPath = "/".try_into().unwrap();
assert_eq!("", path.inner.0);
}
#[test]
fn remove_leading_and_trailing_slashes_from_logical_paths() {
let path: LogicalPath = "//foo/bar/baz//".try_into().unwrap();
assert_eq!("foo/bar/baz", path.inner.0);
}
#[test]
#[should_panic(expected = "Paths may not contain")]
fn reject_logical_paths_with_empty_parts() {
LogicalPath::try_from("foo//bar/baz").unwrap();
}
#[test]
#[should_panic(expected = "Paths may not contain")]
fn reject_logical_paths_with_single_dot() {
LogicalPath::try_from("foo/bar/./baz").unwrap();
}
#[test]
#[should_panic(expected = "Paths may not contain")]
fn reject_logical_paths_with_double_dot() {
LogicalPath::try_from("foo/bar/../baz").unwrap();
}
#[test]
#[should_panic(expected = "Paths may not contain")]
fn reject_logical_paths_with_double_dot_leading() {
LogicalPath::try_from("../foo/bar/baz").unwrap();
}
}