diff --git a/dotenv/parser.go b/dotenv/parser.go index aec72a88..11b6d027 100644 --- a/dotenv/parser.go +++ b/dotenv/parser.go @@ -98,7 +98,11 @@ func (p *parser) locateKeyName(src string) (string, string, bool, error) { var key string var inherited bool // trim "export" and space at beginning - src = strings.TrimLeftFunc(exportRegex.ReplaceAllString(src, ""), isSpace) + if exportRegex.MatchString(src) { + // we use a `strings.trim` to preserve the pointer to the same underlying memory. + // a regexp replace would copy the string. + src = strings.TrimLeftFunc(strings.TrimPrefix(src, "export"), isSpace) + } // locate key name end and validate it in single loop offset := 0 diff --git a/dotenv/parser_test.go b/dotenv/parser_test.go index 340ea3f0..54580e2a 100644 --- a/dotenv/parser_test.go +++ b/dotenv/parser_test.go @@ -1,6 +1,9 @@ package dotenv import ( + "fmt" + "runtime" + "strings" "testing" "gotest.tools/v3/assert" @@ -38,3 +41,24 @@ func TestParseVariable(t *testing.T) { assert.Error(t, err, "line 1: unexpected character \"%\" in variable name \"%!(EXTRA string)=foo\"") } + +func TestMemoryExplosion(t *testing.T) { + p := newParser() + var startMemStats runtime.MemStats + var endMemStats runtime.MemStats + runtime.ReadMemStats(&startMemStats) + + size := 1000 + input := []string{} + for i := 0; i < size; i++ { + input = append(input, fmt.Sprintf("KEY%d=VALUE%d", i, i)) + } + out := map[string]string{} + err := p.parse(strings.Join(input, "\n"), out, nil) + assert.NilError(t, err) + assert.Equal(t, size, len(out)) + runtime.ReadMemStats(&endMemStats) + assert.Assert(t, endMemStats.Alloc-startMemStats.Alloc < uint64(size)*1000, /* assume 1K per line */ + "memory usage should be linear with input size. Memory grew by: %d", + endMemStats.Alloc-startMemStats.Alloc) +}