use sha2::{Digest, Sha256};
use std::path::Path;
pub const RBARA_NS: &str = "https://rustybara.io/ns/1.0/";
#[derive(Clone)]
pub struct RbaraXmpBlock {
pub uuid: String,
pub version: String,
pub timestamp: String,
pub source_hash: String,
pub parent_id: String,
pub ops: Vec<String>,
}
pub fn hash_bytes(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
format!("sha256:{hex}")
}
pub fn hash_file(path: &Path) -> crate::Result<String> {
let bytes = std::fs::read(path)?;
Ok(hash_bytes(&bytes))
}
pub fn generate_uuid() -> String {
uuid::Uuid::new_v4().to_string()
}
pub fn read_parent_id(xmp_bytes: &[u8]) -> String {
let text = std::str::from_utf8(xmp_bytes).unwrap_or("");
let open = "<rbara:uuid>";
let close = "</rbara:uuid>";
if let Some(s) = text.find(open) {
let after = &text[s + open.len()..];
if let Some(e) = after.find(close) {
return after[..e].trim().to_string();
}
}
String::new()
}
pub fn parse_rbara_block(xmp_bytes: &[u8]) -> Option<RbaraXmpBlock> {
let text = std::str::from_utf8(xmp_bytes).ok()?;
const START: &str = "<!-- rbara:start -->";
const END: &str = "<!-- rbara:end -->";
let start_pos = text.find(START)?;
let end_pos = text.find(END)?;
let block = &text[start_pos..end_pos + END.len()];
fn extract(block: &str, tag: &str) -> String {
let open = format!("<rbara:{tag}>");
let close = format!("</rbara:{tag}>");
block
.find(open.as_str())
.and_then(|s| {
let after = &block[s + open.len()..];
after
.find(close.as_str())
.map(|e| after[..e].trim().to_string())
})
.unwrap_or_default()
}
let ops = {
let mut ops = Vec::new();
let li_open = "<rdf:li>";
let li_close = "</rdf:li>";
let mut rest = block;
while let Some(s) = rest.find(li_open) {
let after = &rest[s + li_open.len()..];
match after.find(li_close) {
Some(e) => {
let op = after[..e].trim().to_string();
if !op.is_empty() {
ops.push(op);
}
rest = &after[e + li_close.len()..];
}
None => break,
}
}
ops
};
Some(RbaraXmpBlock {
uuid: extract(block, "uuid"),
version: extract(block, "version"),
timestamp: extract(block, "timestamp"),
source_hash: extract(block, "sourceHash"),
parent_id: extract(block, "parentId"),
ops,
})
}
pub fn render_block(b: &RbaraXmpBlock) -> String {
let ops_xml = if b.ops.is_empty() {
" <rbara:ops/>\n".to_string()
} else {
let items = b
.ops
.iter()
.map(|op| format!(" <rdf:li>{op}</rdf:li>"))
.collect::<Vec<_>>()
.join("\n");
format!(
" <rbara:ops>\n <rdf:Seq>\n{items}\n </rdf:Seq>\n </rbara:ops>\n"
)
};
format!(
"<!-- rbara:start -->\n \
<rdf:Description rdf:about=\"\"\n \
xmlns:rbara=\"{ns}\">\n \
<rbara:uuid>{uuid}</rbara:uuid>\n \
<rbara:version>{ver}</rbara:version>\n \
<rbara:timestamp>{ts}</rbara:timestamp>\n \
<rbara:sourceHash>{hash}</rbara:sourceHash>\n \
<rbara:parentId>{pid}</rbara:parentId>\n\
{ops_xml}\
</rdf:Description>\n\
<!-- rbara:end -->\n",
ns = RBARA_NS,
uuid = b.uuid,
ver = b.version,
ts = b.timestamp,
hash = b.source_hash,
pid = b.parent_id,
)
}
pub fn create_xmp(b: &RbaraXmpBlock) -> String {
format!(
"<?xpacket begin=\"\u{FEFF}\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n\
<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"rustybara {ver}\">\n \
<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n\
{block}\
</rdf:RDF>\n\
</x:xmpmeta>\n\
<?xpacket end=\"w\"?>",
ver = b.version,
block = render_block(b),
)
}
pub fn inject_into_xmp(existing: &str, b: &RbaraXmpBlock) -> String {
const START: &str = "<!-- rbara:start -->";
const END: &str = "<!-- rbara:end -->";
let cleaned: std::borrow::Cow<str> = match (existing.find(START), existing.find(END)) {
(Some(s), Some(e)) => {
let end_pos = e + END.len();
format!("{}{}", &existing[..s], &existing[end_pos..]).into()
}
_ => existing.into(),
};
let block = render_block(b);
const CLOSE: &str = "</rdf:RDF>";
match cleaned.find(CLOSE) {
Some(pos) => format!("{}{}{}", &cleaned[..pos], block, &cleaned[pos..]),
None => format!("{}\n{}", cleaned, block),
}
}