Skip to main content

amiss_git/
index.rs

1use amiss_wire::controls::GitMode;
2use amiss_wire::model::{ObjectFormat, Oid};
3
4use crate::Error;
5use crate::object::{hex, ordinary_digest};
6
7/// One supported stage-zero row of the logical index: the raw path bytes as
8/// stored, the exact paired mode, the object in the declared namespace, and
9/// the ordinary skip-worktree bit.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct IndexEntry {
12    pub path: Vec<u8>,
13    pub mode: GitMode,
14    pub oid: Oid,
15    pub skip_worktree: bool,
16}
17
18/// The complete logical stage-zero index: rows unique, path-byte sorted, and
19/// prefix-free directly after parsing.
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct LogicalIndex {
22    pub entries: Vec<IndexEntry>,
23}
24
25const STAGE_MASK: u16 = 0x3000;
26const EXTENDED_FLAG: u16 = 0x4000;
27const NAME_MASK: u16 = 0x0fff;
28const SKIP_WORKTREE: u16 = 0x4000;
29const INTENT_TO_ADD: u16 = 0x2000;
30const EXTENDED_RESERVED: u16 = 0x8000;
31
32fn be32(bytes: &[u8], at: usize) -> Result<u32, Error> {
33    let raw = bytes
34        .get(at..at.saturating_add(4))
35        .ok_or(Error::IndexInvalid)?;
36    let fixed: [u8; 4] = raw.try_into().map_err(|_short| Error::IndexInvalid)?;
37    Ok(u32::from_be_bytes(fixed))
38}
39
40fn be16(bytes: &[u8], at: usize) -> Result<u16, Error> {
41    let raw = bytes
42        .get(at..at.saturating_add(2))
43        .ok_or(Error::IndexInvalid)?;
44    let fixed: [u8; 2] = raw.try_into().map_err(|_short| Error::IndexInvalid)?;
45    Ok(u16::from_be_bytes(fixed))
46}
47
48fn entry_mode(raw: u32) -> Result<GitMode, Error> {
49    match raw {
50        0o100_644 => Ok(GitMode::RegularFile),
51        0o100_755 => Ok(GitMode::ExecutableFile),
52        0o120_000 => Ok(GitMode::Symlink),
53        0o160_000 => Ok(GitMode::Gitlink),
54        _ => Err(Error::IndexInvalid),
55    }
56}
57
58/// A later path conflicts when some earlier row is one of its directory
59/// prefixes; the sorted order guarantees the prefix would already have been
60/// seen.
61fn prefix_conflict(seen: &[Vec<u8>], path: &[u8]) -> bool {
62    let mut end = path.len();
63    while let Some(at) = path
64        .get(..end)
65        .and_then(|prefix| prefix.iter().rposition(|&byte| byte == b'/'))
66    {
67        let prefix = path.get(..at).unwrap_or_default();
68        if seen
69            .binary_search_by(|candidate| candidate.as_slice().cmp(prefix))
70            .is_ok()
71        {
72            return true;
73        }
74        end = at;
75    }
76    false
77}
78
79/// Parses one complete raw `.git/index` byte string under `git-index-v1`:
80/// versions two through four, ordinary stage-zero rows only, the exact mode
81/// pairings, strictly increasing prefix-free paths, mandatory unknown or
82/// split-index and sparse-directory extensions rejected, and the trailing
83/// checksum verified in the declared namespace.
84///
85/// # Errors
86///
87/// `IndexUnmerged` for any nonzero stage, `IntentToAdd` for that bit, and
88/// `IndexInvalid` for every structural defect.
89pub fn parse_index_file(object_format: ObjectFormat, bytes: &[u8]) -> Result<LogicalIndex, Error> {
90    let oid_width = match object_format {
91        ObjectFormat::Sha1 => 20_usize,
92        ObjectFormat::Sha256 => 32_usize,
93    };
94    let checksum_at = bytes
95        .len()
96        .checked_sub(oid_width)
97        .ok_or(Error::IndexInvalid)?;
98    let content = bytes.get(..checksum_at).ok_or(Error::IndexInvalid)?;
99    let stored = bytes.get(checksum_at..).ok_or(Error::IndexInvalid)?;
100    if ordinary_digest(object_format, content) != stored {
101        return Err(Error::IndexInvalid);
102    }
103    if content.get(..4) != Some(b"DIRC") {
104        return Err(Error::IndexInvalid);
105    }
106    let version = be32(content, 4)?;
107    if !(2..=4).contains(&version) {
108        return Err(Error::IndexInvalid);
109    }
110    let count = usize::try_from(be32(content, 8)?).map_err(|_wide| Error::IndexInvalid)?;
111
112    let mut entries: Vec<IndexEntry> = Vec::new();
113    let mut seen: Vec<Vec<u8>> = Vec::new();
114    let mut previous_path: Vec<u8> = Vec::new();
115    let mut at = 12_usize;
116    for _entry in 0..count {
117        let start = at;
118        let mode = entry_mode(be32(content, start.saturating_add(24))?)?;
119        let oid_start = start.saturating_add(40);
120        let raw_oid = content
121            .get(oid_start..oid_start.saturating_add(oid_width))
122            .ok_or(Error::IndexInvalid)?;
123        let oid = Oid::new(object_format, hex(raw_oid)).ok_or(Error::IndexInvalid)?;
124        let flags_at = oid_start.saturating_add(oid_width);
125        let flags = be16(content, flags_at)?;
126        if flags & STAGE_MASK != 0 {
127            return Err(Error::IndexUnmerged);
128        }
129        let mut skip_worktree = false;
130        let mut path_at = flags_at.saturating_add(2);
131        if flags & EXTENDED_FLAG != 0 {
132            if version == 2 {
133                return Err(Error::IndexInvalid);
134            }
135            let extended = be16(content, path_at)?;
136            if extended & EXTENDED_RESERVED != 0 {
137                return Err(Error::IndexInvalid);
138            }
139            if extended & INTENT_TO_ADD != 0 {
140                return Err(Error::IntentToAdd);
141            }
142            skip_worktree = extended & SKIP_WORKTREE != 0;
143            path_at = path_at.saturating_add(2);
144        }
145
146        let (path, next) = entry_path(content, version, start, path_at, flags, &previous_path)?;
147        at = next;
148
149        if path.is_empty() {
150            return Err(Error::IndexInvalid);
151        }
152        if !previous_path.is_empty() && previous_path.as_slice() >= path.as_slice() {
153            return Err(Error::IndexInvalid);
154        }
155        if prefix_conflict(&seen, &path) {
156            return Err(Error::IndexInvalid);
157        }
158        seen.push(path.clone());
159        previous_path.clone_from(&path);
160        entries.push(IndexEntry {
161            path,
162            mode,
163            oid,
164            skip_worktree,
165        });
166    }
167
168    extensions(content, at)?;
169    Ok(LogicalIndex { entries })
170}
171
172/// One entry's path and the offset of the next entry: version four strips a
173/// varint prefix from the previous path and appends a terminated suffix with
174/// no padding; earlier versions carry the whole path, its redundant length
175/// bits, and one to eight terminating pad bytes to the eight-byte boundary.
176fn entry_path(
177    content: &[u8],
178    version: u32,
179    start: usize,
180    path_at: usize,
181    flags: u16,
182    previous_path: &[u8],
183) -> Result<(Vec<u8>, usize), Error> {
184    if version == 4 {
185        let (strip, after) = varint(content, path_at)?;
186        let nul = content
187            .get(after..)
188            .and_then(|rest| rest.iter().position(|&byte| byte == 0))
189            .ok_or(Error::IndexInvalid)?;
190        let suffix = content
191            .get(after..after.saturating_add(nul))
192            .ok_or(Error::IndexInvalid)?;
193        let keep = previous_path
194            .len()
195            .checked_sub(usize::try_from(strip).map_err(|_wide| Error::IndexInvalid)?)
196            .ok_or(Error::IndexInvalid)?;
197        let mut path = previous_path.get(..keep).unwrap_or_default().to_vec();
198        path.extend_from_slice(suffix);
199        return Ok((path, after.saturating_add(nul).saturating_add(1)));
200    }
201    let nul = content
202        .get(path_at..)
203        .and_then(|rest| rest.iter().position(|&byte| byte == 0))
204        .ok_or(Error::IndexInvalid)?;
205    let path = content
206        .get(path_at..path_at.saturating_add(nul))
207        .ok_or(Error::IndexInvalid)?
208        .to_vec();
209    let name_bits = usize::from(flags & NAME_MASK);
210    if name_bits < usize::from(NAME_MASK) && name_bits != path.len() {
211        return Err(Error::IndexInvalid);
212    }
213    let unpadded = path_at.saturating_add(nul).saturating_sub(start);
214    let remainder = unpadded.checked_rem(8).ok_or(Error::IndexInvalid)?;
215    let pad = 8_usize.saturating_sub(remainder);
216    let entry_end = start.saturating_add(unpadded).saturating_add(pad);
217    for pad_at in path_at.saturating_add(nul)..entry_end {
218        if content.get(pad_at) != Some(&0) {
219            return Err(Error::IndexInvalid);
220        }
221    }
222    Ok((path, entry_end))
223}
224
225/// Extensions after the rows: an uppercase-initial signature is optional and
226/// skipped; split-index backing, sparse directories, and any other mandatory
227/// unknown extension reject the index.
228fn extensions(content: &[u8], mut at: usize) -> Result<(), Error> {
229    while at < content.len() {
230        let signature = content
231            .get(at..at.saturating_add(4))
232            .ok_or(Error::IndexInvalid)?;
233        let length = usize::try_from(be32(content, at.saturating_add(4))?)
234            .map_err(|_wide| Error::IndexInvalid)?;
235        let payload_at = at.saturating_add(8);
236        if payload_at.saturating_add(length) > content.len() {
237            return Err(Error::IndexInvalid);
238        }
239        let optional = signature.first().is_some_and(u8::is_ascii_uppercase);
240        if !optional {
241            return Err(Error::IndexInvalid);
242        }
243        at = payload_at.saturating_add(length);
244    }
245    Ok(())
246}
247
248/// The offset varint of index version four.
249fn varint(content: &[u8], at: usize) -> Result<(u64, usize), Error> {
250    let mut value: u64 = 0;
251    let mut cursor = at;
252    loop {
253        let byte = *content.get(cursor).ok_or(Error::IndexInvalid)?;
254        cursor = cursor.saturating_add(1);
255        value = value
256            .checked_shl(7)
257            .and_then(|shifted| shifted.checked_add(u64::from(byte & 0x7f)))
258            .ok_or(Error::IndexInvalid)?;
259        if byte & 0x80 == 0 {
260            return Ok((value, cursor));
261        }
262        value = value.checked_add(1).ok_or(Error::IndexInvalid)?;
263    }
264}