Skip to main content

git_zero_core/
checkout.rs

1//! Borrowed file contents and bounded checkout traversal for caller-owned stores.
2//!
3//! Sources supply hash-verified objects. A mutable source can evict its previous
4//! object on every lookup: traversal retains IDs and offsets, never object slices.
5use crate::{
6    Error, Object, ObjectFormat, ObjectId, ObjectKind, Result,
7    object::{Commit, Tag, TreeEntry},
8};
9
10/// Canonical Git modes; host permissions and submodule mounting are host policy.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Mode {
13    Directory,
14    File,
15    Executable,
16    Symlink,
17    Gitlink,
18}
19
20impl Mode {
21    pub fn from_raw(mode: u32) -> Result<Self> {
22        match mode {
23            0o040000 => Ok(Self::Directory),
24            0o100644 => Ok(Self::File),
25            0o100755 => Ok(Self::Executable),
26            0o120000 => Ok(Self::Symlink),
27            0o160000 => Ok(Self::Gitlink),
28            _ => Err(Error::InvalidObject),
29        }
30    }
31
32    pub const fn raw(self) -> u32 {
33        match self {
34            Self::Directory => 0o040000,
35            Self::File => 0o100644,
36            Self::Executable => 0o100755,
37            Self::Symlink => 0o120000,
38            Self::Gitlink => 0o160000,
39        }
40    }
41
42    /// File and symlink contents are exact Git blob bytes, with no text or
43    /// newline conversion. Symlink targets retain non-UTF-8 and relative bytes;
44    /// embedded NUL is invalid. Empty-target/path-size rules belong to the host.
45    pub fn contents(self, object: Object<'_>) -> Result<&[u8]> {
46        if !matches!(self, Self::File | Self::Executable | Self::Symlink) {
47            return Err(Error::Unsupported);
48        }
49        if object.kind != ObjectKind::Blob || (self == Self::Symlink && object.data.contains(&0)) {
50            return Err(Error::InvalidObject);
51        }
52        Ok(object.data)
53    }
54}
55
56/// Owned metadata survives eviction of the parent tree.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct Entry {
59    pub id: ObjectId,
60    pub mode: Mode,
61}
62
63/// Find one byte-string component in a previously validated tree. This checks
64/// framing and duplicate matches in one linear pass. Cache insertion should use
65/// `object::Tree::parse_with_format` for complete ordering/alias validation.
66pub fn lookup(data: &[u8], format: ObjectFormat, name: &[u8]) -> Result<Option<Entry>> {
67    if name.is_empty() || matches!(name, b"." | b"..") || name.iter().any(|b| matches!(b, 0 | b'/'))
68    {
69        return Err(Error::InvalidFileName);
70    }
71    let mut offset = 0;
72    let mut found = None;
73    while let Some(entry) = TreeEntry::read_with_format(data, &mut offset, format)? {
74        if entry.name == name {
75            if found.is_some() {
76                return Err(Error::InvalidObject);
77            }
78            found = Some(Entry {
79                id: entry.id,
80                mode: Mode::from_raw(entry.mode)?,
81            });
82        }
83    }
84    Ok(found)
85}
86
87/// An immutable object view is valid until the next mutable source access.
88pub trait ObjectSource {
89    fn object(&mut self, id: ObjectId) -> Result<Object<'_>>;
90}
91
92/// Exact commit-message location within a source object.
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub struct Message {
95    pub id: ObjectId,
96    pub offset: usize,
97    pub length: usize,
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct Root {
102    pub entry: Entry,
103    pub message: Option<Message>,
104}
105
106/// Peel at most `max_tags` annotated tags, check each declared target's actual
107/// type and format, then resolve a commit's tree (or a direct tree/blob ref).
108/// A tag cycle exhausts the explicit bound. No recursion or allocation is used.
109pub fn resolve_root(
110    source: &mut impl ObjectSource,
111    mut id: ObjectId,
112    max_tags: u32,
113) -> Result<Root> {
114    let format = id.format();
115    let mut expected = None;
116    let mut message = None;
117    let mut tags = 0;
118    loop {
119        let object = source.object(id)?;
120        if expected.is_some_and(|kind| kind != object.kind) {
121            return Err(Error::InvalidObject);
122        }
123        match object.kind {
124            ObjectKind::Tag => {
125                if tags == max_tags {
126                    return Err(Error::LimitExceeded);
127                }
128                let tag = Tag::parse(object.data)?;
129                if tag.target.format() != format {
130                    return Err(Error::InvalidObjectId);
131                }
132                id = tag.target;
133                expected = Some(tag.target_kind);
134                tags += 1;
135            }
136            ObjectKind::Commit => {
137                let commit = Commit::parse_with_format(object.data, format)?;
138                message = Some(Message {
139                    id,
140                    offset: commit.message_offset(),
141                    length: commit.message().len(),
142                });
143                id = commit.tree();
144                expected = Some(ObjectKind::Tree);
145            }
146            ObjectKind::Tree | ObjectKind::Blob => {
147                return Ok(Root {
148                    entry: Entry {
149                        id,
150                        mode: if object.kind == ObjectKind::Tree {
151                            Mode::Directory
152                        } else {
153                            Mode::File
154                        },
155                    },
156                    message,
157                });
158            }
159        }
160    }
161}