gix_ref/store/file/log/
line.rs

1use gix_hash::ObjectId;
2
3use crate::{log::Line, store_impl::file::log::LineRef};
4
5impl LineRef<'_> {
6    /// Convert this instance into its mutable counterpart
7    pub fn to_owned(&self) -> Line {
8        (*self).into()
9    }
10}
11
12mod write {
13    use std::io;
14
15    use gix_object::bstr::{BStr, ByteSlice};
16
17    use crate::log::Line;
18
19    /// The Error produced by [`Line::write_to()`] (but wrapped in an io error).
20    #[derive(Debug, thiserror::Error)]
21    #[allow(missing_docs)]
22    enum Error {
23        #[error("Messages must not contain newlines\\n")]
24        IllegalCharacter,
25    }
26
27    impl From<Error> for io::Error {
28        fn from(err: Error) -> Self {
29            io::Error::new(io::ErrorKind::Other, err)
30        }
31    }
32
33    /// Output
34    impl Line {
35        /// Serialize this instance to `out` in the git serialization format for ref log lines.
36        pub fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> {
37            write!(out, "{} {} ", self.previous_oid, self.new_oid)?;
38            self.signature.write_to(out)?;
39            writeln!(out, "\t{}", check_newlines(self.message.as_ref())?)
40        }
41    }
42
43    fn check_newlines(input: &BStr) -> Result<&BStr, Error> {
44        if input.find_byte(b'\n').is_some() {
45            return Err(Error::IllegalCharacter);
46        }
47        Ok(input)
48    }
49}
50
51impl LineRef<'_> {
52    /// The previous object id of the ref. It will be a null hash if there was no previous id as
53    /// this ref is being created.
54    pub fn previous_oid(&self) -> ObjectId {
55        ObjectId::from_hex(self.previous_oid).expect("parse validation")
56    }
57    /// The new object id of the ref, or a null hash if it is removed.
58    pub fn new_oid(&self) -> ObjectId {
59        ObjectId::from_hex(self.new_oid).expect("parse validation")
60    }
61}
62
63impl<'a> From<LineRef<'a>> for Line {
64    fn from(v: LineRef<'a>) -> Self {
65        Line {
66            previous_oid: v.previous_oid(),
67            new_oid: v.new_oid(),
68            signature: v.signature.into(),
69            message: v.message.into(),
70        }
71    }
72}
73
74///
75pub mod decode {
76    use crate::{file::log::LineRef, parse::hex_hash};
77    use gix_object::bstr::{BStr, ByteSlice};
78    use winnow::{
79        combinator::{alt, eof, fail, opt, preceded, rest, terminated},
80        error::{AddContext, ParserError, StrContext},
81        prelude::*,
82        token::take_while,
83    };
84
85    ///
86    mod error {
87        use gix_object::bstr::{BString, ByteSlice};
88
89        /// The error returned by [`from_bytes(…)`][super::Line::from_bytes()]
90        #[derive(Debug)]
91        pub struct Error {
92            pub input: BString,
93        }
94
95        impl std::fmt::Display for Error {
96            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97                write!(
98                    f,
99                    "{:?} did not match '<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\\t<message>'",
100                    self.input
101                )
102            }
103        }
104
105        impl std::error::Error for Error {}
106
107        impl Error {
108            pub(crate) fn new(input: &[u8]) -> Self {
109                Error {
110                    input: input.as_bstr().to_owned(),
111                }
112            }
113        }
114    }
115    pub use error::Error;
116
117    impl<'a> LineRef<'a> {
118        /// Decode a line from the given bytes which are expected to start at a hex sha.
119        pub fn from_bytes(mut input: &'a [u8]) -> Result<LineRef<'a>, Error> {
120            one::<()>(&mut input).map_err(|_| Error::new(input))
121        }
122    }
123
124    fn message<'a, E: ParserError<&'a [u8]>>(i: &mut &'a [u8]) -> PResult<&'a BStr, E> {
125        if i.is_empty() {
126            rest.map(ByteSlice::as_bstr).parse_next(i)
127        } else {
128            terminated(take_while(0.., |c| c != b'\n'), opt(b'\n'))
129                .map(ByteSlice::as_bstr)
130                .parse_next(i)
131        }
132    }
133
134    fn one<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
135        bytes: &mut &'a [u8],
136    ) -> PResult<LineRef<'a>, E> {
137        let mut tokens = bytes.splitn(2, |b| *b == b'\t');
138        if let (Some(mut first), Some(mut second)) = (tokens.next(), tokens.next()) {
139            let (old, new, signature) = (
140                terminated(hex_hash, b" ").context(StrContext::Expected("<old-hexsha>".into())),
141                terminated(hex_hash, b" ").context(StrContext::Expected("<new-hexsha>".into())),
142                gix_actor::signature::decode.context(StrContext::Expected("<name> <<email>> <timestamp>".into())),
143            )
144                .context(StrContext::Expected(
145                    "<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\\t<message>".into(),
146                ))
147                .parse_next(&mut first)?;
148
149            // forward the buffer🤦‍♂️
150            message.parse_next(bytes)?;
151            let message = message(&mut second)?;
152            Ok(LineRef {
153                previous_oid: old,
154                new_oid: new,
155                signature,
156                message,
157            })
158        } else {
159            (
160                (
161                    terminated(hex_hash, b" ").context(StrContext::Expected("<old-hexsha>".into())),
162                    terminated(hex_hash, b" ").context(StrContext::Expected("<new-hexsha>".into())),
163                    gix_actor::signature::decode.context(StrContext::Expected("<name> <<email>> <timestamp>".into())),
164                )
165                    .context(StrContext::Expected(
166                        "<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\\t<message>".into(),
167                    )),
168                alt((
169                    preceded(
170                        b'\t',
171                        message.context(StrContext::Expected("<optional message>".into())),
172                    ),
173                    b'\n'.value(Default::default()),
174                    eof.value(Default::default()),
175                    fail.context(StrContext::Expected(
176                        "log message must be separated from signature with whitespace".into(),
177                    )),
178                )),
179            )
180                .map(|((old, new, signature), message)| LineRef {
181                    previous_oid: old,
182                    new_oid: new,
183                    signature,
184                    message,
185                })
186                .parse_next(bytes)
187        }
188    }
189
190    #[cfg(test)]
191    mod test {
192        use super::*;
193        use gix_date::{time::Sign, Time};
194
195        /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_.
196        fn hex_to_oid(hex: &str) -> gix_hash::ObjectId {
197            gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
198        }
199
200        fn with_newline(mut v: Vec<u8>) -> Vec<u8> {
201            v.push(b'\n');
202            v
203        }
204
205        mod invalid {
206            use gix_testtools::to_bstr_err;
207            use winnow::{error::TreeError, prelude::*};
208
209            use super::one;
210
211            #[test]
212            fn completely_bogus_shows_error_with_context() {
213                let err = one::<TreeError<&[u8], _>>
214                    .parse_peek(b"definitely not a log entry")
215                    .map_err(to_bstr_err)
216                    .expect_err("this should fail");
217                assert!(err.to_string().contains("<old-hexsha> <new-hexsha>"));
218            }
219
220            #[test]
221            fn missing_whitespace_between_signature_and_message() {
222                let line = "0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000message";
223                let err = one::<TreeError<&[u8], _>>
224                    .parse_peek(line.as_bytes())
225                    .map_err(to_bstr_err)
226                    .expect_err("this should fail");
227                assert!(
228                    err.to_string()
229                        .contains("log message must be separated from signature with whitespace"),
230                    "expected\n  `log message must be separated from signature with whitespace`\nin\n```\n{err}\n```"
231                );
232            }
233        }
234
235        const NULL_SHA1: &[u8] = b"0000000000000000000000000000000000000000";
236
237        #[test]
238        fn entry_with_empty_message() {
239            let line_without_nl: Vec<_> = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 name <foo@example.com> 1234567890 -0000".to_vec();
240            let line_with_nl = with_newline(line_without_nl.clone());
241            for input in &[line_without_nl, line_with_nl] {
242                assert_eq!(
243                    one::<winnow::error::InputError<_>>
244                        .parse_peek(input)
245                        .expect("successful parsing")
246                        .1,
247                    LineRef {
248                        previous_oid: NULL_SHA1.as_bstr(),
249                        new_oid: NULL_SHA1.as_bstr(),
250                        signature: gix_actor::SignatureRef {
251                            name: b"name".as_bstr(),
252                            email: b"foo@example.com".as_bstr(),
253                            time: Time {
254                                seconds: 1234567890,
255                                offset: 0,
256                                sign: Sign::Minus
257                            }
258                        },
259                        message: b"".as_bstr(),
260                    }
261                );
262            }
263        }
264
265        #[test]
266        fn entry_with_message_without_newline_and_with_newline() {
267            let line_without_nl: Vec<_> = b"a5828ae6b52137b913b978e16cd2334482eb4c1f 89b43f80a514aee58b662ad606e6352e03eaeee4 Sebastian Thiel <foo@example.com> 1618030561 +0800\tpull --ff-only: Fast-forward".to_vec();
268            let line_with_nl = with_newline(line_without_nl.clone());
269
270            for input in &[line_without_nl, line_with_nl] {
271                let (remaining, res) = one::<winnow::error::InputError<_>>
272                    .parse_peek(input)
273                    .expect("successful parsing");
274                assert!(remaining.is_empty(), "all consuming even without trailing newline");
275                let actual = LineRef {
276                    previous_oid: b"a5828ae6b52137b913b978e16cd2334482eb4c1f".as_bstr(),
277                    new_oid: b"89b43f80a514aee58b662ad606e6352e03eaeee4".as_bstr(),
278                    signature: gix_actor::SignatureRef {
279                        name: b"Sebastian Thiel".as_bstr(),
280                        email: b"foo@example.com".as_bstr(),
281                        time: Time {
282                            seconds: 1618030561,
283                            offset: 28800,
284                            sign: Sign::Plus,
285                        },
286                    },
287                    message: b"pull --ff-only: Fast-forward".as_bstr(),
288                };
289                assert_eq!(res, actual);
290                assert_eq!(
291                    actual.previous_oid(),
292                    hex_to_oid("a5828ae6b52137b913b978e16cd2334482eb4c1f")
293                );
294                assert_eq!(actual.new_oid(), hex_to_oid("89b43f80a514aee58b662ad606e6352e03eaeee4"));
295            }
296        }
297
298        #[test]
299        fn two_lines_in_a_row_with_and_without_newline() {
300            let lines = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000\t\n0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 two <foo@example.com> 1234567890 -0000\thello";
301            let (remainder, parsed) = one::<winnow::error::InputError<_>>
302                .parse_peek(lines)
303                .expect("parse single line");
304            assert_eq!(parsed.message, b"".as_bstr(), "first message is empty");
305
306            let (remainder, parsed) = one::<winnow::error::InputError<_>>
307                .parse_peek(remainder)
308                .expect("parse single line");
309            assert_eq!(
310                parsed.message,
311                b"hello".as_bstr(),
312                "second message is not and contains no newline"
313            );
314            assert!(remainder.is_empty());
315        }
316    }
317}