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}