summaryrefslogtreecommitdiff
path: root/vendor/github.com/joho/godotenv/parser.go
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-05-11 21:12:57 -0600
committermo khan <mo@mokhan.ca>2025-05-11 21:12:57 -0600
commit60440f90dca28e99a31dd328c5f6d5dc0f9b6a2e (patch)
tree2f54adf55086516f162f0a55a5347e6b25f7f176 /vendor/github.com/joho/godotenv/parser.go
parent05ca9b8d3a9c7203a3a3b590beaa400900bd9007 (diff)
chore: vendor go dependencies
Diffstat (limited to 'vendor/github.com/joho/godotenv/parser.go')
-rw-r--r--vendor/github.com/joho/godotenv/parser.go271
1 files changed, 271 insertions, 0 deletions
diff --git a/vendor/github.com/joho/godotenv/parser.go b/vendor/github.com/joho/godotenv/parser.go
new file mode 100644
index 0000000..cc709af
--- /dev/null
+++ b/vendor/github.com/joho/godotenv/parser.go
@@ -0,0 +1,271 @@
+package godotenv
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "regexp"
+ "strings"
+ "unicode"
+)
+
+const (
+ charComment = '#'
+ prefixSingleQuote = '\''
+ prefixDoubleQuote = '"'
+
+ exportPrefix = "export"
+)
+
+func parseBytes(src []byte, out map[string]string) error {
+ src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1)
+ cutset := src
+ for {
+ cutset = getStatementStart(cutset)
+ if cutset == nil {
+ // reached end of file
+ break
+ }
+
+ key, left, err := locateKeyName(cutset)
+ if err != nil {
+ return err
+ }
+
+ value, left, err := extractVarValue(left, out)
+ if err != nil {
+ return err
+ }
+
+ out[key] = value
+ cutset = left
+ }
+
+ return nil
+}
+
+// getStatementPosition returns position of statement begin.
+//
+// It skips any comment line or non-whitespace character.
+func getStatementStart(src []byte) []byte {
+ pos := indexOfNonSpaceChar(src)
+ if pos == -1 {
+ return nil
+ }
+
+ src = src[pos:]
+ if src[0] != charComment {
+ return src
+ }
+
+ // skip comment section
+ pos = bytes.IndexFunc(src, isCharFunc('\n'))
+ if pos == -1 {
+ return nil
+ }
+
+ return getStatementStart(src[pos:])
+}
+
+// locateKeyName locates and parses key name and returns rest of slice
+func locateKeyName(src []byte) (key string, cutset []byte, err error) {
+ // trim "export" and space at beginning
+ src = bytes.TrimLeftFunc(src, isSpace)
+ if bytes.HasPrefix(src, []byte(exportPrefix)) {
+ trimmed := bytes.TrimPrefix(src, []byte(exportPrefix))
+ if bytes.IndexFunc(trimmed, isSpace) == 0 {
+ src = bytes.TrimLeftFunc(trimmed, isSpace)
+ }
+ }
+
+ // locate key name end and validate it in single loop
+ offset := 0
+loop:
+ for i, char := range src {
+ rchar := rune(char)
+ if isSpace(rchar) {
+ continue
+ }
+
+ switch char {
+ case '=', ':':
+ // library also supports yaml-style value declaration
+ key = string(src[0:i])
+ offset = i + 1
+ break loop
+ case '_':
+ default:
+ // variable name should match [A-Za-z0-9_.]
+ if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' {
+ continue
+ }
+
+ return "", nil, fmt.Errorf(
+ `unexpected character %q in variable name near %q`,
+ string(char), string(src))
+ }
+ }
+
+ if len(src) == 0 {
+ return "", nil, errors.New("zero length string")
+ }
+
+ // trim whitespace
+ key = strings.TrimRightFunc(key, unicode.IsSpace)
+ cutset = bytes.TrimLeftFunc(src[offset:], isSpace)
+ return key, cutset, nil
+}
+
+// extractVarValue extracts variable value and returns rest of slice
+func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) {
+ quote, hasPrefix := hasQuotePrefix(src)
+ if !hasPrefix {
+ // unquoted value - read until end of line
+ endOfLine := bytes.IndexFunc(src, isLineEnd)
+
+ // Hit EOF without a trailing newline
+ if endOfLine == -1 {
+ endOfLine = len(src)
+
+ if endOfLine == 0 {
+ return "", nil, nil
+ }
+ }
+
+ // Convert line to rune away to do accurate countback of runes
+ line := []rune(string(src[0:endOfLine]))
+
+ // Assume end of line is end of var
+ endOfVar := len(line)
+ if endOfVar == 0 {
+ return "", src[endOfLine:], nil
+ }
+
+ // Work backwards to check if the line ends in whitespace then
+ // a comment (ie asdasd # some comment)
+ for i := endOfVar - 1; i >= 0; i-- {
+ if line[i] == charComment && i > 0 {
+ if isSpace(line[i-1]) {
+ endOfVar = i
+ break
+ }
+ }
+ }
+
+ trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace)
+
+ return expandVariables(trimmed, vars), src[endOfLine:], nil
+ }
+
+ // lookup quoted string terminator
+ for i := 1; i < len(src); i++ {
+ if char := src[i]; char != quote {
+ continue
+ }
+
+ // skip escaped quote symbol (\" or \', depends on quote)
+ if prevChar := src[i-1]; prevChar == '\\' {
+ continue
+ }
+
+ // trim quotes
+ trimFunc := isCharFunc(rune(quote))
+ value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
+ if quote == prefixDoubleQuote {
+ // unescape newlines for double quote (this is compat feature)
+ // and expand environment variables
+ value = expandVariables(expandEscapes(value), vars)
+ }
+
+ return value, src[i+1:], nil
+ }
+
+ // return formatted error if quoted string is not terminated
+ valEndIndex := bytes.IndexFunc(src, isCharFunc('\n'))
+ if valEndIndex == -1 {
+ valEndIndex = len(src)
+ }
+
+ return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex])
+}
+
+func expandEscapes(str string) string {
+ out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string {
+ c := strings.TrimPrefix(match, `\`)
+ switch c {
+ case "n":
+ return "\n"
+ case "r":
+ return "\r"
+ default:
+ return match
+ }
+ })
+ return unescapeCharsRegex.ReplaceAllString(out, "$1")
+}
+
+func indexOfNonSpaceChar(src []byte) int {
+ return bytes.IndexFunc(src, func(r rune) bool {
+ return !unicode.IsSpace(r)
+ })
+}
+
+// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
+func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) {
+ if len(src) == 0 {
+ return 0, false
+ }
+
+ switch prefix := src[0]; prefix {
+ case prefixDoubleQuote, prefixSingleQuote:
+ return prefix, true
+ default:
+ return 0, false
+ }
+}
+
+func isCharFunc(char rune) func(rune) bool {
+ return func(v rune) bool {
+ return v == char
+ }
+}
+
+// isSpace reports whether the rune is a space character but not line break character
+//
+// this differs from unicode.IsSpace, which also applies line break as space
+func isSpace(r rune) bool {
+ switch r {
+ case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0:
+ return true
+ }
+ return false
+}
+
+func isLineEnd(r rune) bool {
+ if r == '\n' || r == '\r' {
+ return true
+ }
+ return false
+}
+
+var (
+ escapeRegex = regexp.MustCompile(`\\.`)
+ expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
+ unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
+)
+
+func expandVariables(v string, m map[string]string) string {
+ return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
+ submatch := expandVarRegex.FindStringSubmatch(s)
+
+ if submatch == nil {
+ return s
+ }
+ if submatch[1] == "\\" || submatch[2] == "(" {
+ return submatch[0][1:]
+ } else if submatch[4] != "" {
+ return m[submatch[4]]
+ }
+ return s
+ })
+}