1pub const MAX_LINE_LEN: usize = 65536;
11
12const REJECTED_UNQUOTED_METACHARACTERS: &[char] = &['|', ';', '&', '<', '>', '`'];
13
14#[derive(Clone, Debug, Eq, PartialEq)]
16pub enum Token {
17 Word(String),
19 Flag { name: String, value: Option<String> },
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum LexError {
26 UnterminatedQuote,
28 InvalidEscape(char),
30 NulByte,
32 TooLong { max: usize, actual: usize },
34 UnsupportedMetacharacter(char),
36}
37
38pub fn tokenize(line: &str) -> Result<Vec<Token>, LexError> {
40 if line.len() > MAX_LINE_LEN {
41 return Err(LexError::TooLong {
42 max: MAX_LINE_LEN,
43 actual: line.len(),
44 });
45 }
46 if line.contains('\0') {
47 return Err(LexError::NulByte);
48 }
49 if line.contains("$(") {
52 return Err(LexError::UnsupportedMetacharacter('$'));
53 }
54
55 let mut tokens = Vec::new();
56 let mut chars = line.chars().peekable();
57
58 while let Some(&ch) = chars.peek() {
59 if ch.is_whitespace() {
60 chars.next();
61 continue;
62 }
63 if REJECTED_UNQUOTED_METACHARACTERS.contains(&ch) {
64 return Err(LexError::UnsupportedMetacharacter(ch));
65 }
66
67 let word = read_word(&mut chars)?;
68 tokens.push(classify(word));
69 }
70
71 Ok(tokens)
72}
73
74fn classify(word: String) -> Token {
75 match word.strip_prefix("--") {
76 Some(rest) => match rest.split_once('=') {
77 Some((name, value)) => Token::Flag {
78 name: name.to_string(),
79 value: Some(value.to_string()),
80 },
81 None => Token::Flag {
82 name: rest.to_string(),
83 value: None,
84 },
85 },
86 None => Token::Word(word),
87 }
88}
89
90fn read_word(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Result<String, LexError> {
91 let mut word = String::new();
92 let mut started = false;
100
101 while let Some(&ch) = chars.peek() {
102 if ch.is_whitespace() {
103 break;
104 }
105 if REJECTED_UNQUOTED_METACHARACTERS.contains(&ch) && started {
106 return Err(LexError::UnsupportedMetacharacter(ch));
111 }
112
113 match ch {
114 '\'' => {
115 chars.next();
116 word.push_str(&read_single_quoted(chars)?);
117 started = true;
118 }
119 '"' => {
120 chars.next();
121 word.push_str(&read_double_quoted(chars)?);
122 started = true;
123 }
124 _ => {
125 word.push(ch);
126 chars.next();
127 started = true;
128 }
129 }
130 }
131
132 Ok(word)
133}
134
135fn read_single_quoted(
136 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
137) -> Result<String, LexError> {
138 let mut content = String::new();
139 loop {
140 match chars.next() {
141 None => return Err(LexError::UnterminatedQuote),
142 Some('\'') => return Ok(content),
143 Some(ch) => content.push(ch),
144 }
145 }
146}
147
148fn read_double_quoted(
149 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
150) -> Result<String, LexError> {
151 let mut content = String::new();
152 loop {
153 match chars.next() {
154 None => return Err(LexError::UnterminatedQuote),
155 Some('"') => return Ok(content),
156 Some('\\') => match chars.next() {
157 None => return Err(LexError::UnterminatedQuote),
158 Some('n') => content.push('\n'),
159 Some('t') => content.push('\t'),
160 Some('"') => content.push('"'),
161 Some('\\') => content.push('\\'),
162 Some(other) => return Err(LexError::InvalidEscape(other)),
163 },
164 Some(ch) => content.push(ch),
165 }
166 }
167}