1use std::collections::{BTreeMap, BTreeSet};
5
6use gix_hash::ObjectId;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Blob {
11 pub mode: u32,
13 pub oid: ObjectId,
14}
15
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
18pub struct Listing {
19 pub blobs: BTreeMap<Vec<u8>, Blob>,
20 pub trees: BTreeMap<Vec<u8>, ObjectId>,
21 pub gitlinks: BTreeSet<Vec<u8>>,
23}
24
25pub const ATTRIBUTES_FILE: &[u8] = b".gitattributes";
27
28impl Listing {
29 pub fn attribute_files(&self) -> BTreeMap<Vec<u8>, ObjectId> {
31 self.blobs
32 .iter()
33 .filter(|(path, _)| is_attributes_file(path))
34 .map(|(path, blob)| (path.clone(), blob.oid))
35 .collect()
36 }
37}
38
39pub fn is_attributes_file(path: &[u8]) -> bool {
41 path == ATTRIBUTES_FILE
42 || (path.len() > ATTRIBUTES_FILE.len()
43 && path.ends_with(ATTRIBUTES_FILE)
44 && path[path.len() - ATTRIBUTES_FILE.len() - 1] == b'/')
45}
46
47pub fn directory_of(path: &[u8]) -> &[u8] {
49 match path.iter().rposition(|byte| *byte == b'/') {
50 Some(position) => &path[..position + 1],
51 None => &[],
52 }
53}
54
55pub fn parse(output: &[u8]) -> Option<Listing> {
57 let mut listing = Listing::default();
58 for record in output.split(|byte| *byte == 0) {
59 if record.is_empty() {
60 continue;
61 }
62 let tab = record.iter().position(|byte| *byte == b'\t')?;
63 let (meta, path) = record.split_at(tab);
64 let path = &path[1..];
65 let mut fields = meta.split(|byte| *byte == b' ');
66 let mode = std::str::from_utf8(fields.next()?).ok()?;
67 let mode = u32::from_str_radix(mode, 8).ok()?;
68 let kind = fields.next()?;
69 let oid = std::str::from_utf8(fields.next()?).ok()?;
70 let oid = ObjectId::from_hex(oid.as_bytes()).ok()?;
71 match kind {
72 b"blob" => {
73 listing.blobs.insert(path.to_vec(), Blob { mode, oid });
74 }
75 b"tree" => {
76 listing.trees.insert(path.to_vec(), oid);
77 }
78 b"commit" => {
79 listing.gitlinks.insert(path.to_vec());
80 }
81 _ => return None,
82 }
83 }
84 Some(listing)
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 fn record(mode: &str, kind: &str, oid: &str, path: &str) -> Vec<u8> {
92 format!("{mode} {kind} {oid}\t{path}\0").into_bytes()
93 }
94
95 const A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
96 const B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
97
98 #[test]
99 fn reads_blobs_trees_and_gitlinks() {
100 let mut output = record("040000", "tree", A, "src");
101 output.extend(record("100755", "blob", B, "src/run.sh"));
102 output.extend(record("160000", "commit", A, "vendor/lib"));
103 let listing = parse(&output).expect("parses");
104 assert_eq!(listing.trees.len(), 1);
105 assert_eq!(
106 listing.blobs[b"src/run.sh".as_slice()],
107 Blob {
108 mode: 0o100755,
109 oid: ObjectId::from_hex(B.as_bytes()).unwrap()
110 }
111 );
112 assert!(listing.gitlinks.contains(b"vendor/lib".as_slice()));
113 }
114
115 #[test]
116 fn keeps_paths_that_are_not_utf8() {
117 let mut output = b"100644 blob ".to_vec();
118 output.extend(A.as_bytes());
119 output.extend(b"\ta/\xff\0");
120 let listing = parse(&output).expect("parses");
121 assert!(listing.blobs.contains_key(b"a/\xff".as_slice()));
122 }
123
124 #[test]
125 fn recognises_attributes_files_at_any_depth() {
126 assert!(is_attributes_file(b".gitattributes"));
127 assert!(is_attributes_file(b"src/.gitattributes"));
128 assert!(!is_attributes_file(b"src/not.gitattributes"));
129 assert!(!is_attributes_file(b"gitattributes"));
130 }
131
132 #[test]
133 fn splits_the_directory_from_the_path() {
134 assert_eq!(directory_of(b"a/b/c.txt"), b"a/b/");
135 assert_eq!(directory_of(b"c.txt"), b"");
136 }
137}