gix_index/extension/
untracked_cache.rs

1use bstr::BString;
2use gix_hash::ObjectId;
3
4use crate::{
5    entry,
6    extension::{Signature, UntrackedCache},
7    util::{read_u32, split_at_byte_exclusive, split_at_pos, 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)]
12pub 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)]
21pub 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
38pub 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`.
42pub fn decode(data: &[u8], object_hash: gix_hash::Kind) -> Option<UntrackedCache> {
43    if !data.last().is_some_and(|b| *b == 0) {
44        return None;
45    }
46    let (identifier_len, data) = var_int(data)?;
47    let (identifier, data) = split_at_pos(data, identifier_len.try_into().ok()?)?;
48
49    let hash_len = object_hash.len_in_bytes();
50    let (info_exclude, data) = decode_oid_stat(data, hash_len)?;
51    let (excludes_file, data) = decode_oid_stat(data, hash_len)?;
52    let (dir_flags, data) = read_u32(data)?;
53    let (exclude_filename_per_dir, data) = split_at_byte_exclusive(data, 0)?;
54
55    let (num_directory_blocks, data) = var_int(data)?;
56
57    let mut res = UntrackedCache {
58        identifier: identifier.into(),
59        info_exclude: (!info_exclude.id.is_null()).then_some(info_exclude),
60        excludes_file: (!excludes_file.id.is_null()).then_some(excludes_file),
61        exclude_filename_per_dir: exclude_filename_per_dir.into(),
62        dir_flags,
63        directories: Vec::new(),
64    };
65    if num_directory_blocks == 0 {
66        return data.is_empty().then_some(res);
67    }
68
69    let num_directory_blocks = num_directory_blocks.try_into().ok()?;
70    let directories = &mut res.directories;
71    directories.reserve(num_directory_blocks);
72
73    let data = decode_directory_block(data, directories)?;
74    if directories.len() != num_directory_blocks {
75        return None;
76    }
77    let (valid, data) = gix_bitmap::ewah::decode(data).ok()?;
78    let (check_only, data) = gix_bitmap::ewah::decode(data).ok()?;
79    let (hash_valid, mut data) = gix_bitmap::ewah::decode(data).ok()?;
80
81    if valid.num_bits() > num_directory_blocks
82        || check_only.num_bits() > num_directory_blocks
83        || hash_valid.num_bits() > num_directory_blocks
84    {
85        return None;
86    }
87
88    check_only.for_each_set_bit(|index| {
89        directories[index].check_only = true;
90        Some(())
91    })?;
92    valid.for_each_set_bit(|index| {
93        let (stat, rest) = crate::decode::stat(data)?;
94        directories[index].stat = stat.into();
95        data = rest;
96        Some(())
97    });
98    hash_valid.for_each_set_bit(|index| {
99        let (hash, rest) = split_at_pos(data, hash_len)?;
100        data = rest;
101        directories[index].exclude_file_oid = ObjectId::from_bytes_or_panic(hash).into();
102        Some(())
103    });
104
105    // null-byte checked in the beginning
106    if data.len() != 1 {
107        return None;
108    }
109    res.into()
110}
111
112fn decode_directory_block<'a>(data: &'a [u8], directories: &mut Vec<Directory>) -> Option<&'a [u8]> {
113    let (num_untracked, data) = var_int(data)?;
114    let (num_dirs, data) = var_int(data)?;
115    let (name, mut data) = split_at_byte_exclusive(data, 0)?;
116    let mut untracked_entries = Vec::<BString>::with_capacity(num_untracked.try_into().ok()?);
117    for _ in 0..num_untracked {
118        let (name, rest) = split_at_byte_exclusive(data, 0)?;
119        data = rest;
120        untracked_entries.push(name.into());
121    }
122
123    let index = directories.len();
124    directories.push(Directory {
125        name: name.into(),
126        untracked_entries,
127        sub_directories: Vec::with_capacity(num_dirs.try_into().ok()?),
128        // the following are set later through their bitmaps
129        stat: None,
130        exclude_file_oid: None,
131        check_only: false,
132    });
133
134    for _ in 0..num_dirs {
135        let subdir_index = directories.len();
136        let rest = decode_directory_block(data, directories)?;
137        data = rest;
138        directories[index].sub_directories.push(subdir_index);
139    }
140
141    data.into()
142}
143
144fn decode_oid_stat(data: &[u8], hash_len: usize) -> Option<(OidStat, &[u8])> {
145    let (stat, data) = crate::decode::stat(data)?;
146    let (hash, data) = split_at_pos(data, hash_len)?;
147    Some((
148        OidStat {
149            stat,
150            id: ObjectId::from_bytes_or_panic(hash),
151        },
152        data,
153    ))
154}