Coverage Report

Created: 2026-02-14 07:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gitoxide/gix-index/src/extension/untracked_cache.rs
Line
Count
Source
1
use bstr::BString;
2
use gix_hash::ObjectId;
3
4
use crate::{
5
    entry,
6
    extension::{Signature, UntrackedCache},
7
    util::{read_u32, split_at_byte_exclusive, var_int},
8
};
9
10
/// A structure to track filesystem stat information along with an object id, linking a worktree file with what's in our ODB.
11
#[derive(Clone)]
12
pub struct OidStat {
13
    /// The file system stat information
14
    pub stat: entry::Stat,
15
    /// The id of the file in our ODB.
16
    pub id: ObjectId,
17
}
18
19
/// A directory with information about its untracked files, and its sub-directories
20
#[derive(Clone)]
21
pub struct Directory {
22
    /// The directories name, or an empty string if this is the root directory.
23
    pub name: BString,
24
    /// Untracked files and directory names
25
    pub untracked_entries: Vec<BString>,
26
    /// indices for sub-directories similar to this one.
27
    pub sub_directories: Vec<usize>,
28
29
    /// The directories stat data, if available or valid // TODO: or is it the exclude file?
30
    pub stat: Option<entry::Stat>,
31
    /// The oid of a .gitignore file, if it exists
32
    pub exclude_file_oid: Option<ObjectId>,
33
    /// TODO: figure out what this really does
34
    pub check_only: bool,
35
}
36
37
/// Only used as an indicator
38
pub const SIGNATURE: Signature = *b"UNTR";
39
40
// #[allow(unused)]
41
/// Decode an untracked cache extension from `data`, assuming object hashes are of type `object_hash`.
42
0
pub fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Option<UntrackedCache> {
43
0
    if data.last().is_none_or(|b| *b != 0) {
44
0
        return None;
45
0
    }
46
0
    let (identifier_len, data) = var_int(data)?;
47
0
    let (identifier, data) = data.split_at_checked(identifier_len.try_into().ok()?)?;
48
49
0
    let hash_len = object_hash.len_in_bytes();
50
0
    let (info_exclude, data) = decode_oid_stat(data, hash_len)?;
51
0
    let (excludes_file, data) = decode_oid_stat(data, hash_len)?;
52
0
    let (dir_flags, data) = read_u32(data)?;
53
0
    let (exclude_filename_per_dir, data) = split_at_byte_exclusive(data, 0)?;
54
55
0
    let (num_directory_blocks, data) = var_int(data)?;
56
57
0
    let mut res = UntrackedCache {
58
0
        identifier: identifier.into(),
59
0
        info_exclude: (!info_exclude.id.is_null()).then_some(info_exclude),
60
0
        excludes_file: (!excludes_file.id.is_null()).then_some(excludes_file),
61
0
        exclude_filename_per_dir: exclude_filename_per_dir.into(),
62
0
        dir_flags,
63
0
        directories: Vec::new(),
64
0
    };
65
0
    if num_directory_blocks == 0 {
66
0
        return data.is_empty().then_some(res);
67
0
    }
68
69
0
    let num_directory_blocks = num_directory_blocks.try_into().ok()?;
70
0
    let directories = &mut res.directories;
71
0
    directories.reserve(num_directory_blocks);
72
73
0
    let data = decode_directory_block(data, directories)?;
74
0
    if directories.len() != num_directory_blocks {
75
0
        return None;
76
0
    }
77
0
    let (valid, data) = gix_bitmap::ewah::decode(data).ok()?;
78
0
    let (check_only, data) = gix_bitmap::ewah::decode(data).ok()?;
79
0
    let (hash_valid, mut data) = gix_bitmap::ewah::decode(data).ok()?;
80
81
0
    if valid.num_bits() > num_directory_blocks
82
0
        || check_only.num_bits() > num_directory_blocks
83
0
        || hash_valid.num_bits() > num_directory_blocks
84
    {
85
0
        return None;
86
0
    }
87
88
0
    check_only.for_each_set_bit(|index| {
89
0
        directories[index].check_only = true;
90
0
        Some(())
91
0
    })?;
92
0
    valid.for_each_set_bit(|index| {
93
0
        let (stat, rest) = crate::decode::stat(data)?;
94
0
        directories[index].stat = stat.into();
95
0
        data = rest;
96
0
        Some(())
97
0
    });
98
0
    hash_valid.for_each_set_bit(|index| {
99
0
        let (hash, rest) = data.split_at_checked(hash_len)?;
100
0
        data = rest;
101
0
        directories[index].exclude_file_oid = ObjectId::from_bytes_or_panic(hash).into();
102
0
        Some(())
103
0
    });
104
105
    // null-byte checked in the beginning
106
0
    if data.len() != 1 {
107
0
        return None;
108
0
    }
109
0
    res.into()
110
0
}
111
112
0
fn decode_directory_block<'a>(data: &'a [u8], directories: &mut Vec<Directory>) -> Option<&'a [u8]> {
113
0
    let (num_untracked, data) = var_int(data)?;
114
0
    let (num_dirs, data) = var_int(data)?;
115
0
    let (name, mut data) = split_at_byte_exclusive(data, 0)?;
116
0
    let mut untracked_entries = Vec::<BString>::with_capacity(num_untracked.try_into().ok()?);
117
0
    for _ in 0..num_untracked {
118
0
        let (name, rest) = split_at_byte_exclusive(data, 0)?;
119
0
        data = rest;
120
0
        untracked_entries.push(name.into());
121
    }
122
123
0
    let index = directories.len();
124
0
    directories.push(Directory {
125
0
        name: name.into(),
126
0
        untracked_entries,
127
0
        sub_directories: Vec::with_capacity(num_dirs.try_into().ok()?),
128
        // the following are set later through their bitmaps
129
0
        stat: None,
130
0
        exclude_file_oid: None,
131
        check_only: false,
132
    });
133
134
0
    for _ in 0..num_dirs {
135
0
        let subdir_index = directories.len();
136
0
        let rest = decode_directory_block(data, directories)?;
137
0
        data = rest;
138
0
        directories[index].sub_directories.push(subdir_index);
139
    }
140
141
0
    data.into()
142
0
}
143
144
0
fn decode_oid_stat(data: &[u8], hash_len: usize) -> Option<(OidStat, &[u8])> {
145
0
    let (stat, data) = crate::decode::stat(data)?;
146
0
    let (hash, data) = data.split_at_checked(hash_len)?;
147
0
    Some((
148
0
        OidStat {
149
0
            stat,
150
0
            id: ObjectId::from_bytes_or_panic(hash),
151
0
        },
152
0
        data,
153
0
    ))
154
0
}