use std::collections::HashMap;
pub const CONVERTING_ATTRIBUTES: &[&str] =
&["text", "eol", "ident", "filter", "working-tree-encoding"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
Unspecified,
Unset,
Set,
Named(String),
}
impl Value {
fn parse(info: &[u8]) -> Value {
match info {
b"unspecified" => Value::Unspecified,
b"unset" => Value::Unset,
b"set" => Value::Set,
other => Value::Named(String::from_utf8_lossy(other).into_owned()),
}
}
fn applies(&self) -> bool {
matches!(self, Value::Set | Value::Named(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineEndings {
pub autocrlf: bool,
pub crlf: bool,
}
impl LineEndings {
pub fn from_config(autocrlf: Option<&str>, eol: Option<&str>) -> Self {
LineEndings {
autocrlf: matches!(autocrlf, Some("true") | Some("input")),
crlf: matches!(eol, Some("crlf")),
}
}
}
pub fn converts(values: &HashMap<String, Value>, endings: LineEndings) -> bool {
for name in ["ident", "filter", "working-tree-encoding"] {
if values.get(name).is_some_and(Value::applies) {
return true;
}
}
let text = values.get("text").unwrap_or(&Value::Unspecified);
if text == &Value::Unset {
return false;
}
if values.get("eol").is_some_and(Value::applies) {
return true;
}
if text.applies() {
return true;
}
endings.autocrlf || endings.crlf
}
pub fn parse_check_attr(output: &[u8]) -> HashMap<Vec<u8>, HashMap<String, Value>> {
let mut fields = output.split(|byte| *byte == 0);
let mut paths: HashMap<Vec<u8>, HashMap<String, Value>> = HashMap::new();
while let (Some(path), Some(attribute), Some(info)) =
(fields.next(), fields.next(), fields.next())
{
if path.is_empty() {
break;
}
paths.entry(path.to_vec()).or_default().insert(
String::from_utf8_lossy(attribute).into_owned(),
Value::parse(info),
);
}
paths
}
#[cfg(test)]
mod tests {
use super::*;
fn values(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
pairs
.iter()
.map(|(name, value)| ((*name).to_string(), value.clone()))
.collect()
}
const PLAIN: LineEndings = LineEndings {
autocrlf: false,
crlf: false,
};
#[test]
fn a_path_with_no_attributes_is_written_verbatim() {
assert!(!converts(&values(&[]), PLAIN));
}
#[test]
fn an_eol_attribute_rewrites_the_bytes() {
assert!(converts(
&values(&[("text", Value::Set), ("eol", Value::Named("crlf".into()))]),
PLAIN
));
}
#[test]
fn text_alone_rewrites_the_bytes() {
assert!(converts(
&values(&[("text", Value::Named("auto".into()))]),
PLAIN
));
}
#[test]
fn a_binary_path_is_never_rewritten() {
let binary = values(&[("text", Value::Unset)]);
assert!(!converts(&binary, PLAIN));
assert!(!converts(
&binary,
LineEndings {
autocrlf: true,
crlf: true
}
));
}
#[test]
fn autocrlf_rewrites_paths_with_no_attributes_of_their_own() {
assert!(converts(
&values(&[]),
LineEndings {
autocrlf: true,
crlf: false
}
));
}
#[test]
fn a_filter_rewrites_the_bytes() {
assert!(converts(
&values(&[("filter", Value::Named("lfs".into()))]),
PLAIN
));
assert!(converts(&values(&[("ident", Value::Set)]), PLAIN));
assert!(converts(
&values(&[("working-tree-encoding", Value::Named("UTF-16".into()))]),
PLAIN
));
}
#[test]
fn reads_the_check_attr_triples() {
let output = b"a.txt\0text\0set\0a.txt\0eol\0crlf\0b.bin\0text\0unspecified\0".as_slice();
let parsed = parse_check_attr(output);
assert_eq!(parsed[b"a.txt".as_slice()]["text"], Value::Set);
assert_eq!(
parsed[b"a.txt".as_slice()]["eol"],
Value::Named("crlf".into())
);
assert_eq!(parsed[b"b.bin".as_slice()]["text"], Value::Unspecified);
}
#[test]
fn reads_line_ending_configuration() {
assert!(LineEndings::from_config(Some("true"), None).autocrlf);
assert!(LineEndings::from_config(Some("input"), None).autocrlf);
assert!(!LineEndings::from_config(Some("false"), None).autocrlf);
assert!(LineEndings::from_config(None, Some("crlf")).crlf);
assert!(!LineEndings::from_config(None, Some("lf")).crlf);
}
}