use std::collections::HashMap;
use crate::error::{FossilError, Result};
#[derive(Debug, Clone)]
pub struct FileEntry {
pub name: String,
pub hash: Option<String>, pub permissions: Option<String>,
pub old_name: Option<String>,
}
#[derive(Debug)]
pub struct Manifest {
pub baseline: Option<String>, pub comment: String, pub timestamp: String, pub files: Vec<FileEntry>, pub parents: Vec<String>, pub user: String, pub checksum: String, pub mimetype: Option<String>, pub repo_checksum: Option<String>, }
fn decode_fossil_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('s') => result.push(' '),
Some('n') => result.push('\n'),
Some('\\') => result.push('\\'),
Some(other) => {
result.push('\\');
result.push(other);
}
None => result.push('\\'),
}
} else {
result.push(c);
}
}
result
}
pub fn encode_fossil_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace(' ', "\\s")
.replace('\n', "\\n")
}
pub fn is_manifest(content: &[u8]) -> bool {
if let Ok(text) = std::str::from_utf8(content) {
text.lines().any(|l| l.starts_with("D "))
&& text.lines().any(|l| l.starts_with("U "))
&& text.lines().any(|l| l.starts_with("Z "))
} else {
false
}
}
pub fn parse_manifest(content: &[u8]) -> Result<Manifest> {
let text = std::str::from_utf8(content)
.map_err(|e| FossilError::InvalidArtifact(format!("Not valid UTF-8: {}", e)))?;
let mut baseline = None;
let mut comment = String::new();
let mut timestamp = String::new();
let mut files = Vec::new();
let mut parents = Vec::new();
let mut user = String::new();
let mut checksum = String::new();
let mut mimetype = None;
let mut repo_checksum = None;
for line in text.lines() {
if line.is_empty() {
continue;
}
if line.starts_with("-----") {
continue;
}
if line.starts_with("Hash:") {
continue;
}
let mut parts = line.splitn(2, ' ');
let card_type = parts.next().unwrap_or("");
let args = parts.next().unwrap_or("");
match card_type {
"B" => {
baseline = Some(args.to_string());
}
"C" => {
comment = decode_fossil_string(args);
}
"D" => {
timestamp = args.to_string();
}
"F" => {
let parts: Vec<&str> = args.splitn(4, ' ').collect();
let name = decode_fossil_string(parts.first().unwrap_or(&""));
let hash = parts.get(1).and_then(|s| {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
});
let permissions = parts.get(2).and_then(|s| {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
});
let old_name = parts.get(3).map(|s| decode_fossil_string(s));
files.push(FileEntry {
name,
hash,
permissions,
old_name,
});
}
"N" => {
mimetype = Some(args.to_string());
}
"P" => {
parents = args
.split(' ')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
}
"R" => {
repo_checksum = Some(args.to_string());
}
"U" => {
user = decode_fossil_string(args);
}
"Z" => {
checksum = args.to_string();
}
_ => {}
}
}
if timestamp.is_empty() {
return Err(FossilError::InvalidArtifact("Missing D (date) card".into()));
}
if user.is_empty() {
return Err(FossilError::InvalidArtifact("Missing U (user) card".into()));
}
if checksum.is_empty() {
return Err(FossilError::InvalidArtifact(
"Missing Z (checksum) card".into(),
));
}
Ok(Manifest {
baseline,
comment,
timestamp,
files,
parents,
user,
checksum,
mimetype,
repo_checksum,
})
}
pub fn get_full_file_list(
manifest: &Manifest,
get_baseline: &dyn Fn(&str) -> Result<Manifest>,
) -> Result<HashMap<String, FileEntry>> {
let mut files: HashMap<String, FileEntry> = HashMap::new();
if let Some(ref baseline_hash) = manifest.baseline {
let baseline = get_baseline(baseline_hash)?;
let baseline_files = if baseline.baseline.is_some() {
get_full_file_list(&baseline, get_baseline)?
} else {
baseline
.files
.into_iter()
.map(|e| (e.name.clone(), e))
.collect()
};
files = baseline_files;
}
for entry in &manifest.files {
if entry.hash.is_some() {
files.insert(entry.name.clone(), entry.clone());
} else {
files.remove(&entry.name);
}
}
Ok(files)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_fossil_string() {
assert_eq!(decode_fossil_string("hello"), "hello");
assert_eq!(decode_fossil_string(r"hello\sworld"), "hello world");
assert_eq!(decode_fossil_string(r"line1\nline2"), "line1\nline2");
assert_eq!(decode_fossil_string(r"back\\slash"), "back\\slash");
}
#[test]
fn test_encode_fossil_string() {
assert_eq!(encode_fossil_string("hello"), "hello");
assert_eq!(encode_fossil_string("hello world"), r"hello\sworld");
assert_eq!(encode_fossil_string("line1\nline2"), r"line1\nline2");
}
#[test]
fn test_parse_simple_manifest() {
let manifest_text = b"C Initial\\scommit\n\
D 2024-01-15T10:30:00\n\
F README.md abc123\n\
F src/main.rs def456 x\n\
P \n\
U developer\n\
Z 0123456789abcdef0123456789abcdef\n";
let manifest = parse_manifest(manifest_text).unwrap();
assert_eq!(manifest.comment, "Initial commit");
assert_eq!(manifest.timestamp, "2024-01-15T10:30:00");
assert_eq!(manifest.user, "developer");
assert_eq!(manifest.files.len(), 2);
assert_eq!(manifest.files[0].name, "README.md");
assert_eq!(manifest.files[0].hash, Some("abc123".to_string()));
assert_eq!(manifest.files[1].name, "src/main.rs");
assert_eq!(manifest.files[1].permissions, Some("x".to_string()));
}
#[test]
fn test_parse_delta_manifest() {
let manifest_text = b"B abc123def456\n\
C Fix\\sbug\n\
D 2024-01-16T11:00:00\n\
F src/main.rs ghi789\n\
P abc123\n\
U developer\n\
Z fedcba9876543210fedcba9876543210\n";
let manifest = parse_manifest(manifest_text).unwrap();
assert_eq!(manifest.baseline, Some("abc123def456".to_string()));
assert_eq!(manifest.comment, "Fix bug");
assert_eq!(manifest.parents, vec!["abc123"]);
}
}