git_policy_cli/
repo_read_refs.rs1#![cfg(unix)]
14
15use super::private_storage::read_regular_file;
16use std::path::{Path, PathBuf};
17
18const LOOSE_LIMIT: u64 = 4096;
20
21const PACKED_LIMIT: u64 = 256 << 20;
23
24const SYMBOLIC_DEPTH_LIMIT: usize = 5;
26
27#[derive(Clone, Debug, Eq, PartialEq)]
29pub enum RefValue {
30 Direct(String),
32 Symbolic(String),
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum Lookup {
39 Found(RefValue),
41 Missing,
43 Unsure,
45}
46
47pub struct Refs {
49 directory: PathBuf,
51 hex_length: usize,
53}
54
55pub fn is_plain_ref_name(name: &str) -> bool {
58 let Some(rest) = name.strip_prefix("refs/") else {
59 return false;
60 };
61 if rest.is_empty() || rest.ends_with('.') || rest.contains("..") {
62 return false;
63 }
64 return rest.split('/').all(|component| {
65 return !component.is_empty()
66 && !component.starts_with('.')
67 && !component.ends_with(".lock")
68 && component.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'); });
69 });
70}
71
72impl Refs {
73 pub fn new(directory: &Path, hex_length: usize) -> Refs {
75 return Refs { directory: directory.to_path_buf(), hex_length };
76 }
77
78 fn is_name(&self, text: &str) -> bool {
80 return text.len() == self.hex_length
81 && text.bytes().all(|byte| { return matches!(byte, b'0'..=b'9' | b'a'..=b'f'); })
82 && !text.bytes().all(|byte| { return byte == b'0'; });
83 }
84
85 fn parse_loose(&self, text: &str) -> Option<RefValue> {
87 let line: &str = text.strip_suffix('\n')?;
88 if let Some(target) = line.strip_prefix("ref: ") {
89 return is_plain_ref_name(target).then(|| { return RefValue::Symbolic(target.to_string()); });
90 }
91 return self.is_name(line).then(|| { return RefValue::Direct(line.to_string()); });
92 }
93
94 fn packed(&self) -> Option<Vec<(String, String)>> {
96 let bytes: Vec<u8> = match read_regular_file(self.directory.join("packed-refs").as_path(), PACKED_LIMIT) {
97 Ok(bytes) => bytes,
98 Err(super::private_storage::ReadRefusal::Missing) => return Some(Vec::new()),
99 Err(_) => return None,
100 };
101 let text: &str = std::str::from_utf8(bytes.as_slice()).ok()?;
102 let mut packed: Vec<(String, String)> = Vec::new();
103 let mut after_ref: bool = false;
105 for (position, line) in text.lines().enumerate() {
106 if line.starts_with('#') {
107 if position != 0 {
108 return None;
109 }
110 continue;
111 }
112 if let Some(peeled) = line.strip_prefix('^') {
113 if !after_ref || !self.is_name(peeled) {
114 return None;
115 }
116 after_ref = false;
117 continue;
118 }
119 let (object, name) = line.split_once(' ')?;
120 if !self.is_name(object) || !is_plain_ref_name(name) {
121 return None;
122 }
123 packed.push((name.to_string(), object.to_string()));
124 after_ref = true;
125 }
126 return Some(packed);
127 }
128
129 pub fn lookup_head(&self, git_dir: &Path) -> Option<RefValue> {
131 let bytes: Vec<u8> = read_regular_file(git_dir.join("HEAD").as_path(), LOOSE_LIMIT).ok()?;
132 return self.parse_loose(std::str::from_utf8(bytes.as_slice()).ok()?);
133 }
134
135 pub fn lookup(&self, name: &str) -> Lookup {
137 if !is_plain_ref_name(name) {
138 return Lookup::Unsure;
139 }
140 let path: PathBuf = self.directory.join(name);
141 match std::fs::symlink_metadata(&path) {
142 Ok(metadata) if metadata.is_file() => {
143 let Ok(bytes) = read_regular_file(path.as_path(), LOOSE_LIMIT) else {
144 return Lookup::Unsure;
145 };
146 let Some(value) = std::str::from_utf8(bytes.as_slice()).ok().and_then(|text| { return self.parse_loose(text); }) else {
147 return Lookup::Unsure;
148 };
149 return Lookup::Found(value);
150 }
151 Ok(metadata) if metadata.is_dir() => {}
152 Ok(_) => return Lookup::Unsure,
153 Err(error) if matches!(error.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory) => {}
155 Err(_) => return Lookup::Unsure,
156 }
157 let Some(packed) = self.packed() else {
158 return Lookup::Unsure;
159 };
160 return match packed.into_iter().find(|(candidate, _)| { return candidate == name; }) {
161 Some((_, object)) => Lookup::Found(RefValue::Direct(object)),
162 None => Lookup::Missing,
163 };
164 }
165
166 pub fn resolve(&self, name: &str) -> Option<String> {
168 let mut current: String = name.to_string();
169 for _ in 0..SYMBOLIC_DEPTH_LIMIT {
170 match self.lookup(current.as_str()) {
171 Lookup::Found(RefValue::Direct(object)) => return Some(object),
172 Lookup::Found(RefValue::Symbolic(target)) => current = target,
173 Lookup::Missing | Lookup::Unsure => return None,
174 }
175 }
176 return None;
177 }
178
179 pub fn list(&self) -> Option<Vec<(String, RefValue)>> {
181 let mut found: std::collections::BTreeMap<String, RefValue> = std::collections::BTreeMap::new();
182 for (name, object) in self.packed()? {
183 found.insert(name, RefValue::Direct(object));
184 }
185 let mut pending: Vec<String> = vec![String::from("refs")];
186 while let Some(directory) = pending.pop() {
187 let listing = match std::fs::read_dir(self.directory.join(&directory)) {
188 Ok(listing) => listing,
189 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
190 Err(_) => return None,
191 };
192 for candidate in listing {
193 let entry = candidate.ok()?;
194 let name: String = format!("{directory}/{}", entry.file_name().to_str()?);
195 let kind = entry.file_type().ok()?;
196 if kind.is_dir() {
197 pending.push(name);
198 } else if kind.is_file() && !name.ends_with(".lock") {
199 match self.lookup(name.as_str()) {
200 Lookup::Found(value) => {
201 found.insert(name, value);
202 }
203 Lookup::Missing | Lookup::Unsure => return None,
204 }
205 } else {
206 return None;
207 }
208 }
209 }
210 return Some(found.into_iter().collect());
211 }
212}
213
214#[cfg(test)]
215#[path = "repo_read_refs_tests.rs"]
216mod tests;