gix_ref/store/file/log/
line.rs1use gix_hash::ObjectId;
2
3use crate::{log::Line, store_impl::file::log::LineRef};
4
5impl LineRef<'_> {
6 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 #[derive(Debug, thiserror::Error)]
21 enum Error {
22 #[error(r"Messages must not contain newlines (\n)")]
23 IllegalCharacter,
24 }
25
26 impl From<Error> for io::Error {
27 fn from(err: Error) -> Self {
28 io::Error::other(err)
29 }
30 }
31
32 impl Line {
34 pub fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> {
36 write!(out, "{} {} ", self.previous_oid, self.new_oid)?;
37 self.signature.write_to(out)?;
38 writeln!(out, "\t{}", check_newlines(self.message.as_ref())?)
39 }
40 }
41
42 fn check_newlines(input: &BStr) -> Result<&BStr, Error> {
43 if input.find_byte(b'\n').is_some() {
44 return Err(Error::IllegalCharacter);
45 }
46 Ok(input)
47 }
48}
49
50impl LineRef<'_> {
51 pub fn previous_oid(&self) -> ObjectId {
54 ObjectId::from_hex(self.previous_oid).expect("parse validation")
55 }
56 pub fn new_oid(&self) -> ObjectId {
58 ObjectId::from_hex(self.new_oid).expect("parse validation")
59 }
60}
61
62impl<'a> From<LineRef<'a>> for Line {
63 fn from(v: LineRef<'a>) -> Self {
64 Line {
65 previous_oid: v.previous_oid(),
66 new_oid: v.new_oid(),
67 signature: v.signature.into(),
68 message: v.message.into(),
69 }
70 }
71}
72
73pub mod decode {
75 use gix_object::bstr::{BStr, ByteSlice};
76
77 use crate::{file::log::LineRef, parse::hex_hash_any};
78
79 mod error {
81 use gix_object::bstr::{BString, ByteSlice};
82
83 #[derive(Debug)]
85 pub struct Error {
86 pub input: BString,
87 }
88
89 impl std::fmt::Display for Error {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(
92 f,
93 r"{:?} did not match '<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\t<message>'",
94 self.input
95 )
96 }
97 }
98
99 impl std::error::Error for Error {}
100
101 impl Error {
102 pub(crate) fn new(input: &[u8]) -> Self {
103 Error {
104 input: input.as_bstr().to_owned(),
105 }
106 }
107 }
108 }
109 pub use error::Error;
110
111 impl<'a> LineRef<'a> {
112 pub fn from_bytes(input: &'a [u8]) -> Result<LineRef<'a>, Error> {
119 decode(input).map_err(|_| Error::new(first_line(input)))
120 }
121 }
122
123 fn first_line(input: &[u8]) -> &[u8] {
127 let line_end = input.iter().position(|b| *b == b'\n').unwrap_or(input.len());
128 &input[..line_end]
129 }
130
131 fn decode(bytes: &[u8]) -> Result<LineRef<'_>, ()> {
139 let line = first_line(bytes);
140 let (mut head, message) = match line.find_byte(b'\t') {
141 Some(tab) => (&line[..tab], line[tab + 1..].as_bstr()),
142 None => (line, BStr::new(b"")),
143 };
144
145 let old = hex_hash_any(&mut head)?;
146 head = head.strip_prefix(b" ").ok_or(())?;
147 let new = hex_hash_any(&mut head)?;
148 head = head.strip_prefix(b" ").ok_or(())?;
149 let signature = gix_actor::signature::decode(&mut head).map_err(|_| ())?;
150 if !head.is_empty() {
151 return Err(());
152 }
153 Ok(LineRef {
154 previous_oid: old,
155 new_oid: new,
156 signature,
157 message,
158 })
159 }
160
161 #[cfg(test)]
162 mod test_decode {
163 use super::*;
164
165 fn hex_to_oid(hex: &str) -> gix_hash::ObjectId {
167 gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
168 }
169
170 fn with_newline(mut v: Vec<u8>) -> Vec<u8> {
171 v.push(b'\n');
172 v
173 }
174
175 mod invalid {
176 use super::decode;
177
178 #[test]
179 fn completely_bogus_shows_error_with_context() {
180 let input = b"definitely not a log entry".as_slice();
181 decode(input).expect_err("this should fail");
182 }
183
184 #[test]
185 fn missing_whitespace_between_signature_and_message() {
186 let line = "0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000message";
187 decode(line.as_bytes()).expect_err("this should fail");
188 }
189 }
190
191 const NULL_SHA1: &[u8] = b"0000000000000000000000000000000000000000";
192
193 #[test]
194 fn entry_with_empty_message() {
195 let line_without_nl: Vec<_> = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 name <foo@example.com> 1234567890 -0000".to_vec();
196 let line_with_nl = with_newline(line_without_nl.clone());
197 for input in &[line_without_nl, line_with_nl] {
198 assert_eq!(
199 decode(input.as_slice()).expect("successful parsing"),
200 LineRef {
201 previous_oid: NULL_SHA1.as_bstr(),
202 new_oid: NULL_SHA1.as_bstr(),
203 signature: gix_actor::SignatureRef {
204 name: b"name".as_bstr(),
205 email: b"foo@example.com".as_bstr(),
206 time: "1234567890 -0000"
207 },
208 message: b"".as_bstr(),
209 }
210 );
211 }
212 }
213
214 #[test]
215 fn entry_with_message_without_newline_and_with_newline() {
216 let line_without_nl: Vec<_> = b"a5828ae6b52137b913b978e16cd2334482eb4c1f 89b43f80a514aee58b662ad606e6352e03eaeee4 Sebastian Thiel <foo@example.com> 1618030561 +0800\tpull --ff-only: Fast-forward".to_vec();
217 let line_with_nl = with_newline(line_without_nl.clone());
218
219 for input in &[line_without_nl, line_with_nl] {
220 let res = decode(input.as_slice()).expect("successful parsing");
221 let actual = LineRef {
222 previous_oid: b"a5828ae6b52137b913b978e16cd2334482eb4c1f".as_bstr(),
223 new_oid: b"89b43f80a514aee58b662ad606e6352e03eaeee4".as_bstr(),
224 signature: gix_actor::SignatureRef {
225 name: b"Sebastian Thiel".as_bstr(),
226 email: b"foo@example.com".as_bstr(),
227 time: "1618030561 +0800",
228 },
229 message: b"pull --ff-only: Fast-forward".as_bstr(),
230 };
231 assert_eq!(res, actual);
232 assert_eq!(
233 actual.previous_oid(),
234 hex_to_oid("a5828ae6b52137b913b978e16cd2334482eb4c1f")
235 );
236 assert_eq!(actual.new_oid(), hex_to_oid("89b43f80a514aee58b662ad606e6352e03eaeee4"));
237 }
238 }
239
240 #[test]
241 fn two_lines_in_a_row_with_and_without_newline() {
242 let lines = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000\t\n0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 two <foo@example.com> 1234567890 -0000\thello";
243 let parsed = decode(lines.as_slice()).expect("parse single line");
244 assert_eq!(parsed.message, b"".as_bstr(), "first message is empty");
245 }
246 }
247}