Skip to content

Commit 3409a66

Browse files
committed
very basic parsing of attributes (#301)
1 parent 93bf118 commit 3409a66

File tree

2 files changed

+74
-19
lines changed

2 files changed

+74
-19
lines changed

git-attributes/src/parse/attribute.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,21 @@ pub struct Lines<'a> {
2828
}
2929

3030
pub struct Iter<'a> {
31-
attrs: bstr::Split<'a>,
31+
attrs: bstr::Fields<'a>,
3232
}
3333

3434
impl<'a> Iter<'a> {
3535
pub fn new(attrs: &'a BStr) -> Self {
36-
Iter {
37-
attrs: attrs.split_str(b" "),
38-
}
36+
Iter { attrs: attrs.fields() }
3937
}
4038
}
4139

4240
impl<'a> Iterator for Iter<'a> {
4341
type Item = Result<(&'a BStr, crate::State<'a>), Error>;
4442

4543
fn next(&mut self) -> Option<Self::Item> {
46-
let _attr = self.attrs.next().filter(|a| !a.is_empty())?;
47-
todo!("parse attribute")
44+
let attr = self.attrs.next().filter(|a| !a.is_empty())?;
45+
Some(Ok((attr.as_bstr(), crate::State::Set)))
4846
}
4947
}
5048

