objects/object/
tree_entry.rs1use serde::{Deserialize, Serialize};
5
6use super::{ContentHash, EntryType, FileMode, TreeError};
7
8pub(crate) fn validate_name(name: &str) -> Result<(), TreeError> {
10 if name.is_empty() {
11 return Err(TreeError::InvalidName("entry name cannot be empty".into()));
12 }
13 if name == "." || name == ".." {
14 return Err(TreeError::InvalidName(format!(
15 "'{}' is not a valid entry name",
16 name
17 )));
18 }
19 if name.contains('/') || name.contains('\\') {
20 return Err(TreeError::InvalidName(
21 "entry name cannot contain path separators".into(),
22 ));
23 }
24 if name.bytes().any(|b| b < 0x20 || b == 0x7f) {
25 return Err(TreeError::InvalidName(
26 "entry name contains control characters".into(),
27 ));
28 }
29 Ok(())
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub struct TreeEntry {
35 pub name: String,
36 pub mode: FileMode,
37 pub entry_type: EntryType,
38 pub hash: ContentHash,
39}
40
41impl TreeEntry {
42 pub(crate) fn validate(&self) -> Result<(), TreeError> {
43 validate_name(&self.name)
44 }
45
46 pub fn file(
47 name: impl Into<String>,
48 hash: ContentHash,
49 executable: bool,
50 ) -> Result<Self, TreeError> {
51 let name = name.into();
52 validate_name(&name)?;
53 Ok(Self {
54 name,
55 mode: if executable {
56 FileMode::Executable
57 } else {
58 FileMode::Normal
59 },
60 entry_type: EntryType::Blob,
61 hash,
62 })
63 }
64
65 pub fn directory(name: impl Into<String>, hash: ContentHash) -> Result<Self, TreeError> {
66 let name = name.into();
67 validate_name(&name)?;
68 Ok(Self {
69 name,
70 mode: FileMode::Normal,
71 entry_type: EntryType::Tree,
72 hash,
73 })
74 }
75
76 pub fn symlink(name: impl Into<String>, hash: ContentHash) -> Result<Self, TreeError> {
77 let name = name.into();
78 validate_name(&name)?;
79 Ok(Self {
80 name,
81 mode: FileMode::Symlink,
82 entry_type: EntryType::Symlink,
83 hash,
84 })
85 }
86
87 pub fn is_tree(&self) -> bool {
88 self.entry_type == EntryType::Tree
89 }
90
91 pub fn is_blob(&self) -> bool {
92 self.entry_type == EntryType::Blob
93 }
94
95 pub fn is_executable(&self) -> bool {
96 self.mode == FileMode::Executable
97 }
98
99 pub(crate) fn encoded_len(&self) -> usize {
100 1 + 1 + self.hash.as_bytes().len() + self.name.len() + 1
101 }
102
103 pub(crate) fn update_hasher(&self, hasher: &mut blake3::Hasher) {
104 hasher.update(&[self.mode.to_byte()]);
105 hasher.update(&[self.entry_type.to_byte()]);
106 hasher.update(self.hash.as_bytes());
107 hasher.update(self.name.as_bytes());
108 hasher.update(&[0]);
109 }
110}