diff --git a/internal/utils/io.go b/internal/utils/io.go index ba85976a..ccccf5f9 100644 --- a/internal/utils/io.go +++ b/internal/utils/io.go @@ -2,10 +2,13 @@ package utils import ( "bytes" + "io" "os" "unicode" "github.com/pkg/errors" + + "go.step.sm/crypto/internal/utils/utfbom" ) func maybeUnwrap(err error) error { @@ -15,15 +18,32 @@ func maybeUnwrap(err error) error { return err } -// ReadFile reads the file named by filename and returns the contents. -// -// It wraps os.ReadFile wrapping the errors. -func ReadFile(filename string) ([]byte, error) { - b, err := os.ReadFile(filename) +// stdinFilename is the name of the file that is used in many command +// line utilities to denote input is to be read from STDIN. +const stdinFilename = "-" + +// stdin points to STDIN through os.Stdin. +var stdin = os.Stdin + +// ReadFile reads the file identified by filename and returns +// the contents. If filename is equal to "-", it will read from +// STDIN. +func ReadFile(filename string) (b []byte, err error) { + if filename == stdinFilename { + filename = "/dev/stdin" + b, err = io.ReadAll(stdin) + } else { + var contents []byte + contents, err = os.ReadFile(filename) + if err != nil { + return nil, errors.Wrapf(maybeUnwrap(err), "error reading %q", filename) + } + b, err = io.ReadAll(utfbom.SkipOnly(bytes.NewReader(contents))) + } if err != nil { - return nil, errors.Wrapf(maybeUnwrap(err), "error reading %s", filename) + return nil, errors.Wrapf(maybeUnwrap(err), "error reading %q", filename) } - return b, nil + return } // ReadPasswordFromFile reads and returns the password from the given filename. diff --git a/internal/utils/io_test.go b/internal/utils/io_test.go index d083ce6c..d1f29e24 100644 --- a/internal/utils/io_test.go +++ b/internal/utils/io_test.go @@ -1,13 +1,14 @@ package utils import ( - "fmt" + "io" "os" "path/filepath" "reflect" "testing" "github.com/pkg/errors" + "github.com/stretchr/testify/require" ) func TestReadFile(t *testing.T) { @@ -37,6 +38,62 @@ func TestReadFile(t *testing.T) { } } +// Set content to be read from mock STDIN +func setStdinContent(t *testing.T, content string) (cleanup func()) { + f, err := os.CreateTemp("" /* dir */, "utils-read-test") + require.NoError(t, err) + _, err = f.WriteString(content) + require.NoError(t, err) + _, err = f.Seek(0, io.SeekStart) + require.NoError(t, err) + old := stdin + stdin = f + + return func() { + stdin = old + require.NoError(t, f.Close()) + require.NoError(t, os.Remove(f.Name())) + } +} + +func TestReadFromStdin(t *testing.T) { + cleanup := setStdinContent(t, "input on STDIN") + t.Cleanup(func() { + cleanup() + }) + + b, err := ReadFile(stdinFilename) + require.NoError(t, err) + require.Equal(t, "input on STDIN", string(b)) +} + +// Sets STDIN to a file that is already closed, and thus fails +// to be read from. +func setFailingStdin(t *testing.T) (cleanup func()) { + f, err := os.CreateTemp("" /* dir */, "utils-read-test") + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + old := stdin + stdin = f + + return func() { + stdin = old + require.NoError(t, os.Remove(f.Name())) + } +} + +func TestReadFromStdinFails(t *testing.T) { + cleanup := setFailingStdin(t) + t.Cleanup(func() { + cleanup() + }) + + b, err := ReadFile(stdinFilename) + require.Error(t, err) + require.Empty(t, b) +} + func TestReadPasswordFromFile(t *testing.T) { type args struct { filename string @@ -65,11 +122,20 @@ func TestReadPasswordFromFile(t *testing.T) { } } +func TestReadPasswordFromStdin(t *testing.T) { + cleanup := setStdinContent(t, "this-is-a-secret-testing-password") + t.Cleanup(func() { + cleanup() + }) + + b, err := ReadPasswordFromFile(stdinFilename) + require.NoError(t, err) + require.Equal(t, "this-is-a-secret-testing-password", string(b)) +} + func TestWriteFile(t *testing.T) { tmpDir, err := os.MkdirTemp(os.TempDir(), "go-tests") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(tmpDir) }) @@ -97,8 +163,7 @@ func TestWriteFile(t *testing.T) { } func Test_maybeUnwrap(t *testing.T) { - wantErr := fmt.Errorf("the error") - + wantErr := errors.New("the error") type args struct { err error } @@ -113,9 +178,7 @@ func Test_maybeUnwrap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := maybeUnwrap(tt.args.err) - if !reflect.DeepEqual(err, tt.wantErr) { //nolint:govet // legacy deep equal error check - t.Errorf("maybeUnwrap() error = %v, wantErr %v", err, tt.wantErr) - } + require.Equal(t, tt.wantErr, err) }) } } diff --git a/internal/utils/utfbom/LICENSE b/internal/utils/utfbom/LICENSE new file mode 100644 index 00000000..6279cb87 --- /dev/null +++ b/internal/utils/utfbom/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2018-2020, Dmitrij Koniajev (dimchansky@gmail.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/internal/utils/utfbom/README.md b/internal/utils/utfbom/README.md new file mode 100644 index 00000000..8ece2800 --- /dev/null +++ b/internal/utils/utfbom/README.md @@ -0,0 +1,66 @@ +# utfbom [![Godoc](https://godoc.org/github.com/dimchansky/utfbom?status.png)](https://godoc.org/github.com/dimchansky/utfbom) [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Build Status](https://travis-ci.org/dimchansky/utfbom.svg?branch=master)](https://travis-ci.org/dimchansky/utfbom) [![Go Report Card](https://goreportcard.com/badge/github.com/dimchansky/utfbom)](https://goreportcard.com/report/github.com/dimchansky/utfbom) [![Coverage Status](https://coveralls.io/repos/github/dimchansky/utfbom/badge.svg?branch=master)](https://coveralls.io/github/dimchansky/utfbom?branch=master) + +The package utfbom implements the detection of the BOM (Unicode Byte Order Mark) and removing as necessary. It can also return the encoding detected by the BOM. + +## Installation + + go get -u github.com/dimchansky/utfbom + +## Example + +```go +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + + "github.com/dimchansky/utfbom" +) + +func main() { + trySkip([]byte("\xEF\xBB\xBFhello")) + trySkip([]byte("hello")) +} + +func trySkip(byteData []byte) { + fmt.Println("Input:", byteData) + + // just skip BOM + output, err := ioutil.ReadAll(utfbom.SkipOnly(bytes.NewReader(byteData))) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("ReadAll with BOM skipping", output) + + // skip BOM and detect encoding + sr, enc := utfbom.Skip(bytes.NewReader(byteData)) + fmt.Printf("Detected encoding: %s\n", enc) + output, err = ioutil.ReadAll(sr) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("ReadAll with BOM detection and skipping", output) + fmt.Println() +} +``` + +Output: + +``` +$ go run main.go +Input: [239 187 191 104 101 108 108 111] +ReadAll with BOM skipping [104 101 108 108 111] +Detected encoding: UTF8 +ReadAll with BOM detection and skipping [104 101 108 108 111] + +Input: [104 101 108 108 111] +ReadAll with BOM skipping [104 101 108 108 111] +Detected encoding: Unknown +ReadAll with BOM detection and skipping [104 101 108 108 111] +``` + + diff --git a/internal/utils/utfbom/utfbom.go b/internal/utils/utfbom/utfbom.go new file mode 100644 index 00000000..93a144fd --- /dev/null +++ b/internal/utils/utfbom/utfbom.go @@ -0,0 +1,195 @@ +// Package utfbom implements the detection of the BOM (Unicode Byte Order Mark) and removing as necessary. +// It wraps an io.Reader object, creating another object (Reader) that also implements the io.Reader +// interface but provides automatic BOM checking and removing as necessary. +// +// This package was copied from https://github.com/dimchansky/utfbom. Only minor changes +// were made to not depend on the io/ioutil package and to make our linters pass. +package utfbom + +import ( + "errors" + "io" +) + +// Encoding is type alias for detected UTF encoding. +type Encoding int + +// Constants to identify detected UTF encodings. +const ( + // Unknown encoding, returned when no BOM was detected + Unknown Encoding = iota + + // UTF8, BOM bytes: EF BB BF + UTF8 + + // UTF-16, big-endian, BOM bytes: FE FF + UTF16BigEndian + + // UTF-16, little-endian, BOM bytes: FF FE + UTF16LittleEndian + + // UTF-32, big-endian, BOM bytes: 00 00 FE FF + UTF32BigEndian + + // UTF-32, little-endian, BOM bytes: FF FE 00 00 + UTF32LittleEndian +) + +// String returns a user-friendly string representation of the encoding. Satisfies fmt.Stringer interface. +func (e Encoding) String() string { + switch e { + case UTF8: + return "UTF8" + case UTF16BigEndian: + return "UTF16BigEndian" + case UTF16LittleEndian: + return "UTF16LittleEndian" + case UTF32BigEndian: + return "UTF32BigEndian" + case UTF32LittleEndian: + return "UTF32LittleEndian" + default: + return "Unknown" + } +} + +const maxConsecutiveEmptyReads = 100 + +// Skip creates Reader which automatically detects BOM (Unicode Byte Order Mark) and removes it as necessary. +// It also returns the encoding detected by the BOM. +// If the detected encoding is not needed, you can call the SkipOnly function. +func Skip(rd io.Reader) (*Reader, Encoding) { + // Is it already a Reader? + b, ok := rd.(*Reader) + if ok { + return b, Unknown + } + + enc, left, err := detectUtf(rd) + return &Reader{ + rd: rd, + buf: left, + err: err, + }, enc +} + +// SkipOnly creates Reader which automatically detects BOM (Unicode Byte Order Mark) and removes it as necessary. +func SkipOnly(rd io.Reader) *Reader { + r, _ := Skip(rd) + return r +} + +// Reader implements automatic BOM (Unicode Byte Order Mark) checking and +// removing as necessary for an io.Reader object. +type Reader struct { + rd io.Reader // reader provided by the client + buf []byte // buffered data + err error // last error +} + +// Read is an implementation of io.Reader interface. +// The bytes are taken from the underlying Reader, but it checks for BOMs, removing them as necessary. +func (r *Reader) Read(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + + if r.buf == nil { + if r.err != nil { + return 0, r.readErr() + } + + return r.rd.Read(p) + } + + // copy as much as we can + n = copy(p, r.buf) + r.buf = nilIfEmpty(r.buf[n:]) + return n, nil +} + +func (r *Reader) readErr() error { + err := r.err + r.err = nil + return err +} + +var errNegativeRead = errors.New("utfbom: reader returned negative count from Read") + +func detectUtf(rd io.Reader) (enc Encoding, buf []byte, err error) { + buf, err = readBOM(rd) + + if len(buf) >= 4 { + if isUTF32BigEndianBOM4(buf) { + return UTF32BigEndian, nilIfEmpty(buf[4:]), err + } + if isUTF32LittleEndianBOM4(buf) { + return UTF32LittleEndian, nilIfEmpty(buf[4:]), err + } + } + + if len(buf) > 2 && isUTF8BOM3(buf) { + return UTF8, nilIfEmpty(buf[3:]), err + } + + if (err != nil && !errors.Is(err, io.EOF)) || (len(buf) < 2) { + return Unknown, nilIfEmpty(buf), err + } + + if isUTF16BigEndianBOM2(buf) { + return UTF16BigEndian, nilIfEmpty(buf[2:]), err + } + if isUTF16LittleEndianBOM2(buf) { + return UTF16LittleEndian, nilIfEmpty(buf[2:]), err + } + + return Unknown, nilIfEmpty(buf), err +} + +func readBOM(rd io.Reader) (buf []byte, err error) { + const maxBOMSize = 4 + var bom [maxBOMSize]byte // used to read BOM + + // read as many bytes as possible + for nEmpty, n := 0, 0; err == nil && len(buf) < maxBOMSize; buf = bom[:len(buf)+n] { //nolint:wastedassign // copied code + if n, err = rd.Read(bom[len(buf):]); n < 0 { + return nil, errNegativeRead + } + if n > 0 { + nEmpty = 0 + } else { + nEmpty++ + if nEmpty >= maxConsecutiveEmptyReads { + err = io.ErrNoProgress + } + } + } + return +} + +func isUTF32BigEndianBOM4(buf []byte) bool { + return buf[0] == 0x00 && buf[1] == 0x00 && buf[2] == 0xFE && buf[3] == 0xFF +} + +func isUTF32LittleEndianBOM4(buf []byte) bool { + return buf[0] == 0xFF && buf[1] == 0xFE && buf[2] == 0x00 && buf[3] == 0x00 +} + +func isUTF8BOM3(buf []byte) bool { + return buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF +} + +func isUTF16BigEndianBOM2(buf []byte) bool { + return buf[0] == 0xFE && buf[1] == 0xFF +} + +func isUTF16LittleEndianBOM2(buf []byte) bool { + return buf[0] == 0xFF && buf[1] == 0xFE +} + +func nilIfEmpty(buf []byte) (res []byte) { + if len(buf) > 0 { + res = buf + } + return +} diff --git a/internal/utils/utfbom/utfbom_test.go b/internal/utils/utfbom/utfbom_test.go new file mode 100644 index 00000000..9f80027c --- /dev/null +++ b/internal/utils/utfbom/utfbom_test.go @@ -0,0 +1,246 @@ +package utfbom + +import ( + "errors" + "io" + "reflect" + "testing" + "testing/iotest" + "time" +) + +var testCases = []struct { + name string + input []byte + inputError error + encoding Encoding + output []byte +}{ + {"1", []byte{}, nil, Unknown, []byte{}}, + {"2", []byte("hello"), nil, Unknown, []byte("hello")}, + {"3", []byte("\xEF\xBB\xBF"), nil, UTF8, []byte{}}, + {"4", []byte("\xEF\xBB\xBFhello"), nil, UTF8, []byte("hello")}, + {"5", []byte("\xFE\xFF"), nil, UTF16BigEndian, []byte{}}, + {"6", []byte("\xFF\xFE"), nil, UTF16LittleEndian, []byte{}}, + {"7", []byte("\x00\x00\xFE\xFF"), nil, UTF32BigEndian, []byte{}}, + {"8", []byte("\xFF\xFE\x00\x00"), nil, UTF32LittleEndian, []byte{}}, + {"5", []byte("\xFE\xFF\x00\x68\x00\x65\x00\x6C\x00\x6C\x00\x6F"), nil, + UTF16BigEndian, []byte{0x00, 0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F}}, + {"6", []byte("\xFF\xFE\x68\x00\x65\x00\x6C\x00\x6C\x00\x6F\x00"), nil, + UTF16LittleEndian, []byte{0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F, 0x00}}, + {"7", []byte("\x00\x00\xFE\xFF\x00\x00\x00\x68\x00\x00\x00\x65\x00\x00\x00\x6C\x00\x00\x00\x6C\x00\x00\x00\x6F"), nil, + UTF32BigEndian, + []byte{0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x6F}}, + {"8", []byte("\xFF\xFE\x00\x00\x68\x00\x00\x00\x65\x00\x00\x00\x6C\x00\x00\x00\x6C\x00\x00\x00\x6F\x00\x00\x00"), nil, + UTF32LittleEndian, + []byte{0x68, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00}}, + {"9", []byte("\xEF"), nil, Unknown, []byte("\xEF")}, + {"10", []byte("\xEF\xBB"), nil, Unknown, []byte("\xEF\xBB")}, + {"11", []byte("\xEF\xBB\xBF"), io.ErrClosedPipe, UTF8, []byte{}}, + {"12", []byte("\xFE\xFF"), io.ErrClosedPipe, Unknown, []byte("\xFE\xFF")}, + {"13", []byte("\xFE"), io.ErrClosedPipe, Unknown, []byte("\xFE")}, + {"14", []byte("\xFF\xFE"), io.ErrClosedPipe, Unknown, []byte("\xFF\xFE")}, + {"15", []byte("\x00\x00\xFE\xFF"), io.ErrClosedPipe, UTF32BigEndian, []byte{}}, + {"16", []byte("\x00\x00\xFE"), io.ErrClosedPipe, Unknown, []byte{0x00, 0x00, 0xFE}}, + {"17", []byte("\x00\x00"), io.ErrClosedPipe, Unknown, []byte{0x00, 0x00}}, + {"18", []byte("\x00"), io.ErrClosedPipe, Unknown, []byte{0x00}}, + {"19", []byte("\xFF\xFE\x00\x00"), io.ErrClosedPipe, UTF32LittleEndian, []byte{}}, + {"20", []byte("\xFF\xFE\x00"), io.ErrClosedPipe, Unknown, []byte{0xFF, 0xFE, 0x00}}, + {"21", []byte("\xFF\xFE"), io.ErrClosedPipe, Unknown, []byte{0xFF, 0xFE}}, + {"22", []byte("\xFF"), io.ErrClosedPipe, Unknown, []byte{0xFF}}, + {"23", []byte("\x68\x65"), nil, Unknown, []byte{0x68, 0x65}}, +} + +type sliceReader struct { + input []byte + inputError error +} + +func (r *sliceReader) Read(p []byte) (n int, err error) { + if len(p) == 0 { + return + } + + if err = r.getError(); err != nil { + return + } + + n = copy(p, r.input) + r.input = r.input[n:] + err = r.getError() + return +} + +func (r *sliceReader) getError() (err error) { + if len(r.input) == 0 { + if r.inputError == nil { + err = io.EOF + } else { + err = r.inputError + } + } + return +} + +var readMakers = []struct { + name string + fn func(io.Reader) io.Reader +}{ + {"full", func(r io.Reader) io.Reader { return r }}, + {"byte", iotest.OneByteReader}, +} + +func TestSkip(t *testing.T) { + for _, tc := range testCases { + for _, readMaker := range readMakers { + r := readMaker.fn(&sliceReader{tc.input, tc.inputError}) + + sr, enc := Skip(r) + if enc != tc.encoding { + t.Fatalf("test %v reader=%s: expected encoding %v, but got %v", tc.name, readMaker.name, tc.encoding, enc) + } + + output, err := io.ReadAll(sr) + if !reflect.DeepEqual(output, tc.output) { + t.Fatalf("test %v reader=%s: expected to read %+#v, but got %+#v", tc.name, readMaker.name, tc.output, output) + } + if !errors.Is(err, tc.inputError) { + t.Fatalf("test %v reader=%s: expected to get %+#v error, but got %+#v", tc.name, readMaker.name, tc.inputError, err) + } + } + } +} + +func TestSkipSkip(t *testing.T) { + for _, tc := range testCases { + for _, readMaker := range readMakers { + r := readMaker.fn(&sliceReader{tc.input, tc.inputError}) + + sr0, _ := Skip(r) + sr, enc := Skip(sr0) + if enc != Unknown { + t.Fatalf("test %v reader=%s: expected encoding %v, but got %v", tc.name, readMaker.name, Unknown, enc) + } + + output, err := io.ReadAll(sr) + if !reflect.DeepEqual(output, tc.output) { + t.Fatalf("test %v reader=%s: expected to read %+#v, but got %+#v", tc.name, readMaker.name, tc.output, output) + } + if !errors.Is(err, tc.inputError) { + t.Fatalf("test %v reader=%s: expected to get %+#v error, but got %+#v", tc.name, readMaker.name, tc.inputError, err) + } + } + } +} + +func TestSkipOnly(t *testing.T) { + for _, tc := range testCases { + for _, readMaker := range readMakers { + r := readMaker.fn(&sliceReader{tc.input, tc.inputError}) + + sr := SkipOnly(r) + + output, err := io.ReadAll(sr) + if !reflect.DeepEqual(output, tc.output) { + t.Fatalf("test %v reader=%s: expected to read %+#v, but got %+#v", tc.name, readMaker.name, tc.output, output) + } + if !errors.Is(err, tc.inputError) { + t.Fatalf("test %v reader=%s: expected to get %+#v error, but got %+#v", tc.name, readMaker.name, tc.inputError, err) + } + } + } +} + +type zeroReader struct{} + +func (zeroReader) Read(p []byte) (int, error) { + return 0, nil +} + +type readerEncoding struct { + Rd *Reader + Enc Encoding +} + +func TestSkipZeroReader(t *testing.T) { + var z zeroReader + + c := make(chan readerEncoding) + go func() { + r, enc := Skip(z) + c <- readerEncoding{r, enc} + }() + + select { + case re := <-c: + if re.Enc != Unknown { + t.Error("Unknown encoding expected") + } else { + var b [1]byte + n, err := re.Rd.Read(b[:]) + if n != 0 { + t.Error("unexpected bytes count:", n) + } + if !errors.Is(err, io.ErrNoProgress) { + t.Error("unexpected error:", err) + } + } + case <-time.After(time.Second): + t.Error("test timed out (endless loop in Skip?)") + } +} + +func TestSkipOnlyZeroReader(t *testing.T) { + var z zeroReader + + c := make(chan *Reader) + go func() { + r := SkipOnly(z) + c <- r + }() + + select { + case r := <-c: + var b [1]byte + n, err := r.Read(b[:]) + if n != 0 { + t.Error("unexpected bytes count:", n) + } + if !errors.Is(err, io.ErrNoProgress) { + t.Error("unexpected error:", err) + } + case <-time.After(time.Second): + t.Error("test timed out (endless loop in Skip?)") + } +} + +func TestReader_ReadEmpty(t *testing.T) { + for _, tc := range testCases { + for _, readMaker := range readMakers { + r := readMaker.fn(&sliceReader{tc.input, tc.inputError}) + + sr := SkipOnly(r) + + n, err := sr.Read(nil) + if n != 0 { + t.Fatalf("test %v reader=%s: expected to read zero bytes, but got %v", tc.name, readMaker.name, n) + } + if err != nil { + t.Fatalf("test %v reader=%s: expected to get error, but got %+#v", tc.name, readMaker.name, err) + } + } + } +} + +func TestEncoding_String(t *testing.T) { + for e := Unknown; e <= UTF32LittleEndian; e++ { + s := e.String() + if s == "" { + t.Errorf("no string for %#v", e) + } + } + s := Encoding(999).String() + if s != "Unknown" { + t.Errorf("wrong string '%s' for invalid encoding", s) + } +} diff --git a/pemutil/pem_test.go b/pemutil/pem_test.go index bce677a3..95f0959d 100644 --- a/pemutil/pem_test.go +++ b/pemutil/pem_test.go @@ -343,7 +343,7 @@ func TestReadCertificate(t *testing.T) { {"testdata/ca.der", nil, nil}, {"testdata/bundle.crt", []Options{WithFirstBlock()}, nil}, {"testdata/bundle.crt", nil, errors.New("error decoding testdata/bundle.crt: contains more than one PEM encoded block")}, - {"testdata/notexists.crt", nil, errors.New("error reading testdata/notexists.crt: no such file or directory")}, + {"testdata/notexists.crt", nil, errors.New(`error reading "testdata/notexists.crt": no such file or directory`)}, {"testdata/badca.crt", nil, errors.New("error parsing testdata/badca.crt")}, {"testdata/badpem.crt", nil, errors.New("file testdata/badpem.crt does not contain a valid PEM encoded certificate")}, {"testdata/badder.crt", nil, errors.New("error parsing testdata/badder.crt")}, @@ -376,7 +376,7 @@ func TestReadCertificateBundle(t *testing.T) { {"testdata/ca.der", 1, nil}, {"testdata/bundle.crt", 2, nil}, {"testdata/extrajunkbundle.crt", 2, nil}, - {"testdata/notexists.crt", 0, errors.New("error reading testdata/notexists.crt: no such file or directory")}, + {"testdata/notexists.crt", 0, errors.New(`error reading "testdata/notexists.crt": no such file or directory`)}, {"testdata/badca.crt", 0, errors.New("error parsing testdata/badca.crt")}, {"testdata/badpem.crt", 0, errors.New("file testdata/badpem.crt does not contain a valid PEM encoded certificate")}, {"testdata/badder.crt", 0, errors.New("error parsing testdata/badder.crt")},