1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! What:
//! A read of the per-entry flags of an index file that decide whether the index may seed an explicit-path commit:
//! whether any entry is marked assume-unchanged, skip-worktree or intent-to-add.
//! Why:
//! A private index copied from the real index and reset to the base keeps those marks, and `git add --all -- <path>`
//! then skips a marked file the commit named (assume-unchanged) or refuses it (skip-worktree), where Git's own partial
//! commit and the previous flow staged it; an intent-to-add entry of a tracked empty file survives the reset and is then
//! left out of the tree, so the commit loses that file. Finding a mark needs the whole file, and a Git start for it would add a
//! process to every commit, so the entries are read here; anything this reader does not fully understand answers
//! "unreadable", and the caller then builds the index the slower, always-correct way.
//! The layout is the one in Git's `gitformat-index` documentation: a 12-byte header, entries (versions 2 to 4), then
//! extensions and a trailing checksum.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const flags = readIndexFlags(await readFile(indexPath)); // 'plain' | 'marked' | 'unreadable'
//! ```
/// Bit of the 16-bit entry flags that marks assume-unchanged.
const ASSUME_VALID: u16 = 0x8000;
/// Bit of the 16-bit entry flags that says a second 16-bit flags field follows.
const EXTENDED: u16 = 0x4000;
/// Bit of the extended flags that marks skip-worktree.
const SKIP_WORKTREE: u16 = 0x4000;
/// Bit of the extended flags that marks intent-to-add.
/// Such an entry keeps the empty blob's name and is left out of any tree built from the index, so a copy that a one-tree
/// `read-tree --reset` leaves in place drops a tracked empty file from the commit.
const INTENT_TO_ADD: u16 = 0x2000;
/// Bytes of the index header: signature, version and entry count.
const HEADER_LENGTH: usize = 12;
/// Bytes of an entry before its object name: ten 32-bit stat fields.
const STAT_LENGTH: usize = 40;
/// Object name lengths tried in turn: SHA-1 and SHA-256.
const OBJECT_NAME_LENGTHS: [usize; 2] = [20, 32];
/// The extension that makes the entries live partly in another file, which this reader does not follow.
const SPLIT_INDEX_EXTENSION: &[u8; 4] = b"link";
/// What a read of an index file found.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexFlags {
/// Every entry was read and none is marked assume-unchanged, skip-worktree or intent-to-add.
Plain,
/// At least one entry is marked assume-unchanged, skip-worktree or intent-to-add.
Marked,
/// The file is not an index this reader fully understands (an unknown version, a split index, a layout that does
/// not add up), so nothing is known about its marks.
Unreadable,
}
/// Big-endian 16-bit value at `at`.
fn be16(bytes: &[u8], at: usize) -> Option<u16> {
let pair: &[u8] = bytes.get(at..at.checked_add(2)?)?;
return Some(u16::from_be_bytes([pair[0], pair[1]]));
}
/// Big-endian 32-bit value at `at`.
fn be32(bytes: &[u8], at: usize) -> Option<u32> {
let quad: &[u8] = bytes.get(at..at.checked_add(4)?)?;
return Some(u32::from_be_bytes([quad[0], quad[1], quad[2], quad[3]]));
}
/// What:
/// Reads every entry and extension of an index whose object names are `name_length` bytes long.
/// Why:
/// A wrong name length misplaces the entries, so the layout stops adding up and the read answers nothing;
/// a right one consumes the file exactly up to its checksum.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// scanEntries(bytes, nameLength) // true when any entry is marked; undefined when the layout is not understood
/// ```
fn scan_entries(bytes: &[u8], name_length: usize) -> Option<bool> {
let end: usize = bytes.len().checked_sub(name_length)?;
if bytes.get(..4)? != b"DIRC" {
return None;
}
let version: u32 = be32(bytes, 4)?;
if !(2..=4).contains(&version) {
return None;
}
let count: u32 = be32(bytes, 8)?;
let mut marked: bool = false;
let mut at: usize = HEADER_LENGTH;
for _entry in 0..count {
let flags: u16 = be16(bytes, at.checked_add(STAT_LENGTH)?.checked_add(name_length)?)?;
let mut head: usize = STAT_LENGTH + name_length + 2;
if flags & ASSUME_VALID != 0 {
marked = true;
}
if flags & EXTENDED != 0 {
if version < 3 {
return None;
}
if be16(bytes, at.checked_add(head)?)? & (SKIP_WORKTREE | INTENT_TO_ADD) != 0 {
marked = true;
}
head += 2;
}
let name_start: usize = at.checked_add(head)?;
if version == 4 {
// The strip count is a varint; its last byte has no continuation bit.
let mut cursor: usize = name_start;
while bytes.get(cursor)? & 0x80 != 0 {
cursor += 1;
}
cursor += 1;
let terminator: usize = bytes.get(cursor..end)?.iter().position(|byte| { return *byte == 0; })?;
at = cursor + terminator + 1;
} else {
let terminator: usize = bytes.get(name_start..end)?.iter().position(|byte| { return *byte == 0; })?;
// An entry is padded with one to eight NULs to a multiple of eight bytes from its own start.
at = at.checked_add((head + terminator + 8) & !7)?;
}
if at > end {
return None;
}
}
while at < end {
let signature: &[u8] = bytes.get(at..at.checked_add(4)?)?;
if !signature.iter().all(u8::is_ascii_alphanumeric) || signature == SPLIT_INDEX_EXTENSION {
return None;
}
let size: usize = usize::try_from(be32(bytes, at + 4)?).ok()?;
at = at.checked_add(8)?.checked_add(size)?;
}
if at != end {
return None;
}
return Some(marked);
}
/// What:
/// Whether an index file has an entry marked assume-unchanged or skip-worktree.
/// Why:
/// Both object name lengths are tried because the reader is not told the repository's object format;
/// a layout that adds up for neither is unreadable, never "plain".
///
/// In TS you'd write (pseudocode):
/// ```ts
/// readIndexFlags(bytes)
/// ```
pub fn read_index_flags(bytes: &[u8]) -> IndexFlags {
for name_length in OBJECT_NAME_LENGTHS {
match scan_entries(bytes, name_length) {
Some(true) => return IndexFlags::Marked,
Some(false) => return IndexFlags::Plain,
None => {}
}
}
return IndexFlags::Unreadable;
}
/// Reader controls stay out of the release executable.
#[cfg(test)]
#[path = "index_flags_tests.rs"]
mod tests;