1use crate::check_ref_format::{check_refname_format, RefNameOptions};
8use crate::git_date::tm::date_overflows;
9use crate::objects::{ObjectId, ObjectKind};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct FsckError {
14 pub id: &'static str,
16 pub detail: String,
18}
19
20impl FsckError {
21 #[must_use]
23 pub fn new(id: &'static str, detail: impl Into<String>) -> Self {
24 Self {
25 id,
26 detail: detail.into(),
27 }
28 }
29
30 #[must_use]
32 pub fn report_line(&self) -> String {
33 format!("{}: {}", self.id, self.detail)
34 }
35}
36
37pub fn fsck_object(kind: ObjectKind, data: &[u8]) -> Result<(), FsckError> {
42 match kind {
43 ObjectKind::Blob => Ok(()),
44 ObjectKind::Commit => fsck_commit(data),
45 ObjectKind::Tag => fsck_tag(data),
46 ObjectKind::Tree => fsck_tree(data),
47 }
48}
49
50fn verify_headers(data: &[u8], nul_msg_id: &'static str) -> Result<(), FsckError> {
51 for (i, &b) in data.iter().enumerate() {
52 if b == 0 {
53 return Err(FsckError::new(
54 nul_msg_id,
55 format!("unterminated header: NUL at offset {i}"),
56 ));
57 }
58 if b == b'\n' && i + 1 < data.len() && data[i + 1] == b'\n' {
59 return Ok(());
60 }
61 }
62 if !data.is_empty() && data[data.len() - 1] == b'\n' {
63 Ok(())
64 } else {
65 Err(FsckError::new("unterminatedHeader", "unterminated header"))
66 }
67}
68
69fn is_hex_lower(b: u8) -> bool {
70 matches!(b, b'0'..=b'9' | b'a'..=b'f')
71}
72
73fn parse_oid_line(buf: &[u8], bad_sha1_id: &'static str) -> Result<usize, FsckError> {
77 let bad = || {
78 FsckError::new(
79 bad_sha1_id,
80 format!(
81 "invalid '{}' line format - bad sha1",
82 line_kind(bad_sha1_id)
83 ),
84 )
85 };
86 let hex_len = buf.iter().position(|&b| b == b'\n').ok_or_else(bad)?;
88 if !ObjectId::is_hex_len(hex_len) {
89 return Err(bad());
90 }
91 let hex = &buf[..hex_len];
92 if !hex.iter().copied().all(is_hex_lower) {
93 return Err(bad());
94 }
95 let hex_str = std::str::from_utf8(hex).map_err(|_| bad())?;
96 hex_str.parse::<ObjectId>().map_err(|_| bad())?;
97 Ok(hex_len + 1)
98}
99
100fn line_kind(bad_sha1_id: &'static str) -> &'static str {
101 match bad_sha1_id {
102 "badObjectSha1" => "object",
103 "badParentSha1" => "parent",
104 _ => "tree",
105 }
106}
107
108fn fsck_ident(
109 data: &[u8],
110 start: usize,
111 buffer_end: usize,
112 oid_line: &'static str,
113) -> Result<usize, FsckError> {
114 let mut p = start;
115 if p >= buffer_end {
116 return Err(FsckError::new(
117 "missingEmail",
118 format!("invalid {oid_line} line - missing email"),
119 ));
120 }
121
122 let line_end = data[p..buffer_end]
123 .iter()
124 .position(|&b| b == b'\n')
125 .map(|rel| p + rel)
126 .ok_or_else(|| {
127 FsckError::new(
128 "missingEmail",
129 format!("invalid {oid_line} line - missing email"),
130 )
131 })?;
132
133 let ident_end = line_end;
134
135 if data[p] == b'<' {
136 return Err(FsckError::new(
137 "missingNameBeforeEmail",
138 format!("invalid {oid_line} line - missing space before email"),
139 ));
140 }
141
142 loop {
144 if p >= ident_end || data[p] == b'\n' {
145 return Err(FsckError::new(
146 "missingEmail",
147 format!("invalid {oid_line} line - missing email"),
148 ));
149 }
150 if data[p] == b'>' {
151 return Err(FsckError::new(
152 "badName",
153 format!("invalid {oid_line} line - bad name"),
154 ));
155 }
156 if data[p] == b'<' {
157 break;
158 }
159 p += 1;
160 }
161
162 if p == start || data[p - 1] != b' ' {
163 return Err(FsckError::new(
164 "missingSpaceBeforeEmail",
165 format!("invalid {oid_line} line - missing space before email"),
166 ));
167 }
168 p += 1; loop {
172 if p >= ident_end || data[p] == b'<' || data[p] == b'\n' {
173 return Err(FsckError::new(
174 "badEmail",
175 format!("invalid {oid_line} line - bad email"),
176 ));
177 }
178 if data[p] == b'>' {
179 break;
180 }
181 p += 1;
182 }
183 p += 1; if p >= ident_end || data[p] != b' ' {
186 return Err(FsckError::new(
187 "missingSpaceBeforeDate",
188 format!("invalid {oid_line} line - missing space before date"),
189 ));
190 }
191 p += 1;
192
193 while p < ident_end && (data[p] == b' ' || data[p] == b'\t') {
194 p += 1;
195 }
196
197 if p >= ident_end || !data[p].is_ascii_digit() {
198 return Err(FsckError::new(
199 "badDate",
200 format!("invalid {oid_line} line - bad date"),
201 ));
202 }
203
204 if data[p] == b'0' && p + 1 < ident_end && data[p + 1] != b' ' {
205 return Err(FsckError::new(
206 "zeroPaddedDate",
207 format!("invalid {oid_line} line - zero-padded date"),
208 ));
209 }
210
211 let ts_start = p;
212 while p < ident_end && data[p].is_ascii_digit() {
213 p += 1;
214 }
215 let ts_len = p - ts_start;
216 if ts_len > 21 {
217 return Err(FsckError::new(
218 "badDateOverflow",
219 format!("invalid {oid_line} line - date causes integer overflow"),
220 ));
221 }
222 let ts_str = std::str::from_utf8(&data[ts_start..p])
223 .map_err(|_| FsckError::new("badDate", format!("invalid {oid_line} line - bad date")))?;
224 let raw: u128 = ts_str
225 .parse()
226 .map_err(|_| FsckError::new("badDate", format!("invalid {oid_line} line - bad date")))?;
227 if raw > u64::MAX as u128 || date_overflows(raw as u64) {
228 return Err(FsckError::new(
229 "badDateOverflow",
230 format!("invalid {oid_line} line - date causes integer overflow"),
231 ));
232 }
233
234 if p >= ident_end || data[p] != b' ' {
235 return Err(FsckError::new(
236 "badDate",
237 format!("invalid {oid_line} line - bad date"),
238 ));
239 }
240 p += 1;
241
242 if p + 5 > ident_end
244 || (data[p] != b'+' && data[p] != b'-')
245 || !data[p + 1..p + 5].iter().all(|b| b.is_ascii_digit())
246 || data[p + 5] != b'\n'
247 {
248 return Err(FsckError::new(
249 "badTimezone",
250 format!("invalid {oid_line} line - bad time zone"),
251 ));
252 }
253
254 Ok(line_end + 1)
255}
256
257fn fsck_commit(data: &[u8]) -> Result<(), FsckError> {
258 verify_headers(data, "nulInHeader")?;
259
260 let buffer_end = data.len();
261 let mut i = 0usize;
262
263 if i >= buffer_end || !data[i..].starts_with(b"tree ") {
264 return Err(FsckError::new(
265 "missingTree",
266 "invalid format - expected 'tree' line",
267 ));
268 }
269 i += 5;
270 let n = parse_oid_line(&data[i..], "badTreeSha1")?;
271 i += n;
272
273 while i < buffer_end && data[i..].starts_with(b"parent ") {
274 i += 7;
275 let n = parse_oid_line(&data[i..], "badParentSha1")?;
276 i += n;
277 }
278
279 let mut author_count = 0usize;
280 while i < buffer_end && data[i..].starts_with(b"author ") {
281 author_count += 1;
282 i += 7;
283 i = fsck_ident(data, i, buffer_end, "author/committer")?;
284 }
285
286 if author_count < 1 {
287 return Err(FsckError::new(
288 "missingAuthor",
289 "invalid format - expected 'author' line",
290 ));
291 }
292 if author_count > 1 {
293 return Err(FsckError::new(
294 "multipleAuthors",
295 "invalid format - multiple 'author' lines",
296 ));
297 }
298
299 if i >= buffer_end || !data[i..].starts_with(b"committer ") {
300 return Err(FsckError::new(
301 "missingCommitter",
302 "invalid format - expected 'committer' line",
303 ));
304 }
305 i += 10;
306 fsck_ident(data, i, buffer_end, "author/committer")?;
307
308 if data.contains(&0) {
309 return Err(FsckError::new(
310 "nulInCommit",
311 "NUL byte in the commit object body",
312 ));
313 }
314
315 Ok(())
316}
317
318fn parse_tag_headers_through_tagger(data: &[u8]) -> Result<usize, FsckError> {
320 verify_headers(data, "nulInHeader")?;
321
322 let buffer_end = data.len();
323 let mut i = 0usize;
324
325 if i >= buffer_end || !data[i..].starts_with(b"object ") {
326 return Err(FsckError::new(
327 "missingObject",
328 "invalid format - expected 'object' line",
329 ));
330 }
331 i += 7;
332 let n = parse_oid_line(&data[i..], "badObjectSha1")?;
333 i += n;
334
335 if i >= buffer_end || !data[i..].starts_with(b"type ") {
336 return Err(FsckError::new(
337 "missingTypeEntry",
338 "invalid format - expected 'type' line",
339 ));
340 }
341 i += 5;
342 let type_start = i;
343 let eol = data[type_start..buffer_end]
344 .iter()
345 .position(|&b| b == b'\n')
346 .map(|rel| type_start + rel)
347 .ok_or_else(|| {
348 FsckError::new(
349 "missingType",
350 "invalid format - unexpected end after 'type' line",
351 )
352 })?;
353
354 if ObjectKind::from_tag_type_field(&data[type_start..eol]).is_none() {
355 return Err(FsckError::new("badType", "invalid 'type' value"));
356 }
357 i = eol + 1;
358
359 if i >= buffer_end || !data[i..].starts_with(b"tag ") {
360 return Err(FsckError::new(
361 "missingTagEntry",
362 "invalid format - expected 'tag' line",
363 ));
364 }
365 i += 4;
366 let tag_start = i;
367 let eol = data[tag_start..buffer_end]
368 .iter()
369 .position(|&b| b == b'\n')
370 .map(|rel| tag_start + rel)
371 .ok_or_else(|| {
372 FsckError::new(
373 "missingTag",
374 "invalid format - unexpected end after 'type' line",
375 )
376 })?;
377
378 let tag_name = std::str::from_utf8(&data[tag_start..eol])
379 .map_err(|_| FsckError::new("badTagName", "invalid 'tag' name"))?;
380 let refname = format!("refs/tags/{tag_name}");
381 if check_refname_format(&refname, &RefNameOptions::default()).is_err() {
382 return Err(FsckError::new(
383 "badTagName",
384 format!("invalid 'tag' name: {tag_name}"),
385 ));
386 }
387 i = eol + 1;
388
389 if i >= buffer_end || !data[i..].starts_with(b"tagger ") {
390 return Err(FsckError::new(
391 "missingTaggerEntry",
392 "invalid format - expected 'tagger' line",
393 ));
394 }
395 i += 7;
396 fsck_ident(data, i, buffer_end, "author/committer")
397}
398
399fn fsck_tag(data: &[u8]) -> Result<(), FsckError> {
400 parse_tag_headers_through_tagger(data).map(|_| ())
401}
402
403pub fn parse_tag_for_mktag(
412 data: &[u8],
413 strict: bool,
414 on_warn: &mut impl FnMut(&FsckError),
415) -> Result<(ObjectId, ObjectKind, usize, bool), FsckError> {
416 verify_headers(data, "nulInHeader")?;
417
418 let buffer_end = data.len();
419 let mut i = 0usize;
420
421 if i >= buffer_end || !data[i..].starts_with(b"object ") {
422 return Err(FsckError::new(
423 "missingObject",
424 "invalid format - expected 'object' line",
425 ));
426 }
427 i += 7;
428 let n = parse_oid_line(&data[i..], "badObjectSha1")?;
429 let tagged_oid = std::str::from_utf8(&data[i..i + 40])
430 .map_err(|_| FsckError::new("badObjectSha1", "invalid 'object' line format - bad sha1"))?
431 .parse::<ObjectId>()
432 .map_err(|_| FsckError::new("badObjectSha1", "invalid 'object' line format - bad sha1"))?;
433 i += n;
434
435 if i >= buffer_end || !data[i..].starts_with(b"type ") {
436 return Err(FsckError::new(
437 "missingTypeEntry",
438 "invalid format - expected 'type' line",
439 ));
440 }
441 i += 5;
442 let type_start = i;
443 let type_eol = data[type_start..buffer_end]
444 .iter()
445 .position(|&b| b == b'\n')
446 .map(|rel| type_start + rel)
447 .ok_or_else(|| {
448 FsckError::new(
449 "missingType",
450 "invalid format - unexpected end after 'type' line",
451 )
452 })?;
453
454 let tagged_kind = ObjectKind::from_tag_type_field(&data[type_start..type_eol])
455 .ok_or_else(|| FsckError::new("badType", "invalid 'type' value"))?;
456 i = type_eol + 1;
457
458 if i >= buffer_end || !data[i..].starts_with(b"tag ") {
459 return Err(FsckError::new(
460 "missingTagEntry",
461 "invalid format - expected 'tag' line",
462 ));
463 }
464 i += 4;
465 let tag_start = i;
466 let tag_eol = data[tag_start..buffer_end]
467 .iter()
468 .position(|&b| b == b'\n')
469 .map(|rel| tag_start + rel)
470 .ok_or_else(|| {
471 FsckError::new(
472 "missingTag",
473 "invalid format - unexpected end after 'type' line",
474 )
475 })?;
476
477 let tag_name = std::str::from_utf8(&data[tag_start..tag_eol])
478 .map_err(|_| FsckError::new("badTagName", "invalid 'tag' name"))?;
479 let refname = format!("refs/tags/{tag_name}");
480 if check_refname_format(&refname, &RefNameOptions::default()).is_err() {
481 let e = FsckError::new("badTagName", format!("invalid 'tag' name: {tag_name}"));
482 if strict {
483 return Err(e);
484 }
485 on_warn(&e);
486 }
487 i = tag_eol + 1;
488
489 if i >= buffer_end {
490 let e = FsckError::new(
491 "missingTaggerEntry",
492 "invalid format - expected 'tagger' line",
493 );
494 if strict {
495 return Err(e);
496 }
497 on_warn(&e);
498 return Ok((tagged_oid, tagged_kind, i, true));
499 }
500
501 let tg_line_start = i;
502 let tg_eol = data[tg_line_start..buffer_end]
503 .iter()
504 .position(|&b| b == b'\n')
505 .map(|rel| tg_line_start + rel)
506 .ok_or_else(|| FsckError::new("unterminatedHeader", "unterminated header"))?;
507 let tg_line = &data[tg_line_start..tg_eol];
508
509 let missing_tagger = || {
510 FsckError::new(
511 "missingTaggerEntry",
512 "invalid format - expected 'tagger' line",
513 )
514 };
515
516 if tg_line == b"tagger" || !tg_line.starts_with(b"tagger ") {
517 let e = missing_tagger();
518 if strict {
519 return Err(e);
520 }
521 on_warn(&e);
522 i = tg_eol + 1;
523 } else {
524 i = tg_line_start + b"tagger ".len();
525 match fsck_ident(data, i, buffer_end, "author/committer") {
526 Ok(next) => {
527 i = next;
528 return Ok((tagged_oid, tagged_kind, i, true));
529 }
530 Err(e) => {
531 if strict {
532 return Err(e);
533 }
534 on_warn(&e);
535 let tail = &data[tg_line_start..buffer_end];
536 i = if let Some(pos) = tail.windows(2).position(|w| w == b"\n\n") {
537 tg_line_start + pos + 2
538 } else {
539 buffer_end
540 };
541 return Ok((tagged_oid, tagged_kind, i, false));
542 }
543 }
544 }
545
546 Ok((tagged_oid, tagged_kind, i, true))
547}
548
549fn skip_tag_gpgsig_headers(data: &[u8], mut i: usize) -> Result<usize, FsckError> {
550 let buffer_end = data.len();
551 if i < buffer_end
552 && (data[i..].starts_with(b"gpgsig ") || data[i..].starts_with(b"gpgsig-sha256 "))
553 {
554 let sig_start = i;
555 let sig_eol = data[sig_start..buffer_end]
556 .iter()
557 .position(|&b| b == b'\n')
558 .map(|rel| sig_start + rel)
559 .ok_or_else(|| {
560 FsckError::new(
561 "badGpgsig",
562 "invalid format - unexpected end after 'gpgsig' or 'gpgsig-sha256' line",
563 )
564 })?;
565 i = sig_eol + 1;
566 while i < buffer_end && data[i] == b' ' {
567 let cont_eol = data[i..buffer_end]
568 .iter()
569 .position(|&b| b == b'\n')
570 .map(|rel| i + rel)
571 .ok_or_else(|| {
572 FsckError::new(
573 "badHeaderContinuation",
574 "invalid format - unexpected end in 'gpgsig' or 'gpgsig-sha256' continuation line",
575 )
576 })?;
577 i = cont_eol + 1;
578 }
579 }
580 Ok(i)
581}
582
583pub fn fsck_tag_mktag_trailer_from(data: &[u8], start: usize) -> Result<(), FsckError> {
586 let buffer_end = data.len();
587 let i = skip_tag_gpgsig_headers(data, start)?;
588
589 if i < buffer_end && data[i] != b'\n' {
590 return Err(FsckError::new(
591 "extraHeaderEntry",
592 "invalid format - extra header(s) after 'tagger'",
593 ));
594 }
595
596 Ok(())
597}
598
599pub fn fsck_tag_mktag_trailer(data: &[u8]) -> Result<(), FsckError> {
602 let buffer_end = data.len();
603 let mut i = parse_tag_headers_through_tagger(data)?;
604
605 i = skip_tag_gpgsig_headers(data, i)?;
606
607 if i < buffer_end && data[i] != b'\n' {
608 return Err(FsckError::new(
609 "extraHeaderEntry",
610 "invalid format - extra header(s) after 'tagger'",
611 ));
612 }
613
614 Ok(())
615}
616
617fn fsck_tree(data: &[u8]) -> Result<(), FsckError> {
618 let mut pos = 0usize;
619 let mut names: Vec<Vec<u8>> = Vec::new();
620 while pos < data.len() {
621 let sp = data[pos..]
622 .iter()
623 .position(|&b| b == b' ')
624 .ok_or_else(|| FsckError::new("badTree", "cannot be parsed as a tree"))?;
625 let mode_bytes = &data[pos..pos + sp];
626 let mode = std::str::from_utf8(mode_bytes)
627 .ok()
628 .and_then(|s| u32::from_str_radix(s, 8).ok());
629 if !matches!(
630 mode,
631 Some(0o100644 | 0o100755 | 0o120000 | 0o040000 | 0o160000)
632 ) {
633 return Err(FsckError::new(
634 "badFilemode",
635 "malformed mode in tree entry",
636 ));
637 }
638 pos += sp + 1;
639
640 let nul = data[pos..]
641 .iter()
642 .position(|&b| b == 0)
643 .ok_or_else(|| FsckError::new("badTree", "cannot be parsed as a tree"))?;
644 if nul == 0 {
645 return Err(FsckError::new("emptyName", "empty filename in tree entry"));
646 }
647 names.push(data[pos..pos + nul].to_vec());
648 pos += nul + 1;
649
650 if pos + 20 > data.len() {
651 return Err(FsckError::new("badTree", "cannot be parsed as a tree"));
652 }
653 if ObjectId::from_bytes(&data[pos..pos + 20]).is_err() {
654 return Err(FsckError::new("badTree", "cannot be parsed as a tree"));
655 }
656 pos += 20;
657 }
658 let mut sorted = names;
659 sorted.sort();
660 if sorted.windows(2).any(|w| w[0] == w[1]) {
661 return Err(FsckError::new(
662 "duplicateEntries",
663 "duplicateEntries: contains duplicate file entries",
664 ));
665 }
666 Ok(())
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn empty_commit_is_unterminated_header() {
675 let e = fsck_object(ObjectKind::Commit, b"").unwrap_err();
676 assert_eq!(e.id, "unterminatedHeader");
677 }
678
679 #[test]
680 fn commit_missing_tree_matches_git() {
681 let e = fsck_object(ObjectKind::Commit, b"\n\n").unwrap_err();
682 assert_eq!(e.id, "missingTree");
683 }
684
685 #[test]
686 fn tree_truncated_is_bad_tree() {
687 let e = fsck_object(ObjectKind::Tree, b"100644 foo\0\x01\x01\x01\x01").unwrap_err();
688 assert_eq!(e.id, "badTree");
689 }
690}