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