Skip to main content

git_policy_cli/index/
index_flags.rs

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