git2/
message.rs

1use core::ops::Range;
2use std::ffi::CStr;
3use std::ffi::CString;
4use std::iter::FusedIterator;
5use std::ptr;
6
7use libc::{c_char, c_int};
8
9use crate::util::Binding;
10use crate::{raw, Buf, Error, IntoCString};
11
12/// Clean up a message, removing extraneous whitespace, and ensure that the
13/// message ends with a newline. If `comment_char` is `Some`, also remove comment
14/// lines starting with that character.
15pub fn message_prettify<T: IntoCString>(
16    message: T,
17    comment_char: Option<u8>,
18) -> Result<String, Error> {
19    _message_prettify(message.into_c_string()?, comment_char)
20}
21
22fn _message_prettify(message: CString, comment_char: Option<u8>) -> Result<String, Error> {
23    let ret = Buf::new();
24    unsafe {
25        try_call!(raw::git_message_prettify(
26            ret.raw(),
27            message,
28            comment_char.is_some() as c_int,
29            comment_char.unwrap_or(0) as c_char
30        ));
31    }
32    Ok(ret.as_str().unwrap().to_string())
33}
34
35/// The default comment character for `message_prettify` ('#')
36pub const DEFAULT_COMMENT_CHAR: Option<u8> = Some(b'#');
37
38/// Get the trailers for the given message.
39///
40/// Use this function when you are dealing with a UTF-8-encoded message.
41pub fn message_trailers_strs(message: &str) -> Result<MessageTrailersStrs, Error> {
42    _message_trailers(message.into_c_string()?).map(|res| MessageTrailersStrs(res))
43}
44
45/// Get the trailers for the given message.
46///
47/// Use this function when the message might not be UTF-8-encoded,
48/// or if you want to handle the returned trailer key–value pairs
49/// as bytes.
50pub fn message_trailers_bytes<S: IntoCString>(message: S) -> Result<MessageTrailersBytes, Error> {
51    _message_trailers(message.into_c_string()?).map(|res| MessageTrailersBytes(res))
52}
53
54fn _message_trailers(message: CString) -> Result<MessageTrailers, Error> {
55    let ret = MessageTrailers::new();
56    unsafe {
57        try_call!(raw::git_message_trailers(ret.raw(), message));
58    }
59    Ok(ret)
60}
61
62/// Collection of UTF-8-encoded trailers.
63///
64/// Use `iter()` to get access to the values.
65pub struct MessageTrailersStrs(MessageTrailers);
66
67impl MessageTrailersStrs {
68    /// Create a borrowed iterator.
69    pub fn iter(&self) -> MessageTrailersStrsIterator<'_> {
70        MessageTrailersStrsIterator(self.0.iter())
71    }
72    /// The number of trailer key–value pairs.
73    pub fn len(&self) -> usize {
74        self.0.len()
75    }
76    /// Convert to the “bytes” variant.
77    pub fn to_bytes(self) -> MessageTrailersBytes {
78        MessageTrailersBytes(self.0)
79    }
80}
81
82/// Collection of unencoded (bytes) trailers.
83///
84/// Use `iter()` to get access to the values.
85pub struct MessageTrailersBytes(MessageTrailers);
86
87impl MessageTrailersBytes {
88    /// Create a borrowed iterator.
89    pub fn iter(&self) -> MessageTrailersBytesIterator<'_> {
90        MessageTrailersBytesIterator(self.0.iter())
91    }
92    /// The number of trailer key–value pairs.
93    pub fn len(&self) -> usize {
94        self.0.len()
95    }
96}
97
98struct MessageTrailers {
99    raw: raw::git_message_trailer_array,
100}
101
102impl MessageTrailers {
103    fn new() -> MessageTrailers {
104        crate::init();
105        unsafe {
106            Binding::from_raw(&mut raw::git_message_trailer_array {
107                trailers: ptr::null_mut(),
108                count: 0,
109                _trailer_block: ptr::null_mut(),
110            } as *mut _)
111        }
112    }
113    fn iter(&self) -> MessageTrailersIterator<'_> {
114        MessageTrailersIterator {
115            trailers: self,
116            range: Range {
117                start: 0,
118                end: self.raw.count,
119            },
120        }
121    }
122    fn len(&self) -> usize {
123        self.raw.count
124    }
125}
126
127impl Drop for MessageTrailers {
128    fn drop(&mut self) {
129        unsafe {
130            raw::git_message_trailer_array_free(&mut self.raw);
131        }
132    }
133}
134
135impl Binding for MessageTrailers {
136    type Raw = *mut raw::git_message_trailer_array;
137    unsafe fn from_raw(raw: *mut raw::git_message_trailer_array) -> MessageTrailers {
138        MessageTrailers { raw: *raw }
139    }
140    fn raw(&self) -> *mut raw::git_message_trailer_array {
141        &self.raw as *const _ as *mut _
142    }
143}
144
145struct MessageTrailersIterator<'a> {
146    trailers: &'a MessageTrailers,
147    range: Range<usize>,
148}
149
150fn to_raw_tuple(trailers: &MessageTrailers, index: usize) -> (*const c_char, *const c_char) {
151    unsafe {
152        let addr = trailers.raw.trailers.wrapping_add(index);
153        ((*addr).key, (*addr).value)
154    }
155}
156
157/// Borrowed iterator over the UTF-8-encoded trailers.
158pub struct MessageTrailersStrsIterator<'a>(MessageTrailersIterator<'a>);
159
160impl<'pair> Iterator for MessageTrailersStrsIterator<'pair> {
161    type Item = (&'pair str, &'pair str);
162
163    fn next(&mut self) -> Option<Self::Item> {
164        self.0
165            .range
166            .next()
167            .map(|index| to_str_tuple(&self.0.trailers, index))
168    }
169
170    fn size_hint(&self) -> (usize, Option<usize>) {
171        self.0.range.size_hint()
172    }
173}
174
175impl FusedIterator for MessageTrailersStrsIterator<'_> {}
176
177impl ExactSizeIterator for MessageTrailersStrsIterator<'_> {
178    fn len(&self) -> usize {
179        self.0.range.len()
180    }
181}
182
183impl DoubleEndedIterator for MessageTrailersStrsIterator<'_> {
184    fn next_back(&mut self) -> Option<Self::Item> {
185        self.0
186            .range
187            .next_back()
188            .map(|index| to_str_tuple(&self.0.trailers, index))
189    }
190}
191
192fn to_str_tuple(trailers: &MessageTrailers, index: usize) -> (&str, &str) {
193    unsafe {
194        let (rkey, rvalue) = to_raw_tuple(&trailers, index);
195        let key = CStr::from_ptr(rkey).to_str().unwrap();
196        let value = CStr::from_ptr(rvalue).to_str().unwrap();
197        (key, value)
198    }
199}
200
201/// Borrowed iterator over the raw (bytes) trailers.
202pub struct MessageTrailersBytesIterator<'a>(MessageTrailersIterator<'a>);
203
204impl<'pair> Iterator for MessageTrailersBytesIterator<'pair> {
205    type Item = (&'pair [u8], &'pair [u8]);
206
207    fn next(&mut self) -> Option<Self::Item> {
208        self.0
209            .range
210            .next()
211            .map(|index| to_bytes_tuple(&self.0.trailers, index))
212    }
213
214    fn size_hint(&self) -> (usize, Option<usize>) {
215        self.0.range.size_hint()
216    }
217}
218
219impl FusedIterator for MessageTrailersBytesIterator<'_> {}
220
221impl ExactSizeIterator for MessageTrailersBytesIterator<'_> {
222    fn len(&self) -> usize {
223        self.0.range.len()
224    }
225}
226
227impl DoubleEndedIterator for MessageTrailersBytesIterator<'_> {
228    fn next_back(&mut self) -> Option<Self::Item> {
229        self.0
230            .range
231            .next_back()
232            .map(|index| to_bytes_tuple(&self.0.trailers, index))
233    }
234}
235
236fn to_bytes_tuple(trailers: &MessageTrailers, index: usize) -> (&[u8], &[u8]) {
237    unsafe {
238        let (rkey, rvalue) = to_raw_tuple(&trailers, index);
239        let key = CStr::from_ptr(rkey).to_bytes();
240        let value = CStr::from_ptr(rvalue).to_bytes();
241        (key, value)
242    }
243}
244
245#[cfg(test)]
246mod tests {
247
248    #[test]
249    fn prettify() {
250        use crate::{message_prettify, DEFAULT_COMMENT_CHAR};
251
252        // This does not attempt to duplicate the extensive tests for
253        // git_message_prettify in libgit2, just a few representative values to
254        // make sure the interface works as expected.
255        assert_eq!(message_prettify("1\n\n\n2", None).unwrap(), "1\n\n2\n");
256        assert_eq!(
257            message_prettify("1\n\n\n2\n\n\n3", None).unwrap(),
258            "1\n\n2\n\n3\n"
259        );
260        assert_eq!(
261            message_prettify("1\n# comment\n# more", None).unwrap(),
262            "1\n# comment\n# more\n"
263        );
264        assert_eq!(
265            message_prettify("1\n# comment\n# more", DEFAULT_COMMENT_CHAR).unwrap(),
266            "1\n"
267        );
268        assert_eq!(
269            message_prettify("1\n; comment\n; more", Some(';' as u8)).unwrap(),
270            "1\n"
271        );
272    }
273
274    #[test]
275    fn trailers() {
276        use crate::{message_trailers_bytes, message_trailers_strs, MessageTrailersStrs};
277        use std::collections::HashMap;
278
279        // no trailers
280        let message1 = "
281WHAT ARE WE HERE FOR
282
283What are we here for?
284
285Just to be eaten?
286";
287        let expected: HashMap<&str, &str> = HashMap::new();
288        assert_eq!(expected, to_map(&message_trailers_strs(message1).unwrap()));
289
290        // standard PSA
291        let message2 = "
292Attention all
293
294We are out of tomatoes.
295
296Spoken-by: Major Turnips
297Transcribed-by: Seargant Persimmons
298Signed-off-by: Colonel Kale
299";
300        let expected: HashMap<&str, &str> = vec![
301            ("Spoken-by", "Major Turnips"),
302            ("Transcribed-by", "Seargant Persimmons"),
303            ("Signed-off-by", "Colonel Kale"),
304        ]
305        .into_iter()
306        .collect();
307        assert_eq!(expected, to_map(&message_trailers_strs(message2).unwrap()));
308
309        // ignore everything after `---`
310        let message3 = "
311The fate of Seargant Green-Peppers
312
313Seargant Green-Peppers was killed by Caterpillar Battalion 44.
314
315Signed-off-by: Colonel Kale
316---
317I never liked that guy, anyway.
318
319Opined-by: Corporal Garlic
320";
321        let expected: HashMap<&str, &str> = vec![("Signed-off-by", "Colonel Kale")]
322            .into_iter()
323            .collect();
324        assert_eq!(expected, to_map(&message_trailers_strs(message3).unwrap()));
325
326        // Raw bytes message; not valid UTF-8
327        // Source: https://stackoverflow.com/a/3886015/1725151
328        let message4 = b"
329Be honest guys
330
331Am I a malformed brussels sprout?
332
333Signed-off-by: Lieutenant \xe2\x28\xa1prout
334";
335
336        let trailer = message_trailers_bytes(&message4[..]).unwrap();
337        let expected = (&b"Signed-off-by"[..], &b"Lieutenant \xe2\x28\xa1prout"[..]);
338        let actual = trailer.iter().next().unwrap();
339        assert_eq!(expected, actual);
340
341        fn to_map(trailers: &MessageTrailersStrs) -> HashMap<&str, &str> {
342            let mut map = HashMap::with_capacity(trailers.len());
343            for (key, value) in trailers.iter() {
344                map.insert(key, value);
345            }
346            map
347        }
348    }
349}