Skip to content

Commit 65c416b

Browse files
committed
attribute name validation (#301)
1 parent 96b0fca commit 65c416b

File tree

2 files changed

+74
-25
lines changed

2 files changed

+74
-25
lines changed

git-attributes/src/parse/attribute.rs

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ mod error {
1111
PatternNegation { line_number: usize, line: BString } {
1212
display("Line {} has a negative pattern, for literal characters use \\!: {}", line_number, line)
1313
}
14+
AttributeName { line_number: usize, attribute: BString } {
15+
display("Line {} has non-ascii characters or starts with '-': {}", line_number, attribute)
16+
}
1417
Unquote(err: git_quote::ansi_c::undo::Error) {
1518
display("Could not unquote attributes line")
1619
from()
@@ -29,42 +32,63 @@ pub struct Lines<'a> {
2932

3033
pub struct Iter<'a> {
3134
attrs: bstr::Fields<'a>,
35+
line_no: usize,
3236
}
3337

3438
impl<'a> Iter<'a> {
35-
pub fn new(attrs: &'a BStr) -> Self {
36-
Iter { attrs: attrs.fields() }
39+
pub fn new(attrs: &'a BStr, line_no: usize) -> Self {
40+
Iter {
41+
attrs: attrs.fields(),
42+
line_no,
43+
}
44+
}
45+
46+
fn parse_attr(&self, attr: &'a [u8]) -> Result<(&'a BStr, crate::State<'a>), Error> {
47+
let mut tokens = attr.splitn(2, |b| *b == b'=');
48+
let attr = tokens.next().expect("attr itself").as_bstr();
49+
let possibly_value = tokens.next();
50+
let (attr, state) = if attr.first() == Some(&b'-') {
51+
(&attr[1..], crate::State::Unset)
52+
} else if attr.first() == Some(&b'!') {
53+
(&attr[1..], crate::State::Unspecified)
54+
} else {
55+
(
56+
attr,
57+
possibly_value
58+
.map(|v| crate::State::Value(v.as_bstr()))
59+
.unwrap_or(crate::State::Set),
60+
)
61+
};
62+
if !attr_valid(attr) {
63+
return Err(Error::AttributeName {
64+
line_number: self.line_no,
65+
attribute: attr.into(),
66+
});
67+
}
68+
Ok((attr, state))
3769
}
3870
}
3971

72+
fn attr_valid(attr: &BStr) -> bool {
73+
if attr.first() == Some(&b'-') {
74+
return false;
75+
}
76+
77+
attr.bytes().all(|b| match b {
78+
b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' => true,
79+
_ => false,
80+
})
81+
}
82+
4083
impl<'a> Iterator for Iter<'a> {
4184
type Item = Result<(&'a BStr, crate::State<'a>), Error>;
4285

4386
fn next(&mut self) -> Option<Self::Item> {
4487
let attr = self.attrs.next().filter(|a| !a.is_empty())?;
45-
parse_attr(attr).into()
88+
self.parse_attr(attr).into()
4689
}
4790
}
4891

49-
fn parse_attr(attr: &[u8]) -> Result<(&BStr, crate::State<'_>), Error> {
50-
let mut tokens = attr.splitn(2, |b| *b == b'=');
51-
let attr = tokens.next().expect("attr itself").as_bstr();
52-
let possibly_value = tokens.next();
53-
let (attr, state) = if attr.first() == Some(&b'-') {
54-
(&attr[1..], crate::State::Unset)
55-
} else if attr.first() == Some(&b'!') {
56-
(&attr[1..], crate::State::Unspecified)
57-
} else {
58-
(
59-
attr,
60-
possibly_value
61-
.map(|v| crate::State::Value(v.as_bstr()))
62-
.unwrap_or(crate::State::Set),
63-
)
64-
};
65-
Ok((attr, state))
66-
}
67-
6892
impl<'a> Lines<'a> {
6993
pub fn new(buf: &'a [u8]) -> Self {
7094
let bom = unicode_bom::Bom::from(buf);
@@ -85,7 +109,7 @@ impl<'a> Iterator for Lines<'a> {
85109
if line.first() == Some(&b'#') {
86110
continue;
87111
}
88-
match parse_line(line) {
112+
match parse_line(line, self.line_no) {
89113
None => continue,
90114
Some(Ok((pattern, flags, attrs))) => {
91115
return Some(if flags.contains(ignore::pattern::Mode::NEGATIVE) {
@@ -104,7 +128,10 @@ impl<'a> Iterator for Lines<'a> {
104128
}
105129
}
106130

107-
fn parse_line(line: &BStr) -> Option<Result<(BString, crate::ignore::pattern::Mode, Iter<'_>), Error>> {
131+
fn parse_line(
132+
line: &BStr,
133+
line_number: usize,
134+
) -> Option<Result<(BString, crate::ignore::pattern::Mode, Iter<'_>), Error>> {
108135
if line.is_empty() {
109136
return None;
110137
}
@@ -122,7 +149,7 @@ fn parse_line(line: &BStr) -> Option<Result<(BString, crate::ignore::pattern::Mo
122149
};
123150

124151
let (pattern, flags) = super::ignore::parse_line(line.as_ref())?;
125-
Ok((pattern, flags, Iter::new(attrs))).into()
152+
Ok((pattern, flags, Iter::new(attrs, line_number))).into()
126153
}
127154

128155
const BLANKS: &[u8] = b" \t\r";

git-attributes/tests/parse/attribute.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ fn custom_macros_can_be_defined() {
100100
todo!("name validation, leave rejecting them based on location to the caller")
101101
}
102102

103+
#[test]
104+
fn attribute_names_must_not_begin_with_dash_and_must_be_ascii_only() {
105+
assert!(matches!(
106+
try_line(r"p !-a"),
107+
Err(parse::attribute::Error::AttributeName { line_number: 1, .. })
108+
));
109+
assert!(
110+
matches!(
111+
try_line(r#"p !!a"#),
112+
Err(parse::attribute::Error::AttributeName { line_number: 1, .. })
113+
),
114+
"exclamation marks aren't allowed either"
115+
);
116+
assert!(
117+
matches!(
118+
try_line(r#"p 你好"#),
119+
Err(parse::attribute::Error::AttributeName { line_number: 1, .. })
120+
),
121+
"nor is utf-8 encoded characters - gitoxide could consider to relax this when established"
122+
);
123+
}
124+
103125
#[test]
104126
fn attributes_are_parsed_behind_various_whitespace_characters() {
105127
assert_eq!(

0 commit comments

Comments
 (0)