use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::{Store, StoreError};
pub type BlobId = String;
pub const MANIFEST_VERSION: u32 = 1;
pub const MODE_FILE: &str = "100644";
pub const MODE_EXEC: &str = "100755";
pub fn is_blob_id(s: &str) -> bool {
s.len() == 64
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Entry {
pub blob: BlobId,
pub mode: String,
pub size: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
pub version: u32,
pub entries: BTreeMap<String, Entry>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ManifestError {
#[error("manifest is not valid JSON of the expected shape: {0}")]
Malformed(String),
#[error("manifest is not in canonical form")]
NotCanonical,
#[error("unsupported manifest version {0}")]
UnsupportedVersion(u32),
#[error("invalid path `{path}`: {reason}")]
InvalidPath { path: String, reason: &'static str },
#[error("path `{0}` is owned by the op-log (src/**/*.lex, src.lex)")]
ReservedPath(String),
#[error("paths `{0}` and `{1}` differ only in case")]
CaseCollision(String, String),
#[error("`{0}` is a file but `{1}` needs it to be a directory")]
FileDirCollision(String, String),
#[error("`{path}`: unsupported mode `{mode}` (only 100644 and 100755)")]
InvalidMode { path: String, mode: String },
#[error("`{path}`: `{blob}` is not a blob id")]
InvalidBlobId { path: String, blob: String },
}
pub fn is_reserved_path(path: &str) -> bool {
path == "src.lex" || (path.starts_with("src/") && path.ends_with(".lex"))
}
pub fn validate_path(path: &str) -> Result<(), ManifestError> {
let bad = |reason| {
Err(ManifestError::InvalidPath {
path: path.to_string(),
reason,
})
};
if path.is_empty() {
return bad("empty");
}
if path.starts_with('/') {
return bad("absolute");
}
if path.contains('\\') {
return bad("contains `\\`");
}
if path.contains('\0') {
return bad("contains NUL");
}
for (i, comp) in path.split('/').enumerate() {
match comp {
"" => return bad("empty component"),
"." | ".." => return bad("`.` or `..` component"),
c if c.eq_ignore_ascii_case(".git") => return bad("`.git` component"),
c if i == 0 && c.eq_ignore_ascii_case(".lex") => return bad("`.lex` store directory"),
_ => {}
}
}
if is_reserved_path(path) {
return Err(ManifestError::ReservedPath(path.to_string()));
}
Ok(())
}
impl Manifest {
pub fn new() -> Self {
Manifest {
version: MANIFEST_VERSION,
entries: BTreeMap::new(),
}
}
pub fn validate(&self) -> Result<(), ManifestError> {
if self.version != MANIFEST_VERSION {
return Err(ManifestError::UnsupportedVersion(self.version));
}
let mut folded: BTreeMap<String, &str> = BTreeMap::new();
for (path, e) in &self.entries {
validate_path(path)?;
if e.mode != MODE_FILE && e.mode != MODE_EXEC {
return Err(ManifestError::InvalidMode {
path: path.clone(),
mode: e.mode.clone(),
});
}
if !is_blob_id(&e.blob) {
return Err(ManifestError::InvalidBlobId {
path: path.clone(),
blob: e.blob.clone(),
});
}
if let Some(prev) = folded.insert(path.to_lowercase(), path) {
return Err(ManifestError::CaseCollision(prev.to_string(), path.clone()));
}
}
let files: BTreeSet<&str> = self.entries.keys().map(String::as_str).collect();
for path in &files {
let mut end = 0;
while let Some(i) = path[end..].find('/') {
end += i;
let dir = &path[..end];
if files.contains(dir) {
return Err(ManifestError::FileDirCollision(
dir.to_string(),
path.to_string(),
));
}
end += 1;
}
}
Ok(())
}
pub fn to_canonical_bytes(&self) -> Vec<u8> {
serde_json::to_vec(self).expect("manifest serialization is infallible")
}
pub fn id(&self) -> BlobId {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(self.to_canonical_bytes()))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestError> {
let m: Manifest =
serde_json::from_slice(bytes).map_err(|e| ManifestError::Malformed(e.to_string()))?;
if m.to_canonical_bytes() != bytes {
return Err(ManifestError::NotCanonical);
}
m.validate()?;
Ok(m)
}
}
impl Default for Manifest {
fn default() -> Self {
Self::new()
}
}
impl Store {
pub fn put_manifest(&self, manifest: &Manifest) -> Result<BlobId, StoreError> {
manifest.validate().map_err(StoreError::InvalidManifest)?;
self.put_blob_bytes(&manifest.to_canonical_bytes())
}
pub fn get_manifest(&self, id: &str) -> Result<Manifest, StoreError> {
Manifest::from_bytes(&self.get_blob_bytes(id)?).map_err(StoreError::InvalidManifest)
}
pub fn manifest_closure_missing(&self, manifest: &Manifest) -> Vec<BlobId> {
let ids: BTreeSet<&BlobId> = manifest.entries.values().map(|e| &e.blob).collect();
ids.into_iter()
.filter(|id| !self.has_blob(id))
.cloned()
.collect()
}
}