Skip to main content

git_core/
loose.rs

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
11/// Maximum decompressed size accepted (deflate-bomb guard): 512 MiB.
12const MAX_DECOMPRESSED_SIZE: u64 = 512 * 1024 * 1024;
13
14/// Enumerate every loose object under `objects_dir` by scanning
15/// `objects/<xx>/<38hex>`. Malformed names are skipped; never panics. A missing
16/// `objects` directory simply yields an empty list.
17#[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        // Object shards are exactly two hex chars (skip `pack`, `info`, …).
29        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
54/// Read and parse a loose git object file.
55pub 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    // SHA1 over the entire decompressed content (header + NUL + data) must equal the object hash.
78    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    // Object header: "<kind> <size>\0<data>"
85    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}