mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 18:12:25 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			409 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Package gotenv provides functionality to dynamically load the environment variables
 | 
						|
package gotenv
 | 
						|
 | 
						|
import (
 | 
						|
	"bufio"
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"sort"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"golang.org/x/text/encoding/unicode"
 | 
						|
	"golang.org/x/text/transform"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	// Pattern for detecting valid line format
 | 
						|
	linePattern = `\A\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*|:\s+?)('(?:\'|[^'])*'|"(?:\"|[^"])*"|[^#\n]+)?\s*(?:\s*\#.*)?\z`
 | 
						|
 | 
						|
	// Pattern for detecting valid variable within a value
 | 
						|
	variablePattern = `(\\)?(\$)(\{?([A-Z0-9_]+)?\}?)`
 | 
						|
)
 | 
						|
 | 
						|
// Byte order mark character
 | 
						|
var (
 | 
						|
	bomUTF8    = []byte("\xEF\xBB\xBF")
 | 
						|
	bomUTF16LE = []byte("\xFF\xFE")
 | 
						|
	bomUTF16BE = []byte("\xFE\xFF")
 | 
						|
)
 | 
						|
 | 
						|
// Env holds key/value pair of valid environment variable
 | 
						|
type Env map[string]string
 | 
						|
 | 
						|
// Load is a function to load a file or multiple files and then export the valid variables into environment variables if they do not exist.
 | 
						|
// When it's called with no argument, it will load `.env` file on the current path and set the environment variables.
 | 
						|
// Otherwise, it will loop over the filenames parameter and set the proper environment variables.
 | 
						|
func Load(filenames ...string) error {
 | 
						|
	return loadenv(false, filenames...)
 | 
						|
}
 | 
						|
 | 
						|
// OverLoad is a function to load a file or multiple files and then export and override the valid variables into environment variables.
 | 
						|
func OverLoad(filenames ...string) error {
 | 
						|
	return loadenv(true, filenames...)
 | 
						|
}
 | 
						|
 | 
						|
// Must is wrapper function that will panic when supplied function returns an error.
 | 
						|
