Skip to content

Commit 4ce679c

Browse files
committed
fix!: Tree|TreeRef::write_to() will now assure the most basic sanity of entry names.
Previously, it would allow null-bytes in the name which would corrupt the written tree. Now this is forbidden. For some reason, it disallowed newlines, but that is now allowed as validation is should be handled on a higher level.
1 parent 1bf08c8 commit 4ce679c

File tree

5 files changed

+93
-17
lines changed

5 files changed

+93
-17
lines changed

gix-object/src/tree/editor.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ impl<'a> Editor<'a> {
4444
/// Future calls to [`upsert`](Self::upsert) or similar will keep working on the last seen state of the
4545
/// just-written root-tree.
4646
/// If this is not desired, use [set_root()](Self::set_root()).
47+
///
48+
/// ### Validation
49+
///
50+
/// Note that no additional validation is performed to assure correctness of entry-names.
51+
/// It is absolutely and intentionally possible to write out invalid trees with this method.
52+
/// Higher layers are expected to perform detailed validation.
4753
pub fn write<E>(&mut self, out: impl FnMut(&Tree) -> Result<ObjectId, E>) -> Result<ObjectId, E> {
4854
self.path_buf.clear();
4955
self.write_at_pathbuf(out, WriteMode::Normal)

gix-object/src/tree/write.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::{
1212
#[derive(Debug, thiserror::Error)]
1313
#[allow(missing_docs)]
1414
pub enum Error {
15-
#[error("Newlines are invalid in file paths: {name:?}")]
16-
NewlineInFilename { name: BString },
15+
#[error("Nullbytes are invalid in file paths as they are separators: {name:?}")]
16+
NullbyteInFilename { name: BString },
1717
}
1818

1919
impl From<Error> for io::Error {
@@ -40,8 +40,8 @@ impl crate::WriteTo for Tree {
4040
out.write_all(mode.as_bytes(&mut buf))?;
4141
out.write_all(SPACE)?;
4242

43-
if filename.find_byte(b'\n').is_some() {
44-
return Err(Error::NewlineInFilename {
43+
if filename.find_byte(0).is_some() {
44+
return Err(Error::NullbyteInFilename {
4545
name: (*filename).to_owned(),
4646
}
4747
.into());
@@ -87,8 +87,8 @@ impl<'a> crate::WriteTo for TreeRef<'a> {
8787
out.write_all(mode.as_bytes(&mut buf))?;
8888
out.write_all(SPACE)?;
8989

90-
if filename.find_byte(b'\n').is_some() {
91-
return Err(Error::NewlineInFilename {
90+
if filename.find_byte(0).is_some() {
91+
return Err(Error::NullbyteInFilename {
9292
name: (*filename).to_owned(),
9393
}
9494
.into());

gix-object/tests/encode/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,41 @@ mod commit {
8888
}
8989

9090
mod tree {
91+
use gix_object::tree::EntryKind;
92+
use gix_object::{tree, WriteTo};
93+
94+
#[test]
95+
fn write_to_does_not_validate() {
96+
let mut tree = gix_object::Tree::empty();
97+
tree.entries.push(tree::Entry {
98+
mode: EntryKind::Blob.into(),
99+
filename: "".into(),
100+
oid: gix_hash::Kind::Sha1.null(),
101+
});
102+
tree.entries.push(tree::Entry {
103+
mode: EntryKind::Tree.into(),
104+
filename: "something\nwith\newlines\n".into(),
105+
oid: gix_hash::ObjectId::empty_tree(gix_hash::Kind::Sha1),
106+
});
107+
tree.write_to(&mut std::io::sink())
108+
.expect("write succeeds, no validation is performed");
109+
}
110+
111+
#[test]
112+
fn write_to_does_not_allow_separator() {
113+
let mut tree = gix_object::Tree::empty();
114+
tree.entries.push(tree::Entry {
115+
mode: EntryKind::Blob.into(),
116+
filename: "hi\0ho".into(),
117+
oid: gix_hash::Kind::Sha1.null(),
118+
});
119+
let err = tree.write_to(&mut std::io::sink()).unwrap_err();
120+
assert_eq!(
121+
err.to_string(),
122+
"Nullbytes are invalid in file paths as they are separators: \"hi\\0ho\""
123+
);
124+
}
125+
91126
round_trip!(gix_object::Tree, gix_object::TreeRef, "tree/everything.tree");
92127
}
93128

gix-object/tests/tree/editor.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,34 @@ fn from_existing_remove() -> crate::Result {
326326
Ok(())
327327
}
328328

329+
#[test]
330+
fn from_empty_invalid_write() -> crate::Result {
331+
let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes();
332+
let odb = StorageOdb::new(storage.clone());
333+
let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, gix_hash::Kind::Sha1);
334+
335+
let actual = edit
336+
.upsert(["", "\n"], EntryKind::Blob, any_blob())?
337+
.write(&mut write)
338+
.expect("no validation is performed");
339+
assert_eq!(
340+
display_tree(actual, &storage),
341+
"d521ac024b650c93d8b75463f68ad49938d98198
342+
└── \n bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.100644
343+
"
344+
);
345+
346+
let err = edit
347+
.upsert(Some("with\0null"), EntryKind::Blob, any_blob())?
348+
.write(&mut write)
349+
.unwrap_err();
350+
assert_eq!(
351+
err.to_string(),
352+
"Nullbytes are invalid in file paths as they are separators: \"with\\0null\""
353+
);
354+
Ok(())
355+
}
356+
329357
#[test]
330358
fn from_empty_add() -> crate::Result {
331359
let (storage, mut write, num_writes_and_clear) = new_inmemory_writes();
@@ -656,7 +684,7 @@ mod utils {
656684

657685
pub(super) fn new_inmemory_writes() -> (
658686
TreeStore,
659-
impl FnMut(&Tree) -> Result<ObjectId, std::convert::Infallible>,
687+
impl FnMut(&Tree) -> Result<ObjectId, std::io::Error>,
660688
impl Fn() -> usize,
661689
) {
662690
let store = TreeStore::default();
@@ -667,8 +695,7 @@ mod utils {
667695
let mut buf = Vec::with_capacity(512);
668696
move |tree: &Tree| {
669697
buf.clear();
670-
tree.write_to(&mut buf)
671-
.expect("write to memory can't fail and tree is valid");
698+
tree.write_to(&mut buf)?;
672699
let header = gix_object::encode::loose_header(gix_object::Kind::Tree, buf.len() as u64);
673700
let mut hasher = gix_features::hash::hasher(gix_hash::Kind::Sha1);
674701
hasher.update(&header);

gix-object/tests/tree/from_bytes.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
1-
use gix_object::{bstr::ByteSlice, tree, tree::EntryRef, TreeRef, TreeRefIter};
1+
use gix_object::{bstr::ByteSlice, tree, tree::EntryRef, Tree, TreeRef, TreeRefIter, WriteTo};
22

33
use crate::{fixture_name, hex_to_id};
44

55
#[test]
66
fn empty() -> crate::Result {
7+
let tree_ref = TreeRef::from_bytes(&[])?;
78
assert_eq!(
8-
TreeRef::from_bytes(&[])?,
9+
tree_ref,
910
TreeRef { entries: vec![] },
1011
"empty trees are valid despite usually rare in the wild"
1112
);
13+
14+
let mut buf = Vec::new();
15+
tree_ref.write_to(&mut buf)?;
16+
assert!(buf.is_empty());
17+
18+
buf.clear();
19+
Tree::from(tree_ref).write_to(&mut buf)?;
20+
assert!(buf.is_empty());
1221
Ok(())
1322
}
1423

1524
#[test]
1625
fn everything() -> crate::Result {
26+
let fixture = fixture_name("tree", "everything.tree");
27+
let tree_ref = TreeRef::from_bytes(&fixture)?;
1728
assert_eq!(
18-
TreeRef::from_bytes(&fixture_name("tree", "everything.tree"))?,
29+
tree_ref,
1930
TreeRef {
2031
entries: vec![
2132
EntryRef {
@@ -83,11 +94,8 @@ fn special_trees() -> crate::Result {
8394
("special-5", 17),
8495
] {
8596
let fixture = fixture_name("tree", &format!("{name}.tree"));
86-
assert_eq!(
87-
TreeRef::from_bytes(&fixture)?.entries.len(),
88-
expected_entry_count,
89-
"{name}"
90-
);
97+
let actual = TreeRef::from_bytes(&fixture)?;
98+
assert_eq!(actual.entries.len(), expected_entry_count, "{name}");
9199
assert_eq!(
92100
TreeRefIter::from_bytes(&fixture).map(Result::unwrap).count(),
93101
expected_entry_count,

0 commit comments

Comments
 (0)