go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/chunkstream/view.go (about) 1 // Copyright 2015 The LUCI 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 chunkstream 16 17 import ( 18 "bytes" 19 "errors" 20 "io" 21 ) 22 23 // View is static read-only snapshot of the contents of the Buffer, presented 24 // as a contiguous stream of bytes. 25 // 26 // View implements the io.Reader and io.ByteReader interfaces. It also offers a 27 // series of utility functions optimized for the chunks. 28 type View struct { 29 // cur is the first node of the view. 30 cur *chunkNode 31 // cidx is the byte offset within cur of the current byte. 32 cidx int 33 // size is the size of thew view. Accesses beyond this size will fail. 34 size int64 35 // consumed is a count of the number of bytes in the view that have been 36 // consumed via Skip(). 37 consumed int64 38 39 // b is the Buffer from which this View's snapshot was taken. 40 b *Buffer 41 } 42 43 var _ interface { 44 io.Reader 45 io.ByteReader 46 } = (*View)(nil) 47 48 func (r *View) Read(b []byte) (int, error) { 49 total := int64(0) 50 err := error(nil) 51 for len(b) > 0 { 52 chunk := r.chunkBytes() 53 if len(chunk) == 0 { 54 err = io.EOF 55 break 56 } 57 58 amount := copy(b, chunk) 59 total += int64(amount) 60 b = b[amount:] 61 r.Skip(int64(amount)) 62 } 63 if r.Remaining() == 0 { 64 err = io.EOF 65 } 66 return int(total), err 67 } 68 69 // ReadByte implements io.ByteReader, reading a single byte from the buffer. 70 func (r *View) ReadByte() (byte, error) { 71 chunk := r.chunkBytes() 72 if len(chunk) == 0 { 73 return 0, io.EOF 74 } 75 r.Skip(1) 76 return chunk[0], nil 77 } 78 79 // Remaining returns the number of bytes remaining in the Reader view. 80 func (r *View) Remaining() int64 { 81 return r.size 82 } 83 84 // Consumed returns the number of bytes that have been skipped via Skip or 85 // higher-level calls. 86 func (r *View) Consumed() int64 { 87 return r.consumed 88 } 89 90 // Skip advances the View forwards a fixed number of bytes. 91 func (r *View) Skip(count int64) { 92 for count > 0 { 93 if r.cur == nil { 94 panic(errors.New("cannot skip past end buffer")) 95 } 96 97 amount := r.chunkRemaining() 98 if count < int64(amount) { 99 amount = int(count) 100 r.cidx += amount 101 } else { 102 // Finished consuming this chunk, move on to the next. 103 r.cur = r.cur.next 104 r.cidx = 0 105 } 106 107 count -= int64(amount) 108 r.consumed += int64(amount) 109 r.size -= int64(amount) 110 } 111 } 112 113 // Index scans the View for the specified needle bytes. If they are 114 // found, their index in the View is returned. Otherwise, Index returns 115 // -1. 116 // 117 // The View is not modified during the search. 118 func (r *View) Index(needle []byte) int64 { 119 if r.Remaining() == 0 { 120 return -1 121 } 122 if len(needle) == 0 { 123 return 0 124 } 125 126 rc := r.Clone() 127 if !rc.indexDestructive(needle) { 128 return -1 129 } 130 return rc.consumed - r.consumed 131 } 132 133 // indexDestructive implements Index by actively mutating the View. 134 // 135 // It returns true if the needle was found, and false if not. The view will be 136 // mutated regardless. 137 func (r *View) indexDestructive(needle []byte) bool { 138 tbuf := make([]byte, 2*len(needle)) 139 idx := int64(0) 140 for { 141 data := r.chunkBytes() 142 if len(data) == 0 { 143 return false 144 } 145 146 // Scan the current chunk for needle. Note that if the current chunk is too 147 // small to hold needle, this is a no-op. 148 if idx = int64(bytes.Index(data, needle)); idx >= 0 { 149 r.Skip(idx) 150 return true 151 } 152 if len(data) > len(needle) { 153 // The needle is definitely not in this space. 154 r.Skip(int64(len(data) - len(needle))) 155 } 156 157 // needle isn't in the current chunk; however, it may begin at the end of 158 // the current chunk and complete in future chunks. 159 // 160 // We will scan a space twice the size of the needle, as otherwise, this 161 // would end up scanning for one possibility, incrementing by one, and 162 // repeating via 'for' loop iterations. 163 // 164 // Afterwards, we advance only the size of the needle, as we don't want to 165 // preclude the needle starting after our last scan range. 166 // 167 // For example, to find needle "NDL": 168 // 169 // AAAAND|L|AAAA 170 // |------|^- [NDLAAA], 0 171 // 172 // AAAAN|D|NDL|AAAA 173 // |------| [ANDNDL], 3 174 // 175 // AAAA|A|A|NDL 176 // |-------| [AAAAND], -1, consume 3 => A|NDL| 177 // 178 // 179 // Note that we perform the read with a cloned View so we don't 180 // actually consume this data. 181 pr := r.Clone() 182 amt, _ := pr.Read(tbuf) 183 if amt < len(needle) { 184 // All remaining buffers cannot hold the needle. 185 return false 186 } 187 188 if idx = int64(bytes.Index(tbuf[:amt], needle)); idx >= 0 { 189 r.Skip(idx) 190 return true 191 } 192 r.Skip(int64(len(needle))) 193 } 194 } 195 196 // Clone returns a copy of the View view. 197 // 198 // The clone is bound to the same underlying Buffer as the source. 199 func (r *View) Clone() *View { 200 return r.CloneLimit(r.size) 201 } 202 203 // CloneLimit returns a copy of the View view, optionally truncating it. 204 // 205 // The clone is bound to the same underlying Buffer as the source. 206 func (r *View) CloneLimit(limit int64) *View { 207 c := *r 208 if c.size > limit { 209 c.size = limit 210 } 211 return &c 212 } 213 214 func (r *View) chunkRemaining() int { 215 if r.cur == nil { 216 return 0 217 } 218 result := r.cur.length() - r.cidx 219 if int64(result) > r.size { 220 result = int(r.size) 221 } 222 return result 223 } 224 225 func (r *View) chunkBytes() []byte { 226 if r.cur == nil { 227 return nil 228 } 229 data := r.cur.Bytes()[r.cidx:] 230 remaining := r.Remaining() 231 if int64(len(data)) > remaining { 232 data = data[:remaining] 233 } 234 return data 235 }