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