storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/etag/reader.go (about)

     1  // MinIO Cloud Storage, (C) 2021 MinIO, Inc.
     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 etag
    16  
    17  import (
    18  	"crypto/md5"
    19  	"fmt"
    20  	"hash"
    21  	"io"
    22  )
    23  
    24  // Tagger is the interface that wraps the basic ETag method.
    25  type Tagger interface {
    26  	ETag() ETag
    27  }
    28  
    29  type wrapReader struct {
    30  	io.Reader
    31  	Tagger
    32  }
    33  
    34  // ETag returns the ETag of the underlying Tagger.
    35  func (r *wrapReader) ETag() ETag {
    36  	if r.Tagger == nil {
    37  		return nil
    38  	}
    39  	return r.Tagger.ETag()
    40  }
    41  
    42  // Wrap returns an io.Reader that reads from the wrapped
    43  // io.Reader and implements the Tagger interaface.
    44  //
    45  // If content implements Tagger then the returned Reader
    46  // returns ETag of the content. Otherwise, it returns
    47  // nil as ETag.
    48  //
    49  // Wrap provides an adapter for io.Reader implemetations
    50  // that don't implement the Tagger interface.
    51  // It is mainly used to provide a high-level io.Reader
    52  // access to the ETag computed by a low-level io.Reader:
    53  //
    54  //   content := etag.NewReader(r.Body, nil)
    55  //
    56  //   compressedContent := Compress(content)
    57  //   encryptedContent := Encrypt(compressedContent)
    58  //
    59  //   // Now, we need an io.Reader that can access
    60  //   // the ETag computed over the content.
    61  //   reader := etag.Wrap(encryptedContent, content)
    62  //
    63  func Wrap(wrapped, content io.Reader) io.Reader {
    64  	if t, ok := content.(Tagger); ok {
    65  		return wrapReader{
    66  			Reader: wrapped,
    67  			Tagger: t,
    68  		}
    69  	}
    70  	return wrapReader{
    71  		Reader: wrapped,
    72  	}
    73  }
    74  
    75  // A Reader wraps an io.Reader and computes the
    76  // MD5 checksum of the read content as ETag.
    77  //
    78  // Optionally, a Reader can also verify that
    79  // the computed ETag matches an expected value.
    80  // Therefore, it compares both ETags once the
    81  // underlying io.Reader returns io.EOF.
    82  // If the computed ETag does not match the
    83  // expected ETag then Read returns a VerifyError.
    84  //
    85  // Reader implements the Tagger interface.
    86  type Reader struct {
    87  	src io.Reader
    88  
    89  	md5      hash.Hash
    90  	checksum ETag
    91  
    92  	readN int64
    93  }
    94  
    95  // NewReader returns a new Reader that computes the
    96  // MD5 checksum of the content read from r as ETag.
    97  //
    98  // If the provided etag is not nil the returned
    99  // Reader compares the etag with the computed
   100  // MD5 sum once the r returns io.EOF.
   101  func NewReader(r io.Reader, etag ETag) *Reader {
   102  	if er, ok := r.(*Reader); ok {
   103  		if er.readN == 0 && Equal(etag, er.checksum) {
   104  			return er
   105  		}
   106  	}
   107  	return &Reader{
   108  		src:      r,
   109  		md5:      md5.New(),
   110  		checksum: etag,
   111  	}
   112  }
   113  
   114  // Read reads up to len(p) bytes from the underlying
   115  // io.Reader as specified by the io.Reader interface.
   116  func (r *Reader) Read(p []byte) (int, error) {
   117  	n, err := r.src.Read(p)
   118  	r.readN += int64(n)
   119  	r.md5.Write(p[:n])
   120  
   121  	if err == io.EOF && len(r.checksum) != 0 {
   122  		if etag := r.ETag(); !Equal(etag, r.checksum) {
   123  			return n, VerifyError{
   124  				Expected: r.checksum,
   125  				Computed: etag,
   126  			}
   127  		}
   128  	}
   129  	return n, err
   130  }
   131  
   132  // ETag returns the ETag of all the content read
   133  // so far. Reading more content changes the MD5
   134  // checksum. Therefore, calling ETag multiple
   135  // times may return different results.
   136  func (r *Reader) ETag() ETag {
   137  	sum := r.md5.Sum(nil)
   138  	return ETag(sum)
   139  }
   140  
   141  // VerifyError is an error signaling that a
   142  // computed ETag does not match an expected
   143  // ETag.
   144  type VerifyError struct {
   145  	Expected ETag
   146  	Computed ETag
   147  }
   148  
   149  func (v VerifyError) Error() string {
   150  	return fmt.Sprintf("etag: expected ETag %q does not match computed ETag %q", v.Expected, v.Computed)
   151  }