github.com/yandex/pandora@v0.5.32/components/providers/http/decoders/jsonline.go (about) 1 package decoders 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "sync" 12 13 "github.com/yandex/pandora/components/providers/http/config" 14 "github.com/yandex/pandora/components/providers/http/decoders/ammo" 15 "github.com/yandex/pandora/core" 16 "golang.org/x/xerrors" 17 ) 18 19 func newJsonlineDecoder(file io.ReadSeeker, cfg config.Config, decodedConfigHeaders http.Header) (*jsonlineDecoder, error) { 20 scanner := bufio.NewScanner(file) 21 if cfg.MaxAmmoSize != 0 { 22 var buffer []byte 23 scanner.Buffer(buffer, cfg.MaxAmmoSize) 24 } 25 26 isArray, err := isArray(file) 27 if err != nil { 28 return nil, err 29 } 30 31 decoder := &jsonlineDecoder{ 32 protoDecoder: protoDecoder{ 33 file: file, 34 config: cfg, 35 decodedConfigHeaders: decodedConfigHeaders, 36 }, 37 scanner: scanner, 38 pool: &sync.Pool{New: func() any { return &ammo.Ammo{} }}, 39 decoder: json.NewDecoder(file), 40 } 41 if isArray { 42 ammos, err := decoder.readArray() 43 if err != nil { 44 return decoder, fmt.Errorf("cant read json array: %w", err) 45 } 46 decoder.ammos = ammos 47 } 48 return decoder, nil 49 } 50 51 func isArray(r io.ReadSeeker) (bool, error) { 52 d := json.NewDecoder(r) 53 t, err := d.Token() 54 if err != nil { 55 return false, err 56 } 57 delim, ok := t.(json.Delim) 58 if !ok { 59 return false, errors.New("invalid json token") 60 } 61 _, err = r.Seek(0, io.SeekStart) 62 return delim.String() == "[", err 63 } 64 65 type jsonlineDecoder struct { 66 protoDecoder 67 scanner *bufio.Scanner 68 line uint 69 pool *sync.Pool 70 decoder *json.Decoder 71 ammos []DecodedAmmo 72 } 73 74 func (d *jsonlineDecoder) Release(a core.Ammo) { 75 if am, ok := a.(*ammo.Ammo); ok { 76 am.Reset() 77 d.pool.Put(am) 78 } 79 } 80 81 func (d *jsonlineDecoder) LoadAmmo(ctx context.Context) ([]DecodedAmmo, error) { 82 return d.protoDecoder.LoadAmmo(ctx, d.Scan) 83 } 84 85 type entity struct { 86 // Host defines Host header to send. 87 // Request endpoint is defied by gun config. 88 Host string `json:"host"` 89 Method string `json:"method"` 90 URI string `json:"uri"` 91 // Headers defines headers to send. 92 // NOTE: Host header will be silently ignored. 93 Headers map[string]string `json:"headers"` 94 Tag string `json:"tag"` 95 // Body should be string, doublequotes should be escaped for json body 96 Body string `json:"body"` 97 } 98 99 func (d *jsonlineDecoder) Scan(ctx context.Context) (DecodedAmmo, error) { 100 if d.config.Limit != 0 && d.ammoNum >= d.config.Limit { 101 return nil, ErrAmmoLimit 102 } 103 if d.ammos != nil { 104 return d.scanAmmos() 105 } 106 for { 107 if d.config.Passes != 0 && d.passNum >= d.config.Passes { 108 return nil, ErrPassLimit 109 } 110 var da entity 111 err := d.decoder.Decode(&da) 112 if err != nil { 113 if err != io.EOF { 114 return nil, xerrors.Errorf("failed to decode ammo at line: %v; with err: %w", d.line+1, err) 115 } 116 // go to next pass 117 } else { 118 d.line++ 119 d.ammoNum++ 120 121 header := d.decodedConfigHeaders.Clone() 122 for k, v := range da.Headers { 123 header.Set(k, v) 124 } 125 url := "http://" + da.Host + da.URI // schema will be rewrite in gun 126 var body []byte 127 if da.Body != "" { 128 body = []byte(da.Body) 129 } 130 a := d.pool.Get().(*ammo.Ammo) 131 err = a.Setup(da.Method, url, body, header, da.Tag) 132 return a, err 133 } 134 135 err = d.scanner.Err() 136 if err != nil { 137 return nil, err 138 } 139 if d.ammoNum == 0 { 140 return nil, ErrNoAmmo 141 } 142 d.line = 0 143 d.passNum++ 144 145 _, err = d.file.Seek(0, io.SeekStart) 146 if err != nil { 147 return nil, err 148 } 149 d.decoder = json.NewDecoder(d.file) 150 } 151 } 152 153 func (d *jsonlineDecoder) readArray() ([]DecodedAmmo, error) { 154 var data []entity 155 err := d.decoder.Decode(&data) 156 if err != nil { 157 return nil, fmt.Errorf("cant readArray, err: %w", err) 158 } 159 result := make([]DecodedAmmo, len(data)) 160 for i, datum := range data { 161 header := d.decodedConfigHeaders.Clone() 162 for k, v := range datum.Headers { 163 header.Set(k, v) 164 } 165 url := "http://" + datum.Host + datum.URI // schema will be rewrite in gun 166 var body []byte 167 if datum.Body != "" { 168 body = []byte(datum.Body) 169 } 170 a := d.pool.Get().(*ammo.Ammo) 171 err = a.Setup(datum.Method, url, body, header, datum.Tag) 172 if err != nil { 173 return nil, fmt.Errorf("cant readArray, err: %w", err) 174 } 175 result[i] = a 176 } 177 178 return result, nil 179 } 180 181 func (d *jsonlineDecoder) scanAmmos() (DecodedAmmo, error) { 182 length := len(d.ammos) 183 if length == 0 { 184 return nil, ErrNoAmmo 185 } 186 if d.config.Passes != 0 && d.passNum >= d.config.Passes { 187 return nil, ErrPassLimit 188 } 189 i := int(d.ammoNum) % length 190 a := d.ammos[i] 191 if d.ammoNum > 0 && i == length-1 { 192 d.passNum++ 193 } 194 d.ammoNum++ 195 return a, nil 196 }