git_policy_cli/
repo_read_tree.rs1#![cfg(unix)]
15
16use super::repo_read_object::{ObjectKind, name_length};
17use super::repo_read_layout::ObjectFormat;
18use super::repo_read_odb::ObjectDatabase;
19
20const TREE_DEPTH_LIMIT: usize = 256;
22
23const FLAT_ENTRY_LIMIT: usize = 4_000_000;
25
26pub const MODE_TREE: u32 = 0o040_000;
28
29pub const MODE_GITLINK: u32 = 0o160_000;
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct TreeEntry {
35 pub mode: u32,
37 pub name: Vec<u8>,
39 pub id: Vec<u8>,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct FlatEntry {
46 pub path: Vec<u8>,
48 pub mode: u32,
50 pub id: Vec<u8>,
52}
53
54pub fn parse_tree(bytes: &[u8], hash_length: usize) -> Option<Vec<TreeEntry>> {
56 let mut entries: Vec<TreeEntry> = Vec::new();
57 let mut cursor: usize = 0;
58 while cursor < bytes.len() {
59 let space: usize = cursor + bytes[cursor..].iter().position(|byte| { return *byte == b' '; })?;
60 let mode_text: &str = std::str::from_utf8(&bytes[cursor..space]).ok()?;
61 if mode_text.is_empty() || mode_text.starts_with('0') {
63 return None;
64 }
65 let mode: u32 = u32::from_str_radix(mode_text, 8).ok()?;
66 if !matches!(mode, 0o100_644 | 0o100_755 | 0o120_000 | 0o160_000 | MODE_TREE) {
68 return None;
69 }
70 let nul: usize = space + 1 + bytes[space + 1..].iter().position(|byte| { return *byte == 0; })?;
71 let name: Vec<u8> = bytes[space + 1..nul].to_vec();
72 if name.is_empty() || name.contains(&b'/') {
73 return None;
74 }
75 let id: Vec<u8> = bytes.get(nul + 1..nul + 1 + hash_length)?.to_vec();
76 entries.push(TreeEntry { mode, name, id });
77 cursor = nul + 1 + hash_length;
78 }
79 return Some(entries);
80}
81
82pub fn is_tree_mode(mode: u32) -> bool {
84 return mode & 0o170_000 == MODE_TREE;
85}
86
87pub fn flatten_tree(database: &ObjectDatabase, tree: &[u8], format: ObjectFormat) -> Option<Vec<FlatEntry>> {
98 let mut flat: Vec<FlatEntry> = Vec::new();
99 walk(database, tree, format, &mut Vec::new(), 0, &mut flat)?;
100 return Some(flat);
101}
102
103fn walk(
105 database: &ObjectDatabase,
106 tree: &[u8],
107 format: ObjectFormat,
108 prefix: &mut Vec<u8>,
109 depth: usize,
110 flat: &mut Vec<FlatEntry>,
111) -> Option<()> {
112 if depth > TREE_DEPTH_LIMIT {
113 return None;
114 }
115 let object = database.read(tree)?;
116 if object.kind != ObjectKind::Tree {
117 return None;
118 }
119 for entry in parse_tree(object.bytes.as_slice(), name_length(format))? {
120 let length: usize = prefix.len();
121 prefix.extend_from_slice(entry.name.as_slice());
122 if is_tree_mode(entry.mode) {
123 prefix.push(b'/');
124 walk(database, entry.id.as_slice(), format, prefix, depth + 1, flat)?;
125 } else {
126 flat.push(FlatEntry { path: prefix.clone(), mode: entry.mode, id: entry.id });
127 if flat.len() > FLAT_ENTRY_LIMIT {
128 return None;
129 }
130 }
131 prefix.truncate(length);
132 }
133 return Some(());
134}
135
136#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct CommitFields {
139 pub tree: Vec<u8>,
141 pub parents: Vec<Vec<u8>>,
143 pub message: Vec<u8>,
145}
146
147pub fn parse_commit(bytes: &[u8], format: ObjectFormat) -> Option<CommitFields> {
158 let split: usize = bytes.windows(2).position(|pair| { return pair == b"\n\n"; })?;
159 let header: &[u8] = &bytes[..split];
160 let mut tree: Option<Vec<u8>> = None;
161 let mut parents: Vec<Vec<u8>> = Vec::new();
162 for (index, line) in header.split(|byte| { return *byte == b'\n'; }).enumerate() {
163 if let Some(text) = line.strip_prefix(b"tree ") {
164 if index != 0 {
165 return None;
166 }
167 tree = Some(super::repo_read_object::from_hex(text, format)?);
168 } else if let Some(text) = line.strip_prefix(b"parent ") {
169 parents.push(super::repo_read_object::from_hex(text, format)?);
170 } else if line.starts_with(b"encoding ") {
171 return None;
172 }
173 }
174 return Some(CommitFields { tree: tree?, parents, message: bytes[split + 2..].to_vec() });
175}
176
177pub fn subject_of(message: &[u8]) -> Option<Vec<u8>> {
188 if message.contains(&b'\r') || message.contains(&0) {
189 return None;
190 }
191 let mut subject: Vec<u8> = Vec::new();
192 let mut started: bool = false;
193 for line in message.split(|byte| { return *byte == b'\n'; }) {
194 let trimmed: &[u8] = line.trim_ascii_end();
195 let blank: bool = trimmed.is_empty();
196 if blank && !started {
197 continue;
198 }
199 if blank {
200 break;
201 }
202 if started {
203 subject.push(b' ');
204 }
205 started = true;
206 subject.extend_from_slice(line.trim_ascii_end());
207 }
208 return Some(subject);
209}
210
211#[cfg(test)]
212#[path = "repo_read_tree_tests.rs"]
213mod tests;