Skip to content

Commit 6af2abc

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 a9839b6 commit 6af2abc

File tree

5 files changed

+93
-17
lines changed

5 files changed

+93
-17
lines changed

gix-object/src/tree/editor.rs

+6
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

+6-6
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

+35
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

+30-3
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,34 @@ fn from_existing_remove() -> crate::Result {
292292
Ok(())
293293
}
294294

295+
#[test]
296+
fn from_empty_invalid_write() -> crate::Result {
297+
let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes();
298+
let odb = StorageOdb::new(storage.clone());
299+
let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, gix_hash::Kind::Sha1);
300+
301+
let actual = edit
302+
.upsert(["", "\n"], EntryKind::Blob, any_blob())?
303+
.write(&mut write)
304+
.expect("no validation is performed");
305+
assert_eq!(
306+
display_tree(actual, &storage),
307+
"d521ac024b650c93d8b75463f68ad49938d98198
308+
└── \n bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.100644
309+
"
310+
);
311+
312+
let err = edit
313+
.upsert(Some("with\0null"), EntryKind::Blob, any_blob())?
314+
.write(&mut write)
315+
.unwrap_err();
316+
assert_eq!(
317+
err.to_string(),
318+
"Nullbytes are invalid in file paths as they are separators: \"with\\0null\""
319+
);
320+
Ok(())
321+
}
322+
295323
#[test]
296324
fn from_empty_add() -> crate::Result {
297325
let (storage, mut write, num_writes_and_clear) = new_inmemory_writes();
@@ -622,7 +650,7 @@ mod utils {
622650

623651
pub(super) fn new_inmemory_writes() -> (
624652
TreeStore,
625-
impl FnMut(&Tree) -> Result<ObjectId, std::convert::Infallible>,
653+
impl FnMut(&Tree) -> Result<ObjectId, std::io::Error>,
626654
impl Fn() -> usize,
627655
) {
628656
let store = TreeStore::default();
@@ -633,8 +661,7 @@ mod utils {
633661
let mut buf = Vec::with_capacity(512);
634662
move |tree: &Tree| {
635663
buf.clear();
636-
tree.write_to(&mut buf)
637-
.expect("write to memory can't fail and tree is valid");
664+
tree.write_to(&mut buf)?;
638665
let header = gix_object::encode::loose_header(gix_object::Kind::Tree, buf.len() as u64);
639666
let mut hasher = gix_features::hash::hasher(gix_hash::Kind::Sha1);
640667
hasher.update(&header);

gix-object/tests/tree/from_bytes.rs

+16-8
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)