func Must(fn func(filenames ...string) error, filenames ...string) {
 | 
						|
	if err := fn(filenames...); err != nil {
 | 
						|
		panic(err.Error())
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Apply is a function to load an io Reader then export the valid variables into environment variables if they do not exist.
 | 
						|
func Apply(r io.Reader) error {
 | 
						|
	return parset(r, false)
 | 
						|
}
 | 
						|
 | 
						|
// OverApply is a function to load an io Reader then export and override the valid variables into environment variables.
 | 
						|
func OverApply(r io.Reader) error {
 | 
						|
	return parset(r, true)
 | 
						|
}
 | 
						|
 | 
						|
func loadenv(override bool, filenames ...string) error {
 | 
						|
	if len(filenames) == 0 {
 | 
						|
		filenames = []string{".env"}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, filename := range filenames {
 | 
						|
		f, err := os.Open(filename)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		err = parset(f, override)
 | 
						|
		f.Close()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// parse and set :)
 | 
						|
func parset(r io.Reader, override bool) error {
 | 
						|
	env, err := strictParse(r, override)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	for key, val := range env {
 | 
						|
		setenv(key, val, override)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func setenv(key, val string, override bool) {
 | 
						|
	if override {
 | 
						|
		os.Setenv(key, val)
 | 
						|
	} else {
 | 
						|
		if _, present := os.LookupEnv(key); !present {
 | 
						|
			os.Setenv(key, val)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Parse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
 | 
						|
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
 | 
						|
// This function is skipping any invalid lines and only processing the valid one.
 | 
						|
func Parse(r io.Reader) Env {
 | 
						|
	env, _ := strictParse(r, false)
 | 
						|
	return env
 | 
						|
}
 | 
						|
 | 
						|
// StrictParse is a function to parse line by line any io.Reader supplied and returns the valid Env key/value pair of valid variables.
 | 
						|
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
 | 
						|
// This function is returning an error if there are any invalid lines.
 | 
						|
func StrictParse(r io.Reader) (Env, error) {
 | 
						|
	return strictParse(r, false)
 | 
						|
}
 | 
						|
 | 
						|
// Read is a function to parse a file line by line and returns the valid Env key/value pair of valid variables.
 | 
						|
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
 | 
						|
// This function is skipping any invalid lines and only processing the valid one.
 | 
						|
func Read(filename string) (Env, error) {
 | 
						|
	f, err := os.Open(filename)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
	return strictParse(f, false)
 | 
						|
}
 | 
						|
 | 
						|
// Unmarshal reads a string line by line and returns the valid Env key/value pair of valid variables.
 | 
						|
// It expands the value of a variable from the environment variable but does not set the value to the environment itself.
 | 
						|
// This function is returning an error if there are any invalid lines.
 | 
						|
func Unmarshal(str string) (Env, error) {
 | 
						|
	return strictParse(strings.NewReader(str), false)
 | 
						|
}
 | 
						|
 | 
						|
// Marshal outputs the given environment as a env file.
 | 
						|
// Variables will be sorted by name.
 | 
						|
func Marshal(env Env) (string, error) {
 | 
						|
	lines := make([]string, 0, len(env))
 | 
						|
	for k, v := range env {
 | 
						|
		if d, err := strconv.Atoi(v); err == nil {
 | 
						|
			lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
 | 
						|
		} else {
 | 
						|
			lines = append(lines, fmt.Sprintf(`%s=%q`, k, v))
 | 
						|
		}
 | 
						|
	}
 | 
						|
	sort.Strings(lines)
 | 
						|
	return strings.Join(lines, "\n"), nil
 | 
						|
}
 | 
						|
 | 
						|
// Write serializes the given environment and writes it to a file
 | 
						|
func Write(env Env, filename string) error {
 | 
						|
	content, err := Marshal(env)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	// ensure the path exists
 | 
						|
	if err := os.MkdirAll(filepath.Dir(filename), 0o775); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	// create or truncate the file
 | 
						|
	file, err := os.Create(filename)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer file.Close()
 | 
						|
	_, err = file.WriteString(content + "\n")
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return file.Sync()
 | 
						|
}
 | 
						|
 | 
						|
// splitLines is a valid SplitFunc for a bufio.Scanner. It will split lines on CR ('\r'), LF ('\n') or CRLF (any of the three sequences).
 | 
						|
// If a CR is immediately followed by a LF, it is treated as a CRLF (one single line break).
 | 
						|
func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
						|
	if atEOF && len(data) == 0 {
 | 
						|
		return 0, nil, bufio.ErrFinalToken
 | 
						|
	}
 | 
						|
 | 
						|
	idx := bytes.IndexAny(data, "\r\n")
 | 
						|
	switch {
 | 
						|
	case atEOF && idx < 0:
 | 
						|
		return len(data), data, bufio.ErrFinalToken
 | 
						|
 | 
						|
	case idx < 0:
 | 
						|
		return 0, nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	// consume CR or LF
 | 
						|
	eol := idx + 1
 | 
						|
	// detect CRLF
 | 
						|
	if len(data) > eol && data[eol-1] == '\r' && data[eol] == '\n' {
 | 
						|
		eol++
 | 
						|
	}
 | 
						|
 | 
						|
	return eol, data[:idx], nil
 | 
						|
}
 | 
						|
 | 
						|
func strictParse(r io.Reader, override bool) (Env, error) {
 | 
						|
	env := make(Env)
 | 
						|
 | 
						|
	buf := new(bytes.Buffer)
 | 
						|
	tee := io.TeeReader(r, buf)
 | 
						|
 | 
						|
	// There can be a maximum of 3 BOM bytes.
 | 
						|
	bomByteBuffer := make([]byte, 3)
 | 
						|
	_, err := tee.Read(bomByteBuffer)
 | 
						|
	if err != nil && err != io.EOF {
 | 
						|
		return env, err
 | 
						|
	}
 | 
						|
 | 
						|
	z := io.MultiReader(buf, r)
 | 
						|
 | 
						|
	// We chooes a different scanner depending on file encoding.
 | 
						|
	var scanner *bufio.Scanner
 | 
						|
 | 
						|
	if bytes.HasPrefix(bomByteBuffer, bomUTF8) {
 | 
						|
		scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF8BOM.NewDecoder()))
 | 
						|
	} else if bytes.HasPrefix(bomByteBuffer, bomUTF16LE) {
 | 
						|
		scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder()))
 | 
						|
	} else if bytes.HasPrefix(bomByteBuffer, bomUTF16BE) {
 | 
						|
		scanner = bufio.NewScanner(transform.NewReader(z, unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM).NewDecoder()))
 | 
						|
	} else {
 | 
						|
		scanner = bufio.NewScanner(z)
 | 
						|
	}
 | 
						|
 | 
						|
	scanner.Split(splitLines)
 | 
						|
 | 
						|
	for scanner.Scan() {
 | 
						|
		if err := scanner.Err(); err != nil {
 | 
						|
			return env, err
 | 
						|
		}
 | 
						|
 | 
						|
		line := strings.TrimSpace(scanner.Text())
 | 
						|
		if line == "" || line[0] == '#' {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		quote := ""
 | 
						|
		// look for the delimiter character
 | 
						|
		idx := strings.Index(line, "=")
 | 
						|
		if idx == -1 {
 | 
						|
			idx = strings.Index(line, ":")
 | 
						|
		}
 | 
						|
		// look for a quote character
 | 
						|
		if idx > 0 && idx < len(line)-1 {
 | 
						|
			val := strings.TrimSpace(line[idx+1:])
 | 
						|
			if val[0] == '"' || val[0] == '\'' {
 | 
						|
				quote = val[:1]
 | 
						|
				// look for the closing quote character within the same line
 | 
						|
				idx = strings.LastIndex(strings.TrimSpace(val[1:]), quote)
 | 
						|
				if idx >= 0 && val[idx] != '\\' {
 | 
						|
					quote = ""
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		// look for the closing quote character
 | 
						|
		for quote != "" && scanner.Scan() {
 | 
						|
			l := scanner.Text()
 | 
						|
			line += "\n" + l
 | 
						|
			idx := strings.LastIndex(l, quote)
 | 
						|
			if idx > 0 && l[idx-1] == '\\' {
 | 
						|
				// foud a matching quote character but it's escaped
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			if idx >= 0 {
 | 
						|
				// foud a matching quote
 | 
						|
				quote = ""
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if quote != "" {
 | 
						|
			return env, fmt.Errorf("missing quotes")
 | 
						|
		}
 | 
						|
 | 
						|
		err := parseLine(line, env, override)
 | 
						|
		if err != nil {
 | 
						|
			return env, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return env, scanner.Err()
 | 
						|
}
 | 
						|
 | 
						|
var (
 | 
						|
	lineRgx     = regexp.MustCompile(linePattern)
 | 
						|
	unescapeRgx = regexp.MustCompile(`\\([^$])`)
 | 
						|
	varRgx      = regexp.MustCompile(variablePattern)
 | 
						|
)
 | 
						|
 | 
						|
func parseLine(s string, env Env, override bool) error {
 | 
						|
	rm := lineRgx.FindStringSubmatch(s)
 | 
						|
 | 
						|
	if len(rm) == 0 {
 | 
						|
		return checkFormat(s, env)
 | 
						|
	}
 | 
						|
 | 
						|
	key := strings.TrimSpace(rm[1])
 | 
						|
	val := strings.TrimSpace(rm[2])
 | 
						|
 | 
						|
	var hsq, hdq bool
 | 
						|
 | 
						|
	// check if the value is quoted
 | 
						|
	if l := len(val); l >= 2 {
 | 
						|
		l -= 1
 | 
						|
		// has double quotes
 | 
						|
		hdq = val[0] == '"' && val[l] == '"'
 | 
						|
		// has single quotes
 | 
						|
		hsq = val[0] == '\'' && val[l] == '\''
 | 
						|
 | 
						|
		// remove quotes '' or ""
 | 
						|
		if hsq || hdq {
 | 
						|
			val = val[1:l]
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if hdq {
 | 
						|
		val = strings.ReplaceAll(val, `\n`, "\n")
 | 
						|
		val = strings.ReplaceAll(val, `\r`, "\r")
 | 
						|
 | 
						|
		// Unescape all characters except $ so variables can be escaped properly
 | 
						|
		val = unescapeRgx.ReplaceAllString(val, "$1")
 | 
						|
	}
 | 
						|
 | 
						|
	if !hsq {
 | 
						|
		fv := func(s string) string {
 | 
						|
			return varReplacement(s, hsq, env, override)
 | 
						|
		}
 | 
						|
		val = varRgx.ReplaceAllStringFunc(val, fv)
 | 
						|
	}
 | 
						|
 | 
						|
	env[key] = val
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func parseExport(st string, env Env) error {
 | 
						|
	if strings.HasPrefix(st, "export") {
 | 
						|
		vs := strings.SplitN(st, " ", 2)
 | 
						|
 | 
						|
		if len(vs) > 1 {
 | 
						|
			if _, ok := env[vs[1]]; !ok {
 | 
						|
				return fmt.Errorf("line `%s` has an unset variable", st)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
var varNameRgx = regexp.MustCompile(`(\$)(\{?([A-Z0-9_]+)\}?)`)
 | 
						|
 | 
						|
func varReplacement(s string, hsq bool, env Env, override bool) string {
 | 
						|
	if s == "" {
 | 
						|
		return s
 | 
						|
	}
 | 
						|
 | 
						|
	if s[0] == '\\' {
 | 
						|
		// the dollar sign is escaped
 | 
						|
		return s[1:]
 | 
						|
	}
 | 
						|
 | 
						|
	if hsq {
 | 
						|
		return s
 | 
						|
	}
 | 
						|
 | 
						|
	mn := varNameRgx.FindStringSubmatch(s)
 | 
						|
 | 
						|
	if len(mn) == 0 {
 | 
						|
		return s
 | 
						|
	}
 | 
						|
 | 
						|
	v := mn[3]
 | 
						|
 | 
						|
	if replace, ok := os.LookupEnv(v); ok && !override {
 | 
						|
		return replace
 | 
						|
	}
 | 
						|
 | 
						|
	if replace, ok := env[v]; ok {
 | 
						|
		return replace
 | 
						|
	}
 | 
						|
 | 
						|
	return os.Getenv(v)
 | 
						|
}
 | 
						|
 | 
						|
func checkFormat(s string, env Env) error {
 | 
						|
	st := strings.TrimSpace(s)
 | 
						|
 | 
						|
	if st == "" || st[0] == '#' {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	if err := parseExport(st, env); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return fmt.Errorf("line `%s` doesn't match format", s)
 | 
						|
}
 |