-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreader.go
More file actions
146 lines (133 loc) · 3.84 KB
/
Copy pathreader.go
File metadata and controls
146 lines (133 loc) · 3.84 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
package languages
import (
"context"
"errors"
"io"
"sync"
"github.com/git-pkgs/magic"
)
const (
formatBytes = 512
maxEmptyReads = 100
allTextControls = "\x01\x02\x03\x04\x05\x06\x07\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
)
type ReadOptions struct {
Bytes int64 // Zero reads to EOF; positive values set an exact read budget.
Prefix bool // The reader contains a prefix even if it reaches EOF.
// Filename limits extension heuristics to this name; empty evaluates all groups.
Filename string
}
// AnalyzeReader reads content with bounded memory. Reaching a byte budget without
// EOF leaves Prefix true. Errors clear dst. Cancellation is checked between reads;
// it cannot interrupt an io.Reader blocked inside Read.
func AnalyzeReader(ctx context.Context, reader io.Reader, options ReadOptions, dst *Analysis) error {
return analyzeReader(ctx, reader, options, dst, -1)
}
var readerStreams = sync.Pool{New: func() any { return new(readerStream) }}
type readerStream struct {
text textStream
decoder textDecoder
validator magic.TextValidator
buffer [textChunkBytes]byte
head [formatBytes]byte
headSize int
started, binary bool
}
func analyzeReader(ctx context.Context, reader io.Reader, options ReadOptions, dst *Analysis, size int64) error {
*dst = Analysis{}
if options.Bytes < 0 {
return errors.New("bytes must be zero or greater")
}
s := readerStreams.Get().(*readerStream)
defer readerStreams.Put(s)
s.reset()
count, complete, err := s.read(ctx, reader, options.Bytes)
if err != nil {
return err
}
complete = (complete || size >= 0 && count == size) && !options.Prefix
heuristic := allHeuristics
if options.Filename != "" {
heuristic = AnalyzePath(options.Filename).heuristic
}
*dst = s.finish(complete, heuristic)
dst.Bytes = count
return nil
}
func (s *readerStream) reset() {
s.text.reset()
s.decoder = textDecoder{}
s.validator = magic.TextValidator{}
s.headSize, s.started, s.binary = 0, false, false
}
func (s *readerStream) read(ctx context.Context, reader io.Reader, limit int64) (int64, bool, error) {
var count int64
empty := 0
for {
if err := ctx.Err(); err != nil {
return count, false, err
}
next := int64(len(s.buffer))
if limit > 0 {
next = min(next, limit-count)
if next == 0 {
return count, false, nil
}
}
n, err := reader.Read(s.buffer[:next])
if n > 0 {
s.write(s.buffer[:n])
count += int64(n)
empty = 0
} else {
empty++
}
if cancelled := ctx.Err(); cancelled != nil {
return count, false, cancelled
}
if err != nil {
if errors.Is(err, io.EOF) {
return count, true, nil
}
return count, false, err
}
if empty >= maxEmptyReads {
return count, false, io.ErrNoProgress
}
}
}
func (s *readerStream) write(data []byte) {
_, _ = s.validator.Write(data)
if !s.started {
n := copy(s.head[s.headSize:], data)
s.headSize += n
data = data[n:]
if s.headSize < len(s.head) {
return
}
s.start()
}
if !s.binary {
s.decoder.write(data, &s.text)
}
}
func (s *readerStream) start() {
s.started = true
// Controls are validated over the full stream, independently of signatures.
format := magic.DetectWithOptions(s.head[:s.headSize], magic.Options{Prefix: true, TextControls: allTextControls})
s.binary = format.Kind == magic.KindBinary
s.decoder.start(format.Encoding)
if !s.binary {
s.decoder.write(s.head[:s.headSize], &s.text)
}
}
func (s *readerStream) finish(complete bool, heuristic uint16) Analysis {
if !s.started {
s.start()
}
format := s.validator.Result(magic.Options{Prefix: !complete, TextControls: sourceTextControls})
if s.binary || format.Kind == magic.KindBinary || format.Kind == magic.KindUnknown && format.Encoding != "" {
return Analysis{Binary: true, Prefix: !complete}
}
return s.text.finish(complete, heuristic)
}