1use std::fs::File;
2use std::io::Read as _;
3use std::path::Path;
4use std::sync::OnceLock;
5
6use amiss_wire::controls::ResourceName;
7use amiss_wire::model::{ObjectFormat, Oid};
8
9use crate::Error;
10use crate::handle::{open_dir, open_file, open_root};
11use crate::object::{Object, ObjectKind, decode_loose, discard_to_unreadable, hex, verify_oid};
12use crate::pack::{
13 self, PackSet, apply_delta, inflate_exact, kind_of, parse_entry_header, parse_ofs_distance,
14};
15use crate::resources::{GitResources, ValueCap, crossing};
16
17#[derive(Debug)]
18pub struct Repository {
19 git_dir: File,
20 objects: File,
21 object_format: ObjectFormat,
22 packs: OnceLock<Result<PackSet, Error>>,
23 repacked: OnceLock<Result<PackSet, Error>>,
24}
25
26impl Repository {
27 pub fn open(root: &Path, object_format: ObjectFormat) -> Result<Self, Error> {
36 let root_dir = open_root(root)?;
37 let git_dir =
38 open_dir(&root_dir, ".git").map_err(|_defect| Error::RepositoryUnavailable)?;
39 let objects =
40 open_dir(&git_dir, "objects").map_err(|_defect| Error::RepositoryUnavailable)?;
41 Ok(Self {
42 git_dir,
43 objects,
44 object_format,
45 packs: OnceLock::new(),
46 repacked: OnceLock::new(),
47 })
48 }
49
50 pub fn read_object(&self, resources: &mut GitResources, oid: &Oid) -> Result<Object, Error> {
58 self.read_full(resources, oid, 1, None)
59 }
60
61 pub fn read_expected(
66 &self,
67 resources: &mut GitResources,
68 oid: &Oid,
69 expected: ObjectKind,
70 ) -> Result<Object, Error> {
71 let object = self.read_object(resources, oid)?;
72 if object.kind == expected {
73 Ok(object)
74 } else {
75 Err(Error::ObjectWrongKind)
76 }
77 }
78
79 pub fn read_expected_capped(
88 &self,
89 resources: &mut GitResources,
90 oid: &Oid,
91 expected: ObjectKind,
92 cap: ValueCap,
93 ) -> Result<Object, Error> {
94 let object = self.read_full(resources, oid, 1, Some(&cap))?;
95 if object.kind == expected {
96 Ok(object)
97 } else {
98 Err(Error::ObjectWrongKind)
99 }
100 }
101
102 fn read_full(
103 &self,
104 resources: &mut GitResources,
105 oid: &Oid,
106 depth: u64,
107 value_cap: Option<&ValueCap>,
108 ) -> Result<Object, Error> {
109 let limit = resources.limits().delta_depth;
110 if depth > limit {
111 return Err(crossing(ResourceName::GitDeltaDepth, limit, depth));
112 }
113 let hex_text = oid.as_str();
114 let fan = hex_text.get(..2).ok_or(Error::ObjectUnreadable)?;
115 let rest = hex_text.get(2..).ok_or(Error::ObjectUnreadable)?;
116 let Some(fan_dir) = absent_to_none(open_dir(&self.objects, fan))? else {
117 return self.read_packed(resources, oid, depth, value_cap);
118 };
119 let Some(loose) = absent_to_none(open_file(&fan_dir, rest))? else {
120 return self.read_packed(resources, oid, depth, value_cap);
121 };
122 self.decode(resources, oid, loose, value_cap)
123 }
124
125 fn enumeration<'set>(
129 &'set self,
130 cell: &'set OnceLock<Result<PackSet, Error>>,
131 resources: &mut GitResources,
132 ) -> Result<&'set PackSet, Error> {
133 let built = cell.get_or_init(|| pack::build(&self.objects, self.object_format, resources));
134 match built {
135 Ok(set) => {
136 for (name, size) in &set.index_sizes {
137 resources.charge_index(name, *size)?;
138 }
139 Ok(set)
140 }
141 Err(defect) => Err(defect.clone()),
142 }
143 }
144
145 fn locate_packed(
158 &self,
159 resources: &mut GitResources,
160 raw: &[u8],
161 ) -> Result<Option<(&PackSet, usize, u64)>, Error> {
162 let enumerated = self.enumeration(&self.packs, resources)?;
163 if let Some((pack_index, offset)) = enumerated.locate(raw) {
164 return Ok(Some((enumerated, pack_index, offset)));
165 }
166 let repacked = self.enumeration(&self.repacked, resources)?;
167 Ok(repacked
168 .locate(raw)
169 .map(|(pack_index, offset)| (repacked, pack_index, offset)))
170 }
171
172 fn read_packed(
173 &self,
174 resources: &mut GitResources,
175 oid: &Oid,
176 depth: u64,
177 value_cap: Option<&ValueCap>,
178 ) -> Result<Object, Error> {
179 let raw = oid_raw(oid).ok_or(Error::ObjectUnreadable)?;
180 let Some((set, pack_index, offset)) = self.locate_packed(resources, &raw)? else {
181 return Err(Error::ObjectMissing);
182 };
183 let (kind, body) =
184 self.read_pack_at(resources, set, pack_index, offset, depth, value_cap)?;
185 let raw_header = format!("{} {}\0", kind.as_str(), body.len()).into_bytes();
186 verify_oid(self.object_format, oid, &raw_header, &body)?;
187 Ok(Object { kind, body })
188 }
189
190 fn read_pack_at(
191 &self,
192 resources: &mut GitResources,
193 set: &PackSet,
194 pack_index: usize,
195 offset: u64,
196 depth: u64,
197 value_cap: Option<&ValueCap>,
198 ) -> Result<(ObjectKind, Vec<u8>), Error> {
199 let limit = resources.limits().delta_depth;
200 if depth > limit {
201 return Err(crossing(ResourceName::GitDeltaDepth, limit, depth));
202 }
203 let inflated_cap = resources.limits().inflated_object_bytes;
204 let entry = {
205 let pack = set.packs.get(pack_index).ok_or(Error::ObjectUnreadable)?;
206 pack.read_interval(resources, offset)?
207 };
208 let header = parse_entry_header(&entry)?;
209 let after_header = entry
210 .get(header.header_len..)
211 .ok_or(Error::ObjectUnreadable)?;
212
213 match header.type_code {
214 1..=4 => {
215 if let Some(value) = value_cap
216 && header.size > value.limit
217 {
218 return Err(crossing(value.resource, value.limit, header.size));
219 }
220 let body = inflate_exact(after_header, header.size, inflated_cap)?;
221 Ok((kind_of(header.type_code)?, body))
222 }
223 6 => {
224 let (distance, used) = parse_ofs_distance(after_header)?;
225 let base_offset = offset
226 .checked_sub(distance)
227 .ok_or(Error::ObjectUnreadable)?;
228 let base_known = {
229 let pack = set.packs.get(pack_index).ok_or(Error::ObjectUnreadable)?;
230 pack.row_at(base_offset).is_some()
231 };
232 if !base_known {
233 return Err(Error::ObjectUnreadable);
234 }
235 let (kind, base) = self.read_pack_at(
236 resources,
237 set,
238 pack_index,
239 base_offset,
240 depth.saturating_add(1),
241 None,
242 )?;
243 let script_bytes = after_header.get(used..).ok_or(Error::ObjectUnreadable)?;
244 let script = inflate_exact(script_bytes, header.size, inflated_cap)?;
245 Ok((kind, apply_delta(&base, &script, inflated_cap, value_cap)?))
246 }
247 7 => {
248 let width = self.oid_width();
249 let base_raw = after_header.get(..width).ok_or(Error::ObjectUnreadable)?;
250 let base_oid =
251 Oid::new(self.object_format, hex(base_raw)).ok_or(Error::ObjectUnreadable)?;
252 let base = self.read_full(resources, &base_oid, depth.saturating_add(1), None)?;
253 let script_bytes = after_header.get(width..).ok_or(Error::ObjectUnreadable)?;
254 let script = inflate_exact(script_bytes, header.size, inflated_cap)?;
255 Ok((
256 base.kind,
257 apply_delta(&base.body, &script, inflated_cap, value_cap)?,
258 ))
259 }
260 _ => Err(Error::ObjectUnreadable),
261 }
262 }
263
264 #[must_use]
265 pub const fn object_format(&self) -> ObjectFormat {
266 self.object_format
267 }
268
269 pub fn read_index_bytes(&self, resources: &mut GitResources) -> Result<Vec<u8>, Error> {
278 let file = open_file(&self.git_dir, "index").map_err(|_defect| Error::IndexInvalid)?;
279 let metadata = file.metadata().map_err(|_defect| Error::IndexInvalid)?;
280 let cap = resources.limits().index_bytes;
281 if metadata.len() > cap {
282 return Err(crossing(ResourceName::GitIndexBytes, cap, metadata.len()));
283 }
284 let mut bytes = Vec::new();
285 let read = file
286 .take(cap.saturating_add(1))
287 .read_to_end(&mut bytes)
288 .map_err(|_defect| Error::IndexInvalid)?;
289 if u64::try_from(read).unwrap_or(u64::MAX) > cap {
290 return Err(crossing(
291 ResourceName::GitIndexBytes,
292 cap,
293 cap.saturating_add(1),
294 ));
295 }
296 Ok(bytes)
297 }
298
299 pub fn has_object(&self, resources: &mut GitResources, oid: &Oid) -> Result<bool, Error> {
306 let hex_text = oid.as_str();
307 let fan = hex_text.get(..2).ok_or(Error::ObjectUnreadable)?;
308 let rest = hex_text.get(2..).ok_or(Error::ObjectUnreadable)?;
309 if let Ok(fan_dir) = open_dir(&self.objects, fan)
310 && open_file(&fan_dir, rest).is_ok()
311 {
312 return Ok(true);
313 }
314 let Some(raw) = oid_raw(oid) else {
315 return Err(Error::ObjectUnreadable);
316 };
317 Ok(self.locate_packed(resources, &raw)?.is_some())
318 }
319
320 pub fn verify_index_unchanged(
328 &self,
329 resources: &mut GitResources,
330 initial: &[u8],
331 ) -> Result<(), Error> {
332 let current = match self.read_index_bytes(resources) {
333 Ok(bytes) => bytes,
334 Err(Error::ResourceLimit {
335 resource,
336 configured_limit,
337 observed_lower_bound,
338 }) => {
339 return Err(Error::ResourceLimit {
340 resource,
341 configured_limit,
342 observed_lower_bound,
343 });
344 }
345 Err(
346 Error::RepositoryUnavailable
347 | Error::ObjectMissing
348 | Error::ObjectWrongKind
349 | Error::ObjectUnreadable
350 | Error::IndexInvalid
351 | Error::IndexUnmerged
352 | Error::IntentToAdd
353 | Error::SnapshotChanged,
354 ) => return Err(Error::SnapshotChanged),
355 };
356 if current == initial {
357 return Ok(());
358 }
359 let before = crate::index::parse_index_file(self.object_format, initial)
360 .map_err(|_defect| Error::SnapshotChanged)?;
361 let after = crate::index::parse_index_file(self.object_format, ¤t)
362 .map_err(|_defect| Error::SnapshotChanged)?;
363 if before == after {
364 return Ok(());
365 }
366 Err(Error::SnapshotChanged)
367 }
368
369 const fn oid_width(&self) -> usize {
370 match self.object_format {
371 ObjectFormat::Sha1 => 20,
372 ObjectFormat::Sha256 => 32,
373 }
374 }
375
376 fn decode(
377 &self,
378 resources: &mut GitResources,
379 oid: &Oid,
380 file: File,
381 value_cap: Option<&ValueCap>,
382 ) -> Result<Object, Error> {
383 let metadata = file.metadata().map_err(discard_to_unreadable)?;
384 resources.charge_compressed(oid.as_str(), metadata.len())?;
385
386 let stream_cap = resources.limits().compressed_stream_bytes;
387 let mut compressed = Vec::new();
388 let read = file
389 .take(stream_cap.saturating_add(1))
390 .read_to_end(&mut compressed)
391 .map_err(discard_to_unreadable)?;
392 if u64::try_from(read).unwrap_or(u64::MAX) > stream_cap {
393 return Err(crossing(
394 ResourceName::GitCompressedObjectBytes,
395 stream_cap,
396 stream_cap.saturating_add(1),
397 ));
398 }
399 decode_loose(
400 &compressed,
401 self.object_format,
402 oid,
403 resources.limits().inflated_object_bytes,
404 value_cap,
405 )
406 }
407}
408
409fn absent_to_none(opened: std::io::Result<File>) -> Result<Option<File>, Error> {
413 match opened {
414 Ok(file) => Ok(Some(file)),
415 Err(defect) if defect.kind() == std::io::ErrorKind::NotFound => Ok(None),
416 Err(_defect) => Err(Error::ObjectUnreadable),
417 }
418}
419
420fn oid_raw(oid: &Oid) -> Option<Vec<u8>> {
421 let text = oid.as_str();
422 if !text.len().is_multiple_of(2) {
423 return None;
424 }
425 let mut out = Vec::with_capacity(text.len().checked_div(2)?);
426 for pair in text.as_bytes().chunks_exact(2) {
427 let [high, low] = pair else { return None };
428 let value = |byte: u8| match byte {
429 b'0'..=b'9' => Some(byte.wrapping_sub(b'0')),
430 b'a'..=b'f' => Some(byte.wrapping_sub(b'a').wrapping_add(10)),
431 _ => None,
432 };
433 out.push(value(*high)?.wrapping_shl(4) | value(*low)?);
434 }
435 Some(out)
436}