1pub mod ast;
84pub mod error;
85pub mod lexer;
86
87use ast::{
88 Argument, ArgumentValue, BlankLine, Block, Comment, Config, ConfigItem, Directive, Position,
89 Span,
90};
91use error::{ParseError, ParseResult};
92use lexer::{Lexer, Token, TokenKind};
93use std::fs;
94use std::path::Path;
95
96pub fn parse_config(path: &Path) -> ParseResult<Config> {
98 let content = fs::read_to_string(path).map_err(|e| ParseError::IoError(e.to_string()))?;
99 parse_string(&content)
100}
101
102pub fn parse_string(source: &str) -> ParseResult<Config> {
104 let mut lexer = Lexer::new(source);
105 let tokens = lexer.tokenize()?;
106 let mut parser = Parser::new(tokens);
107 parser.parse()
108}
109
110struct Parser {
112 tokens: Vec<Token>,
113 pos: usize,
114}
115
116impl Parser {
117 fn new(tokens: Vec<Token>) -> Self {
118 Self { tokens, pos: 0 }
119 }
120
121 fn current(&self) -> &Token {
122 &self.tokens[self.pos.min(self.tokens.len() - 1)]
123 }
124
125 fn advance(&mut self) -> &Token {
126 let token = &self.tokens[self.pos.min(self.tokens.len() - 1)];
127 if self.pos < self.tokens.len() {
128 self.pos += 1;
129 }
130 token
131 }
132
133 fn skip_newlines(&mut self) {
134 while matches!(self.current().kind, TokenKind::Newline) {
135 self.advance();
136 }
137 }
138
139 fn parse(&mut self) -> ParseResult<Config> {
140 let items = self.parse_items(false)?;
141 Ok(Config {
142 items,
143 include_context: Vec::new(),
144 })
145 }
146
147 fn parse_items(&mut self, in_block: bool) -> ParseResult<Vec<ConfigItem>> {
148 let mut items = Vec::new();
149 let mut consecutive_newlines = 0;
150
151 loop {
152 if in_block && matches!(self.current().kind, TokenKind::CloseBrace) {
154 break;
155 }
156 if matches!(self.current().kind, TokenKind::Eof) {
157 break;
158 }
159
160 match &self.current().kind {
161 TokenKind::Newline => {
162 let span = self.current().span;
163 let content = self.current().leading_whitespace.clone();
164 self.advance();
165 consecutive_newlines += 1;
166 if consecutive_newlines > 1 && !items.is_empty() {
168 items.push(ConfigItem::BlankLine(BlankLine { span, content }));
169 }
170 }
171 TokenKind::Comment(text) => {
172 let mut comment = Comment {
173 text: text.clone(),
174 span: self.current().span,
175 leading_whitespace: self.current().leading_whitespace.clone(),
176 trailing_whitespace: String::new(),
177 };
178 self.advance();
179 if let TokenKind::Newline = &self.current().kind {
181 comment.trailing_whitespace = self.current().leading_whitespace.clone();
182 }
183 items.push(ConfigItem::Comment(comment));
184 consecutive_newlines = 0;
185 }
186 TokenKind::CloseBrace if !in_block => {
187 let pos = self.current().span.start;
188 return Err(ParseError::UnmatchedCloseBrace { position: pos });
189 }
190 TokenKind::Ident(_)
191 | TokenKind::Argument(_)
192 | TokenKind::SingleQuotedString(_)
193 | TokenKind::DoubleQuotedString(_) => {
194 let directive = self.parse_directive()?;
195 items.push(ConfigItem::Directive(Box::new(directive)));
196 consecutive_newlines = 0;
197 }
198 _ => {
199 let token = self.current();
200 return Err(ParseError::UnexpectedToken {
201 expected: "directive or comment".to_string(),
202 found: token.kind.display_name().to_string(),
203 position: token.span.start,
204 });
205 }
206 }
207 }
208
209 Ok(items)
210 }
211
212 fn parse_directive(&mut self) -> ParseResult<Directive> {
213 let start_pos = self.current().span.start;
214 let leading_whitespace = self.current().leading_whitespace.clone();
215
216 let (name, name_span, name_raw) = match &self.current().kind {
218 TokenKind::Ident(name) => (
219 name.clone(),
220 self.current().span,
221 self.current().raw.clone(),
222 ),
223 TokenKind::Argument(name) => (
224 name.clone(),
225 self.current().span,
226 self.current().raw.clone(),
227 ),
228 TokenKind::SingleQuotedString(name) => (
229 name.clone(),
230 self.current().span,
231 self.current().raw.clone(),
232 ),
233 TokenKind::DoubleQuotedString(name) => (
234 name.clone(),
235 self.current().span,
236 self.current().raw.clone(),
237 ),
238 _ => {
239 return Err(ParseError::ExpectedDirectiveName {
240 position: self.current().span.start,
241 });
242 }
243 };
244 let _ = name_raw; self.advance();
246
247 let mut args = Vec::new();
249 let mut trailing_comment = None;
250
251 loop {
252 self.skip_newlines();
253
254 match &self.current().kind {
255 TokenKind::Semicolon => {
256 let space_before_terminator = self.current().leading_whitespace.clone();
257 let end_pos = self.current().span.end;
258 self.advance();
259
260 let trailing_whitespace;
262
263 if let TokenKind::Comment(text) = &self.current().kind {
265 trailing_whitespace = String::new();
267 trailing_comment = Some(Comment {
268 text: text.clone(),
269 span: self.current().span,
270 leading_whitespace: self.current().leading_whitespace.clone(),
271 trailing_whitespace: String::new(), });
273 self.advance();
274 if let TokenKind::Newline = &self.current().kind
276 && let Some(ref mut tc) = trailing_comment
277 {
278 tc.trailing_whitespace = self.current().leading_whitespace.clone();
279 }
280 } else if let TokenKind::Newline = &self.current().kind {
281 trailing_whitespace = self.current().leading_whitespace.clone();
282 } else {
283 trailing_whitespace = String::new();
284 }
285
286 return Ok(Directive {
287 name,
288 name_span,
289 args,
290 block: None,
291 span: Span::new(start_pos, end_pos),
292 trailing_comment,
293 leading_whitespace,
294 space_before_terminator,
295 trailing_whitespace,
296 });
297 }
298 TokenKind::OpenBrace => {
299 let space_before_terminator = self.current().leading_whitespace.clone();
300 let block_start = self.current().span.start;
301 self.advance();
302
303 let opening_brace_trailing = if let TokenKind::Newline = &self.current().kind {
305 self.current().leading_whitespace.clone()
306 } else {
307 String::new()
308 };
309
310 if is_raw_block_directive(&name) {
312 let (raw_content, block_end) = self.read_raw_block(block_start)?;
313
314 let mut block_trailing_whitespace = String::new();
316 if let TokenKind::Comment(text) = &self.current().kind {
317 trailing_comment = Some(Comment {
318 text: text.clone(),
319 span: self.current().span,
320 leading_whitespace: self.current().leading_whitespace.clone(),
321 trailing_whitespace: String::new(),
322 });
323 self.advance();
324 } else if let TokenKind::Newline = &self.current().kind {
325 block_trailing_whitespace = self.current().leading_whitespace.clone();
326 }
327
328 return Ok(Directive {
329 name,
330 name_span,
331 args,
332 block: Some(Block {
333 items: Vec::new(),
334 span: Span::new(block_start, block_end),
335 raw_content: Some(raw_content),
336 closing_brace_leading_whitespace: String::new(),
337 trailing_whitespace: block_trailing_whitespace,
338 }),
339 span: Span::new(start_pos, block_end),
340 trailing_comment,
341 leading_whitespace,
342 space_before_terminator,
343 trailing_whitespace: opening_brace_trailing,
344 });
345 }
346
347 self.skip_newlines();
348 let block_items = self.parse_items(true)?;
349
350 if !matches!(self.current().kind, TokenKind::CloseBrace) {
352 return Err(ParseError::UnclosedBlock {
353 position: block_start,
354 });
355 }
356 let closing_brace_leading_whitespace =
357 self.current().leading_whitespace.clone();
358 let block_end = self.current().span.end;
359 self.advance();
360
361 let mut block_trailing_whitespace = String::new();
363
364 if let TokenKind::Comment(text) = &self.current().kind {
366 trailing_comment = Some(Comment {
367 text: text.clone(),
368 span: self.current().span,
369 leading_whitespace: self.current().leading_whitespace.clone(),
370 trailing_whitespace: String::new(),
371 });
372 self.advance();
373 if let TokenKind::Newline = &self.current().kind
375 && let Some(ref mut tc) = trailing_comment
376 {
377 tc.trailing_whitespace = self.current().leading_whitespace.clone();
378 }
379 } else if let TokenKind::Newline = &self.current().kind {
380 block_trailing_whitespace = self.current().leading_whitespace.clone();
381 }
382
383 return Ok(Directive {
384 name,
385 name_span,
386 args,
387 block: Some(Block {
388 items: block_items,
389 span: Span::new(block_start, block_end),
390 raw_content: None,
391 closing_brace_leading_whitespace,
392 trailing_whitespace: block_trailing_whitespace,
393 }),
394 span: Span::new(start_pos, block_end),
395 trailing_comment,
396 leading_whitespace,
397 space_before_terminator,
398 trailing_whitespace: opening_brace_trailing,
399 });
400 }
401 TokenKind::Ident(value) => {
402 args.push(Argument {
403 value: ArgumentValue::Literal(value.clone()),
404 span: self.current().span,
405 raw: self.current().raw.clone(),
406 });
407 self.advance();
408 }
409 TokenKind::Argument(value) => {
410 args.push(Argument {
411 value: ArgumentValue::Literal(value.clone()),
412 span: self.current().span,
413 raw: self.current().raw.clone(),
414 });
415 self.advance();
416 }
417 TokenKind::DoubleQuotedString(value) => {
418 args.push(Argument {
419 value: ArgumentValue::QuotedString(value.clone()),
420 span: self.current().span,
421 raw: self.current().raw.clone(),
422 });
423 self.advance();
424 }
425 TokenKind::SingleQuotedString(value) => {
426 args.push(Argument {
427 value: ArgumentValue::SingleQuotedString(value.clone()),
428 span: self.current().span,
429 raw: self.current().raw.clone(),
430 });
431 self.advance();
432 }
433 TokenKind::Variable(value) => {
434 args.push(Argument {
435 value: ArgumentValue::Variable(value.clone()),
436 span: self.current().span,
437 raw: self.current().raw.clone(),
438 });
439 self.advance();
440 }
441 TokenKind::Comment(text) => {
442 trailing_comment = Some(Comment {
445 text: text.clone(),
446 span: self.current().span,
447 leading_whitespace: self.current().leading_whitespace.clone(),
448 trailing_whitespace: String::new(),
449 });
450 self.advance();
451 if let TokenKind::Newline = &self.current().kind
453 && let Some(ref mut tc) = trailing_comment
454 {
455 tc.trailing_whitespace = self.current().leading_whitespace.clone();
456 }
457 self.skip_newlines();
459 }
460 TokenKind::Eof => {
461 return Err(ParseError::UnexpectedEof {
462 position: self.current().span.start,
463 });
464 }
465 TokenKind::CloseBrace => {
466 return Err(ParseError::MissingSemicolon {
468 position: self.current().span.start,
469 });
470 }
471 _ => {
472 let token = self.current();
473 return Err(ParseError::UnexpectedToken {
474 expected: "argument, ';', or '{'".to_string(),
475 found: token.kind.display_name().to_string(),
476 position: token.span.start,
477 });
478 }
479 }
480 }
481 }
482
483 fn read_raw_block(&mut self, block_start: Position) -> ParseResult<(String, Position)> {
486 let mut content = String::new();
487 let mut brace_depth = 1;
488
489 loop {
490 match &self.current().kind {
491 TokenKind::OpenBrace => {
492 content.push('{');
493 brace_depth += 1;
494 self.advance();
495 }
496 TokenKind::CloseBrace => {
497 brace_depth -= 1;
498 if brace_depth == 0 {
499 let end_pos = self.current().span.end;
500 self.advance();
501 let trimmed = content.trim().to_string();
503 return Ok((trimmed, end_pos));
504 }
505 content.push('}');
506 self.advance();
507 }
508 TokenKind::Eof => {
509 return Err(ParseError::UnclosedBlock {
510 position: block_start,
511 });
512 }
513 _ => {
514 content.push_str(&self.current().raw);
516 if !matches!(self.current().kind, TokenKind::Newline) {
518 self.advance();
520 if !matches!(
521 self.current().kind,
522 TokenKind::Newline
523 | TokenKind::Eof
524 | TokenKind::CloseBrace
525 | TokenKind::Semicolon
526 ) {
527 content.push(' ');
528 }
529 } else {
530 content.push('\n');
531 self.advance();
532 }
533 }
534 }
535 }
536 }
537}
538
539pub fn is_raw_block_directive(name: &str) -> bool {
553 name.ends_with("_by_lua_block")
556}
557
558const BLOCK_DIRECTIVES: &[&str] = &[
560 "http",
562 "server",
563 "location",
564 "upstream",
565 "events",
566 "stream",
567 "mail",
568 "types",
569 "if",
571 "limit_except",
572 "geo",
573 "map",
574 "split_clients",
575 "match",
576];
577
578pub fn is_block_directive(name: &str) -> bool {
589 BLOCK_DIRECTIVES.contains(&name) || is_raw_block_directive(name)
590}
591
592pub fn is_block_directive_with_extras(name: &str, additional: &[String]) -> bool {
606 is_block_directive(name) || additional.iter().any(|s| s == name)
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn test_simple_directive() {
615 let config = parse_string("worker_processes auto;").unwrap();
616 let directives: Vec<_> = config.directives().collect();
617 assert_eq!(directives.len(), 1);
618 assert_eq!(directives[0].name, "worker_processes");
619 assert_eq!(directives[0].first_arg(), Some("auto"));
620 }
621
622 #[test]
623 fn test_block_directive() {
624 let config = parse_string("http {\n server {\n listen 80;\n }\n}").unwrap();
625 let directives: Vec<_> = config.directives().collect();
626 assert_eq!(directives.len(), 1);
627 assert_eq!(directives[0].name, "http");
628 assert!(directives[0].block.is_some());
629
630 let all_directives: Vec<_> = config.all_directives().collect();
631 assert_eq!(all_directives.len(), 3);
632 assert_eq!(all_directives[0].name, "http");
633 assert_eq!(all_directives[1].name, "server");
634 assert_eq!(all_directives[2].name, "listen");
635 }
636
637 #[test]
638 fn test_extension_directive() {
639 let config = parse_string(r#"more_set_headers "Server: Custom";"#).unwrap();
640 let directives: Vec<_> = config.directives().collect();
641 assert_eq!(directives.len(), 1);
642 assert_eq!(directives[0].name, "more_set_headers");
643 assert_eq!(directives[0].first_arg(), Some("Server: Custom"));
644 }
645
646 #[test]
647 fn test_ssl_protocols() {
648 let config = parse_string("ssl_protocols TLSv1.2 TLSv1.3;").unwrap();
649 let directives: Vec<_> = config.directives().collect();
650 assert_eq!(directives.len(), 1);
651 assert_eq!(directives[0].name, "ssl_protocols");
652 assert_eq!(directives[0].args.len(), 2);
653 assert_eq!(directives[0].args[0].as_str(), "TLSv1.2");
654 assert_eq!(directives[0].args[1].as_str(), "TLSv1.3");
655 }
656
657 #[test]
658 fn test_autoindex() {
659 let config = parse_string("autoindex on;").unwrap();
660 let directives: Vec<_> = config.directives().collect();
661 assert_eq!(directives.len(), 1);
662 assert_eq!(directives[0].name, "autoindex");
663 assert!(directives[0].args[0].is_on());
664 }
665
666 #[test]
667 fn test_comment() {
668 let config = parse_string("# This is a comment\nworker_processes auto;").unwrap();
669 assert_eq!(config.items.len(), 2);
670 match &config.items[0] {
671 ConfigItem::Comment(c) => assert_eq!(c.text, "# This is a comment"),
672 _ => panic!("Expected comment"),
673 }
674 }
675
676 #[test]
677 fn test_full_config() {
678 let source = r#"
679# Good nginx configuration
680worker_processes auto;
681error_log /var/log/nginx/error.log;
682
683http {
684 server_tokens off;
685 gzip on;
686
687 server {
688 listen 80;
689 server_name example.com;
690
691 location / {
692 root /var/www/html;
693 index index.html;
694 }
695 }
696}
697"#;
698 let config = parse_string(source).unwrap();
699
700 let all_directives: Vec<_> = config.all_directives().collect();
701 let names: Vec<&str> = all_directives.iter().map(|d| d.name.as_str()).collect();
702
703 assert!(names.contains(&"worker_processes"));
704 assert!(names.contains(&"error_log"));
705 assert!(names.contains(&"server_tokens"));
706 assert!(names.contains(&"gzip"));
707 assert!(names.contains(&"listen"));
708 assert!(names.contains(&"server_name"));
709 assert!(names.contains(&"root"));
710 assert!(names.contains(&"index"));
711 }
712
713 #[test]
714 fn test_server_tokens_on() {
715 let config = parse_string("server_tokens on;").unwrap();
716 let directive = config.directives().next().unwrap();
717 assert_eq!(directive.name, "server_tokens");
718 assert!(directive.first_arg_is("on"));
719 assert!(directive.args[0].is_on());
720 }
721
722 #[test]
723 fn test_gzip_on() {
724 let config = parse_string("gzip on;").unwrap();
725 let directive = config.directives().next().unwrap();
726 assert_eq!(directive.name, "gzip");
727 assert!(directive.first_arg_is("on"));
728 }
729
730 #[test]
731 fn test_position_tracking() {
732 let config = parse_string("http {\n listen 80;\n}").unwrap();
733 let all_directives: Vec<_> = config.all_directives().collect();
734
735 assert_eq!(all_directives[0].span.start.line, 1);
737
738 assert_eq!(all_directives[1].span.start.line, 2);
740 }
741
742 #[test]
743 fn test_error_unmatched_brace() {
744 let result = parse_string("http {\n listen 80;\n");
745 assert!(result.is_err());
746 match result.unwrap_err() {
747 ParseError::UnclosedBlock { .. } => {}
748 e => panic!("Expected UnclosedBlock error, got {:?}", e),
749 }
750 }
751
752 #[test]
753 fn test_error_missing_semicolon() {
754 let result = parse_string("listen 80\n}");
755 assert!(result.is_err());
756 }
757
758 #[test]
759 fn test_roundtrip() {
760 let source = "worker_processes auto;\nhttp {\n listen 80;\n}\n";
761 let config = parse_string(source).unwrap();
762 let output = config.to_source();
763
764 let reparsed = parse_string(&output).unwrap();
766 let names1: Vec<&str> = config.all_directives().map(|d| d.name.as_str()).collect();
767 let names2: Vec<&str> = reparsed.all_directives().map(|d| d.name.as_str()).collect();
768 assert_eq!(names1, names2);
769 }
770
771 #[test]
772 fn test_lua_directive() {
773 let config = parse_string("lua_code_cache on;").unwrap();
774 let directive = config.directives().next().unwrap();
775 assert_eq!(directive.name, "lua_code_cache");
776 assert!(directive.first_arg_is("on"));
777 }
778
779 #[test]
780 fn test_gzip_types() {
781 let config = parse_string("gzip_types text/plain text/css application/json;").unwrap();
782 let directive = config.directives().next().unwrap();
783 assert_eq!(directive.name, "gzip_types");
784 assert_eq!(directive.args.len(), 3);
785 }
786
787 #[test]
788 fn test_lua_block_directive() {
789 let config = parse_string(
790 r#"content_by_lua_block {
791 local cjson = require "cjson"
792 ngx.say(cjson.encode({status = "ok"}))
793}"#,
794 )
795 .unwrap();
796 let directive = config.directives().next().unwrap();
797 assert_eq!(directive.name, "content_by_lua_block");
798 assert!(directive.block.is_some());
799
800 let block = directive.block.as_ref().unwrap();
801 assert!(block.is_raw());
802 assert!(block.raw_content.is_some());
803
804 let content = block.raw_content.as_ref().unwrap();
805 assert!(content.contains("local cjson = require"));
806 assert!(content.contains("ngx.say"));
807 }
808
809 #[test]
810 fn test_map_with_empty_string_key() {
811 let config = parse_string(
812 r#"map $http_upgrade $connection_upgrade {
813 default upgrade;
814 '' close;
815}"#,
816 )
817 .unwrap();
818 let directive = config.directives().next().unwrap();
819 assert_eq!(directive.name, "map");
820 assert!(directive.block.is_some());
821
822 let block = directive.block.as_ref().unwrap();
823 let directives: Vec<_> = block.directives().collect();
824 assert_eq!(directives.len(), 2);
825 assert_eq!(directives[0].name, "default");
826 assert_eq!(directives[1].name, ""); }
828
829 #[test]
830 fn test_init_by_lua_block() {
831 let config = parse_string(
832 r#"init_by_lua_block {
833 require "resty.core"
834 cjson = require "cjson"
835}"#,
836 )
837 .unwrap();
838 let directive = config.directives().next().unwrap();
839 assert_eq!(directive.name, "init_by_lua_block");
840 assert!(directive.block.is_some());
841
842 let block = directive.block.as_ref().unwrap();
843 assert!(block.is_raw());
844
845 let content = block.raw_content.as_ref().unwrap();
846 assert!(content.contains("require \"resty.core\""));
847 }
848
849 #[test]
850 fn test_whitespace_capture() {
851 let config = parse_string("http {\n listen 80;\n}").unwrap();
852 let all_directives: Vec<_> = config.all_directives().collect();
853
854 assert_eq!(all_directives[0].leading_whitespace, "");
856 assert_eq!(all_directives[0].space_before_terminator, " ");
858
859 assert_eq!(all_directives[1].leading_whitespace, " ");
861 assert_eq!(all_directives[1].space_before_terminator, "");
863 }
864
865 #[test]
866 fn test_comment_whitespace_capture() {
867 let config = parse_string(" # test comment\nlisten 80;").unwrap();
868
869 if let ConfigItem::Comment(comment) = &config.items[0] {
871 assert_eq!(comment.leading_whitespace, " ");
872 } else {
873 panic!("Expected comment");
874 }
875 }
876
877 #[test]
878 fn test_roundtrip_preserves_whitespace() {
879 let source = "http {\n server {\n listen 80;\n }\n}\n";
881 let config = parse_string(source).unwrap();
882 let output = config.to_source();
883
884 let reparsed = parse_string(&output).unwrap();
886 let all_directives: Vec<_> = reparsed.all_directives().collect();
887
888 assert_eq!(all_directives[0].leading_whitespace, "");
890 assert_eq!(all_directives[1].leading_whitespace, " ");
892 assert_eq!(all_directives[2].leading_whitespace, " ");
894 }
895
896 #[test]
899 fn test_variable_in_argument() {
900 let config = parse_string("set $var value;").unwrap();
901 let directive = config.directives().next().unwrap();
902 assert_eq!(directive.name, "set");
903 assert_eq!(directive.args[0].as_str(), "var");
905 assert!(directive.args[0].is_variable());
906 assert_eq!(directive.args[0].raw, "$var");
908 }
909
910 #[test]
911 fn test_variable_in_proxy_pass() {
912 let config = parse_string("proxy_pass http://$backend;").unwrap();
914 let directive = config.directives().next().unwrap();
915 assert_eq!(directive.args[0].as_str(), "http://");
917 assert!(directive.args[0].is_literal());
918 assert_eq!(directive.args[1].as_str(), "backend");
920 assert!(directive.args[1].is_variable());
921 }
922
923 #[test]
924 fn test_braced_variable() {
925 let config = parse_string(r#"add_header X-Request-Id "${request_id}";"#).unwrap();
926 let directive = config.directives().next().unwrap();
927 assert!(directive.args[1].is_quoted());
929 assert!(directive.args[1].as_str().contains("request_id"));
930 }
931
932 #[test]
935 fn test_location_exact_match() {
936 let config = parse_string("location = /exact { return 200; }").unwrap();
937 let directive = config.directives().next().unwrap();
938 assert_eq!(directive.name, "location");
939 assert_eq!(directive.args[0].as_str(), "=");
940 assert_eq!(directive.args[1].as_str(), "/exact");
941 }
942
943 #[test]
944 fn test_location_prefix_match() {
945 let config = parse_string("location ^~ /prefix { return 200; }").unwrap();
946 let directive = config.directives().next().unwrap();
947 assert_eq!(directive.args[0].as_str(), "^~");
948 assert_eq!(directive.args[1].as_str(), "/prefix");
949 }
950
951 #[test]
952 fn test_location_regex_case_sensitive() {
953 let config = parse_string(r#"location ~ \.php$ { return 200; }"#).unwrap();
954 let directive = config.directives().next().unwrap();
955 assert_eq!(directive.args[0].as_str(), "~");
956 assert_eq!(directive.args[1].as_str(), r"\.php$");
957 }
958
959 #[test]
960 fn test_location_regex_case_insensitive() {
961 let config = parse_string(r#"location ~* \.(gif|jpg|png)$ { return 200; }"#).unwrap();
962 let directive = config.directives().next().unwrap();
963 assert_eq!(directive.args[0].as_str(), "~*");
964 assert_eq!(directive.args[1].as_str(), r"\.(gif|jpg|png)$");
965 }
966
967 #[test]
968 fn test_named_location() {
969 let config = parse_string("location @backend { proxy_pass http://backend; }").unwrap();
970 let directive = config.directives().next().unwrap();
971 assert_eq!(directive.args[0].as_str(), "@backend");
972 }
973
974 #[test]
977 fn test_if_variable_check() {
978 let config = parse_string("if ($request_uri ~* /admin) { return 403; }").unwrap();
979 let directive = config.directives().next().unwrap();
980 assert_eq!(directive.name, "if");
981 assert!(directive.block.is_some());
982 }
983
984 #[test]
985 fn test_if_file_exists() {
986 let config = parse_string("if (-f $request_filename) { break; }").unwrap();
987 let directive = config.directives().next().unwrap();
988 assert_eq!(directive.name, "if");
989 assert_eq!(directive.args[0].as_str(), "(-f");
990 }
991
992 #[test]
995 fn test_upstream_basic() {
996 let config = parse_string(
997 r#"upstream backend {
998 server 127.0.0.1:8080;
999 server 127.0.0.1:8081;
1000}"#,
1001 )
1002 .unwrap();
1003 let directive = config.directives().next().unwrap();
1004 assert_eq!(directive.name, "upstream");
1005 assert_eq!(directive.args[0].as_str(), "backend");
1006
1007 let servers: Vec<_> = directive.block.as_ref().unwrap().directives().collect();
1008 assert_eq!(servers.len(), 2);
1009 }
1010
1011 #[test]
1012 fn test_upstream_with_options() {
1013 let config = parse_string(
1014 r#"upstream backend {
1015 server 127.0.0.1:8080 weight=5 max_fails=3 fail_timeout=30s;
1016 keepalive 32;
1017}"#,
1018 )
1019 .unwrap();
1020 let directive = config.directives().next().unwrap();
1021 let block = directive.block.as_ref().unwrap();
1022 let items: Vec<_> = block.directives().collect();
1023
1024 assert_eq!(items[0].name, "server");
1025 assert!(items[0].args.iter().any(|a| a.as_str().contains("weight")));
1026 assert_eq!(items[1].name, "keepalive");
1027 }
1028
1029 #[test]
1032 fn test_geo_directive() {
1033 let config = parse_string(
1034 r#"geo $geo {
1035 default unknown;
1036 127.0.0.1 local;
1037 10.0.0.0/8 internal;
1038}"#,
1039 )
1040 .unwrap();
1041 let directive = config.directives().next().unwrap();
1042 assert_eq!(directive.name, "geo");
1043 assert!(directive.block.is_some());
1044 }
1045
1046 #[test]
1047 fn test_map_directive() {
1048 let config = parse_string(
1049 r#"map $uri $new_uri {
1050 default $uri;
1051 /old /new;
1052 ~^/api/v1/(.*) /api/v2/$1;
1053}"#,
1054 )
1055 .unwrap();
1056 let directive = config.directives().next().unwrap();
1057 assert_eq!(directive.name, "map");
1058 assert_eq!(directive.args.len(), 2);
1059 }
1060
1061 #[test]
1064 fn test_single_quoted_string() {
1065 let config = parse_string(r#"set $var 'single quoted';"#).unwrap();
1066 let directive = config.directives().next().unwrap();
1067 assert_eq!(directive.args[1].as_str(), "single quoted");
1068 assert!(directive.args[1].is_quoted());
1069 }
1070
1071 #[test]
1072 fn test_double_quoted_string() {
1073 let config = parse_string(r#"set $var "double quoted";"#).unwrap();
1074 let directive = config.directives().next().unwrap();
1075 assert_eq!(directive.args[1].as_str(), "double quoted");
1076 assert!(directive.args[1].is_quoted());
1077 }
1078
1079 #[test]
1080 fn test_quoted_string_with_spaces() {
1081 let config = parse_string(r#"add_header X-Custom "value with spaces";"#).unwrap();
1082 let directive = config.directives().next().unwrap();
1083 assert_eq!(directive.args[1].as_str(), "value with spaces");
1084 }
1085
1086 #[test]
1087 fn test_escaped_quote_in_string() {
1088 let config = parse_string(r#"set $var "say \"hello\"";"#).unwrap();
1089 let directive = config.directives().next().unwrap();
1090 let value = directive.args[1].as_str();
1092 assert!(value.contains("hello"), "value was: {}", value);
1093 }
1094
1095 #[test]
1098 fn test_include_directive() {
1099 let config = parse_string("include /etc/nginx/conf.d/*.conf;").unwrap();
1100 let directive = config.directives().next().unwrap();
1101 assert_eq!(directive.name, "include");
1102 assert_eq!(directive.args[0].as_str(), "/etc/nginx/conf.d/*.conf");
1103 }
1104
1105 #[test]
1106 fn test_include_with_glob() {
1107 let config = parse_string("include sites-enabled/*;").unwrap();
1108 let directive = config.directives().next().unwrap();
1109 assert!(directive.args[0].as_str().contains("*"));
1110 }
1111
1112 #[test]
1115 fn test_error_unexpected_closing_brace() {
1116 let result = parse_string("listen 80; }");
1117 assert!(result.is_err());
1118 }
1119
1120 #[test]
1121 fn test_error_unclosed_string() {
1122 let result = parse_string(r#"set $var "unclosed;"#);
1123 assert!(result.is_err());
1124 }
1125
1126 #[test]
1127 fn test_error_empty_directive_name() {
1128 let result = parse_string("map $a $b { '' x; }");
1130 assert!(result.is_ok());
1131 }
1132
1133 #[test]
1136 fn test_try_files_directive() {
1137 let config = parse_string("try_files $uri $uri/ /index.php?$args;").unwrap();
1138 let directive = config.directives().next().unwrap();
1139 assert_eq!(directive.name, "try_files");
1140 assert!(directive.args.len() >= 3);
1143 assert!(directive.args.iter().any(|a| a.as_str() == "uri"));
1144 }
1145
1146 #[test]
1147 fn test_rewrite_directive() {
1148 let config = parse_string("rewrite ^/old/(.*)$ /new/$1 permanent;").unwrap();
1149 let directive = config.directives().next().unwrap();
1150 assert_eq!(directive.name, "rewrite");
1151 assert!(directive.args.len() >= 3);
1153 assert_eq!(directive.args[0].as_str(), "^/old/(.*)$");
1154 assert!(directive.args.iter().any(|a| a.as_str() == "permanent"));
1155 }
1156
1157 #[test]
1158 fn test_return_directive() {
1159 let config = parse_string("return 301 https://$host$request_uri;").unwrap();
1160 let directive = config.directives().next().unwrap();
1161 assert_eq!(directive.name, "return");
1162 assert_eq!(directive.args[0].as_str(), "301");
1163 }
1164
1165 #[test]
1166 fn test_limit_except_block() {
1167 let config = parse_string(
1168 r#"location / {
1169 limit_except GET POST {
1170 deny all;
1171 }
1172}"#,
1173 )
1174 .unwrap();
1175 let all: Vec<_> = config.all_directives().collect();
1176 assert!(all.iter().any(|d| d.name == "limit_except"));
1177 }
1178
1179 #[test]
1182 fn test_ssl_configuration() {
1183 let config = parse_string(
1184 r#"server {
1185 listen 443 ssl http2;
1186 ssl_certificate /etc/ssl/cert.pem;
1187 ssl_certificate_key /etc/ssl/key.pem;
1188 ssl_protocols TLSv1.2 TLSv1.3;
1189 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256;
1190 ssl_prefer_server_ciphers on;
1191}"#,
1192 )
1193 .unwrap();
1194
1195 let all: Vec<_> = config.all_directives().collect();
1196 assert!(all.iter().any(|d| d.name == "ssl_certificate"));
1197 assert!(all.iter().any(|d| d.name == "ssl_protocols"));
1198 }
1199
1200 #[test]
1201 fn test_proxy_configuration() {
1202 let config = parse_string(
1203 r#"location /api {
1204 proxy_pass http://backend;
1205 proxy_set_header Host $host;
1206 proxy_set_header X-Real-IP $remote_addr;
1207 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1208 proxy_connect_timeout 60s;
1209 proxy_read_timeout 60s;
1210}"#,
1211 )
1212 .unwrap();
1213
1214 let all: Vec<_> = config.all_directives().collect();
1215 let proxy_headers: Vec<_> = all
1216 .iter()
1217 .filter(|d| d.name == "proxy_set_header")
1218 .collect();
1219 assert_eq!(proxy_headers.len(), 3);
1220 }
1221
1222 #[test]
1223 fn test_deeply_nested_blocks() {
1224 let config = parse_string(
1225 r#"http {
1226 server {
1227 location / {
1228 if ($request_method = POST) {
1229 return 405;
1230 }
1231 }
1232 }
1233}"#,
1234 )
1235 .unwrap();
1236
1237 let all: Vec<_> = config.all_directives().collect();
1238 assert_eq!(all.len(), 5); }
1240
1241 #[test]
1244 fn test_argument_is_on_off() {
1245 let config = parse_string("gzip on; gzip_static off;").unwrap();
1246 let directives: Vec<_> = config.directives().collect();
1247
1248 assert!(directives[0].args[0].is_on());
1249 assert!(!directives[0].args[0].is_off());
1250
1251 assert!(directives[1].args[0].is_off());
1252 assert!(!directives[1].args[0].is_on());
1253 }
1254
1255 #[test]
1256 fn test_argument_is_literal() {
1257 let config = parse_string(r#"set $var "quoted"; set $var2 literal;"#).unwrap();
1258 let directives: Vec<_> = config.directives().collect();
1259
1260 assert!(!directives[0].args[1].is_literal());
1261 assert!(directives[1].args[1].is_literal());
1262 }
1263
1264 #[test]
1267 fn test_blank_lines_preserved() {
1268 let config =
1269 parse_string("worker_processes 1;\n\nerror_log /var/log/error.log;\n").unwrap();
1270
1271 assert_eq!(config.items.len(), 3);
1273 assert!(matches!(config.items[1], ConfigItem::BlankLine(_)));
1274 }
1275
1276 #[test]
1277 fn test_multiple_blank_lines() {
1278 let config = parse_string("a 1;\n\n\nb 2;\n").unwrap();
1279
1280 let blank_count = config
1281 .items
1282 .iter()
1283 .filter(|i| matches!(i, ConfigItem::BlankLine(_)))
1284 .count();
1285 assert_eq!(blank_count, 2);
1286 }
1287
1288 #[test]
1291 fn test_events_block() {
1292 let config = parse_string(
1293 r#"events {
1294 worker_connections 1024;
1295 use epoll;
1296 multi_accept on;
1297}"#,
1298 )
1299 .unwrap();
1300
1301 let directive = config.directives().next().unwrap();
1302 assert_eq!(directive.name, "events");
1303
1304 let inner: Vec<_> = directive.block.as_ref().unwrap().directives().collect();
1305 assert_eq!(inner.len(), 3);
1306 }
1307
1308 #[test]
1311 fn test_stream_block() {
1312 let config = parse_string(
1313 r#"stream {
1314 server {
1315 listen 12345;
1316 proxy_pass backend;
1317 }
1318}"#,
1319 )
1320 .unwrap();
1321
1322 let directive = config.directives().next().unwrap();
1323 assert_eq!(directive.name, "stream");
1324 }
1325
1326 #[test]
1329 fn test_types_block() {
1330 let config = parse_string(
1331 r#"types {
1332 text/html html htm;
1333 text/css css;
1334 application/javascript js;
1335}"#,
1336 )
1337 .unwrap();
1338
1339 let directive = config.directives().next().unwrap();
1340 assert_eq!(directive.name, "types");
1341
1342 let inner: Vec<_> = directive.block.as_ref().unwrap().directives().collect();
1343 assert_eq!(inner.len(), 3);
1344 assert_eq!(inner[0].name, "text/html");
1345 }
1346}