github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/content/reader.go (about) 1 /* 2 Copyright The ORAS Authors. 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 16 package content 17 18 import ( 19 "crypto/sha256" 20 "encoding" 21 "encoding/hex" 22 "errors" 23 "fmt" 24 "hash" 25 "io" 26 "strconv" 27 28 "github.com/opencontainers/go-digest" 29 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 30 "oras.land/oras-go/v2/internal/spec" 31 ) 32 33 var ( 34 // ErrInvalidDescriptorSize is returned by ReadAll() when 35 // the descriptor has an invalid size. 36 ErrInvalidDescriptorSize = errors.New("invalid descriptor size") 37 38 // ErrMismatchedDigest is returned by ReadAll() when 39 // the descriptor has an invalid digest. 40 ErrMismatchedDigest = errors.New("mismatched digest") 41 42 // ErrTrailingData is returned by ReadAll() when 43 // there exists trailing data unread when the read terminates. 44 ErrTrailingData = errors.New("trailing data") 45 ) 46 47 var ( 48 // errEarlyVerify is returned by VerifyReader.Verify() when 49 // Verify() is called before completing reading the entire content blob. 50 errEarlyVerify = errors.New("early verify") 51 ) 52 53 // VerifyReader reads the content described by its descriptor and verifies 54 // against its size and digest. 55 type VerifyReader struct { 56 base *io.LimitedReader 57 verifier digest.Verifier 58 verified bool 59 err error 60 resume bool 61 } 62 63 // Read reads up to len(p) bytes into p. It returns the number of bytes 64 // read (0 <= n <= len(p)) and any error encountered. 65 func (vr *VerifyReader) Read(p []byte) (n int, err error) { 66 if vr.err != nil { 67 return 0, vr.err 68 } 69 70 n, err = vr.base.Read(p) 71 if err != nil { 72 if err == io.EOF && vr.base.N > 0 && !vr.resume { 73 err = io.ErrUnexpectedEOF 74 } 75 vr.err = err 76 } 77 return 78 } 79 80 // Verify checks for remaining unread content and verifies the read content against the digest 81 func (vr *VerifyReader) Verify() error { 82 if vr.verified { 83 return nil 84 } 85 if vr.err == nil { 86 if vr.base.N > 0 { 87 return errEarlyVerify 88 } 89 } else if vr.err != io.EOF { 90 return vr.err 91 } 92 93 if err := ensureEOF(vr.base.R); err != nil { 94 vr.err = err 95 return vr.err 96 } 97 if !vr.verifier.Verified() { 98 vr.err = ErrMismatchedDigest 99 return vr.err 100 } 101 102 vr.verified = true 103 vr.err = io.EOF 104 return nil 105 } 106 107 // NewVerifyReader wraps r for reading content with verification against desc. 108 func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader { 109 var verifier digest.Verifier 110 111 // Ignore error, if we can't parse it assume zero 112 offset, _ := strconv.ParseInt(desc.Annotations[spec.AnnotationResumeOffset], 10, 64) 113 114 // All error cases below fall through to create a digest.Verifier 115 if offset > 0 { 116 // Attempt to resume 117 newHash, err := DecodeHash(desc.Annotations[spec.AnnotationResumeHash], desc.Digest) 118 if err == nil { 119 // Create a verifier with our in-progress hash and the final digest 120 verifier = hashVerifier{ 121 hash: newHash, 122 digest: desc.Digest, 123 } 124 } 125 } 126 if verifier == nil { 127 // Did not get a verifier for resume, make a new empty one 128 verifier = desc.Digest.Verifier() 129 } 130 131 lr := &io.LimitedReader{ 132 R: io.TeeReader(r, verifier), 133 N: desc.Size, 134 } 135 return &VerifyReader{ 136 base: lr, 137 verifier: verifier, 138 resume: offset > 0, 139 } 140 } 141 142 // ReadAll safely reads the content described by the descriptor. 143 // The read content is verified against the size and the digest 144 // using a VerifyReader. 145 func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) { 146 if desc.Size < 0 { 147 return nil, ErrInvalidDescriptorSize 148 } 149 buf := make([]byte, desc.Size) 150 151 vr := NewVerifyReader(r, desc) 152 if n, err := io.ReadFull(vr, buf); err != nil { 153 if errors.Is(err, io.ErrUnexpectedEOF) { 154 if err == io.ErrUnexpectedEOF && vr.base.N > 0 && vr.resume { 155 // In resume mode the buffers may not be exact 156 err = io.EOF 157 } 158 return nil, fmt.Errorf("read failed: expected content size of %d, got %d, for digest %s: %w", desc.Size, n, desc.Digest.String(), err) 159 } 160 return nil, fmt.Errorf("read failed: %w", err) 161 } 162 if err := vr.Verify(); err != nil { 163 return nil, err 164 } 165 return buf, nil 166 } 167 168 // ensureEOF ensures the read operation ends with an EOF and no 169 // trailing data is present. 170 func ensureEOF(r io.Reader) error { 171 var peek [1]byte 172 _, err := io.ReadFull(r, peek[:]) 173 if err != io.EOF { 174 return ErrTrailingData 175 } 176 return nil 177 } 178 179 // Decode a Hash object 180 func DecodeHash(encHash string, d digest.Digest) (hash.Hash, error) { 181 // Recover hash of existing data 182 state, err := hex.DecodeString(encHash) 183 if err == nil { 184 // Recover Hash object 185 newHash := d.Algorithm().Hash() 186 unmarshaler, ok := newHash.(encoding.BinaryUnmarshaler) 187 if ok { 188 if err := unmarshaler.UnmarshalBinary(state); err == nil { 189 return newHash, nil 190 } 191 } 192 } 193 // Return new empty Hash with error 194 return sha256.New(), err 195 } 196 197 // Encode a Hash object 198 func EncodeHash(h hash.Hash) (string, error) { 199 // Yay! Serialize the hash to pass to the Verifier 200 marshaler, ok := h.(encoding.BinaryMarshaler) 201 if ok { 202 state, err := marshaler.MarshalBinary() 203 if err == nil { 204 // Save the new Hash as an Annotation to pass to the Verifier 205 buf := make([]byte, hex.EncodedLen(len(state))) 206 hex.Encode(buf, state) 207 return string(buf), nil 208 } 209 } 210 return "", fmt.Errorf("error encoding Hash") 211 }