haproxy_config/
parser.rs

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
9/// Parse a string representing a haproxy config to list of [`sections`](Section).
10/// Preservers comments and the order of the sections and their options.
11/// Unknown sections will result in multiple [`UnknownLine`][Section::UnknownLine] entries.
12///
13/// You can build a more strongly typed [`Config`](super::Config) struct from the output, see example
14/// below.
15///
16/// # Errors
17/// Returns an error on unsupported or wrong haproxy config.
18///
19/// # Examples
20/// ```
21/// use haproxy_config::parse_sections;
22/// use haproxy_config::Config;
23///
24/// let file = include_str!("../tests/medium_haproxy.cfg");
25/// let sections = parse_sections(file).unwrap();
26///
27/// // Build a config from the sections
28/// let config = Config::try_from(sections.as_slice()).unwrap();
29/// ```
30pub 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}