mirror of
https://github.com/gopasspw/gopass.git
synced 2025-12-08 19:24:54 +00:00
RELEASE_NOTES=n/a Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
388 lines
8.8 KiB
Go
388 lines
8.8 KiB
Go
package gitconfig
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gopasspw/gopass/pkg/debug"
|
|
)
|
|
|
|
var keyValueTpl = "\t%s = %s%s"
|
|
|
|
// Config is a single parsed config file. It contains a reference of the input file, if any.
|
|
// It can only be populated only by reading the environment variables.
|
|
type Config struct {
|
|
path string
|
|
readonly bool // do not allow modifying values (even in memory)
|
|
noWrites bool // do not persist changes to disk (e.g. for tests)
|
|
raw strings.Builder
|
|
// TODO(#2457): support multi-vars
|
|
vars map[string]string
|
|
}
|
|
|
|
// Unset deletes a key.
|
|
func (c *Config) Unset(key string) error {
|
|
if c.readonly {
|
|
return nil
|
|
}
|
|
|
|
_, present := c.vars[key]
|
|
if !present {
|
|
return nil
|
|
}
|
|
|
|
delete(c.vars, key)
|
|
|
|
return c.rewriteRaw(key, "", func(fKey, key, value, comment string) (string, bool) {
|
|
return "", true
|
|
})
|
|
}
|
|
|
|
// IsSet returns true if the key was set in this config.
|
|
func (c *Config) IsSet(key string) bool {
|
|
_, present := c.vars[key]
|
|
|
|
return present
|
|
}
|
|
|
|
// Set updates or adds a key in the config. If possible it will also update the underlying
|
|
// config file on disk.
|
|
func (c *Config) Set(key, value string) error {
|
|
section, _, subkey := splitKey(key)
|
|
if section == "" || subkey == "" {
|
|
return fmt.Errorf("invalid key: %s", key)
|
|
}
|
|
|
|
// can't set env vars
|
|
if c.readonly {
|
|
debug.Log("can not write to a readonly config")
|
|
|
|
return nil
|
|
}
|
|
|
|
if c.vars == nil {
|
|
c.vars = make(map[string]string, 16)
|
|
}
|
|
|
|
// already present at the same value, no need to rewrite the config
|
|
if v, found := c.vars[key]; found && v == value {
|
|
debug.Log("key %q with value %q already present. Not re-writing.", key, value)
|
|
|
|
return nil
|
|
}
|
|
|
|
_, present := c.vars[key]
|
|
c.vars[key] = value
|
|
|
|
debug.Log("set %q to %q", key, value)
|
|
|
|
// a new key, insert it into an existing section, if any
|
|
if !present {
|
|
debug.Log("inserting value")
|
|
|
|
return c.insertValue(key, value)
|
|
}
|
|
|
|
debug.Log("updating value")
|
|
|
|
return c.rewriteRaw(key, value, func(fKey, sKey, value, comment string) (string, bool) {
|
|
return fmt.Sprintf(keyValueTpl, sKey, value, comment), false
|
|
})
|
|
}
|
|
|
|
func (c *Config) insertValue(key, value string) error {
|
|
debug.Log("input (%s: %s): ---------\n%s\n-----------\n", key, value, c.raw.String())
|
|
|
|
wSection, wSubsection, wKey := splitKey(key)
|
|
|
|
s := bufio.NewScanner(strings.NewReader(c.raw.String()))
|
|
|
|
lines := make([]string, 0, 128)
|
|
var section string
|
|
var subsection string
|
|
var written bool
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
|
|
lines = append(lines, line)
|
|
|
|
if written {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, ";") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "[") {
|
|
s, subs, skip := parseSectionHeader(line)
|
|
if skip {
|
|
continue
|
|
}
|
|
section = s
|
|
subsection = subs
|
|
}
|
|
|
|
if section != wSection {
|
|
continue
|
|
}
|
|
if subsection != wSubsection {
|
|
continue
|
|
}
|
|
|
|
lines = append(lines, fmt.Sprintf(keyValueTpl, wKey, value, ""))
|
|
written = true
|
|
}
|
|
|
|
// not added to an existing section, so add it at the end
|
|
if !written {
|
|
sect := fmt.Sprintf("[%s]", wSection)
|
|
if wSubsection != "" {
|
|
sect = fmt.Sprintf("[%s \"%s\"]", wSection, wSubsection)
|
|
}
|
|
lines = append(lines, sect)
|
|
lines = append(lines, fmt.Sprintf(keyValueTpl, wKey, value, ""))
|
|
}
|
|
|
|
c.raw = strings.Builder{}
|
|
c.raw.WriteString(strings.Join(lines, "\n"))
|
|
c.raw.WriteString("\n")
|
|
|
|
debug.Log("output: ---------\n%s\n-----------\n", c.raw.String())
|
|
|
|
return c.flushRaw()
|
|
}
|
|
|
|
func parseSectionHeader(line string) (section, subsection string, skip bool) { //nolint:nonamedreturns
|
|
line = strings.Trim(line, "[]")
|
|
if line == "" {
|
|
return "", "", true
|
|
}
|
|
wsp := strings.Index(line, " ")
|
|
if wsp < 0 {
|
|
return line, "", false
|
|
}
|
|
|
|
section = line[:wsp]
|
|
subsection = line[wsp+1:]
|
|
subsection = strings.ReplaceAll(subsection, "\\", "")
|
|
subsection = strings.TrimPrefix(subsection, "\"")
|
|
subsection = strings.TrimSuffix(subsection, "\"")
|
|
|
|
return section, subsection, false
|
|
}
|
|
|
|
// rewriteRaw is used to rewrite the raw config copy. It is used for set and unset operations
|
|
// with different callbacks each.
|
|
func (c *Config) rewriteRaw(key, value string, cb parseFunc) error {
|
|
debug.Log("input (%s: %s): ---------\n%s\n-----------\n", key, value, c.raw.String())
|
|
|
|
lines := parseConfig(strings.NewReader(c.raw.String()), key, value, cb)
|
|
|
|
c.raw = strings.Builder{}
|
|
c.raw.WriteString(strings.Join(lines, "\n"))
|
|
c.raw.WriteString("\n")
|
|
|
|
debug.Log("output: ---------\n%s\n-----------\n", c.raw.String())
|
|
|
|
return c.flushRaw()
|
|
}
|
|
|
|
func (c *Config) flushRaw() error {
|
|
if c.noWrites || c.path == "" {
|
|
debug.Log("not writing changes to disk (noWrites %t, path %q)", c.noWrites, c.path)
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(c.path), 0o700); err != nil {
|
|
return err
|
|
}
|
|
|
|
debug.Log("writing config to %s: -----------\n%s\n--------------", c.path, c.raw.String())
|
|
|
|
if err := os.WriteFile(c.path, []byte(c.raw.String()), 0o600); err != nil {
|
|
return fmt.Errorf("failed to write config to %s: %w", c.path, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type parseFunc func(fqkn, skn, value, comment string) (newLine string, skipLine bool)
|
|
|
|
// parseConfig implements a simple parser for the gitconfig subset we support.
|
|
// The idea is to save all lines unaltered so we can reproduce the config
|
|
// almost exactly. Then we skip comments and extract section and subsection
|
|
// header. The next steps depend on the mode. Either we want to extract the
|
|
// values when loading (key and value empty, parseFunc adds the key-value pairs
|
|
// to the vars map), update a key (key is the target key, value the new value)
|
|
// or delete a key (parseFunc returns skip).
|
|
func parseConfig(in io.Reader, key, value string, cb parseFunc) []string {
|
|
wSection, wSubsection, wKey := splitKey(key)
|
|
|
|
s := bufio.NewScanner(in)
|
|
|
|
lines := make([]string, 0, 128)
|
|
var section string
|
|
var subsection string
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
|
|
lines = append(lines, line)
|
|
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, ";") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "[") {
|
|
s, subs, skip := parseSectionHeader(line)
|
|
if skip {
|
|
continue
|
|
}
|
|
section = s
|
|
subsection = subs
|
|
}
|
|
|
|
if key != "" && (section != wSection && subsection != wSubsection) {
|
|
continue
|
|
}
|
|
|
|
kvp := strings.Split(line, "=")
|
|
trim(kvp)
|
|
if len(kvp) < 2 {
|
|
continue
|
|
}
|
|
|
|
fKey := section + "."
|
|
if subsection != "" {
|
|
fKey += subsection + "."
|
|
}
|
|
fKey += kvp[0]
|
|
if key == "" {
|
|
wKey = kvp[0]
|
|
}
|
|
|
|
oValue := kvp[1]
|
|
comment := ""
|
|
|
|
if strings.ContainsAny(oValue, "#;") {
|
|
comment = " " + oValue[strings.IndexAny(oValue, "#;"):]
|
|
oValue = oValue[:strings.IndexAny(oValue, "#;")]
|
|
oValue = strings.TrimSpace(oValue)
|
|
}
|
|
|
|
if key != "" && (key != fKey) {
|
|
continue
|
|
}
|
|
if key != "" {
|
|
oValue = value
|
|
}
|
|
|
|
newLine, skip := cb(fKey, wKey, oValue, comment)
|
|
if skip {
|
|
// remove the last line
|
|
lines = lines[:len(lines)-1]
|
|
|
|
continue
|
|
}
|
|
lines[len(lines)-1] = newLine
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
// NewFromMap allows creating a new preset config from a map.
|
|
func NewFromMap(data map[string]string) *Config {
|
|
c := &Config{
|
|
readonly: true,
|
|
vars: make(map[string]string, len(data)),
|
|
}
|
|
|
|
for k, v := range data {
|
|
c.vars[k] = v
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// LoadConfig tries to load a gitconfig from the given path.
|
|
func LoadConfig(fn string) (*Config, error) {
|
|
fh, err := os.Open(fn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fh.Close() //nolint:errcheck
|
|
|
|
c := ParseConfig(fh)
|
|
c.path = fn
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// ParseConfig will try to parse a gitconfig from the given io.Reader. It never fails.
|
|
// Invalid configs will be silently rejceted.
|
|
func ParseConfig(r io.Reader) *Config {
|
|
c := &Config{
|
|
vars: make(map[string]string, 42),
|
|
}
|
|
|
|
lines := parseConfig(r, "", "", func(fk, k, v, comment string) (string, bool) {
|
|
c.vars[fk] = v
|
|
|
|
return fmt.Sprintf(keyValueTpl, k, v, comment), false
|
|
})
|
|
|
|
c.raw.WriteString(strings.Join(lines, "\n"))
|
|
c.raw.WriteString("\n")
|
|
|
|
debug.Log("processed config: %s\nvars: %+v", c.raw.String(), c.vars)
|
|
|
|
return c
|
|
}
|
|
|
|
// LoadConfigFromEnv will try to parse an overlay config from the environment variables.
|
|
// If no environment variables are set the resulting config will be valid but empty.
|
|
// Either way it will not be writeable.
|
|
func LoadConfigFromEnv(envPrefix string) *Config {
|
|
c := &Config{
|
|
noWrites: true,
|
|
}
|
|
|
|
count, err := strconv.Atoi(os.Getenv(envPrefix + "_CONFIG_COUNT"))
|
|
if err != nil || count < 1 {
|
|
return &Config{
|
|
noWrites: true,
|
|
}
|
|
}
|
|
|
|
c.vars = make(map[string]string, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
keyVar := fmt.Sprintf("%s%d", envPrefix+"_CONFIG_KEY_", i)
|
|
key := os.Getenv(keyVar)
|
|
|
|
valVar := fmt.Sprintf("%s%d", envPrefix+"_CONFIG_VALUE_", i)
|
|
value, found := os.LookupEnv(valVar)
|
|
|
|
if key == "" || !found {
|
|
return &Config{
|
|
noWrites: true,
|
|
}
|
|
}
|
|
|
|
c.vars[key] = value
|
|
debug.Log("added %s from env", key)
|
|
}
|
|
|
|
return c
|
|
}
|