| // Copyright 2012 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package quotedprintable |
| |
| import ( |
| "bufio" |
| "bytes" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "os/exec" |
| "regexp" |
| "sort" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| func TestReader(t *testing.T) { |
| tests := []struct { |
| in, want string |
| err any |
| }{ |
| {in: "", want: ""}, |
| {in: "foo bar", want: "foo bar"}, |
| {in: "foo bar=3D", want: "foo bar="}, |
| {in: "foo bar=3d", want: "foo bar="}, // lax. |
| {in: "foo bar=\n", want: "foo bar"}, |
| {in: "foo bar\n", want: "foo bar\n"}, // somewhat lax. |
| {in: "foo bar=0", want: "foo bar=0"}, // lax |
| {in: "foo bar=0D=0A", want: "foo bar\r\n"}, |
| {in: " A B \r\n C ", want: " A B\r\n C"}, |
| {in: " A B =\r\n C ", want: " A B C"}, |
| {in: " A B =\n C ", want: " A B C"}, // lax. treating LF as CRLF |
| {in: "foo=\nbar", want: "foobar"}, |
| {in: "foo\x00bar", want: "foo", err: "quotedprintable: invalid unescaped byte 0x00 in body"}, |
| {in: "foo bar\xff", want: "foo bar\xff"}, |
| |
| // Equal sign. |
| {in: "=3D30\n", want: "=30\n"}, |
| {in: "=00=FF0=\n", want: "\x00\xff0"}, |
| |
| // Trailing whitespace |
| {in: "foo \n", want: "foo\n"}, |
| {in: "foo \n\nfoo =\n\nfoo=20\n\n", want: "foo\n\nfoo \nfoo \n\n"}, |
| |
| // Tests that we allow bare \n and \r through, despite it being strictly |
| // not permitted per RFC 2045, Section 6.7 Page 22 bullet (4). |
| {in: "foo\nbar", want: "foo\nbar"}, |
| {in: "foo\rbar", want: "foo\rbar"}, |
| {in: "foo\r\nbar", want: "foo\r\nbar"}, |
| |
| // Different types of soft line-breaks. |
| {in: "foo=\r\nbar", want: "foobar"}, |
| {in: "foo=\nbar", want: "foobar"}, |
| {in: "foo=\rbar", want: "foo", err: "quotedprintable: invalid hex byte 0x0d"}, |
| {in: "foo=\r\r\r \nbar", want: "foo", err: `quotedprintable: invalid bytes after =: "\r\r\r \n"`}, |
| // Issue 15486, accept trailing soft line-break at end of input. |
| {in: "foo=", want: "foo"}, |
| {in: "=", want: "", err: `quotedprintable: invalid bytes after =: ""`}, |
| |
| // Example from RFC 2045: |
| {in: "Now's the time =\n" + "for all folk to come=\n" + " to the aid of their country.", |
| want: "Now's the time for all folk to come to the aid of their country."}, |
| {in: "accept UTF-8 right quotation mark: ’", |
| want: "accept UTF-8 right quotation mark: ’"}, |
| } |
| for _, tt := range tests { |
| var buf bytes.Buffer |
| _, err := io.Copy(&buf, NewReader(strings.NewReader(tt.in))) |
| if got := buf.String(); got != tt.want { |
| t.Errorf("for %q, got %q; want %q", tt.in, got, tt.want) |
| } |
| switch verr := tt.err.(type) { |
| case nil: |
| if err != nil { |
| t.Errorf("for %q, got unexpected error: %v", tt.in, err) |
| } |
| case string: |
| if got := fmt.Sprint(err); got != verr { |
| t.Errorf("for %q, got error %q; want %q", tt.in, got, verr) |
| } |
| case error: |
| if err != verr { |
| t.Errorf("for %q, got error %q; want %q", tt.in, err, verr) |
| } |
| } |
| } |
| |
| } |
| |
| func everySequence(base, alpha string, length int, fn func(string)) { |
| if len(base) == length { |
| fn(base) |
| return |
| } |
| for i := 0; i < len(alpha); i++ { |
| everySequence(base+alpha[i:i+1], alpha, length, fn) |
| } |
| } |
| |
| var useQprint = flag.Bool("qprint", false, "Compare against the 'qprint' program.") |
| |
| var badSoftRx = regexp.MustCompile(`=([^\r\n]+?\n)|([^\r\n]+$)|(\r$)|(\r[^\n]+\n)|( \r\n)`) |
| |
| func TestExhaustive(t *testing.T) { |
| if *useQprint { |
| _, err := exec.LookPath("qprint") |
| if err != nil { |
| t.Fatalf("Error looking for qprint: %v", err) |
| } |
| } |
| |
| var buf bytes.Buffer |
| res := make(map[string]int) |
| n := 6 |
| if testing.Short() { |
| n = 4 |
| } |
| everySequence("", "0A \r\n=", n, func(s string) { |
| if strings.HasSuffix(s, "=") || strings.Contains(s, "==") { |
| return |
| } |
| buf.Reset() |
| _, err := io.Copy(&buf, NewReader(strings.NewReader(s))) |
| if err != nil { |
| errStr := err.Error() |
| if strings.Contains(errStr, "invalid bytes after =:") { |
| errStr = "invalid bytes after =" |
| } |
| res[errStr]++ |
| if strings.Contains(errStr, "invalid hex byte ") { |
| if strings.HasSuffix(errStr, "0x20") && (strings.Contains(s, "=0 ") || strings.Contains(s, "=A ") || strings.Contains(s, "= ")) { |
| return |
| } |
| if strings.HasSuffix(errStr, "0x3d") && (strings.Contains(s, "=0=") || strings.Contains(s, "=A=")) { |
| return |
| } |
| if strings.HasSuffix(errStr, "0x0a") || strings.HasSuffix(errStr, "0x0d") { |
| // bunch of cases; since whitespace at the end of a line before \n is removed. |
| return |
| } |
| } |
| if strings.Contains(errStr, "unexpected EOF") { |
| return |
| } |
| if errStr == "invalid bytes after =" && badSoftRx.MatchString(s) { |
| return |
| } |
| t.Errorf("decode(%q) = %v", s, err) |
| return |
| } |
| if *useQprint { |
| cmd := exec.Command("qprint", "-d") |
| cmd.Stdin = strings.NewReader(s) |
| stderr, err := cmd.StderrPipe() |
| if err != nil { |
| panic(err) |
| } |
| qpres := make(chan any, 2) |
| go func() { |
| br := bufio.NewReader(stderr) |
| s, _ := br.ReadString('\n') |
| if s != "" { |
| qpres <- errors.New(s) |
| if cmd.Process != nil { |
| // It can get stuck on invalid input, like: |
| // echo -n "0000= " | qprint -d |
| cmd.Process.Kill() |
| } |
| } |
| }() |
| go func() { |
| want, err := cmd.Output() |
| if err == nil { |
| qpres <- want |
| } |
| }() |
| select { |
| case got := <-qpres: |
| if want, ok := got.([]byte); ok { |
| if string(want) != buf.String() { |
| t.Errorf("go decode(%q) = %q; qprint = %q", s, want, buf.String()) |
| } |
| } else { |
| t.Logf("qprint -d(%q) = %v", s, got) |
| } |
| case <-time.After(5 * time.Second): |
| t.Logf("qprint timeout on %q", s) |
| } |
| } |
| res["OK"]++ |
| }) |
| var outcomes []string |
| for k, v := range res { |
| outcomes = append(outcomes, fmt.Sprintf("%v: %d", k, v)) |
| } |
| sort.Strings(outcomes) |
| got := strings.Join(outcomes, "\n") |
| want := `OK: 28934 |
| invalid bytes after =: 3949 |
| quotedprintable: invalid hex byte 0x0d: 2048 |
| unexpected EOF: 194` |
| if testing.Short() { |
| want = `OK: 896 |
| invalid bytes after =: 100 |
| quotedprintable: invalid hex byte 0x0d: 26 |
| unexpected EOF: 3` |
| } |
| |
| if got != want { |
| t.Errorf("Got:\n%s\nWant:\n%s", got, want) |
| } |
| } |