1use amiss_wire::controls::GitMode;
2use amiss_wire::model::{ObjectFormat, Oid};
3
4use crate::Error;
5use crate::object::{hex, ordinary_digest};
6
7#[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#[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
58fn 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
79pub 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
172fn 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
225fn 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
248fn 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}