fix(script): enforce script body limits without streaming

Streaming accepted a chunked body cut at a chunk boundary as complete,
so the limit now comes from the client that reads the body, picked per
requested size. Script requests keep their own default, maxBodySize
accepts sizes like 10MiB, and files sarin downloads for itself are
capped at 64MiB.
This commit is contained in:
aykhans committed 2026-09-29 00:41:48 +04:00
1 parent f1d91e3d77
commit 1956eb1c44
16 files changed
+212 -163

No files matched your search

+3 -3
View File
@@ -179,7 +179,7 @@ The value is a whole number of bytes, with or without a unit: `B`, `KB`, `MB`, `
A server can declare a `Content-Length` far larger than it sends, and the body is allocated from that declared length, so without a limit a single response can exhaust memory. A response above the limit fails with `body size exceeds the given limit` and is counted like any other error.
The limit applies per response in flight, so the worst case is roughly the limit times the concurrency. It also applies to the requests scripts make, unless the script passes its own `maxBodySize` (see [HTTP Requests in Scripts](#http-requests-in-scripts)).
The limit applies per response in flight, so the worst case is roughly the limit times the concurrency. Requests made by scripts have their own limit, see [HTTP Requests in Scripts](#http-requests-in-scripts).
## Concurrency
@@ -596,7 +596,7 @@ http(url, opts)
| `timeout` | duration string (`"500ms"`) | `30s` | Request timeout (the `timeout` config does not apply), applied to each hop when redirects are followed |
| `insecure` | boolean | `false` | Skip TLS verification (the `insecure` config does not apply) |
| `maxRedirects` | number | `0` | How many redirects to follow, `0` follows none, maximum 100 |
| `maxBodySize` | number | - | Largest response body to accept in bytes, maximum 1099511627776 |
| `maxBodySize` | number or size (`"10MiB"`) | `10MiB` | Largest response body to accept (the max response body config does not apply), maximum `1TiB` |
Unknown options and values of the wrong type raise an error.
@@ -619,7 +619,7 @@ In Lua, call the helpers as `res:header("name")` or `res.header("name")`.
- **Same proxy:** when proxies are configured, a script's requests go through the same proxy as the main request they belong to.
- **Status codes:** non-2xx responses are returned normally; check `status` in the script.
- **Errors:** network failures and timeouts raise a script error. The main request is not sent and the error is counted in the results. Scripts can catch it with `pcall` (Lua) or `try`/`catch` (JavaScript).
- **Response bodies:** without `maxBodySize` the [Max Response Body](#max-response-body) config applies, as it does to the main request. With `maxBodySize` the body is read as it arrives, so a `Content-Length` the server never sends costs nothing, the limit is exact, and it may be larger than the config. Passing the limit raises an error instead of truncating the body.
- **Response bodies:** script requests carry their own limit, like their own timeout, so the max response body config does not apply to them. A body above `maxBodySize` raises an error instead of being truncated, and a body whose declared `Content-Length` is already above it is refused before it is read.
- **Only inside `transform`:** `http` works anywhere while `transform` runs, including in helper functions it calls. Calling it at the top level of a script raises an error, because that code also runs during config validation and when each worker starts.
- **Dry run:** script requests are still sent in dry-run mode.
- **Not measured:** script requests are not included in the results.
+2 -76
View File
@@ -3,9 +3,6 @@ package config
import (
"flag"
"fmt"
"math"
"strconv"
"strings"
"go.aykhans.me/sarin/internal/types"
"go.yaml.in/yaml/v4"
@@ -19,84 +16,13 @@ var (
// byteSize is a size in bytes that reads and prints unit suffixes such as 10MiB.
type byteSize uint64
// byteSizeUnits run from the largest scale down, so formatting picks the largest unit
// that fits. A bare "K" is out: the common libraries disagree on whether it counts
// 1000 or 1024 bytes.
var byteSizeUnits = []struct {
suffix string
scale uint64
}{
{"TiB", 1 << 40},
{"TB", 1000 * 1000 * 1000 * 1000},
{"GiB", 1 << 30},
{"GB", 1000 * 1000 * 1000},
{"MiB", 1 << 20},
{"MB", 1000 * 1000},
{"KiB", 1 << 10},
{"KB", 1000},
{"B", 1},
}
// parseByteSize reads a byte count, with or without a unit suffix.
func parseByteSize(value string) (uint64, error) {
text := strings.TrimSpace(value)
// The suffix is split off by index, so no case mapping can shift the digits.
digitsEnd := len(text)
for digitsEnd > 0 && isASCIILetter(text[digitsEnd-1]) {
digitsEnd--
}
scale := uint64(1)
if suffix := text[digitsEnd:]; suffix != "" {
unitScale, ok := byteSizeScale(suffix)
if !ok {
return 0, types.ErrByteSizeInvalid
}
scale = unitScale
}
size, err := strconv.ParseUint(strings.TrimSpace(text[:digitsEnd]), 10, 64)
if err != nil {
return 0, types.ErrByteSizeInvalid
}
if size > math.MaxUint64/scale {
return 0, types.ErrByteSizeInvalid
}
return size * scale, nil
}
// byteSizeScale looks up a unit suffix, ignoring case.
func byteSizeScale(suffix string) (uint64, bool) {
for _, unit := range byteSizeUnits {
if strings.EqualFold(suffix, unit.suffix) {
return unit.scale, true
}
}
return 0, false
}
func isASCIILetter(char byte) bool {
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
}
// formatByteSize renders a byte count so that parseByteSize reads back the same value.
func formatByteSize(size uint64) string {
for _, unit := range byteSizeUnits {
if unit.scale > 1 && size >= unit.scale && size%unit.scale == 0 {
return strconv.FormatUint(size/unit.scale, 10) + unit.suffix
}
}
return strconv.FormatUint(size, 10)
}
func (size byteSize) String() string {
return formatByteSize(uint64(size))
return types.FormatByteSize(uint64(size))
}
// Set fills the size from a command line value.
func (size *byteSize) Set(value string) error {
parsed, err := parseByteSize(value)
parsed, err := types.ParseByteSize(value)
if err != nil {
return err
}
+1 -1
View File
@@ -293,7 +293,7 @@ func (parser ConfigCLIParser) PrintHelp() {
Defaults.Method,
Defaults.RequestTimeout,
formatByteSize(Defaults.MaxResponseBody),
types.FormatByteSize(Defaults.MaxResponseBody),
Defaults.Insecure,
)
}
+4 -7
View File
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"math"
"net/url"
"os"
"path/filepath"
@@ -59,8 +58,6 @@ var (
ValidLogLevels = []string{"info", "error"}
)
const maxResponseBodyLimit = min(1<<40, math.MaxInt) // 1 TiB on 64 bit, 2 GiB - 1 on 32 bit
var (
StyleYellow = lipgloss.NewStyle().Foreground(lipgloss.Color("220"))
StyleRed = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
@@ -184,7 +181,7 @@ func (config Config) MarshalYAML() (any, error) {
addField(content, "timeout", toNode(*config.Timeout), "")
}
if config.MaxResponseBody != nil {
addField(content, "maxResponseBody", toNode(formatByteSize(*config.MaxResponseBody)), "")
addField(content, "maxResponseBody", toNode(types.FormatByteSize(*config.MaxResponseBody)), "")
}
if config.Concurrency != nil {
addField(content, "concurrency", toNode(*config.Concurrency), "")
@@ -469,11 +466,11 @@ func (config Config) Validate() error {
validationErrors = append(validationErrors, types.NewFieldValidationError("Timeout", "0", errors.New("timeout must be greater than 0")))
}
if config.MaxResponseBody != nil && *config.MaxResponseBody > maxResponseBodyLimit {
if config.MaxResponseBody != nil && *config.MaxResponseBody > types.ByteSizeLimit {
validationErrors = append(validationErrors, types.NewFieldValidationError(
"MaxResponseBody",
formatByteSize(*config.MaxResponseBody),
errors.New("max response body must not exceed "+formatByteSize(maxResponseBodyLimit)),
types.FormatByteSize(*config.MaxResponseBody),
errors.New("max response body must not exceed "+types.FormatByteSize(types.ByteSizeLimit)),
))
}
+1 -1
View File
@@ -196,7 +196,7 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
}
if maxResponseBody := parser.getEnv("MAX_RESPONSE_BODY"); maxResponseBody != "" {
maxResponseBodyParsed, err := parseByteSize(maxResponseBody)
maxResponseBodyParsed, err := types.ParseByteSize(maxResponseBody)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
+4 -1
View File
@@ -79,10 +79,13 @@ func fetchHTTP(ctx context.Context, url string) ([]byte, error) {
return nil, types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
}
data, err := io.ReadAll(resp.Body)
data, err := io.ReadAll(io.LimitReader(resp.Body, types.RemoteFetchLimit+1))
if err != nil {
return nil, types.NewHTTPFetchError(url, err)
}
if len(data) > types.RemoteFetchLimit {
return nil, types.NewHTTPFetchError(url, types.ErrRemoteFileTooLarge)
}
return data, nil
}
+4 -1
View File
@@ -93,10 +93,13 @@ func (fc *FileCache) fetchURL(url string) ([]byte, string, error) {
return nil, "", types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
}
content, err := io.ReadAll(resp.Body)
content, err := io.ReadAll(io.LimitReader(resp.Body, types.RemoteFetchLimit+1))
if err != nil {
return nil, "", types.NewHTTPFetchError(url, err)
}
if len(content) > types.RemoteFetchLimit {
return nil, "", types.NewHTTPFetchError(url, types.ErrRemoteFileTooLarge)
}
// Extract filename from URL path
filename := path.Base(url)
+7
View File
@@ -34,3 +34,10 @@ func safeUint64ToInt(u uint64) int {
}
return int(u)
}
func safeInt64ToInt(i int64) int {
if i > math.MaxInt {
return math.MaxInt
}
return int(i)
}
+1 -1
View File
@@ -281,7 +281,7 @@ func NewSarin(
var scriptHTTPClients []*scriptHTTPClient
if !scriptChain.IsEmpty() {
scriptHTTPClients, err = newScriptHTTPClients(jobsCtx, proxyURLs, workers, maxResponseBody)
scriptHTTPClients, err = newScriptHTTPClients(jobsCtx, proxyURLs, workers)
if err != nil {
jobsCancel()
return nil, err
+54 -56
View File
@@ -1,13 +1,12 @@
package sarin
import (
"bytes"
"context"
"crypto/tls"
"errors"
"io"
"net"
"net/url"
"sync"
"time"
"github.com/valyala/fasthttp"
@@ -18,11 +17,22 @@ import (
// scriptHTTPDefaultTimeout is the default timeout for script requests, independent of -T.
const scriptHTTPDefaultTimeout = 30 * time.Second
// scriptHTTPDefaultMaxBodySize is the default body limit for script requests,
// independent of the max response body config.
const scriptHTTPDefaultMaxBodySize = 10 << 20 // 10 MiB
// scriptHTTPClientKey picks the client a request needs, since fasthttp takes the body
// limit per client rather than per request.
type scriptHTTPClientKey struct {
insecure bool
maxBodySize int
}
// scriptHTTPClient sends scripts' http.* requests. It is safe for concurrent use.
type scriptHTTPClient struct {
ctx context.Context //nolint:containedctx
client *fasthttp.Client
insecureClient *fasthttp.Client
newClient func(key scriptHTTPClientKey) *fasthttp.Client
clients sync.Map
defaultTimeout time.Duration
}
@@ -31,9 +41,9 @@ var _ script.HTTPDoer = (*scriptHTTPClient)(nil)
// newScriptHTTPClients creates one client per proxy, in the same order as NewHostClients.
// It can return the following errors:
// - types.ProxyDialError
func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint, maxResponseBody uint64) ([]*scriptHTTPClient, error) {
func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint) ([]*scriptHTTPClient, error) {
if len(proxies) == 0 {
return []*scriptHTTPClient{newScriptHTTPClient(ctx, fasthttp.DialDualStackTimeout, scriptHTTPDefaultTimeout, maxConns, maxResponseBody)}, nil
return []*scriptHTTPClient{newScriptHTTPClient(ctx, fasthttp.DialDualStackTimeout, scriptHTTPDefaultTimeout, maxConns)}, nil
}
clients := make([]*scriptHTTPClient, 0, len(proxies))
@@ -42,36 +52,47 @@ func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint,
if err != nil {
return nil, types.NewProxyDialError(proxy.String(), err)
}
clients = append(clients, newScriptHTTPClient(ctx, dial, scriptHTTPDefaultTimeout, maxConns, maxResponseBody))
clients = append(clients, newScriptHTTPClient(ctx, dial, scriptHTTPDefaultTimeout, maxConns))
}
return clients, nil
}
func newScriptHTTPClient(ctx context.Context, dial fasthttp.DialFuncWithTimeout, defaultTimeout time.Duration, maxConns uint, maxResponseBody uint64) *scriptHTTPClient {
newClient := func(insecure bool) *fasthttp.Client {
func newScriptHTTPClient(ctx context.Context, dial fasthttp.DialFuncWithTimeout, defaultTimeout time.Duration, maxConns uint) *scriptHTTPClient {
return &scriptHTTPClient{
ctx: ctx,
newClient: func(key scriptHTTPClientKey) *fasthttp.Client {
// Read and write timeouts stay unset because they would cap the per-call timeout,
// so dialWithDeadline is what bounds the TLS handshake.
return &fasthttp.Client{
DialTimeout: dialWithDeadline(dial),
MaxConnsPerHost: safeUintToInt(maxConns),
MaxResponseBodySize: safeUint64ToInt(maxResponseBody),
MaxResponseBodySize: key.maxBodySize,
TLSConfig: &tls.Config{
InsecureSkipVerify: insecure, //nolint:gosec
InsecureSkipVerify: key.insecure, //nolint:gosec
},
DisableHeaderNamesNormalizing: true,
DisablePathNormalizing: true,
NoDefaultUserAgentHeader: true,
}
}
return &scriptHTTPClient{
ctx: ctx,
client: newClient(false),
insecureClient: newClient(true),
},
defaultTimeout: defaultTimeout,
}
}
// scriptHTTPBodyCap rounds a limit up to a power of two, so a script that varies its
// limit cannot mint a client, and a connection pool with it, per request.
func scriptHTTPBodyCap(maxBodySize int64) int {
if maxBodySize <= 0 {
return scriptHTTPDefaultMaxBodySize
}
size := int64(1)
for size < maxBodySize {
size <<= 1
}
return safeInt64ToInt(size)
}
// dialWithDeadline dials within the request timeout and keeps it as the connection deadline,
// so the TLS handshake fasthttp runs on the first write cannot outlive the request.
func dialWithDeadline(dial fasthttp.DialFuncWithTimeout) fasthttp.DialFuncWithTimeout {
@@ -142,64 +163,41 @@ func (c *scriptHTTPClient) Do(r *script.HTTPRequest) (*script.HTTPResponse, erro
timeout = c.defaultTimeout
}
client := c.client
if r.Insecure {
client = c.insecureClient
}
client := c.clientFor(r.Insecure, r.MaxBodySize)
req.SetTimeout(timeout)
// A body limit streams the response, so a declared Content-Length is never allocated.
resp.StreamBody = r.MaxBodySize > 0
if r.MaxRedirects > 0 {
err = client.DoRedirects(req, resp, r.MaxRedirects)
} else {
err = client.Do(req, resp)
}
if err != nil {
if errors.Is(err, fasthttp.ErrBodyTooLarge) {
err = types.ErrScriptHTTPBodyTooLarge
}
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
}
body, err := readScriptBody(resp, r.MaxBodySize)
if err != nil {
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
// The client caps the body at the rounded up limit, so the exact one is checked here.
body := resp.Body()
if r.MaxBodySize > 0 && int64(len(body)) > r.MaxBodySize {
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, types.ErrScriptHTTPBodyTooLarge)
}
return &script.HTTPResponse{
Status: resp.StatusCode(),
Headers: collectRespHeaders(resp),
Body: body,
Body: string(body),
}, nil
}
// readScriptBody reads the streamed body, refusing it once it passes maxBodySize.
// It can return the following errors:
// - types.ErrScriptHTTPBodyTooLarge
func readScriptBody(resp *fasthttp.Response, maxBodySize int64) (string, error) {
stream := resp.BodyStream()
if stream == nil {
return string(resp.Body()), nil
// clientFor returns the client whose body limit covers maxBodySize, building it once.
func (c *scriptHTTPClient) clientFor(insecure bool, maxBodySize int64) *fasthttp.Client {
key := scriptHTTPClientKey{insecure: insecure, maxBodySize: scriptHTTPBodyCap(maxBodySize)}
if client, ok := c.clients.Load(key); ok {
return client.(*fasthttp.Client) //nolint:forcetypeassert
}
var (
body bytes.Buffer
err error
)
if maxBodySize > 0 {
// One byte past the limit tells an exact fit from an overflow.
_, err = io.CopyN(&body, stream, maxBodySize+1)
if err == nil {
err = types.ErrScriptHTTPBodyTooLarge
} else if errors.Is(err, io.EOF) {
err = nil
}
} else {
_, err = io.Copy(&body, stream)
}
// An unfinished stream makes fasthttp drop the connection instead of pooling it.
_ = resp.CloseBodyStream()
if err != nil {
return "", err
}
return body.String(), nil
client, _ := c.clients.LoadOrStore(key, c.newClient(key))
return client.(*fasthttp.Client) //nolint:forcetypeassert
}
+20 -3
View File
@@ -67,8 +67,8 @@ const httpDefaultMethod = http.MethodGet
// httpMaxRedirectsLimit is the highest maxRedirects a script may ask for.
const httpMaxRedirectsLimit = 100
// httpMaxBodySizeLimit is the highest maxBodySize a script may ask for, 1 TiB.
const httpMaxBodySizeLimit int64 = 1 << 40
// httpMaxBodySizeLimit is the highest maxBodySize a script may ask for.
const httpMaxBodySizeLimit int64 = types.ByteSizeLimit
// Option keys accepted by the http function.
const (
@@ -132,13 +132,30 @@ func parseHTTPMaxBodySize(size float64) (int64, error) {
httpOptionMaxBodySize,
types.NewScriptTypeError(
"a whole number of bytes between 0 and "+strconv.FormatInt(httpMaxBodySizeLimit, 10),
strconv.FormatFloat(size, 'g', -1, 64),
strconv.FormatFloat(size, 'f', -1, 64),
),
)
}
return int64(size), nil
}
// parseHTTPMaxBodySizeString reads a body limit written with a unit, such as "10MiB".
// It can return the following errors:
// - types.ScriptHTTPOptionError
func parseHTTPMaxBodySizeString(value string) (int64, error) {
size, err := types.ParseByteSize(value)
if err != nil {
return 0, types.NewScriptHTTPOptionError(httpOptionMaxBodySize, err)
}
if size > uint64(httpMaxBodySizeLimit) {
return 0, types.NewScriptHTTPOptionError(
httpOptionMaxBodySize,
types.NewScriptTypeError("a size up to "+types.FormatByteSize(uint64(httpMaxBodySizeLimit)), value),
)
}
return int64(size), nil
}
// parseHTTPTimeout parses a Go duration string such as "500ms" or "2s".
// It can return the following errors:
// - types.ScriptHTTPOptionError
+11 -6
View File
@@ -118,13 +118,18 @@ func (e *JsEngine) parseHTTPOptions(opts *goja.Object, req *HTTPRequest) error {
return err
}
case httpOptionMaxBodySize:
size, ok := numberValue(value)
if !ok {
return types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("number", jsTypeName(value)))
}
var err error
if req.MaxBodySize, err = parseHTTPMaxBodySize(size); err != nil {
switch size := value.Export().(type) {
case string:
req.MaxBodySize, err = parseHTTPMaxBodySizeString(size)
default:
number, ok := numberValue(value)
if !ok {
return types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("number or size string", jsTypeName(value)))
}
req.MaxBodySize, err = parseHTTPMaxBodySize(number)
}
if err != nil {
return err
}
default:
+7 -5
View File
@@ -107,12 +107,14 @@ func (e *LuaEngine) parseHTTPOptions(opts *lua.LTable, req *HTTPRequest) error {
}
req.MaxRedirects, err = parseHTTPMaxRedirects(float64(count))
case httpOptionMaxBodySize:
size, ok := v.(lua.LNumber)
if !ok {
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("number", v.Type().String()))
return
}
switch size := v.(type) {
case lua.LNumber:
req.MaxBodySize, err = parseHTTPMaxBodySize(float64(size))
case lua.LString:
req.MaxBodySize, err = parseHTTPMaxBodySizeString(string(size))
default:
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("number or size string", v.Type().String()))
}
default:
err = types.NewScriptHTTPOptionError(name, types.ErrScriptHTTPUnknownOption)
}
+4 -1
View File
@@ -171,10 +171,13 @@ func fetchURL(ctx context.Context, url string) (string, error) {
return "", types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
}
data, err := io.ReadAll(resp.Body)
data, err := io.ReadAll(io.LimitReader(resp.Body, types.RemoteFetchLimit+1))
if err != nil {
return "", types.NewHTTPFetchError(url, err)
}
if len(data) > types.RemoteFetchLimit {
return "", types.NewHTTPFetchError(url, types.ErrRemoteFileTooLarge)
}
return string(data), nil
}
+86
View File
@@ -0,0 +1,86 @@
package types
import (
"math"
"strconv"
"strings"
)
// ByteSizeLimit is the largest size any option may ask for, 1 TiB, less where int is 32 bit.
const ByteSizeLimit = min(1<<40, math.MaxInt)
// RemoteFetchLimit bounds a file sarin downloads for itself, such as a config file or a script.
const RemoteFetchLimit = 64 << 20 // 64 MiB
// byteSizeUnits run from the largest scale down, so formatting picks the largest unit
// that fits. A bare "K" is out: the common libraries disagree on whether it counts
// 1000 or 1024 bytes.
var byteSizeUnits = []struct {
suffix string
scale uint64
}{
{"TiB", 1 << 40},
{"TB", 1000 * 1000 * 1000 * 1000},
{"GiB", 1 << 30},
{"GB", 1000 * 1000 * 1000},
{"MiB", 1 << 20},
{"MB", 1000 * 1000},
{"KiB", 1 << 10},
{"KB", 1000},
{"B", 1},
}
// ParseByteSize reads a byte count, with or without a unit suffix.
// It can return the following errors:
// - ErrByteSizeInvalid
func ParseByteSize(value string) (uint64, error) {
text := strings.TrimSpace(value)
// The suffix is split off by index, so no case mapping can shift the digits.
digitsEnd := len(text)
for digitsEnd > 0 && isASCIILetter(text[digitsEnd-1]) {
digitsEnd--
}
scale := uint64(1)
if suffix := text[digitsEnd:]; suffix != "" {
unitScale, ok := byteSizeScale(suffix)
if !ok {
return 0, ErrByteSizeInvalid
}
scale = unitScale
}
size, err := strconv.ParseUint(strings.TrimSpace(text[:digitsEnd]), 10, 64)
if err != nil {
return 0, ErrByteSizeInvalid
}
if size > math.MaxUint64/scale {
return 0, ErrByteSizeInvalid
}
return size * scale, nil
}
// FormatByteSize renders a byte count so that ParseByteSize reads back the same value.
func FormatByteSize(size uint64) string {
for _, unit := range byteSizeUnits {
if unit.scale > 1 && size >= unit.scale && size%unit.scale == 0 {
return strconv.FormatUint(size/unit.scale, 10) + unit.suffix
}
}
return strconv.FormatUint(size, 10)
}
// byteSizeScale looks up a unit suffix, ignoring case.
func byteSizeScale(suffix string) (uint64, bool) {
for _, unit := range byteSizeUnits {
if strings.EqualFold(suffix, unit.suffix) {
return unit.scale, true
}
}
return 0, false
}
func isASCIILetter(char byte) bool {
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
}
+3 -1
View File
@@ -15,6 +15,8 @@ var (
"invalid value for size, expected a whole number of bytes or a whole number with a unit: " +
"B, KB, MB, GB, TB (1000 based) or KiB, MiB, GiB, TiB (1024 based), e.g. '1048576', '10MiB', '2MB'",
)
ErrRemoteFileTooLarge = errors.New("remote file exceeds " + FormatByteSize(RemoteFetchLimit))
)
type FieldParseError struct {
@@ -413,7 +415,7 @@ var (
ErrScriptHTTPMethodEmpty = errors.New("http method cannot be empty")
ErrScriptHTTPURLInvalid = errors.New("URL must be an absolute http or https URL")
ErrScriptHTTPUnknownOption = errors.New("unknown option")
ErrScriptHTTPBodyTooLarge = errors.New("response body exceeds maxBodySize")
ErrScriptHTTPBodyTooLarge = errors.New("response body exceeds the body limit")
ErrScriptJSONCycle = errors.New("cannot encode a table that contains itself")
)