Skip to main content

objects/object/
tree_entry.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Tree entry definitions.
3
4use serde::{Deserialize, Serialize};
5
6use super::{ContentHash, EntryType, FileMode, TreeError};
7
8/// Validates that a tree entry name is valid.
9pub(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/// A single entry in a tree.
33#[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}