Skip to main content

weavatrix_git/
index.rs

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