1use std::io::Read;
2use std::path::Path;
3
4use flate2::read::ZlibDecoder;
5use sha1::{Digest, Sha1};
6
7use crate::error::{GitError, Result};
8use crate::hash::GitHash;
9use crate::object::{ObjectKind, RawObject};
10
11const MAX_DECOMPRESSED_SIZE: u64 = 512 * 1024 * 1024;
13
14#[must_use]
18pub fn list_loose(objects_dir: &Path) -> Vec<GitHash> {
19 let mut out = Vec::new();
20 let Ok(shards) = std::fs::read_dir(objects_dir) else {
21 return out;
22 };
23 for shard in shards.flatten() {
24 let shard_name = shard.file_name();
25 let Some(prefix) = shard_name.to_str() else {
26 continue;
27 };
28 if prefix.len() != 2 || !prefix.bytes().all(|b| b.is_ascii_hexdigit()) {
30 continue;
31 }
32 let Ok(files) = std::fs::read_dir(shard.path()) else {
33 continue;
34 };
35 for file in files.flatten() {
36 let file_name = file.file_name();
37 let Some(rest) = file_name.to_str() else {
38 continue;
39 };
40 if rest.len() != 38 {
41 continue;
42 }
43 let mut hex = String::with_capacity(40);
44 hex.push_str(prefix);
45 hex.push_str(rest);
46 if let Ok(hash) = GitHash::from_hex(&hex) {
47 out.push(hash);
48 }
49 }
50 }
51 out
52}
53
54pub fn read_loose(objects_dir: &Path, hash: &GitHash) -> Result<RawObject> {
56 let (dir, file) = hash.object_path();
57 let path = objects_dir.join(&dir).join(&file);
58
59 let compressed = std::fs::read(&path).map_err(|e| {
60 if e.kind() == std::io::ErrorKind::NotFound {
61 GitError::ObjectNotFound(*hash)
62 } else {
63 GitError::Io(e)
64 }
65 })?;
66
67 decompress_and_parse(hash, &compressed)
68}
69
70pub fn decompress_and_parse(expected: &GitHash, compressed: &[u8]) -> Result<RawObject> {
71 let mut decompressed = Vec::new();
72 ZlibDecoder::new(compressed)
73 .take(MAX_DECOMPRESSED_SIZE)
74 .read_to_end(&mut decompressed)
75 .map_err(|e| GitError::InvalidObject(format!("zlib decompression failed: {e}")))?;
76
77 let mut hasher = Sha1::new();
79 hasher.update(&decompressed);
80 let digest: [u8; 20] = hasher.finalize().into();
81 let got = GitHash(digest);
82 let verified = got == *expected;
83
84 let nul = decompressed
86 .iter()
87 .position(|&b| b == 0)
88 .ok_or_else(|| GitError::InvalidObject("missing NUL in object header".into()))?;
89
90 let header = std::str::from_utf8(&decompressed[..nul])
91 .map_err(|e| GitError::InvalidObject(format!("object header not UTF-8: {e}")))?;
92
93 let (kind_str, size_str) = header
94 .split_once(' ')
95 .ok_or_else(|| GitError::InvalidObject("object header missing space".into()))?;
96
97 let kind = ObjectKind::from_bytes(kind_str.as_bytes())?;
98
99 let declared_size: usize = size_str
100 .parse()
101 .map_err(|_| GitError::InvalidObject(format!("invalid size in header: {size_str:?}")))?;
102
103 let data = decompressed[nul + 1..].to_vec();
104
105 if data.len() != declared_size {
106 return Err(GitError::InvalidObject(format!(
107 "declared size {declared_size} but data is {} bytes",
108 data.len()
109 )));
110 }
111
112 Ok(RawObject {
113 kind,
114 data,
115 verified,
116 })
117}