1use std::collections::HashMap;
5
6pub const CONVERTING_ATTRIBUTES: &[&str] =
8 &["text", "eol", "ident", "filter", "working-tree-encoding"];
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Value {
13 Unspecified,
14 Unset,
15 Set,
16 Named(String),
17}
18
19impl Value {
20 fn parse(info: &[u8]) -> Value {
21 match info {
22 b"unspecified" => Value::Unspecified,
23 b"unset" => Value::Unset,
24 b"set" => Value::Set,
25 other => Value::Named(String::from_utf8_lossy(other).into_owned()),
26 }
27 }
28
29 fn applies(&self) -> bool {
30 matches!(self, Value::Set | Value::Named(_))
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct LineEndings {
37 pub autocrlf: bool,
40 pub crlf: bool,
42}
43
44impl LineEndings {
45 pub fn from_config(autocrlf: Option<&str>, eol: Option<&str>) -> Self {
46 LineEndings {
47 autocrlf: matches!(autocrlf, Some("true") | Some("input")),
48 crlf: matches!(eol, Some("crlf")),
49 }
50 }
51}
52
53pub fn converts(values: &HashMap<String, Value>, endings: LineEndings) -> bool {
63 for name in ["ident", "filter", "working-tree-encoding"] {
64 if values.get(name).is_some_and(Value::applies) {
65 return true;
66 }
67 }
68 let text = values.get("text").unwrap_or(&Value::Unspecified);
69 if text == &Value::Unset {
70 return false;
71 }
72 if values.get("eol").is_some_and(Value::applies) {
73 return true;
74 }
75 if text.applies() {
76 return true;
77 }
78 endings.autocrlf || endings.crlf
79}
80
81pub fn parse_check_attr(output: &[u8]) -> HashMap<Vec<u8>, HashMap<String, Value>> {
83 let mut fields = output.split(|byte| *byte == 0);
84 let mut paths: HashMap<Vec<u8>, HashMap<String, Value>> = HashMap::new();
85 while let (Some(path), Some(attribute), Some(info)) =
86 (fields.next(), fields.next(), fields.next())
87 {
88 if path.is_empty() {
89 break;
90 }
91 paths.entry(path.to_vec()).or_default().insert(
92 String::from_utf8_lossy(attribute).into_owned(),
93 Value::parse(info),
94 );
95 }
96 paths
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn values(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
104 pairs
105 .iter()
106 .map(|(name, value)| ((*name).to_string(), value.clone()))
107 .collect()
108 }
109
110 const PLAIN: LineEndings = LineEndings {
111 autocrlf: false,
112 crlf: false,
113 };
114
115 #[test]
116 fn a_path_with_no_attributes_is_written_verbatim() {
117 assert!(!converts(&values(&[]), PLAIN));
118 }
119
120 #[test]
121 fn an_eol_attribute_rewrites_the_bytes() {
122 assert!(converts(
123 &values(&[("text", Value::Set), ("eol", Value::Named("crlf".into()))]),
124 PLAIN
125 ));
126 }
127
128 #[test]
129 fn text_alone_rewrites_the_bytes() {
130 assert!(converts(
131 &values(&[("text", Value::Named("auto".into()))]),
132 PLAIN
133 ));
134 }
135
136 #[test]
137 fn a_binary_path_is_never_rewritten() {
138 let binary = values(&[("text", Value::Unset)]);
139 assert!(!converts(&binary, PLAIN));
140 assert!(!converts(
141 &binary,
142 LineEndings {
143 autocrlf: true,
144 crlf: true
145 }
146 ));
147 }
148
149 #[test]
150 fn autocrlf_rewrites_paths_with_no_attributes_of_their_own() {
151 assert!(converts(
152 &values(&[]),
153 LineEndings {
154 autocrlf: true,
155 crlf: false
156 }
157 ));
158 }
159
160 #[test]
161 fn a_filter_rewrites_the_bytes() {
162 assert!(converts(
163 &values(&[("filter", Value::Named("lfs".into()))]),
164 PLAIN
165 ));
166 assert!(converts(&values(&[("ident", Value::Set)]), PLAIN));
167 assert!(converts(
168 &values(&[("working-tree-encoding", Value::Named("UTF-16".into()))]),
169 PLAIN
170 ));
171 }
172
173 #[test]
174 fn reads_the_check_attr_triples() {
175 let output = b"a.txt\0text\0set\0a.txt\0eol\0crlf\0b.bin\0text\0unspecified\0".as_slice();
176 let parsed = parse_check_attr(output);
177 assert_eq!(parsed[b"a.txt".as_slice()]["text"], Value::Set);
178 assert_eq!(
179 parsed[b"a.txt".as_slice()]["eol"],
180 Value::Named("crlf".into())
181 );
182 assert_eq!(parsed[b"b.bin".as_slice()]["text"], Value::Unspecified);
183 }
184
185 #[test]
186 fn reads_line_ending_configuration() {
187 assert!(LineEndings::from_config(Some("true"), None).autocrlf);
188 assert!(LineEndings::from_config(Some("input"), None).autocrlf);
189 assert!(!LineEndings::from_config(Some("false"), None).autocrlf);
190 assert!(LineEndings::from_config(None, Some("crlf")).crlf);
191 assert!(!LineEndings::from_config(None, Some("lf")).crlf);
192 }
193}