@@ -64,6 +62,7 @@ impl<'a> Iterator for Lines<'a> {
6462
fn next(&mut self) -> Option<Self::Item> {
6563
for line in self.lines.by_ref() {
6664
self.line_no += 1;
65+
let line = skip_blanks(line.into());
6766
if line.first() == Some(&b'#') {
6867
continue;
6968
}
@@ -86,26 +85,29 @@ impl<'a> Iterator for Lines<'a> {
8685
}
8786
}
8887

89-
fn parse_line(line: &[u8]) -> Option<Result<(BString, crate::ignore::pattern::Mode, Iter<'_>), Error>> {
88+
fn parse_line(line: &BStr) -> Option<Result<(BString, crate::ignore::pattern::Mode, Iter<'_>), Error>> {
9089
if line.is_empty() {
9190
return None;
9291
}
9392

94-
let line = line.as_bstr();
9593
let (line, attrs): (Cow<'_, _>, _) = if line.starts_with(b"\"") {
9694
let (unquoted, consumed) = match git_quote::ansi_c::undo(line) {
9795
Ok(res) => res,
9896
Err(err) => return Some(Err(err.into())),
9997
};
10098
(unquoted, &line[consumed..])
10199
} else {
102-
let mut tokens = line.splitn(2, |n| *n == b' ');
103-
(
104-
tokens.next().expect("at least a line").as_bstr().into(),
105-
tokens.next().unwrap_or_default().as_bstr(),
106-
)
100+
line.find_byteset(BLANKS)
101+
.map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr()))
102+
.unwrap_or((line.into(), [].as_bstr()))
107103
};
108104

109105
let (pattern, flags) = super::ignore::parse_line(line.as_ref())?;
110106
Ok((pattern, flags, Iter::new(attrs))).into()
111107
}
108+
109+
const BLANKS: &[u8] = b" \t\r";
110+
111+
fn skip_blanks(line: &BStr) -> &BStr {
112+
line.find_not_byteset(BLANKS).map(|pos| &line[pos..]).unwrap_or(line)
113+
}

git-attributes/tests/parse/attribute.rs

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use bstr::{BStr, BString};
1+
use bstr::{BStr, BString, ByteSlice};
22
use git_attributes::ignore::pattern::Mode;
3-
use git_attributes::{ignore, parse};
3+
use git_attributes::{ignore, parse, State};
44
use git_testtools::fixture_path;
55

66
#[test]
@@ -43,6 +43,16 @@ fn line_endings_can_be_windows_or_unix() {
4343
fn comment_lines_are_ignored() {
4444
assert!(git_attributes::parse(b"# hello world").next().is_none());
4545
assert!(git_attributes::parse(b"# \"hello world\"").next().is_none());
46+
assert!(
47+
git_attributes::parse(b" \t\r# \"hello world\"").next().is_none(),
48+
"also behind leading whitespace"
49+
);
50+
}
51+
52+
#[test]
53+
fn leading_whitespace_is_ignored() {
54+
assert_eq!(line(" \r\tp"), (r"p".into(), Mode::NO_SUB_DIR, vec![], 1));
55+
assert_eq!(line(" \r\t\"p\""), (r"p".into(), Mode::NO_SUB_DIR, vec![], 1));
4656
}
4757

4858
#[test]
@@ -56,7 +66,7 @@ fn comment_can_be_escaped_like_gitignore_or_quoted() {
5666
}
5767

5868
#[test]
59-
fn esclamation_marks_must_be_escaped_or_error_unlike_gitignore() {
69+
fn exclamation_marks_must_be_escaped_or_error_unlike_gitignore() {
6070
assert_eq!(line(r"\!hello"), (r"!hello".into(), Mode::NO_SUB_DIR, vec![], 1));
6171
assert!(matches!(
6272
try_line(r"!hello"),
@@ -72,7 +82,7 @@ fn esclamation_marks_must_be_escaped_or_error_unlike_gitignore() {
7282
assert_eq!(
7383
line(r#""\\!hello""#),
7484
(r"!hello".into(), Mode::NO_SUB_DIR, vec![], 1),
75-
"…and must be escaped"
85+
"…and must be double-escaped, once to get through quote, then to get through parse ignore line"
7686
);
7787
}
7888

@@ -86,9 +96,48 @@ fn invalid_escapes_in_quotes_are_an_error() {
8696

8797
#[test]
8898
#[ignore]
99+
fn custom_macros_can_be_defined() {
100+
todo!("name validation, leave rejecting them based on location to the caller")
101+
}
102+
103+
#[test]
89104
fn attributes_are_parsed_behind_various_whitespace_characters() {
90-
// see https://github.com/git/git/blob/master/attr.c#L280:L280
91-
todo!()
105+
assert_eq!(
106+
line(r#"p a b"#),
107+
("p".into(), Mode::NO_SUB_DIR, vec![set("a"), set("b")], 1),
108+
"behind space"
109+
);
110+
assert_eq!(
111+
line(r#""p" a b"#),
112+
("p".into(), Mode::NO_SUB_DIR, vec![set("a"), set("b")], 1),
113+
"behind space"
114+
);
115+
assert_eq!(
116+
line("p\ta\tb"),
117+
("p".into(), Mode::NO_SUB_DIR, vec![set("a"), set("b")], 1),
118+
"behind tab"
119+
);
120+
assert_eq!(
121+
line("\"p\"\ta\tb"),
122+
("p".into(), Mode::NO_SUB_DIR, vec![set("a"), set("b")], 1),
123+
"behind tab"
124+
);
125+
assert_eq!(
126+
line("p \t a \t b"),
127+
("p".into(), Mode::NO_SUB_DIR, vec![set("a"), set("b")], 1),
128+
"behind a mix of space and tab"
129+
);
130+
assert_eq!(
131+
line("\"p\" \t a \t b"),
132+
("p".into(), Mode::NO_SUB_DIR, vec![set("a"), set("b")], 1),
133+
"behind a mix of space and tab"
134+
);
135+
}
136+
137+
#[test]
138+
fn trailing_whitespace_in_attributes_is_ignored() {
139+
assert_eq!(line("p a \r\t"), ("p".into(), Mode::NO_SUB_DIR, vec![set("a")], 1),);
140+
assert_eq!(line("\"p\" a \r\t"), ("p".into(), Mode::NO_SUB_DIR, vec![set("a")], 1),);
92141
}
93142

94143
type ExpandedAttribute<'a> = (
@@ -98,6 +147,10 @@ type ExpandedAttribute<'a> = (
98147
usize,
99148
);
100149

150+
fn set(attr: &str) -> (&BStr, State) {
151+
(attr.as_bytes().as_bstr(), State::Set)
152+
}
153+
101154
fn try_line(input: &str) -> Result<ExpandedAttribute, parse::attribute::Error> {
102155
let mut lines = git_attributes::parse(input.as_bytes());
103156
let res = expand(lines.next().unwrap())?;

0 commit comments

Comments
 (0)