Skip to main content

weavatrix_git/
index.rs

1use std::fs;
2
3use crate::{GitError, ObjectId, Repository, Result, error::invalid};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct IndexEntry {
7    pub ctime_seconds: u32,
8    pub mtime_seconds: u32,
9    pub mode: u32,
10    pub size: u32,
11    pub id: ObjectId,
12    pub stage: u8,
13    pub assume_valid: bool,
14    pub skip_worktree: bool,
15    pub intent_to_add: bool,
16    pub path: Vec<u8>,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct Index {
21    version: u32,
22    entries: Vec<IndexEntry>,
23}
24
25impl Index {
26    #[must_use]
27    pub const fn version(&self) -> u32 {
28        self.version
29    }
30
31    #[must_use]
32    pub fn entries(&self) -> &[IndexEntry] {
33        &self.entries
34    }
35
36    fn parse(data: &[u8], repository: &Repository) -> Result<Self> {
37        if data.get(..4) != Some(b"DIRC") {
38            return Err(invalid("invalid index signature"));
39        }
40        let version = read_u32(data, 4)?;
41        if !(2..=4).contains(&version) {
42            return Err(GitError::Unsupported(format!("index version {version}")));
43        }
44        let count = usize::try_from(read_u32(data, 8)?)
45            .map_err(|_| invalid("index entry count overflow"))?;
46        if count > repository.limits().max_index_entries {
47            return Err(GitError::LimitExceeded {
48                resource: "index entries",
49                limit: repository.limits().max_index_entries,
50            });
51        }
52        let trailer = repository.hash_kind().bytes();
53        if data.len() < 12 + trailer {
54            return Err(invalid("truncated index"));
55        }
56        let content_end = data.len() - trailer;
57        let mut cursor = 12;
58        let mut entries = Vec::with_capacity(count);
59        let mut previous_path = Vec::new();
60        for _ in 0..count {
61            let entry = parse_entry(
62                data,
63                &mut cursor,
64                content_end,
65                version,
66                repository.hash_kind(),
67                &previous_path,
68            )?;
69            previous_path.clone_from(&entry.path);
70            entries.push(entry);
71        }
72        if !entries.windows(2).all(|pair| {
73            (pair[0].path.as_slice(), pair[0].stage) < (pair[1].path.as_slice(), pair[1].stage)
74        }) {
75            return Err(invalid("index entries are not sorted"));
76        }
77        parse_extensions(data, cursor, content_end)?;
78        Ok(Self { version, entries })
79    }
80}
81
82impl Repository {
83    pub fn index(&self) -> Result<Index> {
84        let data = fs::read(self.git_dir().join("index"))?;
85        Index::parse(&data, self)
86    }
87}
88
89fn parse_entry(
90    data: &[u8],
91    cursor: &mut usize,
92    end: usize,
93    version: u32,
94    hash: crate::HashKind,
95    previous_path: &[u8],
96) -> Result<IndexEntry> {
97    let start = *cursor;
98    let fixed = 40_usize
99        .checked_add(hash.bytes())
100        .and_then(|value| value.checked_add(2))
101        .ok_or_else(|| invalid("index entry length overflow"))?;
102    if start.saturating_add(fixed) > end {
103        return Err(invalid("truncated index entry"));
104    }
105    let ctime_seconds = read_u32(data, start)?;
106    let mtime_seconds = read_u32(data, start + 8)?;
107    let mode = read_u32(data, start + 24)?;
108    let size = read_u32(data, start + 36)?;
109    let oid_start = start + 40;
110    let id = ObjectId::from_bytes(
111        data.get(oid_start..oid_start + hash.bytes())
112            .ok_or_else(|| invalid("truncated index object id"))?,
113    )?;
114    *cursor = oid_start + hash.bytes();
115    let flags = read_u16(data, *cursor)?;
116    *cursor += 2;
117    let extended = flags & 0x4000 != 0;
118    if version == 2 && extended {
119        return Err(invalid("index v2 entry has extended flags"));
120    }
121    let extended_flags = if extended {
122        let value = read_u16(data, *cursor)?;
123        *cursor += 2;
124        if value & 0x1fff != 0 {
125            return Err(invalid("index entry has reserved extended flags"));
126        }
127        value
128    } else {
129        0
130    };
131    let path = if version == 4 {
132        parse_v4_path(data, cursor, end, previous_path)?
133    } else {
134        let path = take_path(data, cursor, end)?;
135        let entry_len = cursor
136            .checked_sub(start)
137            .ok_or_else(|| invalid("index entry cursor underflow"))?;
138        *cursor = start
139            .checked_add(entry_len.div_ceil(8) * 8)
140            .ok_or_else(|| invalid("index entry padding overflow"))?;
141        path
142    };
143    validate_path(&path)?;
144    Ok(IndexEntry {
145        ctime_seconds,
146        mtime_seconds,
147        mode,
148        size,
149        id,
150        stage: u8::try_from((flags >> 12) & 3).expect("two bits fit u8"),
151        assume_valid: flags & 0x8000 != 0,
152        skip_worktree: extended_flags & 0x4000 != 0,
153        intent_to_add: extended_flags & 0x2000 != 0,
154        path,
155    })
156}
157
158fn parse_v4_path(data: &[u8], cursor: &mut usize, end: usize, previous: &[u8]) -> Result<Vec<u8>> {
159    let remove = variable_width(data, cursor, end)?;
160    if remove > previous.len() {
161        return Err(invalid("index v4 path prefix is out of bounds"));
162    }
163    let suffix = take_path(data, cursor, end)?;
164    let mut path = previous[..previous.len() - remove].to_vec();
165    path.extend(suffix);
166    Ok(path)
167}
168
169fn variable_width(data: &[u8], cursor: &mut usize, end: usize) -> Result<usize> {
170    let mut byte = take(data, cursor, end)?;
171    let mut value = usize::from(byte & 0x7f);
172    while byte & 0x80 != 0 {
173        byte = take(data, cursor, end)?;
174        value = value
175            .checked_add(1)
176            .and_then(|value| value.checked_shl(7))
177            .and_then(|value| value.checked_add(usize::from(byte & 0x7f)))
178            .ok_or_else(|| invalid("index v4 path prefix overflow"))?;
179    }
180    Ok(value)
181}
182
183fn take_path(data: &[u8], cursor: &mut usize, end: usize) -> Result<Vec<u8>> {
184    let nul = data
185        .get(*cursor..end)
186        .and_then(|bytes| bytes.iter().position(|byte| *byte == 0))
187        .map(|offset| *cursor + offset)
188        .ok_or_else(|| invalid("index path has no terminator"))?;
189    let path = data[*cursor..nul].to_vec();
190    *cursor = nul + 1;
191    Ok(path)
192}
193
194fn validate_path(path: &[u8]) -> Result<()> {
195    if path.is_empty() || path.first() == Some(&b'/') || path.last() == Some(&b'/') {
196        return Err(invalid("invalid index path"));
197    }
198    if path
199        .split(|byte| *byte == b'/')
200        .any(|part| matches!(part, b"." | b".." | b".git"))
201    {
202        return Err(invalid("unsafe index path component"));
203    }
204    Ok(())
205}
206
207fn parse_extensions(data: &[u8], mut cursor: usize, end: usize) -> Result<()> {
208    while cursor < end {
209        if cursor.saturating_add(8) > end {
210            return Err(invalid("truncated index extension"));
211        }
212        let signature = &data[cursor..cursor + 4];
213        let length = usize::try_from(read_u32(data, cursor + 4)?)
214            .map_err(|_| invalid("index extension length overflow"))?;
215        if signature[0].is_ascii_lowercase() {
216            return Err(GitError::Unsupported(format!(
217                "mandatory index extension {}",
218                String::from_utf8_lossy(signature)
219            )));
220        }
221        cursor = cursor
222            .checked_add(8 + length)
223            .ok_or_else(|| invalid("index extension overflow"))?;
224        if cursor > end {
225            return Err(invalid("index extension is out of bounds"));
226        }
227    }
228    Ok(())
229}
230
231fn take(data: &[u8], cursor: &mut usize, end: usize) -> Result<u8> {
232    if *cursor >= end {
233        return Err(invalid("truncated index data"));
234    }
235    let value = data[*cursor];
236    *cursor += 1;
237    Ok(value)
238}
239
240fn read_u16(input: &[u8], offset: usize) -> Result<u16> {
241    let bytes = input
242        .get(offset..offset + 2)
243        .ok_or_else(|| invalid("truncated index integer"))?;
244    Ok(u16::from_be_bytes(bytes.try_into().expect("two bytes")))
245}
246
247fn read_u32(input: &[u8], offset: usize) -> Result<u32> {
248    let bytes = input
249        .get(offset..offset + 4)
250        .ok_or_else(|| invalid("truncated index integer"))?;
251    Ok(u32::from_be_bytes(bytes.try_into().expect("four bytes")))
252}