1use std::net::Ipv4Addr;
2
3mod error;
4pub use error::Error;
5use super::section::{AddressRef, BackendModifier, HostRef, PasswordRef};
6use crate::section::borrowed::Section;
7use crate::line::borrowed::Line;
8
9pub fn parse_sections(input: &str) -> Result<Vec<Section<'_>>, Error> {
31 parser::configuration(input).map_err(|e| Error {
32 inner: e,
33 source: (*input).to_string(),
34 path: None,
35 })
36}
37
38peg::parser! {
39 grammar parser() for str {
40 pub(super) rule configuration() -> Vec<Section<'input>>
41 = (config_comment() / config_blank() / global_section() / defaults_section() / userlist_section() / listen_section() / frontend_section() / backend_section()/ unknown_line())*
42
43 rule unknown_line() -> Section<'input>
44 = line:$([^ '\n']+) line_break() {
45 Section::UnknownLine{ line }
46 }
47
48 pub(super) rule global_section() -> Section<'input>
49 = comment:global_header() lines:config_block() {
50 Section::Global{ comment, lines }
51 }
52
53 rule defaults_section() -> Section<'input>
54 = h:defaults_header() lines:config_block() {
55 Section::Default{ comment: h.1, proxy: h.0, lines }
56 }
57
58 rule userlist_section() -> Section<'input>
59 = h:userlist_header() lines:config_block() {
60 Section::Userlist{ comment: h.1, name: h.0 , lines}
61 }
62
63 rule listen_section() -> Section<'input>
64 = h:listen_header() lines:config_block() {
65 Section::Listen{ comment: h.1, proxy: h.0, header_addr: h.2, lines}
66 }
67
68 rule frontend_section() -> Section<'input>
69 = h:frontend_header() lines:config_block() {
70 Section::Frontend{ comment: h.1, proxy: h.0, header_addr: h.2, lines }
71 }
72
73 rule backend_section() -> Section<'input>
74 = h:backend_header() lines:config_block() {
75 Section::Backend{ comment: h.1, proxy: h.0 , lines}
76 }
77
78 rule global_header() -> Option<&'input str>
79 = _ "global" _ c:comment_text()? line_break() { c }
80
81 rule userlist_header() -> (&'input str, Option<&'input str>)
82 = _ "userlist" _ p:proxy_name() c:comment_text()? line_break() {(p,c)}
83
84 rule defaults_header() -> (Option<&'input str>, Option<&'input str>)
85 = _ "defaults" _ p:proxy_name()? _ c:comment_text()? line_break() {(p,c)}
86
87 rule header_bind() -> (AddressRef<'input>, Option<&'input str>)
88 = s:service_address() v:value()? {(s, v)}
89
90 rule listen_header() -> (&'input str, Option<&'input str>, Option<(AddressRef<'input>, Option<&'input str>)>)
91 = _ "listen" _ p:proxy_name() _ hb:header_bind()? c:comment_text()? line_break() {(p, c, hb)}
92
93 rule frontend_header() -> (&'input str, Option<&'input str>, Option<(AddressRef<'input>, Option<&'input str>)>)
94 = _ "frontend" _ p:proxy_name() _ hb:header_bind()? c:comment_text()? line_break() {(p, c, hb)}
95
96 pub(super) rule backend_header() -> (&'input str, Option<&'input str>)
97 = _ "backend" _ p:proxy_name() _ value()? c:comment_text()? line_break() {(p,c)}
98
99 rule config_block() -> Vec<Line<'input>>
100 = e:(server_line() / option_line() / bind_line() / acl_line() / backend_line() / group_line() / user_line() / system_user_line() / config_line() / comment_line() / blank_line())* { e }
101
102 rule server_line() -> Line<'input>
103 = _ "server" _ name:server_name() _ addr:service_address() option:value()? comment:comment_text()? line_break() eof()? {
104 Line::Server { name, addr, option, comment }
105 }
106
107 rule option_line() -> Line<'input>
108 = _ "option" _ keyword:keyword() value:value()? comment:comment_text()? line_break() eof()? {
109 Line::Option { keyword, value, comment }
110 }
111
112 pub(super) rule bind_line() -> Line<'input>
113 = _ "bind" whitespaceplus() addr:service_address() value:value()? _ comment:comment_text()? line_break() eof()? {
114 Line::Bind { addr, value, comment }
115 }
116
117 rule acl_line() -> Line<'input>
118 = _ "acl" _ name:acl_name() r:value()? comment:comment_text()? line_break() eof()? {
119 Line::Acl { name, rule: r, comment }
120 }
121
122 rule modifier() -> BackendModifier
123 = "if" { BackendModifier::If } / "unless" { BackendModifier::Unless }
124
125 rule backend_line() -> Line<'input>
126 = _ ("use_backend" / "default_backend") _ name:backend_name() _ modifier:modifier()? _ condition:backend_condition()? comment:comment_text()? line_break() eof()? {
127 Line::Backend {name, modifier, condition, comment }
128 }
129
130 rule users() -> Vec<&'input str>
131 = "users" users:(value() ++ whitespaceplus()) {
132 let mut users = users;
133 for user in &mut users {
134 *user = user.trim();
135 }
136 users
137 }
138
139 pub(super) rule group_line() -> Line<'input>
140 = _ "group" _ name:group_name() _ users:users()? comment:comment_text()? line_break() eof()? {
141 Line::Group { name, users: users.unwrap_or_default(), comment }
142 }
143
144 rule password_type() -> bool
145 = "password" { true } / "insecure-password" { false }
146
147 rule groups() -> Vec<&'input str>
148 = whitespaceplus() "groups" groups:(value() ++ whitespaceplus()) {
149 let mut groups = groups;
150 for group in &mut groups {
151 *group = group.trim();
152 }
153 groups
154 }
155
156 rule system_user_line() -> Line<'input>
157 = _ "user" _ name:user_name() _ comment:comment_text()? line_break() eof()? {
158 Line::SysUser {
159 name,
160 }
161 }
162
163 pub(super) rule user_line() -> Line<'input>
164 = _ "user" _ name:user_name() _ secure:password_type() whitespaceplus() password:password() groups:groups()? comment:comment_text()? line_break() eof()? {
165 let password = if secure {
166 PasswordRef::Secure(password)
167 } else {
168 PasswordRef::Insecure(password)
169 };
170 let groups = groups.unwrap_or_default();
171 Line::User { name, password, groups, comment}
172 }
173
174 pub(super) rule config_line() -> Line<'input>
175 = _ !("defaults" / "global" / "userlist" / "listen" / "frontend" / "backend" / "server") key:keyword() value:value()? comment:comment_text()? line_break() eof()? {
176 Line::Config { key, value, comment }
177 }
178
179 rule config_comment() -> Section<'input>
180 = _ t:comment_text() line_break() eof()? { Section::Comment(t) }
181
182 rule comment_line() -> Line<'input>
183 = _ t:comment_text() line_break() eof()? { Line::Comment(t) }
184
185 rule blank_line() -> Line<'input>
186 = _ line_break() eof()? { Line::Blank }
187
188 rule config_blank() -> Section<'input>
189 = _ line_break() eof()? { Section::BlankLine }
190
191 pub(super) rule comment_text() -> &'input str
192 = "#" s:$(char()*) &(line_break() / eof()) { s }
193
194 rule line_break()
195 = quiet!{['\n']}
196
197 rule eof()
198 = quiet!{![_]}
199
200 rule keyword() -> &'input str
201 = $((("errorfile" / "timeout") _)? ['a'..='z' | '0'..='9' | '-' | '_' | '.']+)
202
203 rule alphanumeric_plus() -> &'input str
204 = $(['a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | ':']+)
205
206 rule server_name() -> &'input str
207 = alphanumeric_plus()
208
209 rule acl_name() -> &'input str
210 = alphanumeric_plus()
211
212 rule backend_name() -> &'input str
213 = alphanumeric_plus()
214
215 rule group_name() -> &'input str
216 = alphanumeric_plus()
217
218 rule user_name() -> &'input str
219 = alphanumeric_plus()
220
221 rule not_comment_or_end() -> &'input str
222 = $([^ '#' | '\n']+)
223
224 rule password() -> &'input str
225 = $([^ '#' | '\n' | ' ']+)
226
227 rule backend_condition() -> &'input str
228 = not_comment_or_end()
229
230 rule service_address() -> AddressRef<'input>
231 = host:host() [':']? port:port()? {
232 AddressRef {host, port}
233 }
234
235 rule host() -> HostRef<'input>
236 = ipv4_host() / dns_host() / wildcard_host()
237
238 rule port() -> u16
239 = p:$(['0'..='9']+) { p.parse().expect("port must fit in a u16") }
240
241 rule digits_u8() -> u8
242 = d:$(['0'..='9']*<1,3>) {
243 d.parse().expect("digits must represent unsigned 8 bit integer")
244 }
245
246 rule ipv4_host() -> HostRef<'input>
247 = a:digits_u8() "." b:digits_u8() "." c:digits_u8() "." d:digits_u8() {
248 HostRef::Ipv4(Ipv4Addr::new(a,b,c,d))
249 }
250
251 rule dns_host() -> HostRef<'input>
252 = s:$(['a'..='z' | 'A'..='Z' | '-' | '.' | '0'..='9']+) { HostRef::Dns(s) }
253
254 rule wildcard_host() -> HostRef<'input>
255 = "*" { HostRef::Wildcard }
256
257 rule proxy_name() -> &'input str
258 = alphanumeric_plus()
259
260 rule value() -> &'input str
261 = whitespaceplus() s:not_comment_or_end() { s }
262
263 rule char()
264 = [^ '\n']
265
266 rule _
267 = [' ' | '\t']*
268
269 rule whitespaceplus()
270 = quiet!{[' ' | '\t']+}
271
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::parser;
278 use crate::line::borrowed::Line;
279 use crate::section::{AddressRef, PasswordRef};
280
281 #[test]
282 fn global() {
283 parser::configuration(include_str!("global_section.txt")).unwrap();
284 }
285
286 #[test]
287 fn config_line() {
288 parser::config_line(include_str!("config_line.txt")).unwrap();
289 }
290
291 #[test]
292 fn backend_with_comment() {
293 parser::backend_header(include_str!("backend_with_comment.txt")).unwrap();
294 }
295
296 #[test]
297 fn comment_text() {
298 parser::comment_text("# testing comment_text, *=* () hi!").unwrap();
299 }
300
301 #[test]
302 fn user_with_group() {
303 let line = parser::user_line(include_str!("user_with_group.txt")).unwrap();
304 match line {
305 Line::User { groups, .. } if groups == vec!["G1"] => (),
306 _ => panic!("groups not correct, line: {line:?}"),
307 }
308 }
309
310 #[test]
311 fn user() {
312 let line = parser::user_line(include_str!("user.txt")).unwrap();
313 match line {
314 Line::User {
315 groups,
316 password: PasswordRef::Insecure(pass),
317 ..
318 } => {
319 assert_eq!(groups, Vec::<&str>::new());
320 assert_eq!(pass, "test");
321 }
322 _ => panic!("user not correct, line: {line:?}"),
323 }
324 }
325
326 #[test]
327 fn group_with_users() {
328 let line = parser::group_line(include_str!("group_with_users.txt")).unwrap();
329 match line {
330 Line::Group { users, .. } if users == vec!["haproxy"] => (),
331 _ => panic!("group not correct, line: {line:?}"),
332 }
333 }
334
335 #[test]
336 fn group_with_single_user() {
337 let line = parser::group_line(include_str!("group_with_single_user.txt")).unwrap();
338 match line {
339 Line::Group { name, users, .. } => {
340 assert!(users.is_empty());
341 assert_eq!(name, "G1");
342 }
343 _ => panic!("group not correct, line: {line:?}"),
344 }
345 }
346
347 #[test]
348 fn bind_with_comment() {
349 let line = parser::bind_line(include_str!("bind_with_comment.txt")).unwrap();
350 match line {
351 Line::Bind { addr, value, .. } => {
352 assert_eq!(value, None);
353 assert_eq!(
354 addr,
355 AddressRef {
356 host: crate::section::HostRef::Wildcard,
357 port: Some(80)
358 }
359 );
360 }
361 _ => panic!("group not correct, line: {line:?}"),
362 }
363 }
364}