mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 01:53:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			236 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			236 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package godotenv
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
	"unicode"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	charComment       = '#'
 | 
						|
	prefixSingleQuote = '\''
 | 
						|
	prefixDoubleQuote = '"'
 | 
						|
 | 
						|
	exportPrefix = "export"
 | 
						|
)
 | 
						|
 | 
						|
func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error {
 | 
						|
	cutset := src
 | 
						|
	for {
 | 
						|
		cutset = getStatementStart(cutset)
 | 
						|
		if cutset == nil {
 | 
						|
			// reached end of file
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		key, left, inherited, err := locateKeyName(cutset)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if strings.Contains(key, " ") {
 | 
						|
			return errors.New("key cannot contain a space")
 | 
						|
		}
 | 
						|
 | 
						|
		if inherited {
 | 
						|
			if lookupFn == nil {
 | 
						|
				lookupFn = noLookupFn
 | 
						|
			}
 | 
						|
 | 
						|
			value, ok := lookupFn(key)
 | 
						|
			if ok {
 | 
						|
				out[key] = value
 | 
						|
				cutset = left
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		value, left, err := extractVarValue(left, out, lookupFn)
 | 
						|
		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, inherited bool, err error) {
 | 
						|
	// trim "export" and space at beginning
 | 
						|
	src = bytes.TrimLeftFunc(bytes.TrimPrefix(src, []byte(exportPrefix)), 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 '=', ':', '\n':
 | 
						|
			// library also supports yaml-style value declaration
 | 
						|
			key = string(src[0:i])
 | 
						|
			offset = i + 1
 | 
						|
			inherited = char == '\n'
 | 
						|
			break loop
 | 
						|
		case '_':
 | 
						|
		default:
 | 
						|
			// variable name should match [A-Za-z0-9_]
 | 
						|
			if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			return "", nil, inherited, fmt.Errorf(
 | 
						|
				`unexpected character %q in variable name near %q`,
 | 
						|
				string(char), string(src))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(src) == 0 {
 | 
						|
		return "", nil, inherited, errors.New("zero length string")
 | 
						|
	}
 | 
						|
 | 
						|
	// trim whitespace
 | 
						|
	key = strings.TrimRightFunc(key, unicode.IsSpace)
 | 
						|
	cutset = bytes.TrimLeftFunc(src[offset:], isSpace)
 | 
						|
	return key, cutset, inherited, nil
 | 
						|
}
 | 
						|
 | 
						|
// extractVarValue extracts variable value and returns rest of slice
 | 
						|
func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (value string, rest []byte, err error) {
 | 
						|
	quote, isQuoted := hasQuotePrefix(src)
 | 
						|
	if !isQuoted {
 | 
						|
		// unquoted value - read until new line
 | 
						|
		end := bytes.IndexFunc(src, isNewLine)
 | 
						|
		var rest []byte
 | 
						|
		var value string
 | 
						|
		if end < 0 {
 | 
						|
			value := strings.TrimRightFunc(string(src), unicode.IsSpace)
 | 
						|
			rest = nil
 | 
						|
			return expandVariables(value, envMap, lookupFn), rest, nil
 | 
						|
		}
 | 
						|
 | 
						|
		value = strings.TrimRightFunc(string(src[0:end]), unicode.IsSpace)
 | 
						|
		rest = src[end:]
 | 
						|
		return expandVariables(value, envMap, lookupFn), rest, 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), envMap, lookupFn)
 | 
						|
		}
 | 
						|
 | 
						|
		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) (quote byte, isQuoted 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
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
// isNewLine reports whether the rune is a new line character
 | 
						|
func isNewLine(r rune) bool {
 | 
						|
	return r == '\n'
 | 
						|
}
 |