-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtypes.go
More file actions
477 lines (431 loc) · 12.1 KB
/
types.go
File metadata and controls
477 lines (431 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
package imx
import (
"bytes"
"encoding/json"
"fmt"
"io"
"sync"
"github.com/gomantics/imx/internal/parser"
)
// Re-export parser types as the public API types
type (
TagID = parser.TagID
Tag = parser.Tag
Directory = parser.Directory
)
// Metadata is the top-level container for all parsed metadata.
// Fields are unexported to prevent external mutation; use accessor methods instead.
type Metadata struct {
directories []Directory // All parsed directories
errors []error // All errors encountered during parsing
index map[TagID]*Tag // Lazy-built index for O(1) tag lookup
mu sync.RWMutex // Protects index during lazy initialization
}
// Directories returns a slice of all parsed metadata directories.
// The returned slice is a copy to prevent external modification.
func (m *Metadata) Directories() []Directory {
if m == nil {
return nil
}
dirs := make([]Directory, len(m.directories))
copy(dirs, m.directories)
return dirs
}
// Errors returns a slice of all errors encountered during parsing.
// The returned slice is a copy to prevent external modification.
func (m *Metadata) Errors() []error {
if m == nil {
return nil
}
errs := make([]error, len(m.errors))
copy(errs, m.errors)
return errs
}
// Directory returns the directory with the given name
func (m *Metadata) Directory(name string) (Directory, bool) {
for _, dir := range m.directories {
if dir.Name == name {
return dir, true
}
}
return Directory{}, false
}
// Tag returns the tag with the given ID using an efficient index.
// The index is built lazily on first call and cached for subsequent calls.
func (m *Metadata) Tag(id TagID) (Tag, bool) {
// Fast path: check if index exists (read lock)
m.mu.RLock()
if m.index != nil {
tag, ok := m.index[id]
m.mu.RUnlock()
if ok {
return *tag, true
}
return Tag{}, false
}
m.mu.RUnlock()
// Slow path: build index (write lock)
m.mu.Lock()
// Double-check in case another goroutine built it
if m.index == nil {
m.buildIndex()
}
tag, ok := m.index[id]
m.mu.Unlock()
if ok {
return *tag, true
}
return Tag{}, false
}
// buildIndex builds the internal index for O(1) tag lookup.
// Caller must hold m.mu.
func (m *Metadata) buildIndex() {
m.index = make(map[TagID]*Tag)
for i := range m.directories {
dir := &m.directories[i]
for j := range dir.Tags {
tag := &dir.Tags[j]
m.index[tag.ID] = tag
}
}
}
// GetAll returns a map of values for the given tag IDs
func (m *Metadata) GetAll(ids ...TagID) map[TagID]any {
result := make(map[TagID]any, len(ids))
for _, id := range ids {
if tag, ok := m.Tag(id); ok {
result[id] = tag.Value
}
}
return result
}
// Each iterates over all tags, calling fn for each tag.
// If fn returns false, iteration stops.
func (m *Metadata) Each(fn func(Directory, Tag) bool) {
for _, dir := range m.directories {
for _, tag := range dir.Tags {
if !fn(dir, tag) {
return
}
}
}
}
// EachTag iterates over all tags across all directories.
// If fn returns false, iteration stops.
func (m *Metadata) EachTag(fn func(Tag) bool) {
for _, dir := range m.directories {
for _, tag := range dir.Tags {
if !fn(tag) {
return
}
}
}
}
// EachInDirectory iterates over tags in the given directory.
// If fn returns false, iteration stops.
func (m *Metadata) EachInDirectory(name string, fn func(Tag) bool) {
for _, dir := range m.directories {
if dir.Name == name {
for _, tag := range dir.Tags {
if !fn(tag) {
return
}
}
return
}
}
}
// AllTags returns a flat slice of all tags across all directories.
// The order matches the iteration order (directory order, then tag order within each directory).
func (m *Metadata) AllTags() []Tag {
var tags []Tag
for _, dir := range m.directories {
tags = append(tags, dir.Tags...)
}
return tags
}
// DirectoryNames returns a list of all directory names present in the metadata.
func (m *Metadata) DirectoryNames() []string {
names := make([]string, 0, len(m.directories))
for _, dir := range m.directories {
names = append(names, dir.Name)
}
return names
}
// TagCount returns the total number of tags across all directories.
func (m *Metadata) TagCount() int {
count := 0
for _, dir := range m.directories {
count += len(dir.Tags)
}
return count
}
// GetString returns the tag value as a string.
//
// Conversion rules:
// - string: returned as-is
// - []byte: converted to string
// - fmt.Stringer: calls String() method
// - all other types: converted using fmt.Sprintf("%v", value)
//
// The fallback conversion allows numeric types (int, float, etc.) commonly found
// in metadata to be displayed as strings. For type-safe numeric conversions,
// use GetInt or GetFloat instead.
//
// Returns an error only if the tag doesn't exist.
func (m *Metadata) GetString(id TagID) (string, error) {
tag, ok := m.Tag(id)
if !ok {
return "", fmt.Errorf("tag %q not found", id)
}
switch v := tag.Value.(type) {
case string:
return v, nil
case []byte:
return string(v), nil
case fmt.Stringer:
return v.String(), nil
default:
// Fallback for numeric and other types commonly found in metadata
return fmt.Sprintf("%v", v), nil
}
}
// GetInt returns the tag value as an int64.
// Returns an error if the tag doesn't exist or cannot be converted to int64.
func (m *Metadata) GetInt(id TagID) (int64, error) {
tag, ok := m.Tag(id)
if !ok {
return 0, fmt.Errorf("tag %q not found", id)
}
switch v := tag.Value.(type) {
case int:
return int64(v), nil
case int8:
return int64(v), nil
case int16:
return int64(v), nil
case int32:
return int64(v), nil
case int64:
return v, nil
case uint:
return int64(v), nil
case uint8:
return int64(v), nil
case uint16:
return int64(v), nil
case uint32:
return int64(v), nil
case uint64:
if v > 1<<63-1 {
return 0, fmt.Errorf("value %d overflows int64", v)
}
return int64(v), nil
default:
return 0, fmt.Errorf("cannot convert %T to int64", v)
}
}
// GetFloat returns the tag value as a float64.
// Returns an error if the tag doesn't exist or cannot be converted to float64.
func (m *Metadata) GetFloat(id TagID) (float64, error) {
tag, ok := m.Tag(id)
if !ok {
return 0, fmt.Errorf("tag %q not found", id)
}
switch v := tag.Value.(type) {
case float32:
return float64(v), nil
case float64:
return v, nil
case int:
return float64(v), nil
case int8:
return float64(v), nil
case int16:
return float64(v), nil
case int32:
return float64(v), nil
case int64:
return float64(v), nil
case uint:
return float64(v), nil
case uint8:
return float64(v), nil
case uint16:
return float64(v), nil
case uint32:
return float64(v), nil
case uint64:
return float64(v), nil
default:
return 0, fmt.Errorf("cannot convert %T to float64", v)
}
}
// GetBytes returns the tag value as a byte slice.
// Returns an error if the tag doesn't exist or is not a byte slice or string.
func (m *Metadata) GetBytes(id TagID) ([]byte, error) {
tag, ok := m.Tag(id)
if !ok {
return nil, fmt.Errorf("tag %q not found", id)
}
switch v := tag.Value.(type) {
case []byte:
return v, nil
case string:
return []byte(v), nil
default:
return nil, fmt.Errorf("cannot convert %T to []byte", v)
}
}
// MarshalJSON implements json.Marshaler for Metadata.
// The JSON structure is:
//
// {
// "directories": [...],
// "errors": [...]
// }
func (m *Metadata) MarshalJSON() ([]byte, error) {
type Alias Metadata
// Convert errors to strings for JSON serialization
var errorStrings []string
if len(m.errors) > 0 {
errorStrings = make([]string, len(m.errors))
for i, err := range m.errors {
errorStrings[i] = err.Error()
}
}
return json.Marshal(&struct {
Directories []Directory `json:"directories"`
Errors []string `json:"errors,omitempty"`
}{
Directories: m.directories,
Errors: errorStrings,
})
}
// readerAdapter implements io.ReaderAt by buffering data from an io.Reader.
//
// This adapter enables parsers that require random access (io.ReaderAt) to work
// with streaming sources (io.Reader) like HTTP responses or pipes. It achieves
// this by buffering data on-demand as the parser requests it.
//
// Buffering strategy:
// - Data is read from the underlying io.Reader only when needed
// - All read data is cached in an internal buffer
// - Subsequent reads from already-buffered regions are served from cache
// - Memory usage grows only as needed by the parser
//
// Performance characteristics:
// - First read at offset N: O(N) - must buffer all data up to N
// - Subsequent reads: O(1) - served directly from buffer
// - Memory: O(max offset accessed)
// - Best for: Sequential or forward-seeking access patterns
// - Worst for: Random backward seeks (entire stream must be buffered)
//
// This design is optimized for image metadata parsers, which typically:
// - Read headers sequentially from the beginning
// - Occasionally seek to known offsets for specific data blocks
// - Rarely seek backward to earlier positions
type readerAdapter struct {
r io.Reader // Underlying streaming source
buffer *bytes.Buffer // Accumulated data buffer
eof bool // Whether we've reached EOF on the source
limit int64 // Maximum bytes to buffer (0 = unlimited)
bufSize int // Read chunk size
lastErr error // Sticky error (e.g., max bytes exceeded)
}
// boundedReaderAt wraps an io.ReaderAt and enforces a byte limit.
type boundedReaderAt struct {
r io.ReaderAt
limit int64 // 0 = unlimited
lastErr error
}
// newReaderAdapter creates a new adapter that wraps an io.Reader.
// The adapter starts with an empty buffer and reads data on-demand.
func newReaderAdapter(r io.Reader, maxBytes int64, bufferSize int) *readerAdapter {
if bufferSize <= 0 {
bufferSize = 64 << 10 // default 64KB
}
return &readerAdapter{
r: r,
buffer: &bytes.Buffer{},
eof: false,
limit: maxBytes,
bufSize: bufferSize,
}
}
// ReadAt enforces the configured byte limit before delegating.
func (b *boundedReaderAt) ReadAt(p []byte, off int64) (int, error) {
if b.limit > 0 && off+int64(len(p)) > b.limit {
b.lastErr = ErrMaxBytesExceeded
return 0, ErrMaxBytesExceeded
}
return b.r.ReadAt(p, off)
}
// LastError returns the most recent error encountered by the bounded reader.
func (b *boundedReaderAt) LastError() error {
return b.lastErr
}
// ReadAt reads len(p) bytes into p starting at offset off.
// It implements the io.ReaderAt interface by buffering data from the underlying reader.
// Returns io.ErrUnexpectedEOF if we hit EOF before reading all requested bytes.
func (ra *readerAdapter) ReadAt(p []byte, off int64) (n int, err error) {
// Enforce max bytes limit
if ra.limit > 0 && off+int64(len(p)) > ra.limit {
ra.lastErr = ErrMaxBytesExceeded
return 0, ErrMaxBytesExceeded
}
// Ensure we have enough data buffered
currentSize := int64(ra.buffer.Len())
needed := off + int64(len(p))
if needed > currentSize && !ra.eof {
// Need to read more data from the source
toRead := needed - currentSize
chunkSize := int64(ra.bufSize)
if chunkSize <= 0 {
chunkSize = toRead
}
for toRead > 0 {
readLen := chunkSize
if toRead < readLen {
readLen = toRead
}
chunk := make([]byte, readLen)
nr, readErr := io.ReadFull(ra.r, chunk)
if nr > 0 {
ra.buffer.Write(chunk[:nr])
toRead -= int64(nr)
}
if readErr == io.EOF || readErr == io.ErrUnexpectedEOF {
ra.eof = true
break
} else if readErr != nil && readErr != io.EOF {
ra.lastErr = readErr
return 0, readErr
}
// Stop early if we've met the required buffer size
if toRead <= 0 {
break
}
// Respect limit
if ra.limit > 0 && int64(ra.buffer.Len()) >= ra.limit {
ra.lastErr = ErrMaxBytesExceeded
return 0, ErrMaxBytesExceeded
}
}
}
// Read from buffer
bufData := ra.buffer.Bytes()
if off >= int64(len(bufData)) {
return 0, io.EOF
}
n = copy(p, bufData[off:])
if n < len(p) {
// Couldn't read all requested bytes - return UnexpectedEOF
return n, io.ErrUnexpectedEOF
}
return n, nil
}
// LastError returns the last sticky error encountered by the adapter.
func (ra *readerAdapter) LastError() error {
return ra.lastErr
}