]> git.proxmox.com Git - rustc.git/blob - vendor/gix-ref/src/store/file/log/line.rs
New upstream version 1.74.1+dfsg1
[rustc.git] / vendor / gix-ref / src / store / file / log / line.rs
1 use gix_hash::ObjectId;
2
3 use crate::{log::Line, store_impl::file::log::LineRef};
4
5 impl<'a> LineRef<'a> {
6 /// Convert this instance into its mutable counterpart
7 pub fn to_owned(&self) -> Line {
8 self.clone().into()
9 }
10 }
11
12 mod 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
51 impl<'a> LineRef<'a> {
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
63 impl<'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 ///
75 pub mod decode {
76 use gix_object::bstr::{BStr, ByteSlice};
77 use winnow::{
78 combinator::{alt, eof, fail, opt, preceded, rest, terminated},
79 error::{AddContext, ParserError, StrContext},
80 prelude::*,
81 token::take_while,
82 };
83
84 use crate::{file::log::LineRef, parse::hex_hash};
85
86 ///
87 mod error {
88 use gix_object::bstr::{BString, ByteSlice};
89
90 /// The error returned by [`from_bytes(…)`][super::Line::from_bytes()]
91 #[derive(Debug)]
92 pub struct Error {
93 pub input: BString,
94 }
95
96 impl std::fmt::Display for Error {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(
99 f,
100 "{:?} did not match '<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\\t<message>'",
101 self.input
102 )
103 }
104 }
105
106 impl std::error::Error for Error {}
107
108 impl Error {
109 pub(crate) fn new(input: &[u8]) -> Self {
110 Error {
111 input: input.as_bstr().to_owned(),
112 }
113 }
114 }
115 }
116 pub use error::Error;
117
118 impl<'a> LineRef<'a> {
119 /// Decode a line from the given bytes which are expected to start at a hex sha.
120 pub fn from_bytes(mut input: &'a [u8]) -> Result<LineRef<'a>, Error> {
121 one::<()>(&mut input).map_err(|_| Error::new(input))
122 }
123 }
124
125 fn message<'a, E: ParserError<&'a [u8]>>(i: &mut &'a [u8]) -> PResult<&'a BStr, E> {
126 if i.is_empty() {
127 rest.map(ByteSlice::as_bstr).parse_next(i)
128 } else {
129 terminated(take_while(0.., |c| c != b'\n'), opt(b'\n'))
130 .map(ByteSlice::as_bstr)
131 .parse_next(i)
132 }
133 }
134
135 fn one<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>(
136 bytes: &mut &'a [u8],
137 ) -> PResult<LineRef<'a>, E> {
138 (
139 (
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 alt((
148 preceded(
149 b'\t',
150 message.context(StrContext::Expected("<optional message>".into())),
151 ),
152 b'\n'.value(Default::default()),
153 eof.value(Default::default()),
154 fail.context(StrContext::Expected(
155 "log message must be separated from signature with whitespace".into(),
156 )),
157 )),
158 )
159 .map(|((old, new, signature), message)| LineRef {
160 previous_oid: old,
161 new_oid: new,
162 signature,
163 message,
164 })
165 .parse_next(bytes)
166 }
167
168 #[cfg(test)]
169 mod test {
170 use gix_date::{time::Sign, Time};
171 use gix_object::bstr::ByteSlice;
172
173 use super::*;
174
175 /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_.
176 fn hex_to_oid(hex: &str) -> gix_hash::ObjectId {
177 gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
178 }
179
180 fn with_newline(mut v: Vec<u8>) -> Vec<u8> {
181 v.push(b'\n');
182 v
183 }
184
185 mod invalid {
186 use gix_testtools::to_bstr_err;
187 use winnow::{error::TreeError, prelude::*};
188
189 use super::one;
190
191 #[test]
192 fn completely_bogus_shows_error_with_context() {
193 let err = one::<TreeError<&[u8], _>>
194 .parse_peek(b"definitely not a log entry")
195 .map_err(to_bstr_err)
196 .expect_err("this should fail");
197 assert!(err.to_string().contains("<old-hexsha> <new-hexsha>"));
198 }
199
200 #[test]
201 fn missing_whitespace_between_signature_and_message() {
202 let line = "0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000message";
203 let err = one::<TreeError<&[u8], _>>
204 .parse_peek(line.as_bytes())
205 .map_err(to_bstr_err)
206 .expect_err("this should fail");
207 assert!(
208 err.to_string()
209 .contains("log message must be separated from signature with whitespace"),
210 "expected\n `log message must be separated from signature with whitespace`\nin\n```\n{err}\n```"
211 );
212 }
213 }
214
215 const NULL_SHA1: &[u8] = b"0000000000000000000000000000000000000000";
216
217 #[test]
218 fn entry_with_empty_message() {
219 let line_without_nl: Vec<_> = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 name <foo@example.com> 1234567890 -0000".to_vec();
220 let line_with_nl = with_newline(line_without_nl.clone());
221 for input in &[line_without_nl, line_with_nl] {
222 assert_eq!(
223 one::<winnow::error::InputError<_>>
224 .parse_peek(input)
225 .expect("successful parsing")
226 .1,
227 LineRef {
228 previous_oid: NULL_SHA1.as_bstr(),
229 new_oid: NULL_SHA1.as_bstr(),
230 signature: gix_actor::SignatureRef {
231 name: b"name".as_bstr(),
232 email: b"foo@example.com".as_bstr(),
233 time: Time {
234 seconds: 1234567890,
235 offset: 0,
236 sign: Sign::Minus
237 }
238 },
239 message: b"".as_bstr(),
240 }
241 );
242 }
243 }
244
245 #[test]
246 fn entry_with_message_without_newline_and_with_newline() {
247 let line_without_nl: Vec<_> = b"a5828ae6b52137b913b978e16cd2334482eb4c1f 89b43f80a514aee58b662ad606e6352e03eaeee4 Sebastian Thiel <foo@example.com> 1618030561 +0800\tpull --ff-only: Fast-forward".to_vec();
248 let line_with_nl = with_newline(line_without_nl.clone());
249
250 for input in &[line_without_nl, line_with_nl] {
251 let (remaining, res) = one::<winnow::error::InputError<_>>
252 .parse_peek(input)
253 .expect("successful parsing");
254 assert!(remaining.is_empty(), "all consuming even without trailing newline");
255 let actual = LineRef {
256 previous_oid: b"a5828ae6b52137b913b978e16cd2334482eb4c1f".as_bstr(),
257 new_oid: b"89b43f80a514aee58b662ad606e6352e03eaeee4".as_bstr(),
258 signature: gix_actor::SignatureRef {
259 name: b"Sebastian Thiel".as_bstr(),
260 email: b"foo@example.com".as_bstr(),
261 time: Time {
262 seconds: 1618030561,
263 offset: 28800,
264 sign: Sign::Plus,
265 },
266 },
267 message: b"pull --ff-only: Fast-forward".as_bstr(),
268 };
269 assert_eq!(res, actual);
270 assert_eq!(
271 actual.previous_oid(),
272 hex_to_oid("a5828ae6b52137b913b978e16cd2334482eb4c1f")
273 );
274 assert_eq!(actual.new_oid(), hex_to_oid("89b43f80a514aee58b662ad606e6352e03eaeee4"));
275 }
276 }
277
278 #[test]
279 fn two_lines_in_a_row_with_and_without_newline() {
280 let lines = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000\t\n0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 two <foo@example.com> 1234567890 -0000\thello";
281 let (remainder, parsed) = one::<winnow::error::InputError<_>>
282 .parse_peek(lines)
283 .expect("parse single line");
284 assert_eq!(parsed.message, b"".as_bstr(), "first message is empty");
285
286 let (remainder, parsed) = one::<winnow::error::InputError<_>>
287 .parse_peek(remainder)
288 .expect("parse single line");
289 assert_eq!(
290 parsed.message,
291 b"hello".as_bstr(),
292 "second message is not and contains no newline"
293 );
294 assert!(remainder.is_empty());
295 }
296 }
297 }