use crate::inventory::Entry;
use crate::{FileId, RevisionId, NULL_REVISION};
use std::collections::HashSet;
use std::iter::FromIterator;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct InventoryDeltaEntry {
pub old_path: Option<String>,
pub new_path: Option<String>,
pub file_id: FileId,
pub new_entry: Option<Entry>,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct InventoryDelta(pub Vec<InventoryDeltaEntry>);
impl FromIterator<InventoryDeltaEntry> for InventoryDelta {
fn from_iter<T: IntoIterator<Item = InventoryDeltaEntry>>(iter: T) -> Self {
InventoryDelta(iter.into_iter().collect())
}
}
impl From<Vec<InventoryDeltaEntry>> for InventoryDelta {
fn from(v: Vec<InventoryDeltaEntry>) -> Self {
InventoryDelta(v)
}
}
impl std::ops::Deref for InventoryDelta {
type Target = Vec<InventoryDeltaEntry>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for InventoryDelta {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug)]
pub enum InventoryDeltaInconsistency {
DuplicateFileId(String, FileId),
DuplicateOldPath(String, FileId),
DuplicateNewPath(String, FileId),
NoPath,
MismatchedId(String, FileId, FileId),
EntryWithoutPath(String, FileId),
PathWithoutEntry(String, FileId),
PathMismatch(FileId, String, String),
OrphanedChild(FileId),
ParentNotDirectory(String, FileId),
ParentMissing(FileId),
NoSuchId(FileId),
InvalidEntryName(String),
FileIdCycle(FileId, String, String),
PathAlreadyVersioned(String, String),
}
impl InventoryDelta {
pub fn check(&self) -> Result<(), InventoryDeltaInconsistency> {
let mut ids = HashSet::new();
let mut old_paths = HashSet::new();
let mut new_paths = HashSet::new();
for entry in self.iter() {
let path = if let Some(old_path) = &entry.old_path {
old_path
} else if let Some(new_path) = &entry.new_path {
new_path
} else {
return Err(InventoryDeltaInconsistency::NoPath);
};
if !ids.insert(&entry.file_id) {
return Err(InventoryDeltaInconsistency::DuplicateFileId(
path.clone(),
entry.file_id.clone(),
));
}
if entry.old_path.is_some() {
let old_path = entry.old_path.as_ref().unwrap();
if !old_paths.insert(old_path) {
return Err(InventoryDeltaInconsistency::DuplicateOldPath(
old_path.clone(),
entry.file_id.clone(),
));
}
}
if entry.new_path.is_some() {
let new_path = entry.new_path.as_ref().unwrap();
if !new_paths.insert(new_path) {
return Err(InventoryDeltaInconsistency::DuplicateNewPath(
new_path.clone(),
entry.file_id.clone(),
));
}
}
if let Some(ref new_entry) = entry.new_entry {
if &entry.file_id != new_entry.file_id() {
return Err(InventoryDeltaInconsistency::MismatchedId(
path.clone(),
entry.file_id.clone(),
new_entry.file_id().clone(),
));
}
}
if entry.new_entry.is_some() && entry.new_path.is_none() {
return Err(InventoryDeltaInconsistency::EntryWithoutPath(
path.clone(),
entry.file_id.clone(),
));
}
if entry.new_entry.is_none() && entry.new_path.is_some() {
return Err(InventoryDeltaInconsistency::PathWithoutEntry(
path.clone(),
entry.file_id.clone(),
));
}
}
Ok(())
}
pub fn sort(&mut self) {
fn key(entry: &InventoryDeltaEntry) -> (&str, &str, &FileId, Option<&Entry>) {
(
entry.old_path.as_deref().unwrap_or(""),
entry.new_path.as_deref().unwrap_or(""),
&entry.file_id,
entry.new_entry.as_ref(),
)
}
self.sort_by(|x, y| key(y).cmp(&key(x)));
}
}
#[derive(Debug)]
pub enum InventoryDeltaSerializeError {
Invalid(String),
UnsupportedKind(String),
}
const FORMAT_1: &str = "bzr inventory delta v1 (bzr 1.14)";
pub fn serialize_inventory_entry(e: &Entry) -> Result<Vec<u8>, InventoryDeltaSerializeError> {
Ok(match e {
Entry::Directory { .. } | Entry::Root { .. } => b"dir".to_vec(),
Entry::File {
executable,
text_size,
ref text_sha1,
..
} => {
let mut v = b"file".to_vec();
v.push(b'\x00');
if text_size.is_none() {
return Err(InventoryDeltaSerializeError::Invalid(
"text_size is None".to_string(),
));
}
v.extend_from_slice(text_size.unwrap().to_string().as_bytes());
v.push(b'\x00');
if *executable {
v.push(b'Y');
}
v.push(b'\x00');
let text_sha1 = text_sha1.as_ref();
if text_sha1.is_none() {
return Err(InventoryDeltaSerializeError::Invalid(
"text_sha1 is None".to_string(),
));
}
v.extend_from_slice(text_sha1.unwrap().as_slice());
v
}
Entry::Link { symlink_target, .. } => {
let mut v = b"link".to_vec();
v.push(b'\x00');
if symlink_target.is_none() {
return Err(InventoryDeltaSerializeError::Invalid(
"symlink_target is None".to_string(),
));
}
v.extend_from_slice(symlink_target.as_ref().unwrap().as_bytes());
v
}
Entry::TreeReference {
reference_revision, ..
} => {
let mut v = b"tree".to_vec();
v.push(b'\x00');
if reference_revision.is_none() {
return Err(InventoryDeltaSerializeError::Invalid(
"reference_revision is None".to_string(),
));
}
v.extend_from_slice(reference_revision.as_ref().unwrap().as_bytes());
v
}
})
}
pub fn serialize_inventory_delta(
old_name: &RevisionId,
new_name: &RevisionId,
delta_to_new: &InventoryDelta,
versioned_root: bool,
tree_references: bool,
) -> Result<Vec<Vec<u8>>, InventoryDeltaSerializeError> {
let mut lines = vec![
format!("format: {}\n", FORMAT_1).into_bytes(),
[&b"parent: "[..], old_name.as_bytes(), &b"\n"[..]].concat(),
[&b"version: "[..], new_name.as_bytes(), &b"\n"[..]].concat(),
format!("versioned_root: {}\n", serialize_bool(versioned_root)).into_bytes(),
format!("tree_references: {}\n", serialize_bool(tree_references)).into_bytes(),
];
let mut extra_lines = delta_to_new
.iter()
.map(|entry| {
if let Some(entry) = entry.new_entry.as_ref() {
if !tree_references && entry.kind() == crate::osutils::Kind::TreeReference {
return Err(InventoryDeltaSerializeError::UnsupportedKind(
"tree-reference".to_string(),
));
}
}
delta_entry_to_line(entry, new_name, Some(versioned_root))
})
.collect::<Result<Vec<_>, _>>()?;
extra_lines.sort();
lines.extend(extra_lines);
Ok(lines)
}
fn delta_entry_to_line(
delta_item: &InventoryDeltaEntry,
new_version: &RevisionId,
versioned_root: Option<bool>,
) -> Result<Vec<u8>, InventoryDeltaSerializeError> {
let versioned_root = versioned_root.unwrap_or(true);
let last_modified;
let parent_id;
let oldpath_utf8;
let newpath_utf8;
let content;
if delta_item.new_path.is_none() {
if delta_item.old_path.is_none() {
return Err(InventoryDeltaSerializeError::Invalid(format!(
"Bad inventory delta: old_path is None in delta item {:?}",
delta_item
)));
}
oldpath_utf8 = format!("/{}", delta_item.old_path.as_ref().unwrap());
newpath_utf8 = "None".to_string();
parent_id = &b""[..];
last_modified = RevisionId::from(NULL_REVISION);
content = b"deleted\x00\x00".to_vec();
} else {
oldpath_utf8 = if let Some(ref old_path) = delta_item.old_path {
format!("/{}", old_path)
} else {
"None".to_string()
};
if delta_item.new_entry.is_none() {
return Err(InventoryDeltaSerializeError::Invalid(format!(
"Bad inventory delta: new_entry is None in delta item {:?}",
delta_item
)));
}
let new_entry = delta_item.new_entry.as_ref().unwrap();
if delta_item.new_path == Some("/".to_string()) {
return Err(InventoryDeltaSerializeError::Invalid(format!(
"Bad inventory delta: '/' is not a valid newpath (should be '') in delta item {:?}",
delta_item
)));
}
newpath_utf8 = format!(
"/{}",
delta_item.new_path.as_ref().unwrap_or(&"".to_string())
);
parent_id = new_entry
.parent_id()
.as_ref()
.map_or(&b""[..], |x| x.as_bytes());
if new_entry.revision().is_none() {
return Err(InventoryDeltaSerializeError::Invalid(format!(
"no version for fileid {:?}",
delta_item.file_id
)));
}
last_modified = new_entry.revision().unwrap().clone();
if newpath_utf8 == "/" && !versioned_root {
if &last_modified != new_version {
return Err(InventoryDeltaSerializeError::Invalid(format!(
"Version present for / in {:?} ({:?} != {:?})",
new_entry.file_id(),
last_modified,
new_version
)));
}
}
content = serialize_inventory_entry(new_entry)?;
}
let entries = [
oldpath_utf8.as_bytes(),
newpath_utf8.as_bytes(),
delta_item.file_id.as_bytes(),
parent_id,
last_modified.as_bytes(),
content.as_slice(),
];
let mut line = entries.join(&b"\x00"[..]);
line.push(b'\n');
Ok(line)
}
pub fn parse_inventory_entry(
file_id: FileId,
name: String,
parent_id: Option<FileId>,
revision: Option<RevisionId>,
data: &[u8],
) -> Entry {
let mut parts = data.split(|&c| c == b'\x00');
let entry_type = parts.next().unwrap();
match entry_type {
b"dir" => {
if parent_id.is_none() {
Entry::Root { file_id, revision }
} else {
Entry::Directory {
file_id,
name,
parent_id: parent_id.unwrap(),
revision,
}
}
}
b"file" => {
let text_size = parts.next().unwrap();
let executable = parts.next().unwrap();
let text_sha1 = parts.next().unwrap();
Entry::File {
file_id,
name,
parent_id: parent_id.unwrap(),
executable: executable == b"Y",
text_id: None,
text_size: Some(
String::from_utf8(text_size.to_vec())
.unwrap()
.parse()
.unwrap(),
),
text_sha1: Some(text_sha1.to_vec()),
revision,
}
}
b"link" => {
let symlink_target = parts.next().unwrap();
Entry::Link {
file_id,
name,
parent_id: parent_id.unwrap(),
symlink_target: Some(String::from_utf8(symlink_target.to_vec()).unwrap()),
revision,
}
}
b"tree" => {
let reference_revision = parts.next().unwrap();
Entry::TreeReference {
file_id,
name,
parent_id: parent_id.unwrap(),
reference_revision: Some(RevisionId::from(reference_revision)),
revision,
}
}
_ => panic!("Invalid entry type: {:?}", entry_type),
}
}
fn serialize_bool(value: bool) -> &'static str {
if value {
"true"
} else {
"false"
}
}
fn parse_bool(value: &[u8]) -> Result<bool, String> {
match value {
b"true" => Ok(true),
b"false" => Ok(false),
_ => Err(format!("Invalid boolean value: {:?}", value)),
}
}
pub fn parse_inventory_delta_item(
line: &[u8],
versioned_root: bool,
tree_references: bool,
delta_version_id: &RevisionId,
) -> Result<InventoryDeltaEntry, InventoryDeltaParseError> {
let parts = line.splitn(6, |&c| c == b'\x00').collect::<Vec<_>>();
let oldpath_utf8 = parts[0];
let newpath_utf8 = parts[1];
let file_id = FileId::from(parts[2]);
let parent_id = if parts[3].is_empty() {
None
} else {
Some(FileId::from(parts[3]))
};
let last_modified = RevisionId::from(parts[4]);
let content = parts[5];
if newpath_utf8 == b"/" && !versioned_root && &last_modified != delta_version_id {
return Err(InventoryDeltaParseError::Invalid(
"Versioned root found".to_string(),
));
} else if newpath_utf8 != b"None" && last_modified.is_reserved() {
return Err(InventoryDeltaParseError::Invalid(format!(
"special revisionid found: {:?}",
last_modified
)));
}
if content.starts_with(b"tree\x00") && !tree_references {
return Err(InventoryDeltaParseError::Invalid(
"Tree reference found (but header said tree_references: false)".to_string(),
));
}
fn parse_path(kind: &str, path: &[u8]) -> Result<Option<String>, InventoryDeltaParseError> {
if path == b"None" {
Ok(None)
} else if !path.starts_with(b"/") {
Err(InventoryDeltaParseError::Invalid(format!(
"{} invalid: {} (does not start with /)",
kind,
String::from_utf8_lossy(path)
)))
} else {
Ok(Some(String::from_utf8(path[1..].to_vec()).map_err(
|x| {
InventoryDeltaParseError::Invalid(format!(
"{} invalid: {} (invalid utf8: {})",
kind,
String::from_utf8_lossy(path),
x
))
},
)?))
}
}
let old_path = parse_path("oldpath", oldpath_utf8)?;
let new_path = parse_path("newpath", newpath_utf8)?;
let new_entry = if content.starts_with(b"deleted\x00") {
None
} else {
let name = new_path.as_ref().unwrap().rsplit_once('/').map_or_else(
|| new_path.as_ref().unwrap().clone(),
|(_, name)| name.to_string(),
);
Some(parse_inventory_entry(
file_id.clone(),
name,
parent_id,
Some(last_modified),
content,
))
};
Ok(InventoryDeltaEntry {
old_path,
new_path,
file_id,
new_entry,
})
}
#[derive(Debug)]
pub enum InventoryDeltaParseError {
Incompatible(String),
Invalid(String),
}
pub fn parse_inventory_delta(
lines: &[&[u8]],
allow_versioned_root: Option<bool>,
allow_tree_references: Option<bool>,
) -> Result<(RevisionId, RevisionId, bool, bool, InventoryDelta), InventoryDeltaParseError> {
let allow_versioned_root = allow_versioned_root.unwrap_or(true);
let allow_tree_references = allow_tree_references.unwrap_or(true);
if lines.is_empty() {
return Err(InventoryDeltaParseError::Invalid(
"Invalid inventory delta is empty".to_string(),
));
}
if !lines[lines.len() - 1].ends_with(b"\n") {
return Err(InventoryDeltaParseError::Invalid(
"last line not empty".to_string(),
));
}
let lines = lines
.iter()
.map(|x| x.strip_suffix(b"\n").unwrap())
.collect::<Vec<_>>();
if lines.is_empty() || lines[0] != [&b"format: "[..], FORMAT_1.as_bytes()].concat() {
return Err(InventoryDeltaParseError::Invalid(format!(
"unknown format: {}",
String::from_utf8_lossy(&lines[0][8..])
)));
}
if lines.len() < 2 || !lines[1].starts_with(b"parent: ") {
return Err(InventoryDeltaParseError::Invalid(
"missing parent: marker".to_string(),
));
}
let delta_parent_id = RevisionId::from(lines[1][8..].to_vec());
if lines.len() < 3 || !lines[2].starts_with(b"version: ") {
return Err(InventoryDeltaParseError::Invalid(
"missing version: marker".to_string(),
));
}
let delta_version = RevisionId::from(lines[2][9..].to_vec());
if lines.len() < 4 || !lines[3].starts_with(b"versioned_root: ") {
return Err(InventoryDeltaParseError::Invalid(
"missing versioned_root: marker".to_string(),
));
}
let delta_versioned_root = parse_bool(&lines[3][16..]).unwrap();
if !allow_versioned_root && delta_versioned_root {
return Err(InventoryDeltaParseError::Incompatible(
"versioned_root not allowed".to_string(),
));
}
if lines.len() < 5 || !lines[4].starts_with(b"tree_references: ") {
return Err(InventoryDeltaParseError::Invalid(
"missing tree_references: marker".to_string(),
));
}
let delta_tree_references = parse_bool(&lines[4][17..]).unwrap();
let mut result = Vec::new();
let mut ids = HashSet::new();
for line in lines.iter().skip(5) {
let item = parse_inventory_delta_item(
line,
delta_versioned_root,
delta_tree_references,
&delta_version,
)?;
if !allow_tree_references
&& item.new_entry.is_some()
&& item.new_entry.as_ref().unwrap().kind() == crate::osutils::Kind::TreeReference
{
return Err(InventoryDeltaParseError::Incompatible(
"Tree reference not allowed".to_string(),
));
}
if !ids.insert(item.file_id.clone()) {
return Err(InventoryDeltaParseError::Invalid(format!(
"duplicate file id: {:?}",
item.file_id
)));
}
result.push(item);
}
Ok((
delta_parent_id,
delta_version,
delta_versioned_root,
delta_tree_references,
InventoryDelta(result),
))
}
#[cfg(test)]
mod tests {
use super::*;
fn split(s: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut start = 0;
for (i, &b) in s.iter().enumerate() {
if b == b'\n' {
out.push(&s[start..=i]);
start = i + 1;
}
}
if start < s.len() {
out.push(&s[start..]);
}
out
}
fn parse(
bytes: &[u8],
) -> Result<(RevisionId, RevisionId, bool, bool, InventoryDelta), InventoryDeltaParseError>
{
let lines = split(bytes);
parse_inventory_delta(&lines, None, None)
}
#[test]
fn parse_no_bytes_errors() {
let err = parse_inventory_delta(&[], None, None).unwrap_err();
match err {
InventoryDeltaParseError::Invalid(msg) => assert!(msg.contains("empty")),
_ => panic!("expected Invalid"),
}
}
#[test]
fn parse_bad_format_errors() {
let err = parse(b"format: foo\n").unwrap_err();
match err {
InventoryDeltaParseError::Invalid(msg) => assert!(msg.contains("unknown format")),
_ => panic!("expected Invalid"),
}
}
#[test]
fn parse_no_parent_marker_errors() {
let err = parse(b"format: bzr inventory delta v1 (bzr 1.14)\n").unwrap_err();
match err {
InventoryDeltaParseError::Invalid(msg) => assert!(msg.contains("missing parent")),
_ => panic!("expected Invalid"),
}
}
#[test]
fn parse_no_version_marker_errors() {
let err = parse(b"format: bzr inventory delta v1 (bzr 1.14)\nparent: null:\n").unwrap_err();
match err {
InventoryDeltaParseError::Invalid(msg) => assert!(msg.contains("missing version")),
_ => panic!("expected Invalid"),
}
}
#[test]
fn parse_versioned_root_only_round_trip() {
let bytes = b"format: bzr inventory delta v1 (bzr 1.14)\n\
parent: null:\n\
version: entry-version\n\
versioned_root: true\n\
tree_references: true\n\
None\x00/\x00an-id\x00\x00a@e\xc3\xa5ample.com--2004\x00dir\n";
let (parent, version, versioned_root, tree_refs, delta) = parse(bytes).unwrap();
assert_eq!(parent.as_bytes(), b"null:");
assert_eq!(version.as_bytes(), b"entry-version");
assert!(versioned_root);
assert!(tree_refs);
assert_eq!(delta.0.len(), 1);
let item = &delta.0[0];
assert_eq!(item.old_path, None);
assert_eq!(item.new_path.as_deref(), Some(""));
assert_eq!(item.file_id.as_bytes(), b"an-id");
}
#[test]
fn parse_duplicate_file_id_errors() {
let bytes = b"format: bzr inventory delta v1 (bzr 1.14)\n\
parent: null:\n\
version: null:\n\
versioned_root: true\n\
tree_references: true\n\
None\x00/\x00an-id\x00\x00a@e\xc3\xa5ample.com--2004\x00dir\n\
None\x00/\x00an-id\x00\x00a@e\xc3\xa5ample.com--2004\x00dir\n";
let err = parse(bytes).unwrap_err();
match err {
InventoryDeltaParseError::Invalid(msg) => assert!(msg.contains("duplicate file id")),
_ => panic!("expected Invalid"),
}
}
#[test]
fn parse_versioned_root_when_disabled_errors() {
let bytes = b"format: bzr inventory delta v1 (bzr 1.14)\n\
parent: null:\n\
version: null:\n\
versioned_root: true\n\
tree_references: true\n";
let lines = split(bytes);
let err = parse_inventory_delta(&lines, Some(false), None).unwrap_err();
match err {
InventoryDeltaParseError::Incompatible(msg) => assert!(msg.contains("versioned_root")),
_ => panic!("expected Incompatible"),
}
}
#[test]
fn parse_last_line_not_empty_errors() {
let err = parse(b"format: bzr inventory delta v1 (bzr 1.14)\nparent: null:\nversion: x")
.unwrap_err();
match err {
InventoryDeltaParseError::Invalid(msg) => assert!(msg.contains("last line")),
_ => panic!("expected Invalid"),
}
}
use crate::inventory::Entry;
use crate::FileId;
fn parent_id() -> FileId {
FileId::from(b"parent".to_vec())
}
fn file_id() -> FileId {
FileId::from(b"file-id".to_vec())
}
#[test]
fn serialize_dir_entry() {
let entry = Entry::directory(file_id(), "a dir".to_string(), parent_id(), None);
assert_eq!(serialize_inventory_entry(&entry).unwrap(), b"dir".to_vec());
}
#[test]
fn serialize_file_zero_length_short_sha() {
let entry = Entry::file(
file_id(),
"a file".to_string(),
parent_id(),
None,
Some(b"".to_vec()),
Some(0),
Some(false),
None,
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"file\x000\x00\x00".to_vec()
);
}
#[test]
fn serialize_file_with_sha_and_size() {
let entry = Entry::file(
file_id(),
"a file".to_string(),
parent_id(),
None,
Some(b"foo".to_vec()),
Some(10),
Some(false),
None,
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"file\x0010\x00\x00foo".to_vec()
);
}
#[test]
fn serialize_file_executable() {
let entry = Entry::file(
file_id(),
"a file".to_string(),
parent_id(),
None,
Some(b"foo".to_vec()),
Some(10),
Some(true),
None,
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"file\x0010\x00Y\x00foo".to_vec()
);
}
#[test]
fn serialize_file_without_size_errors() {
let entry = Entry::file(
file_id(),
"a file".to_string(),
parent_id(),
None,
Some(b"foo".to_vec()),
None,
Some(false),
None,
);
assert!(serialize_inventory_entry(&entry).is_err());
}
#[test]
fn serialize_file_without_sha1_errors() {
let entry = Entry::file(
file_id(),
"a file".to_string(),
parent_id(),
None,
None,
Some(10),
Some(false),
None,
);
assert!(serialize_inventory_entry(&entry).is_err());
}
#[test]
fn serialize_link_empty_target() {
let entry = Entry::link(
file_id(),
"a link".to_string(),
parent_id(),
None,
Some("".to_string()),
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"link\x00".to_vec()
);
}
#[test]
fn serialize_link_unicode_target() {
let entry = Entry::link(
file_id(),
"a link".to_string(),
parent_id(),
None,
Some(" \u{e5}".to_string()),
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"link\x00 \xc3\xa5".to_vec()
);
}
#[test]
fn serialize_link_space_target() {
let entry = Entry::link(
file_id(),
"a link".to_string(),
parent_id(),
None,
Some(" ".to_string()),
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"link\x00 ".to_vec()
);
}
#[test]
fn serialize_link_no_target_errors() {
let entry = Entry::link(file_id(), "a link".to_string(), parent_id(), None, None);
assert!(serialize_inventory_entry(&entry).is_err());
}
#[test]
fn serialize_reference_null() {
let entry = Entry::tree_reference(
file_id(),
"a tree".to_string(),
parent_id(),
None,
Some(crate::RevisionId::from(crate::NULL_REVISION.to_vec())),
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"tree\x00null:".to_vec()
);
}
#[test]
fn serialize_reference_revision() {
let entry = Entry::tree_reference(
file_id(),
"a tree".to_string(),
parent_id(),
None,
Some(crate::RevisionId::from(b"foo@\xc3\xa5b-lah".to_vec())),
);
assert_eq!(
serialize_inventory_entry(&entry).unwrap(),
b"tree\x00foo@\xc3\xa5b-lah".to_vec()
);
}
#[test]
fn serialize_reference_no_reference_errors() {
let entry = Entry::tree_reference(file_id(), "a tree".to_string(), parent_id(), None, None);
assert!(serialize_inventory_entry(&entry).is_err());
}
fn rev(s: &[u8]) -> RevisionId {
RevisionId::from(s)
}
fn null_rev() -> RevisionId {
RevisionId::from(NULL_REVISION)
}
fn joined(lines: Vec<Vec<u8>>) -> Vec<u8> {
lines.concat()
}
#[test]
fn serialize_empty_delta_to_lines() {
let lines = serialize_inventory_delta(
&null_rev(),
&null_rev(),
&InventoryDelta(vec![]),
true,
true,
)
.unwrap();
assert_eq!(
joined(lines),
b"format: bzr inventory delta v1 (bzr 1.14)\n\
parent: null:\nversion: null:\n\
versioned_root: true\ntree_references: true\n"
.to_vec()
);
}
#[test]
fn serialize_root_only_to_lines() {
let root = Entry::root(
FileId::from(&b"an-id"[..]),
Some(rev(b"a@e\xc3\xa5ample.com--2004")),
);
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: None,
new_path: Some("".to_string()),
file_id: FileId::from(&b"an-id"[..]),
new_entry: Some(root),
}]);
let lines =
serialize_inventory_delta(&null_rev(), &rev(b"entry-version"), &delta, true, true)
.unwrap();
assert_eq!(
joined(lines),
b"format: bzr inventory delta v1 (bzr 1.14)\n\
parent: null:\nversion: entry-version\n\
versioned_root: true\ntree_references: true\n\
None\x00/\x00an-id\x00\x00a@e\xc3\xa5ample.com--2004\x00dir\n"
.to_vec()
);
}
#[test]
fn serialize_unversioned_root_to_lines() {
let root = Entry::root(FileId::from(&b"TREE_ROOT"[..]), Some(rev(b"entry-version")));
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: None,
new_path: Some("".to_string()),
file_id: FileId::from(&b"TREE_ROOT"[..]),
new_entry: Some(root),
}]);
let lines =
serialize_inventory_delta(&null_rev(), &rev(b"entry-version"), &delta, false, false)
.unwrap();
assert_eq!(
joined(lines),
b"format: bzr inventory delta v1 (bzr 1.14)\n\
parent: null:\nversion: entry-version\n\
versioned_root: false\ntree_references: false\n\
None\x00/\x00TREE_ROOT\x00\x00entry-version\x00dir\n"
.to_vec()
);
}
#[test]
fn serialize_delete_entry_line() {
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: Some("foo".to_string()),
new_path: None,
file_id: FileId::from(&b"foo-id"[..]),
new_entry: None,
}]);
let lines =
serialize_inventory_delta(&rev(b"old"), &rev(b"new"), &delta, true, true).unwrap();
assert_eq!(
lines.last().unwrap().as_slice(),
b"/foo\x00None\x00foo-id\x00\x00null:\x00deleted\x00\x00\n"
);
}
#[test]
fn serialize_errors_when_no_version_for_fileid() {
let entry = Entry::directory(
FileId::from(&b"id"[..]),
"foo".to_string(),
FileId::from(&b"TREE_ROOT"[..]),
None,
);
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: None,
new_path: Some("foo".to_string()),
file_id: FileId::from(&b"id"[..]),
new_entry: Some(entry),
}]);
let err =
serialize_inventory_delta(&null_rev(), &rev(b"entry-version"), &delta, true, true)
.unwrap_err();
match err {
InventoryDeltaSerializeError::Invalid(msg) => assert!(msg.contains("no version")),
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn serialize_errors_on_versioned_root_when_unversioned() {
let root = Entry::root(
FileId::from(&b"TREE_ROOT"[..]),
Some(rev(b"some-other-rev")),
);
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: None,
new_path: Some("".to_string()),
file_id: FileId::from(&b"TREE_ROOT"[..]),
new_entry: Some(root),
}]);
let err =
serialize_inventory_delta(&null_rev(), &rev(b"entry-version"), &delta, false, false)
.unwrap_err();
match err {
InventoryDeltaSerializeError::Invalid(msg) => assert!(msg.contains("Version present")),
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn serialize_errors_on_tree_reference_when_disabled() {
let entry = Entry::tree_reference(
FileId::from(&b"ref-id"[..]),
"sub".to_string(),
FileId::from(&b"TREE_ROOT"[..]),
Some(rev(b"ref-rev")),
Some(rev(b"entry-version")),
);
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: None,
new_path: Some("sub".to_string()),
file_id: FileId::from(&b"ref-id"[..]),
new_entry: Some(entry),
}]);
let err =
serialize_inventory_delta(&null_rev(), &rev(b"entry-version"), &delta, true, false)
.unwrap_err();
match err {
InventoryDeltaSerializeError::UnsupportedKind(k) => assert_eq!(k, "tree-reference"),
other => panic!("expected UnsupportedKind, got {:?}", other),
}
}
#[test]
fn serialize_errors_on_slash_newpath() {
let entry = Entry::directory(
FileId::from(&b"id"[..]),
"x".to_string(),
FileId::from(&b"TREE_ROOT"[..]),
Some(rev(b"r")),
);
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: None,
new_path: Some("/".to_string()),
file_id: FileId::from(&b"id"[..]),
new_entry: Some(entry),
}]);
let err =
serialize_inventory_delta(&null_rev(), &rev(b"v"), &delta, true, true).unwrap_err();
match err {
InventoryDeltaSerializeError::Invalid(msg) => {
assert!(msg.contains("not a valid newpath"))
}
other => panic!("expected Invalid, got {:?}", other),
}
}
}