1use std::fmt;
6
7use crate::ast::{
8 self, Argument, Caddyfile, Directive, GlobalOptions, Matcher, NamedRoute, SiteBlock, Snippet,
9};
10use crate::token::{Span, Token, TokenKind};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ParseErrorKind {
15 ExpectedOpenBrace { found: Option<String> },
17 ExpectedCloseBrace { found: Option<String> },
19}
20
21impl fmt::Display for ParseErrorKind {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self {
24 Self::ExpectedOpenBrace { found: None } => {
25 write!(f, "expected '{{'")
26 }
27 Self::ExpectedOpenBrace { found: Some(t) } => {
28 write!(f, "expected '{{', got '{t}'")
29 }
30 Self::ExpectedCloseBrace { found: None } => {
31 write!(f, "expected '}}'")
32 }
33 Self::ExpectedCloseBrace { found: Some(t) } => {
34 write!(f, "expected '}}', got '{t}'")
35 }
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42#[error("{kind} at line {}, column {}", span.line, span.column)]
43pub struct ParseError {
44 pub kind: ParseErrorKind,
45 pub span: Span,
46}
47
48pub fn parse(tokens: &[Token]) -> Result<Caddyfile, ParseError> {
55 Parser::new(tokens).parse()
56}
57
58struct Parser<'a> {
59 tokens: &'a [Token],
60 pos: usize,
61}
62
63impl<'a> Parser<'a> {
64 const fn new(tokens: &'a [Token]) -> Self {
65 Self { tokens, pos: 0 }
66 }
67
68 fn parse(mut self) -> Result<Caddyfile, ParseError> {
69 let mut caddyfile = Caddyfile {
70 global_options: None,
71 snippets: Vec::new(),
72 named_routes: Vec::new(),
73 sites: Vec::new(),
74 };
75
76 self.skip_newlines_and_comments();
77
78 if self.is_global_options_block() {
81 caddyfile.global_options = Some(self.parse_global_options()?);
82 self.skip_newlines_and_comments();
83 }
84
85 while self.pos < self.tokens.len() {
87 self.skip_newlines_and_comments();
88 if self.pos >= self.tokens.len() {
89 break;
90 }
91
92 let token = &self.tokens[self.pos];
93
94 if token.text.starts_with('(') && token.text.ends_with(')') && token.text.len() > 2 {
96 caddyfile.snippets.push(self.parse_snippet()?);
97 }
98 else if token.text.starts_with("&(")
100 && token.text.ends_with(')')
101 && token.text.len() > 3
102 {
103 caddyfile.named_routes.push(self.parse_named_route()?);
104 }
105 else {
107 caddyfile.sites.push(self.parse_site_block()?);
108 }
109 }
110
111 Ok(caddyfile)
112 }
113
114 fn is_global_options_block(&self) -> bool {
115 self.pos < self.tokens.len() && self.tokens[self.pos].kind == TokenKind::OpenBrace
117 }
118
119 fn parse_global_options(&mut self) -> Result<GlobalOptions, ParseError> {
120 self.expect_open_brace()?;
121 let directives = self.parse_directives()?;
122 self.expect_close_brace()?;
123 Ok(GlobalOptions { directives })
124 }
125
126 fn parse_snippet(&mut self) -> Result<Snippet, ParseError> {
127 let token = &self.tokens[self.pos];
128 let name = token.text[1..token.text.len() - 1].to_string();
129 self.pos += 1;
130 self.skip_whitespace_tokens();
131 self.expect_open_brace()?;
132 let directives = self.parse_directives()?;
133 self.expect_close_brace()?;
134 Ok(Snippet { name, directives })
135 }
136
137 fn parse_named_route(&mut self) -> Result<NamedRoute, ParseError> {
138 let token = &self.tokens[self.pos];
139 let name = token.text[2..token.text.len() - 1].to_string();
140 self.pos += 1;
141 self.skip_whitespace_tokens();
142 self.expect_open_brace()?;
143 let directives = self.parse_directives()?;
144 self.expect_close_brace()?;
145 Ok(NamedRoute { name, directives })
146 }
147
148 fn parse_site_block(&mut self) -> Result<SiteBlock, ParseError> {
149 let mut addresses = Vec::new();
150
151 while self.pos < self.tokens.len() {
153 let token = &self.tokens[self.pos];
154 match &token.kind {
155 TokenKind::OpenBrace => break,
156 TokenKind::Newline => {
157 self.pos += 1;
158 break;
162 }
163 TokenKind::Comment => {
164 self.pos += 1;
165 }
166 _ => {
167 let text = token.text.trim_end_matches(',');
169 addresses.push(ast::parse_address(text));
170 self.pos += 1;
171 }
172 }
173 }
174
175 self.skip_newlines_and_comments();
176
177 if self.pos >= self.tokens.len() || self.tokens[self.pos].kind != TokenKind::OpenBrace {
179 return Ok(SiteBlock {
180 addresses,
181 directives: Vec::new(),
182 });
183 }
184
185 self.expect_open_brace()?;
186 let directives = self.parse_directives()?;
187 self.expect_close_brace()?;
188
189 Ok(SiteBlock {
190 addresses,
191 directives,
192 })
193 }
194
195 fn parse_directives(&mut self) -> Result<Vec<Directive>, ParseError> {
196 let mut directives = Vec::new();
197
198 loop {
199 self.skip_newlines_and_comments();
200
201 if self.pos >= self.tokens.len() {
202 break;
203 }
204
205 if self.tokens[self.pos].kind == TokenKind::CloseBrace {
207 break;
208 }
209
210 directives.push(self.parse_directive()?);
211 }
212
213 Ok(directives)
214 }
215
216 fn parse_directive(&mut self) -> Result<Directive, ParseError> {
217 let name = self.tokens[self.pos].text.clone();
218 self.pos += 1;
219
220 let matcher = self.try_parse_matcher();
222
223 let mut arguments = Vec::new();
225 while self.pos < self.tokens.len() {
226 let tok = &self.tokens[self.pos];
227 match &tok.kind {
228 TokenKind::Newline => {
229 self.pos += 1;
230 break;
231 }
232 TokenKind::OpenBrace | TokenKind::CloseBrace => break,
233 TokenKind::Comment => {
234 self.pos += 1;
235 }
236 _ => {
237 arguments.push(Self::token_to_argument(tok));
238 self.pos += 1;
239 }
240 }
241 }
242
243 let block =
245 if self.pos < self.tokens.len() && self.tokens[self.pos].kind == TokenKind::OpenBrace {
246 self.pos += 1; let sub = self.parse_directives()?;
248 self.expect_close_brace()?;
249 Some(sub)
250 } else {
251 None
252 };
253
254 Ok(Directive {
255 name,
256 matcher,
257 arguments,
258 block,
259 })
260 }
261
262 fn try_parse_matcher(&mut self) -> Option<Matcher> {
263 if self.pos >= self.tokens.len() {
264 return None;
265 }
266
267 let tok = &self.tokens[self.pos];
268 match &tok.kind {
269 TokenKind::Newline
270 | TokenKind::OpenBrace
271 | TokenKind::CloseBrace
272 | TokenKind::Comment => None,
273 _ => {
274 if tok.text == "*" {
275 self.pos += 1;
276 Some(Matcher::All)
277 } else if tok.text.starts_with('@') {
278 let name = tok.text[1..].to_string();
279 self.pos += 1;
280 Some(Matcher::Named(name))
281 } else if tok.text.starts_with('/') {
282 let path = tok.text.clone();
283 self.pos += 1;
284 Some(Matcher::Path(path))
285 } else {
286 None
287 }
288 }
289 }
290 }
291
292 fn token_to_argument(token: &Token) -> Argument {
293 match &token.kind {
294 TokenKind::QuotedString => Argument::Quoted(token.text.clone()),
295 TokenKind::BacktickString => Argument::Backtick(token.text.clone()),
296 TokenKind::Heredoc { marker } => Argument::Heredoc {
297 marker: marker.clone(),
298 content: token.text.clone(),
299 },
300 _ => Argument::Unquoted(token.text.clone()),
301 }
302 }
303
304 fn skip_newlines_and_comments(&mut self) {
305 while self.pos < self.tokens.len() {
306 match self.tokens[self.pos].kind {
307 TokenKind::Newline | TokenKind::Comment => {
308 self.pos += 1;
309 }
310 _ => break,
311 }
312 }
313 }
314
315 fn skip_whitespace_tokens(&mut self) {
316 while self.pos < self.tokens.len() {
317 if self.tokens[self.pos].kind == TokenKind::Newline {
318 self.pos += 1;
319 } else {
320 break;
321 }
322 }
323 }
324
325 fn expect_open_brace(&mut self) -> Result<(), ParseError> {
326 self.skip_newlines_and_comments();
327 if self.pos >= self.tokens.len() {
328 return Err(ParseError {
329 kind: ParseErrorKind::ExpectedOpenBrace { found: None },
330 span: self.eof_span(),
331 });
332 }
333 if self.tokens[self.pos].kind != TokenKind::OpenBrace {
334 return Err(ParseError {
335 kind: ParseErrorKind::ExpectedOpenBrace {
336 found: Some(self.tokens[self.pos].text.clone()),
337 },
338 span: self.tokens[self.pos].span.clone(),
339 });
340 }
341 self.pos += 1;
342 Ok(())
343 }
344
345 fn expect_close_brace(&mut self) -> Result<(), ParseError> {
346 self.skip_newlines_and_comments();
347 if self.pos >= self.tokens.len() {
348 return Err(ParseError {
349 kind: ParseErrorKind::ExpectedCloseBrace { found: None },
350 span: self.eof_span(),
351 });
352 }
353 if self.tokens[self.pos].kind != TokenKind::CloseBrace {
354 return Err(ParseError {
355 kind: ParseErrorKind::ExpectedCloseBrace {
356 found: Some(self.tokens[self.pos].text.clone()),
357 },
358 span: self.tokens[self.pos].span.clone(),
359 });
360 }
361 self.pos += 1;
362 Ok(())
363 }
364
365 fn eof_span(&self) -> Span {
366 self.tokens
367 .last()
368 .map_or(Span { line: 1, column: 1 }, |last| last.span.clone())
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::ast::Scheme;
376 use crate::lexer::tokenize;
377
378 fn parse_input(input: &str) -> Result<Caddyfile, ParseError> {
379 let tokens = tokenize(input).expect("tokenize failed");
380 parse(&tokens)
381 }
382
383 #[test]
384 fn simple_site_block() {
385 let cf =
386 parse_input("example.com {\n reverse_proxy app:3000\n}\n").expect("parse failed");
387 assert_eq!(cf.sites.len(), 1);
388 assert_eq!(cf.sites[0].addresses[0].host, "example.com");
389 assert_eq!(cf.sites[0].directives.len(), 1);
390 assert_eq!(cf.sites[0].directives[0].name, "reverse_proxy");
391 }
392
393 #[test]
394 fn global_options() {
395 let cf = parse_input(
396 "{\n email admin@example.com\n}\n\
397 example.com {\n log\n}\n",
398 )
399 .expect("parse failed");
400 assert!(cf.global_options.is_some());
401 let go = cf.global_options.as_ref().unwrap();
402 assert_eq!(go.directives[0].name, "email");
403 assert_eq!(cf.sites.len(), 1);
404 }
405
406 #[test]
407 fn snippet() {
408 let cf = parse_input(
409 "(logging) {\n log\n}\n\
410 example.com {\n import logging\n}\n",
411 )
412 .expect("parse failed");
413 assert_eq!(cf.snippets.len(), 1);
414 assert_eq!(cf.snippets[0].name, "logging");
415 }
416
417 #[test]
418 fn named_route() {
419 let cf =
420 parse_input("&(myroute) {\n reverse_proxy app:3000\n}\n").expect("parse failed");
421 assert_eq!(cf.named_routes.len(), 1);
422 assert_eq!(cf.named_routes[0].name, "myroute");
423 }
424
425 #[test]
426 fn directive_with_sub_block() {
427 let cf = parse_input(
428 "example.com {\n\
429 \theader {\n\
430 \t\tX-Frame-Options DENY\n\
431 \t}\n\
432 }\n",
433 )
434 .expect("parse failed");
435 let header = &cf.sites[0].directives[0];
436 assert_eq!(header.name, "header");
437 assert!(header.block.is_some());
438 let sub = header.block.as_ref().unwrap();
439 assert_eq!(sub[0].name, "X-Frame-Options");
440 }
441
442 #[test]
443 fn matcher_all() {
444 let cf = parse_input("example.com {\n respond * 200\n}\n").expect("parse failed");
445 assert_eq!(cf.sites[0].directives[0].matcher, Some(Matcher::All));
446 }
447
448 #[test]
449 fn matcher_path() {
450 let cf = parse_input("example.com {\n respond /health 200\n}\n").expect("parse failed");
451 assert_eq!(
452 cf.sites[0].directives[0].matcher,
453 Some(Matcher::Path("/health".to_string()))
454 );
455 }
456
457 #[test]
458 fn matcher_named() {
459 let cf = parse_input(
460 "example.com {\n\
461 \tbasic_auth @protected {\n\
462 \t\tadmin hash\n\
463 \t}\n\
464 }\n",
465 )
466 .expect("parse failed");
467 assert_eq!(
468 cf.sites[0].directives[0].matcher,
469 Some(Matcher::Named("protected".to_string()))
470 );
471 }
472
473 #[test]
474 fn address_parsing() {
475 let a = ast::parse_address("https://example.com:443/api");
476 assert_eq!(a.scheme, Some(Scheme::Https));
477 assert_eq!(a.host, "example.com");
478 assert_eq!(a.port, Some(443));
479 assert_eq!(a.path, Some("/api".to_string()));
480 }
481
482 #[test]
483 fn address_simple() {
484 let a = ast::parse_address("example.com");
485 assert_eq!(a.scheme, None);
486 assert_eq!(a.host, "example.com");
487 assert_eq!(a.port, None);
488 assert_eq!(a.path, None);
489 }
490
491 #[test]
492 fn unclosed_brace() {
493 let result = parse_input("example.com {\n log\n");
494 assert!(result.is_err());
495 }
496
497 #[test]
498 fn multiple_sites() {
499 let cf = parse_input("a.com {\n log\n}\n\nb.com {\n log\n}\n").expect("parse failed");
500 assert_eq!(cf.sites.len(), 2);
501 }
502}