@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 1,166 lines 29.6 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddyfile
16
17 import (
18 "bytes"
19 "os"
20 "reflect"
21 "strings"
22 "testing"
23 )
24
25 func TestMain(m *testing.M) {
26 // Pin an empty environment so replaceEnvVars is deterministic for fuzzing.
27 os.Clearenv()
28 os.Exit(m.Run())
29 }
30
31 func TestFormatWindowsTrailingBackslash(t *testing.T) {
32 input := "localhost {\nroot * C:\\site\\\nfile_server\n}\n"
33 want := "localhost {\n\troot * C:\\site\\\n\tfile_server\n}\n"
34 for _, in := range []string{input, strings.ReplaceAll(input, "\n", "\r\n")} {
35 out := Format([]byte(in))
36 if strings.ReplaceAll(string(out), "\r\n", "\n") != want {
37 t.Fatalf("got %q, want %q", out, want)
38 }
39 if !bytes.Equal(Format(out), out) {
40 t.Fatal("formatting is not idempotent")
41 }
42 blocks, err := Parse("", out)
43 if err != nil {
44 t.Fatal(err)
45 }
46 if len(blocks) != 1 || len(blocks[0].Segments) != 2 {
47 t.Fatalf("expected separate root and file_server directives, got %+v", blocks)
48 }
49 }
50 }
51
52 func TestFormatter(t *testing.T) {
53 for i, tc := range []struct {
54 description string
55 input string
56 expect string
57 }{
58 {
59 description: "very simple",
60 input: `abc def
61 g hi jkl
62 mn`,
63 expect: `abc def
64 g hi jkl
65 mn`,
66 },
67 {
68 description: "basic indentation, line breaks, and nesting",
69 input: ` a
70 b
71
72 c {
73 d
74 }
75
76 e { f
77 }
78
79
80
81 g {
82 h {
83 i
84 }
85 }
86
87 j { k {
88 l
89 }
90 }
91
92 m {
93 n { o
94 }
95 p { q r
96 s }
97 }
98
99 {
100 { t
101 u
102
103 v
104
105 w
106 }
107 }`,
108 expect: `a
109 b
110
111 c {
112 d
113 }
114
115 e {
116 f
117 }
118
119 g {
120 h {
121 i
122 }
123 }
124
125 j {
126 k {
127 l
128 }
129 }
130
131 m {
132 n {
133 o
134 }
135 p {
136 q r
137 s
138 }
139 }
140
141 {
142 {
143 t
144 u
145
146 v
147
148 w
149 }
150 }`,
151 },
152 {
153 description: "block spacing",
154 input: `a{
155 b
156 }
157
158 c{ d
159 }`,
160 expect: `a{
161 b
162 }
163
164 c{ d
165 }`,
166 },
167 {
168 // sanctioned divergence: invalid glued braces left literal (design decision)
169 description: "advanced spacing",
170 input: `abc {
171 def
172 }ghi{
173 jkl mno
174 pqr}`,
175 expect: `abc {
176 def
177 }ghi{
178 jkl mno
179 pqr}`,
180 },
181 {
182 description: "env var placeholders",
183 input: `{$A}
184
185 b {
186 {$C}
187 }
188
189 d { {$E}
190 }
191
192 { {$F}
193 }
194 `,
195 expect: `{$A}
196
197 b {
198 {$C}
199 }
200
201 d {
202 {$E}
203 }
204
205 {
206 {$F}
207 }`,
208 },
209 {
210 description: "env var placeholders with port",
211 input: `:{$PORT}`,
212 expect: `:{$PORT}`,
213 },
214 {
215 description: "comments",
216 input: `#a "\n"
217
218 #b {
219 c
220 }
221
222 d {
223 e#f
224 # g
225 }
226
227 h { # i
228 }`,
229 expect: `#a "\n"
230
231 #b {
232 c
233 }
234
235 d {
236 e#f
237 # g
238 }
239
240 h { # i
241 }`,
242 },
243 {
244 description: "quotes and escaping",
245 input: `"a \"b\" "#c
246 d
247
248 e {
249 "f"
250 }
251
252 g { "h"
253 }
254
255 i {
256 "foo
257 bar"
258 }
259
260 j {
261 "\"k\" l m"
262 }`,
263 expect: `"a \"b\" "#c
264 d
265
266 e {
267 "f"
268 }
269
270 g {
271 "h"
272 }
273
274 i {
275 "foo
276 bar"
277 }
278
279 j {
280 "\"k\" l m"
281 }`,
282 },
283 {
284 description: "bad nesting (too many open)",
285 input: `a
286 {
287 {
288 }`,
289 expect: `a {
290 {
291 }
292 `,
293 },
294 {
295 // sanctioned divergence: invalid glued braces left literal (design decision)
296 description: "bad nesting (too many close)",
297 input: `a
298 {
299 {
300 }}}`,
301 expect: `a {
302 {
303 }}}
304 `,
305 },
306 {
307 description: "json",
308 input: `foo
309 bar "{\"key\":34}"
310 `,
311 expect: `foo
312 bar "{\"key\":34}"`,
313 },
314 {
315 description: "escaping after spaces",
316 input: `foo \"literal\"`,
317 expect: `foo \"literal\"`,
318 },
319 {
320 description: "simple placeholders as standalone tokens",
321 input: `foo {bar}`,
322 expect: `foo {bar}`,
323 },
324 {
325 description: "simple placeholders within tokens",
326 input: `foo{bar} foo{bar}baz`,
327 expect: `foo{bar} foo{bar}baz`,
328 },
329 {
330 // sanctioned divergence: invalid glued braces left literal (design decision)
331 description: "placeholders and malformed braces",
332 input: `foo{bar} foo{ bar}baz`,
333 expect: `foo{bar} foo{ bar}baz`,
334 },
335 {
336 description: "hash within string is not a comment",
337 input: `redir / /some/#/path`,
338 expect: `redir / /some/#/path`,
339 },
340 {
341 description: "brace does not fold into comment above",
342 input: `# comment
343 {
344 foo
345 }`,
346 expect: `# comment
347 {
348 foo
349 }`,
350 },
351 {
352 description: "matthewpi/vscode-caddyfile-support#13",
353 input: `{
354 email {$ACMEEMAIL}
355 #debug
356 }
357
358 block {
359 }
360 `,
361 expect: `{
362 email {$ACMEEMAIL}
363 #debug
364 }
365
366 block {
367 }
368 `,
369 },
370 {
371 description: "matthewpi/vscode-caddyfile-support#13 - bad formatting",
372 input: `{
373 email {$ACMEEMAIL}
374 #debug
375 }
376
377 block {
378 }
379 `,
380 expect: `{
381 email {$ACMEEMAIL}
382 #debug
383 }
384
385 block {
386 }
387 `,
388 },
389 {
390 description: "keep heredoc as-is",
391 input: `block {
392 heredoc <<HEREDOC
393 Here's more than one space Here's more than one space
394 HEREDOC
395 }
396 `,
397 expect: `block {
398 heredoc <<HEREDOC
399 Here's more than one space Here's more than one space
400 HEREDOC
401 }
402 `,
403 },
404 {
405 description: "Mixing heredoc with regular part",
406 input: `block {
407 heredoc <<HEREDOC
408 Here's more than one space Here's more than one space
409 HEREDOC
410 respond "More than one space will be eaten" 200
411 }
412
413 block2 {
414 heredoc <<HEREDOC
415 Here's more than one space Here's more than one space
416 HEREDOC
417 respond "More than one space will be eaten" 200
418 }
419 `,
420 expect: `block {
421 heredoc <<HEREDOC
422 Here's more than one space Here's more than one space
423 HEREDOC
424 respond "More than one space will be eaten" 200
425 }
426
427 block2 {
428 heredoc <<HEREDOC
429 Here's more than one space Here's more than one space
430 HEREDOC
431 respond "More than one space will be eaten" 200
432 }
433 `,
434 },
435 {
436 description: "Heredoc as regular token",
437 input: `block {
438 heredoc <<HEREDOC "More than one space will be eaten"
439 }
440 `,
441 expect: `block {
442 heredoc <<HEREDOC "More than one space will be eaten"
443 }
444 `,
445 },
446 {
447 description: "Escape heredoc",
448 input: `block {
449 heredoc \<<HEREDOC
450 respond "More than one space will be eaten" 200
451 }
452 `,
453 expect: `block {
454 heredoc \<<HEREDOC
455 respond "More than one space will be eaten" 200
456 }
457 `,
458 },
459 {
460 description: "Preserve braces wrapped by backquotes",
461 input: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
462 expect: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
463 },
464 {
465 description: "Preserve braces wrapped by quotes",
466 input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
467 expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
468 },
469 {
470 description: "Preserve quoted brace arguments",
471 input: "block {\n\trespond \"{\"\n\trespond \"}\"\n}",
472 expect: "block {\n\trespond \"{\"\n\trespond \"}\"\n}",
473 },
474 {
475 description: "Preserve quoted backticks and backticked quotes",
476 input: "block { respond \"`\" } block { respond `\"`}",
477 expect: "block {\n\trespond \"`\"\n}\n\nblock {\n\trespond `\"`\n}",
478 },
479 {
480 description: "No trailing space on line before env variable",
481 input: `{
482 a
483
484 {$ENV_VAR}
485 }
486 `,
487 expect: `{
488 a
489
490 {$ENV_VAR}
491 }
492 `,
493 },
494 {
495 description: "issue #7425: multiline backticked string indentation",
496 input: `https://localhost:8953 {
497 respond ` + "`" + `Here are some random numbers:
498
499 {{randNumeric 16}}
500
501 Hope this helps.` + "`" + `
502 }`,
503 expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
504 },
505 {
506 description: "imports before global options block keep standalone brace",
507 input: `import ./conf.d/matcher_my_subnet.caddy
508 import ./conf.d/matcher_not_my_subnet.caddy
509 {
510 order crowdsec first
511 order appsec after crowdsec
512 }`,
513 expect: `import ./conf.d/matcher_my_subnet.caddy
514 import ./conf.d/matcher_not_my_subnet.caddy
515 {
516 order crowdsec first
517 order appsec after crowdsec
518 }`,
519 },
520 {
521 description: "keep an opening brace that ends the input",
522 input: `localhost {`,
523 expect: `localhost {`,
524 },
525 {
526 description: "keep a standalone opening brace that ends the input",
527 input: `{`,
528 expect: `{`,
529 },
530 {
531 description: "keep adjacent opening braces that end the input",
532 input: `{{`,
533 // Glued braces are a literal token, not structural blocks.
534 expect: `{{`,
535 },
536 {
537 description: "keep spaced opening braces that end the input",
538 input: `{ {`,
539 expect: `{
540 {`,
541 },
542 } {
543 // the formatter should output a trailing newline,
544 // even if the tests aren't written to expect that
545 if !strings.HasSuffix(tc.expect, "\n") {
546 tc.expect += "\n"
547 }
548
549 actual := Format([]byte(tc.input))
550
551 if string(actual) != tc.expect {
552 t.Errorf("\n[TEST %d: %s]\n====== EXPECTED ======\n%s\n====== ACTUAL ======\n%s^^^^^^^^^^^^^^^^^^^^^",
553 i, tc.description, string(tc.expect), string(actual))
554 }
555 }
556 }
557
558 func TestFormatImportStandaloneBrace(t *testing.T) {
559 // immediately after a top-level import line: keep the brace standalone
560 in1 := "import a.caddy\nimport b.caddy\n{\n\torder x first\n}\n"
561 if got := string(Format([]byte(in1))); got != in1 {
562 t.Errorf("standalone brace after import not preserved:\n got %q\nwant %q", got, in1)
563 }
564 // An intervening blank line must not turn the global options block into an
565 // import block mapping.
566 in2 := "import a.caddy\n\n{\n\torder x first\n}\n"
567 want2 := in2
568 if got := string(Format([]byte(in2))); got != want2 {
569 t.Errorf("blank-line case:\n got %q\nwant %q", got, want2)
570 }
571 }
572
573 func TestFormatDeepNestingCapsIndent(t *testing.T) {
574 // Brace matching remains unbounded, but rendered indentation is capped to
575 // keep linear-size malicious input from producing quadratic output.
576 var b strings.Builder
577 const depth = 1000
578 for range depth {
579 b.WriteString("l {\n")
580 }
581 b.WriteString("x\n")
582 for range depth {
583 b.WriteString("}\n")
584 }
585 out := string(Format([]byte(b.String())))
586 if !strings.Contains(out, "\n"+strings.Repeat("\t", maxIndent)+"x\n") {
587 t.Errorf("expected %d-tab indent for deepest token", maxIndent)
588 }
589 if len(out) > b.Len()+(2*depth+1)*maxIndent {
590 t.Fatalf("formatted output grew unexpectedly: input=%d output=%d", b.Len(), len(out))
591 }
592 }
593
594 func TestFormatPreservesLiteralBraceArguments(t *testing.T) {
595 for _, tc := range []struct {
596 in string
597 want string
598 }{
599 {"localhost\nrespond foo{\n", "localhost\nrespond foo{\n"},
600 {"localhost\nrespond {} 200\n", "localhost\nrespond {} 200\n"},
601 } {
602 before, err := Tokenize([]byte(tc.in), "")
603 if err != nil {
604 t.Fatalf("input %q: %v", tc.in, err)
605 }
606 out := Format([]byte(tc.in))
607 if string(out) != tc.want {
608 t.Errorf("Format(%q) = %q, want %q", tc.in, out, tc.want)
609 }
610 after, err := Tokenize(out, "")
611 if err != nil {
612 t.Fatalf("formatted input %q no longer parses: %v\nout=%q", tc.in, err, out)
613 }
614 if !sameTokenTexts(before, after) {
615 t.Errorf("formatting changed parser tokens for %q:\nbefore=%v\nafter=%v\nout=%q", tc.in, before, after, out)
616 }
617 }
618 }
619
620 func TestFormatEscapedDelimitersDoesNotDisableFormatting(t *testing.T) {
621 in := "localhost {\nrespond foo\\\"bar\nrespond foo\\`bar\n}\n"
622 want := "localhost {\n\trespond foo\\\"bar\n\trespond foo\\`bar\n}\n"
623 if got := string(Format([]byte(in))); got != want {
624 t.Errorf("got %q, want %q", got, want)
625 }
626 }
627
628 func TestFinalizeHeredocLargeBody(t *testing.T) {
629 line := strings.Repeat("x", 1024)
630 var val strings.Builder
631 for range 1024 {
632 val.WriteByte('\t')
633 val.WriteString(line)
634 val.WriteByte('\n')
635 }
636 val.WriteString("\tEND")
637
638 got, err := new(lexer).finalizeHeredoc([]rune(val.String()), "END")
639 if err != nil {
640 t.Fatal(err)
641 }
642 wantLen := 1024*(len(line)+1) - 1
643 if len(string(got)) != wantLen {
644 t.Fatalf("large heredoc length = %d, want %d", len(string(got)), wantLen)
645 }
646 }
647
648 func TestWrapUnbracedSiteRejectsLeadingImport(t *testing.T) {
649 in := "import common.caddy\nlocalhost\nrespond 200\n"
650 want := string(Format([]byte(in)))
651 if got := string(FormatWithOptions([]byte(in), FormatOptions{WrapUnbracedSite: true})); got != want {
652 t.Errorf("leading import was wrapped:\n got %q\nwant %q", got, want)
653 }
654 }
655
656 func TestWrapUnbracedSiteAfterNestedDirective(t *testing.T) {
657 in := "localhost\nroute {\nrespond ok\n}\nfile_server\n"
658 want := "localhost {\n\troute {\n\t\trespond ok\n\t}\n\tfile_server\n}\n"
659 if got := string(FormatWithOptions([]byte(in), FormatOptions{WrapUnbracedSite: true})); got != want {
660 t.Errorf("got %q, want %q", got, want)
661 }
662 }
663
664 func TestFormatAngleNotQuirked(t *testing.T) {
665 if got := string(Format([]byte("foo < bar"))); got != "foo < bar\n" {
666 t.Errorf("got %q, want %q", got, "foo < bar\n")
667 }
668 }
669
670 func TestFormatEmptyBlocksExpand(t *testing.T) {
671 cases := []struct{ in, want string }{
672 {"route {}", "route {}\n"},
673 {"route { }", "route {\n}\n"},
674 {"a { b {} }", "a {\n\tb {}\n}\n"},
675 }
676 for _, c := range cases {
677 if got := string(Format([]byte(c.in))); got != c.want {
678 t.Errorf("in %q: got %q, want %q", c.in, got, c.want)
679 }
680 }
681 }
682
683 func TestFormatTokenAfterCloseBraceBreaks(t *testing.T) {
684 in := "a {\n\tb {\n\t\tc\n\t} d\n}"
685 want := "a {\n\tb {\n\t\tc\n\t}\n\td\n}\n"
686 if got := string(Format([]byte(in))); got != want {
687 t.Errorf("got %q, want %q", got, want)
688 }
689 }
690
691 func TestFormatCommentsOnBraceLines(t *testing.T) {
692 cases := []struct{ in, want string }{
693 {"site {\n\tfoo\n} # after close\n", "site {\n\tfoo\n} # after close\n"},
694 {"site { # note\n\tfoo\n}\n", "site { # note\n\tfoo\n}\n"},
695 {"site # note\n{\n\tfoo\n}\n", "site { # note\n\tfoo\n}\n"},
696 }
697 for _, c := range cases {
698 if got := string(Format([]byte(c.in))); got != c.want {
699 t.Errorf("in %q:\n got %q\nwant %q", c.in, got, c.want)
700 }
701 }
702 }
703
704 func TestFormatBlankLineCapAfterComment(t *testing.T) {
705 got := string(Format([]byte("foo # inline\n\n\nbar\n")))
706 want := "foo # inline\n\nbar\n"
707 if got != want {
708 t.Errorf("got %q, want %q", got, want)
709 }
710 }
711
712 func TestFormatContinuationHangingIndent(t *testing.T) {
713 in := "route {\n\treverse_proxy \\\n\ta \\\n\tb\n}\n"
714 want := "route {\n\treverse_proxy \\\n\t\ta \\\n\t\tb\n}\n"
715 if got := string(Format([]byte(in))); got != want {
716 t.Errorf("got %q, want %q", got, want)
717 }
718 }
719
720 func TestFormatStrayCloseBraceKeepsTokens(t *testing.T) {
721 for _, in := range []string{"0 }", "a }"} {
722 once := Format([]byte(in))
723 twice := Format(once)
724 if !bytes.Equal(once, twice) {
725 t.Errorf("not idempotent for %q:\n once=%q\ntwice=%q", in, once, twice)
726 }
727 a, err1 := Parse("Caddyfile", []byte(in))
728 if err1 != nil {
729 t.Fatalf("input %q did not parse: %v", in, err1)
730 }
731 b, err2 := Parse("Caddyfile", once)
732 if err2 != nil {
733 t.Fatalf("formatted %q did not parse: %v", in, err2)
734 }
735 if !sameStructure(a, b) {
736 t.Errorf("stray close brace merged tokens for %q: got %q", in, once)
737 }
738 }
739 }
740
741 func TestFormatIncompleteHeredocIdempotent(t *testing.T) {
742 for _, in := range []string{"0 <<0", "x <<E"} {
743 once := Format([]byte(in))
744 twice := Format(once)
745 if !bytes.Equal(once, twice) {
746 t.Errorf("not idempotent for %q:\n once=%q\ntwice=%q", in, once, twice)
747 }
748 }
749 }
750
751 // formatTokenText returns the format-mode non-comment token-text sequence for
752 // input, or nil and false on a lex error. It is the basis of the semantic-
753 // preservation invariant: Format must reproduce this exact sequence. Comment
754 // tokens are omitted because the formatter intentionally normalizes comment
755 // whitespace (e.g. trimming a trailing space, "# " -> "#"), which is not a
756 // semantic change.
757 func formatTokenText(in []byte) ([]string, bool) {
758 toks, err := Lex(in, "", LexOptions{Comments: true, Raw: true})
759 if err != nil {
760 return nil, false
761 }
762 var texts []string
763 for _, t := range toks {
764 if !t.isComment {
765 texts = append(texts, t.Text)
766 }
767 }
768 return texts, true
769 }
770
771 // formatAndParseLexersDisagree reports whether the format-mode lexer and the
772 // parse-path lexer produce different token-text sequences for the input (the
773 // parse baseline is newline-terminated to match Format's mandatory trailing
774 // newline, and comments — which the parse path discards — are ignored on the
775 // format side). When the two lexers already disagree before any formatting, the
776 // input is degenerate or structurally ambiguous — an unterminated or lone
777 // quote/backtick, an escaped quote swallowing to EOF, a "{}" or a "{"/"}" glued
778 // to a literal word that only one path splits — and has no well-defined
779 // formatting. Such inputs are a sanctioned divergence excluded from the
780 // invariant. Inputs on which both lexers agree, including the stray-close-brace
781 // bug's "0 }" and standalone braces in argument position, stay in scope.
782 func formatAndParseLexersDisagree(in []byte) bool {
783 fmtToks, err := Lex(in, "", LexOptions{Comments: true, Raw: true})
784 if err != nil {
785 return true
786 }
787 var fmtText []string
788 for _, t := range fmtToks {
789 if t.isComment {
790 continue
791 }
792 fmtText = append(fmtText, t.Text)
793 // A non-quoted token whose verbatim source ends in whitespace, contains an
794 // escaped quote, or whose text carries a newline results from an escaped
795 // quote ("\"") swallowing trailing whitespace to EOF. The format-mode and
796 // parse-path lexers can agree on such a token, but Format emits the source
797 // verbatim and then trims trailing whitespace, changing the token on
798 // re-lex. Treat these degenerate escaped-quote tokens as a disagreement.
799 if t.wasQuoted == 0 {
800 r := t.Raw()
801 if n := len(r); n > 0 && (r[n-1] == ' ' || r[n-1] == '\t' || r[n-1] == '\v' || r[n-1] == '\f' || r[n-1] == '\r' || r[n-1] == '\n') {
802 return true
803 }
804 if strings.Contains(r, `\"`) || strings.Contains(r, "\\`") {
805 return true
806 }
807 if strings.ContainsRune(t.Text, '\n') {
808 return true
809 }
810 }
811 }
812 base := in
813 if n := len(base); n == 0 || base[n-1] != '\n' {
814 base = append(append([]byte{}, base...), '\n')
815 }
816 parseToks, err := Tokenize(base, "")
817 if err != nil {
818 return true
819 }
820 parseText := make([]string, len(parseToks))
821 for i, t := range parseToks {
822 parseText[i] = t.Text
823 }
824 return !reflect.DeepEqual(fmtText, parseText)
825 }
826
827 // hasHeredocOpenerShapedToken reports whether the format-mode lexer produces a
828 // token whose text begins with "<<" (a heredoc opener shape). Such a token is a
829 // literal word only because no newline follows it (or a separating space breaks
830 // the heredoc), but Format's mandatory trailing newline can turn it into a
831 // heredoc opener, so the formatted output re-lexes to a different token stream.
832 // This degenerate opener is a sanctioned divergence excluded from the invariant;
833 // valid heredocs (whose token text does not begin with "<<") stay in scope. It
834 // is detected by token text, so a "<<" split by a stripped carriage return
835 // ("<\r<") is caught too.
836 func hasHeredocOpenerShapedToken(in []byte) bool {
837 toks, err := Lex(in, "", LexOptions{Comments: true, Raw: true})
838 if err != nil {
839 return true
840 }
841 for _, t := range toks {
842 if t.wasQuoted == 0 && !t.isComment && strings.HasPrefix(t.Text, "<<") {
843 return true
844 }
845 }
846 return false
847 }
848
849 // TestFormatDoesNotMutateInput guards the fallback paths, which trim the input
850 // and append the mandatory newline. bytes.TrimSpace returns a subslice sharing
851 // the caller's backing array, so appending to it without copying writes into
852 // the caller's buffer. os.ReadFile returns a slice with spare capacity, so this
853 // corrupted the input that "caddy fmt --diff" compares its output against.
854 func TestFormatDoesNotMutateInput(t *testing.T) {
855 cases := []string{
856 "foo \"unterminated \n\n", // unterminated quote + trailing whitespace
857 "a b\\ \n\n", // literal trailing backslash + whitespace
858 ` \ \`, // fallback with significant leading whitespace
859 " site {\n\tfoo\n} \n\n", // ordinary input with surrounding whitespace
860 }
861 for _, in := range cases {
862 // Spare capacity, as os.ReadFile and pooled buffers provide.
863 buf := append(make([]byte, 0, len(in)+8), in...)
864 out := Format(buf)
865 if string(buf) != in {
866 t.Errorf("Format mutated its input:\n in %q\n after %q", in, string(buf))
867 }
868 if len(out) > 0 && len(buf) > 0 && &out[0] == &buf[0] {
869 t.Errorf("Format output aliases its input for %q", in)
870 }
871 }
872 }
873
874 func FuzzFormatIdempotent(f *testing.F) {
875 for _, s := range []string{
876 "", " ", "a{\nb\n}", "site {\n\tfoo # c\n}\n", "x <<E\nhi\nE\n",
877 // Tokens that swallow end-of-input, and a lone CR that does not.
878 "\"\"\"", "\"\"`", "``\\\"", "0 0\r",
879 `0 \`, ` \ \`, // dangling continuation and significant leading space
880 } {
881 f.Add([]byte(s))
882 }
883 f.Fuzz(func(t *testing.T, in []byte) {
884 once := Format(in)
885 twice := Format(once)
886 if !bytes.Equal(once, twice) {
887 t.Errorf("not idempotent:\n once=%q\ntwice=%q", once, twice)
888 }
889 })
890 }
891
892 func FuzzFormatNoPanic(f *testing.F) {
893 f.Add([]byte("\x00\x00"))
894 f.Add([]byte("`unterminated"))
895 f.Add([]byte("x <<E\nno end marker"))
896 f.Fuzz(func(t *testing.T, in []byte) {
897 _ = Format(in) // must not panic
898 })
899 }
900
901 func FuzzFormatSemanticPreserve(f *testing.F) {
902 for _, s := range []string{"site {\n\troot * /srv\n\tfile_server\n}\n"} {
903 f.Add([]byte(s))
904 }
905 f.Fuzz(func(t *testing.T, in []byte) {
906 // The semantic-preservation invariant: formatting only changes structural
907 // whitespace, so the format-mode token-text sequence of Format(in) must
908 // equal that of the input. This is stronger and more robust than comparing
909 // parse structures, which the parse path's lenient/quirky grouping of
910 // ambiguous brace layouts (e.g. standalone braces in argument position)
911 // would spuriously flag even though every token is preserved.
912 //
913 // One class is genuinely out of scope: a heredoc-opener-shaped token
914 // ("<<MARKER") that stays a literal word only because no newline follows
915 // it. Format's mandatory trailing newline turns it into a real heredoc
916 // opener, so the output legitimately re-lexes to a different token stream.
917 // Valid heredocs (whose token text does not begin with "<<") stay in scope.
918 // Also out of scope: any input on which the format-mode and parse-path
919 // lexers already disagree (unterminated/lone quotes, escaped quotes at EOF,
920 // "{}" or a brace glued to a literal word) — these are degenerate or
921 // structurally ambiguous and have no well-defined formatting.
922 if hasHeredocOpenerShapedToken(in) || formatAndParseLexersDisagree(in) {
923 return
924 }
925 // Compare the format-mode token text of a newline-terminated baseline
926 // against that of Format's output. Format always ends its output in a
927 // single newline (Invariant); normalizing the baseline the same way keeps
928 // the comparison about token content, not the mandatory trailing newline.
929 base := in
930 if n := len(base); n == 0 || base[n-1] != '\n' {
931 base = append(append([]byte{}, base...), '\n')
932 }
933 before, ok := formatTokenText(base)
934 if !ok {
935 return // input does not lex; Format falls back and there is nothing to preserve
936 }
937 out := Format(in)
938 after, ok := formatTokenText(out)
939 if !ok {
940 t.Fatalf("formatted output no longer lexes\ninput=%q\nout=%q", in, out)
941 }
942 if !reflect.DeepEqual(before, after) {
943 t.Errorf("Format changed the token stream\ninput=%q\nout=%q\nbefore=%q\nafter=%q", in, out, before, after)
944 }
945 })
946 }
947
948 // TestFormatFuzzerAngles covers tricky / pathological inputs that a fuzzer would
949 // exercise. Every case asserts idempotency (Format(Format(x)) == Format(x)) and
950 // that Format does not panic. The subset of clearly-idiomatic inputs also asserts
951 // an exact expected output.
952 func TestFormatFuzzerAngles(t *testing.T) {
953 type tc struct {
954 name string
955 input string
956 exactExpect string // non-empty → assert this exact output in addition to idempotency
957 }
958
959 cases := []tc{
960 // ---- clearly-idiomatic: assert exact output ----
961
962 {
963 // Empty input: Format always emits a single trailing newline.
964 name: "empty input",
965 input: "",
966 exactExpect: "\n",
967 },
968 {
969 // Whitespace-only input collapses to the same single newline.
970 name: "whitespace-only input",
971 input: " \n\t\n",
972 exactExpect: "\n",
973 },
974 {
975 // A bare env-var placeholder is a literal token and must be preserved.
976 name: "env var {$X}",
977 input: "{$X}",
978 exactExpect: "{$X}\n",
979 },
980 {
981 // Default-value env var.
982 name: "env var {$X:def}",
983 input: "{$X:def}",
984 exactExpect: "{$X:def}\n",
985 },
986 {
987 // Minimal env var with only the sigil.
988 name: "env var {$}",
989 input: "{$}",
990 exactExpect: "{$}\n",
991 },
992 {
993 // Backtick token as the first token inside a block must indent correctly.
994 name: "backtick as first token after {",
995 input: "a {\n\t`foo`\n}\n",
996 exactExpect: "a {\n\t`foo`\n}\n",
997 },
998 {
999 // { } expands to a proper block.
1000 name: "{ } empty block",
1001 input: "route { }",
1002 exactExpect: "route {\n}\n",
1003 },
1004 {
1005 // Hash inside a double-quoted string is not a comment.
1006 name: "hash inside double-quoted string",
1007 input: `foo "bar#baz" quux`,
1008 exactExpect: "foo \"bar#baz\" quux\n",
1009 },
1010 {
1011 // Hash inside a heredoc body is not a comment.
1012 name: "hash inside heredoc body",
1013 input: "x <<END\nfoo # not a comment\nEND\n",
1014 exactExpect: "x <<END\nfoo # not a comment\nEND\n",
1015 },
1016 {
1017 // Escaped heredoc opener (\<<) is a regular token, not a real heredoc.
1018 name: "escaped heredoc \\<<",
1019 input: "block {\n\theredoc \\<<HEREDOC\n\trespond hello 200\n}\n",
1020 exactExpect: "block {\n\theredoc \\<<HEREDOC\n\trespond hello 200\n}\n",
1021 },
1022 {
1023 // Heredoc whose marker appears as a substring of a body line (fooEND ≠ END).
1024 name: "heredoc marker as substring of body line",
1025 input: "x <<END\nfooEND\nEND\n",
1026 exactExpect: "x <<END\nfooEND\nEND\n",
1027 },
1028
1029 // ---- pathological: idempotency + no-panic only ----
1030
1031 {
1032 // Unbalanced: one more opening brace than closing braces.
1033 name: "unbalanced braces (too many open)",
1034 input: "a {\n\tb {\n\t\tc\n\t}\n",
1035 },
1036 {
1037 // Unbalanced: stray closing brace.
1038 name: "unbalanced braces (too many close)",
1039 input: "a }\n",
1040 },
1041 {
1042 // CRLF line endings must not break idempotency.
1043 name: "CRLF line endings",
1044 input: "site {\r\n\tfoo\r\n}\r\n",
1045 },
1046 {
1047 // Unterminated double-quoted string.
1048 name: "unterminated double-quote",
1049 input: "foo \"unterminated",
1050 },
1051 {
1052 // Unterminated backtick string.
1053 name: "unterminated backtick",
1054 input: "foo `unterminated",
1055 },
1056 {
1057 // An empty quoted token followed by a lone opening quote. The first
1058 // pass falls back and appends the mandatory newline; on the second
1059 // pass that newline is swallowed by the still-unterminated quote, so
1060 // the unterminated token must be detected from the token itself, not
1061 // from whether appending a newline changes the stream.
1062 name: "empty quoted token followed by lone quote",
1063 input: "\"\"\"",
1064 },
1065 {
1066 // Same shape with a backtick opening the unterminated token.
1067 name: "empty quoted token followed by lone backtick",
1068 input: "\"\"`",
1069 },
1070 {
1071 // An escaped quote running to end-of-input after a closed backtick
1072 // token. Like the cases above, the second pass must still recognize
1073 // the swallowing token once the mandatory newline sits inside it.
1074 name: "empty backtick token followed by escaped quote",
1075 input: "``\\\"",
1076 },
1077 {
1078 // A lone CR does not terminate a token, so it stays in the token's
1079 // verbatim source without meaning the token swallowed end-of-input.
1080 // Treating it as swallowed would suppress normal rendering here and
1081 // leave the double space uncollapsed on the first pass only.
1082 name: "double space with trailing lone CR",
1083 input: "0 0\r",
1084 exactExpect: "0 0\n",
1085 },
1086 {
1087 // Trailing backslash (dangling escape — not a line continuation).
1088 name: "trailing backslash",
1089 input: "a b\\",
1090 },
1091 {
1092 // UTF-8 BOM at the start of the file; the BOM should not survive into
1093 // the formatted output (it is part of the first token's raw source but
1094 // the formatter strips leading/trailing whitespace).
1095 name: "UTF-8 BOM prefix",
1096 input: "\xef\xbb\xbfsite {\n\tfoo\n}\n",
1097 },
1098 {
1099 // NUL byte embedded in a token.
1100 name: "NUL byte in token",
1101 input: "site {\nfoo\x00bar\n}\n",
1102 },
1103 {
1104 // Arbitrary control bytes in a token.
1105 name: "control bytes in token",
1106 input: "a\x01b\x02c",
1107 },
1108 {
1109 // Lone carriage-return (CR without LF). The lexer treats \r as part of
1110 // the token raw text but strips it from the token's text field; the raw
1111 // bytes are emitted verbatim by the formatter.
1112 name: "lone CR (respond hello\\rworld)",
1113 input: "respond hello\rworld",
1114 },
1115 }
1116
1117 for _, c := range cases {
1118 t.Run(c.name, func(t *testing.T) {
1119 // No-panic: the call itself must complete.
1120 once := Format([]byte(c.input))
1121
1122 // Idempotency: a second pass must reproduce the first pass exactly.
1123 twice := Format(once)
1124 if !bytes.Equal(once, twice) {
1125 t.Errorf("not idempotent:\n once=%q\ntwice=%q", once, twice)
1126 }
1127
1128 // Exact-output assertion for clearly-idiomatic cases.
1129 if c.exactExpect != "" && string(once) != c.exactExpect {
1130 t.Errorf("unexpected output:\n got %q\n want %q", string(once), c.exactExpect)
1131 }
1132 })
1133 }
1134 }
1135
1136 // TestFormattingDifferenceStableOnFormatted verifies that an already-formatted
1137 // config produces no diff from FormattingDifference (i.e. the formatter and the
1138 // diff checker agree that a well-formed file needs no changes).
1139 func TestFormattingDifferenceStableOnFormatted(t *testing.T) {
1140 in := []byte("site {\n\troot * /srv\n\tfile_server\n}\n")
1141 formatted := Format(in)
1142 if _, diff := FormattingDifference("Caddyfile", formatted); diff {
1143 t.Error("FormattingDifference reported a diff on already-formatted input")
1144 }
1145 }
1146
1147 // sameStructure compares two parses by their per-segment token Text sequences.
1148 func sameStructure(a, b []ServerBlock) bool {
1149 seq := func(blocks []ServerBlock) []string {
1150 var s []string
1151 for _, blk := range blocks {
1152 s = append(s, "K")
1153 for _, k := range blk.Keys {
1154 s = append(s, k.Text)
1155 }
1156 for _, seg := range blk.Segments {
1157 s = append(s, "S")
1158 for _, tk := range seg {
1159 s = append(s, tk.Text)
1160 }
1161 }
1162 }
1163 return s
1164 }
1165 return reflect.DeepEqual(seq(a), seq(b))
1166 }