package config import ( "errors" "io" "os" "strings" "unicode/utf8" ) const ( DefaultERPEnvironmentFile = ".env" maximumERPEnvironmentFileBytes = 32 << 10 maximumERPEnvironmentLineBytes = 4 << 10 ) var ( errERPEnvironmentFileInvalid = errors.New("ERP .env file is invalid") errERPEnvironmentLookup = errors.New("ERP .env lookup is required") ) // WithERPEnvironmentFile returns a lookup that uses the process environment // first and only falls back to approved ERP values from path. func WithERPEnvironmentFile( path string, parent LookupEnvironment, ) (LookupEnvironment, error) { if parent == nil { return nil, errERPEnvironmentLookup } values, err := readERPEnvironmentFile(path) if err != nil { return nil, err } return func(name string) (string, bool) { if value, exists := parent(name); exists { return value, true } value, exists := values[name] return value, exists }, nil } func readERPEnvironmentFile(path string) (map[string]string, error) { values := make(map[string]string) if path == "" { return nil, errERPEnvironmentFileInvalid } file, err := os.Open(path) if errors.Is(err, os.ErrNotExist) { return values, nil } if err != nil { return nil, errors.New("ERP .env file cannot be read") } defer file.Close() contents, err := io.ReadAll(io.LimitReader( file, maximumERPEnvironmentFileBytes+1, )) if err != nil || len(contents) > maximumERPEnvironmentFileBytes || !utf8.Valid(contents) || strings.IndexByte(string(contents), 0) >= 0 { return nil, errERPEnvironmentFileInvalid } for _, rawLine := range strings.Split(string(contents), "\n") { line := strings.TrimSuffix(rawLine, "\r") if len(line) > maximumERPEnvironmentLineBytes { return nil, errERPEnvironmentFileInvalid } line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "export ") { line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) } name, rawValue, found := strings.Cut(line, "=") name = strings.TrimSpace(name) if !found || !validEnvironmentName(name) { return nil, errERPEnvironmentFileInvalid } if !isERPEnvironmentName(name) { if strings.HasPrefix(name, "CMROUBAO_") { return nil, errERPEnvironmentFileInvalid } continue } if _, exists := values[name]; exists { return nil, errERPEnvironmentFileInvalid } value, err := dotenvValue(rawValue) if err != nil { return nil, errERPEnvironmentFileInvalid } values[name] = value } return values, nil } func dotenvValue(rawValue string) (string, error) { value := strings.TrimSpace(rawValue) if value == "" { return value, nil } quote := value[0] if quote == '\'' || quote == '"' { if len(value) < 2 || value[len(value)-1] != quote { return "", errERPEnvironmentFileInvalid } return value[1 : len(value)-1], nil } if strings.HasSuffix(value, "\"") || strings.HasSuffix(value, "'") { return "", errERPEnvironmentFileInvalid } return value, nil } func isERPEnvironmentName(name string) bool { switch name { case ShunyunbaoURLEnvironment, ShunyunbaoUsernameEnvironment, ShunyunbaoPasswordEnvironment, OCRAPIURLEnvironment, ERPDebugLogEnvironment: return true default: return false } } func validEnvironmentName(name string) bool { if name == "" { return false } for index := 0; index < len(name); index++ { character := name[index] if (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9' && index > 0) || character == '_' { continue } return false } return true }