Skip to main content

grit_lib/objects/
extra_headers.rs

1//! Raw extra header bytes in Git commit and tag objects.
2//!
3//! Extra headers are the lines between the fixed headers (`author` / `tagger`, …) and the blank
4//! line that starts the message body. Each logical header is one or more lines: the first line is
5//! `name SP value LF`, and continuation lines start with SP.
6
7/// Advance past one header block (first line plus any SP-prefixed continuations).
8///
9/// `start` must be the offset of a header line within `extra` (not past its end). Returns
10/// `(key, end_offset, _continued)` where `key` is empty for orphan continuation lines.
11#[must_use]
12pub fn next_header_block(extra: &[u8], start: usize) -> (&[u8], usize, bool) {
13    let pos = start;
14    let line_start = pos;
15    let mut line_end = pos;
16    while line_end < extra.len() && extra[line_end] != b'\n' {
17        line_end += 1;
18    }
19    let after_nl = (line_end + 1).min(extra.len());
20    let line = &extra[line_start..line_end];
21    if line.first() == Some(&b' ') {
22        return (b"", after_nl, true);
23    }
24    let key_end = line.iter().position(|&b| b == b' ').unwrap_or(line.len());
25    let key = &line[..key_end];
26    let mut end = after_nl;
27    while end < extra.len() {
28        let ls = end;
29        let mut le = end;
30        while le < extra.len() && extra[le] != b'\n' {
31            le += 1;
32        }
33        let an = (le + 1).min(extra.len());
34        if extra.get(ls) == Some(&b' ') {
35            end = an;
36        } else {
37            break;
38        }
39    }
40    (key, end, false)
41}
42
43/// Iterator over `(name, value)` pairs in a raw extra-header blob.
44#[derive(Debug, Clone)]
45pub struct ExtraHeaderIter<'a> {
46    extra: &'a [u8],
47    pos: usize,
48}
49
50impl<'a> ExtraHeaderIter<'a> {
51    /// Iterate header pairs in `extra`.
52    #[must_use]
53    pub fn new(extra: &'a [u8]) -> Self {
54        Self { extra, pos: 0 }
55    }
56}
57
58/// Iterate `(name, value)` pairs in a raw extra-header blob.
59#[must_use]
60pub fn iter_extra_headers(extra: &[u8]) -> ExtraHeaderIter<'_> {
61    ExtraHeaderIter::new(extra)
62}
63
64impl<'a> Iterator for ExtraHeaderIter<'a> {
65    type Item = (&'a [u8], &'a [u8]);
66
67    fn next(&mut self) -> Option<Self::Item> {
68        if self.pos >= self.extra.len() {
69            return None;
70        }
71        let line_start = self.pos;
72        let mut line_end = self.pos;
73        while line_end < self.extra.len() && self.extra[line_end] != b'\n' {
74            line_end += 1;
75        }
76        if self.extra[line_start..line_end].first() == Some(&b' ') {
77            self.pos = (line_end + 1).min(self.extra.len());
78            return self.next();
79        }
80        let key_end = self.extra[line_start..line_end]
81            .iter()
82            .position(|&b| b == b' ')
83            .unwrap_or(line_end - line_start);
84        let key = &self.extra[line_start..line_start + key_end];
85        let (_, block_end, _) = next_header_block(self.extra, line_start);
86        let value_start = line_start
87            + key_end
88            + if key_end < line_end - line_start {
89                1
90            } else {
91                0
92            };
93        let mut value_end = block_end;
94        if value_end > value_start && self.extra[value_end - 1] == b'\n' {
95            value_end -= 1;
96        }
97        let value = &self.extra[value_start..value_end];
98        self.pos = block_end;
99        Some((key, value))
100    }
101}
102
103/// All values for `name` (one entry per header line with that name).
104#[must_use]
105pub fn extra_header_values<'a>(extra: &'a [u8], name: &str) -> Vec<&'a [u8]> {
106    let name = name.as_bytes();
107    ExtraHeaderIter::new(extra)
108        .filter(|(k, _)| *k == name)
109        .map(|(_, v)| v)
110        .collect()
111}
112
113/// Remove every header block whose name satisfies `drop_key`.
114#[must_use]
115pub fn remove_header_lines(extra: &[u8], drop_key: impl Fn(&[u8]) -> bool) -> Vec<u8> {
116    let mut out = Vec::new();
117    let mut pos = 0usize;
118    while pos < extra.len() {
119        let (key, block_end, _) = next_header_block(extra, pos);
120        if drop_key(key) {
121            pos = block_end;
122            continue;
123        }
124        out.extend_from_slice(&extra[pos..block_end]);
125        pos = block_end;
126    }
127    out
128}
129
130/// Replace every header block named `name` with `line`, or append `line` when no such block exists.
131///
132/// `line` must be a full header record (`name SP value LF`, plus any SP-prefixed continuation
133/// lines and their trailing LFs) in the same encoding used in
134/// [`crate::objects::CommitData::extra_headers`] / [`crate::objects::TagData::extra_headers`].
135pub fn set_extra_header(extra: &mut Vec<u8>, name: &str, line: &[u8]) {
136    let name_bytes = name.as_bytes();
137    *extra = remove_header_lines(extra, |key| key == name_bytes);
138    extra.extend_from_slice(line);
139}
140
141/// Insert `line` before the first `gpgsig` / `gpgsig-sha256` block, dropping existing blocks
142/// whose names satisfy `drop_key`.
143#[must_use]
144pub fn insert_header_before_signature(
145    extra: &[u8],
146    line: &[u8],
147    drop_key: impl Fn(&[u8]) -> bool,
148) -> Vec<u8> {
149    let mut out = Vec::new();
150    let mut pos = 0usize;
151    let mut inserted = false;
152    while pos < extra.len() {
153        let (key, block_end, _) = next_header_block(extra, pos);
154        if !inserted && (key == b"gpgsig" || key == b"gpgsig-sha256") {
155            out.extend_from_slice(line);
156            inserted = true;
157        }
158        if drop_key(key) {
159            pos = block_end;
160            continue;
161        }
162        out.extend_from_slice(&extra[pos..block_end]);
163        pos = block_end;
164    }
165    if !inserted {
166        out.extend_from_slice(line);
167    }
168    out
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn iter_multivalued_and_continuation() {
177        let raw = b"foo one\nfoo two\nbar multi\n line\n";
178        let pairs: Vec<_> = ExtraHeaderIter::new(raw).collect();
179        assert_eq!(pairs.len(), 3);
180        assert_eq!(pairs[0], (b"foo".as_slice(), b"one".as_slice()));
181        assert_eq!(pairs[1], (b"foo".as_slice(), b"two".as_slice()));
182        assert_eq!(pairs[2], (b"bar".as_slice(), b"multi\n line".as_slice()));
183        assert_eq!(
184            extra_header_values(raw, "foo"),
185            vec![b"one".as_slice(), b"two".as_slice()]
186        );
187    }
188
189    #[test]
190    fn set_replaces_all_same_name() {
191        let mut extra = b"foo old\nbar keep\nfoo also\n".to_vec();
192        set_extra_header(&mut extra, "foo", b"foo new\n");
193        assert_eq!(extra, b"bar keep\nfoo new\n");
194    }
195
196    #[test]
197    fn insert_before_gpgsig_drops_matching() {
198        let extra = b"change-id old\nencoding UTF-8\n";
199        let out = insert_header_before_signature(extra, b"change-id new\n", |key| {
200            key == b"change-id" || key == b"gitbutler-change-id"
201        });
202        assert_eq!(out, b"encoding UTF-8\nchange-id new\n");
203
204        let signed = b"change-id x\nencoding UTF-8\ngpgsig sig\n line\n";
205        let out2 =
206            insert_header_before_signature(signed, b"change-id y\n", |key| key == b"change-id");
207        assert_eq!(out2, b"encoding UTF-8\nchange-id y\ngpgsig sig\n line\n");
208    }
209}