-
-
Notifications
You must be signed in to change notification settings - Fork 796
Better XDG directory variable fix for Snap #1701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
3f008d5
Test parsing PAM to get shell envs
esimkowitz 525f33c
move code
esimkowitz 4f2138a
update tests
esimkowitz 7e06dc9
update comments for regex
esimkowitz 89d3869
add license identifier to test
esimkowitz 1d1000b
only set the XDG vars from pam, ignore others
esimkowitz 903365c
fix so we aren't doing more work than we need to
esimkowitz 797cc0d
one more fix
esimkowitz 93257a5
add logs
esimkowitz a142505
replace home and shell with vals from /etc/passwd
esimkowitz 5bc4a34
apply coderabbit suggestion
esimkowitz 7fe0499
Update pkg/util/pamparse/pamparse.go
esimkowitz 5349581
apply some coderabbit suggestions
esimkowitz dbaa772
remove unnecessary *
esimkowitz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| // Copyright 2025, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // Package pamparse provides functions for parsing environment files in the format of /etc/environment, /etc/security/pam_env.conf, and ~/.pam_environment. | ||
| package pamparse | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "fmt" | ||
| "os" | ||
| "regexp" | ||
| "strings" | ||
| ) | ||
|
|
||
| // Parses a file in the format of /etc/environment. Accepts a path to the file and returns a map of environment variables. | ||
| func ParseEnvironmentFile(path string) (map[string]string, error) { | ||
| rtn := make(map[string]string) | ||
| file, err := os.Open(path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer file.Close() | ||
| scanner := bufio.NewScanner(file) | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| key, val := parseEnvironmentLine(line) | ||
| if key == "" { | ||
| continue | ||
| } | ||
| rtn[key] = val | ||
| } | ||
| return rtn, nil | ||
| } | ||
|
|
||
| // Parses a file in the format of /etc/security/pam_env.conf or ~/.pam_environment. Accepts a path to the file and returns a map of environment variables. | ||
| func ParseEnvironmentConfFile(path string) (map[string]string, error) { | ||
| rtn := make(map[string]string) | ||
| file, err := os.Open(path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer file.Close() | ||
| home, shell, err := parsePasswd() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| scanner := bufio.NewScanner(file) | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| key, val := parseEnvironmentConfLine(line) | ||
|
|
||
| // Fall back to ParseEnvironmentLine if ParseEnvironmentConfLine fails | ||
| if key == "" { | ||
| key, val = parseEnvironmentLine(line) | ||
| if key == "" { | ||
| continue | ||
| } | ||
| } | ||
| rtn[key] = replaceHomeAndShell(val, home, shell) | ||
| } | ||
| return rtn, nil | ||
| } | ||
|
|
||
| // Gets the home directory and shell from /etc/passwd for the current user. | ||
| func parsePasswd() (string, string, error) { | ||
| file, err := os.Open("/etc/passwd") | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
| defer file.Close() | ||
| userPrefix := fmt.Sprintf("%s:", os.Getenv("USER")) | ||
| scanner := bufio.NewScanner(file) | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| if strings.HasPrefix(line, userPrefix) { | ||
| parts := strings.Split(line, ":") | ||
| if len(parts) < 7 { | ||
| return "", "", fmt.Errorf("invalid passwd entry: insufficient fields") | ||
| } | ||
| return parts[5], parts[6], nil | ||
| } | ||
| } | ||
| if err := scanner.Err(); err != nil { | ||
| return "", "", fmt.Errorf("error reading passwd file: %w", err) | ||
| } | ||
| return "", "", nil | ||
| } | ||
|
|
||
| // Replaces @{HOME} and @{SHELL} placeholders in a string with the provided values. Follows guidance from https://wiki.archlinux.org/title/Environment_variables#Using_pam_env | ||
| func replaceHomeAndShell(val string, home string, shell string) string { | ||
| val = strings.ReplaceAll(val, "@{HOME}", home) | ||
| val = strings.ReplaceAll(val, "@{SHELL}", shell) | ||
| return val | ||
| } | ||
|
|
||
| // Regex to parse a line from /etc/environment. Follows the guidance from https://wiki.archlinux.org/title/Environment_variables#Using_pam_env | ||
| var envFileLineRe = regexp.MustCompile(`^(?:export\s+)?([A-Z0-9_]+[A-Za-z0-9]*)=(.*)$`) | ||
|
|
||
| func parseEnvironmentLine(line string) (string, string) { | ||
| m := envFileLineRe.FindStringSubmatch(line) | ||
| if m == nil { | ||
| return "", "" | ||
| } | ||
| return m[1], sanitizeEnvVarValue(m[2]) | ||
| } | ||
|
|
||
| // Regex to parse a line from /etc/security/pam_env.conf or ~/.pam_environment. Follows the guidance from https://wiki.archlinux.org/title/Environment_variables#Using_pam_env | ||
| var confFileLineRe = regexp.MustCompile(`^([A-Z0-9_]+[A-Za-z0-9]*)\s+(?:(?:DEFAULT=)([^\s]+(?: \w+)*))\s*(?:(?:OVERRIDE=)([^\s]+(?: \w+)*))?\s*$`) | ||
|
|
||
| func parseEnvironmentConfLine(line string) (string, string) { | ||
| m := confFileLineRe.FindStringSubmatch(line) | ||
| if m == nil { | ||
| return "", "" | ||
| } | ||
| var vals []string | ||
| if len(m) > 3 && m[3] != "" { | ||
| vals = []string{sanitizeEnvVarValue(m[3]), sanitizeEnvVarValue(m[2])} | ||
| } else { | ||
| vals = []string{sanitizeEnvVarValue(m[2])} | ||
| } | ||
| return m[1], strings.Join(vals, ":") | ||
| } | ||
|
|
||
| // Sanitizes an environment variable value by stripping comments and trimming quotes. | ||
| func sanitizeEnvVarValue(val string) string { | ||
| return stripComments(trimQuotes(val)) | ||
| } | ||
|
|
||
| // Trims quotes as defined by https://unix.stackexchange.com/questions/748790/where-is-the-syntax-for-etc-environment-documented | ||
| func trimQuotes(val string) string { | ||
| if strings.HasPrefix(val, "\"") || strings.HasPrefix(val, "'") { | ||
| val = val[1:] | ||
| if strings.HasSuffix(val, "\"") || strings.HasSuffix(val, "'") { | ||
| val = val[0 : len(val)-1] | ||
| } | ||
| } | ||
| return val | ||
| } | ||
|
|
||
| // Strips comments as defined by https://unix.stackexchange.com/questions/748790/where-is-the-syntax-for-etc-environment-documented | ||
| func stripComments(val string) string { | ||
| commentIdx := strings.Index(val, "#") | ||
| if commentIdx == -1 { | ||
| return val | ||
| } | ||
| return val[0:commentIdx] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| // Copyright 2025, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package pamparse_test | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/wavetermdev/waveterm/pkg/util/pamparse" | ||
| ) | ||
|
|
||
| // Tests influenced by https://unix.stackexchange.com/questions/748790/where-is-the-syntax-for-etc-environment-documented | ||
| func TestParseEnvironmentFile(t *testing.T) { | ||
| const fileContent = ` | ||
| FOO1=bar | ||
| FOO2="bar" | ||
| FOO3="bar | ||
| FOO4=bar" | ||
| FOO5='bar' | ||
| FOO6='bar" | ||
| export FOO7=bar | ||
| FOO8=bar bar bar | ||
| #FOO9=bar | ||
| FOO10=$PATH | ||
| FOO11="foo#bar" | ||
| ` | ||
|
|
||
| // create a temporary file with the content | ||
| tempFile := filepath.Join(t.TempDir(), "pam_env") | ||
| if err := os.WriteFile(tempFile, []byte(fileContent), 0644); err != nil { | ||
| t.Fatalf("failed to write file: %v", err) | ||
| } | ||
|
|
||
| // parse the file | ||
| got, err := pamparse.ParseEnvironmentFile(tempFile) | ||
| if err != nil { | ||
| t.Fatalf("failed to parse pam environment file: %v", err) | ||
| } | ||
|
|
||
| want := map[string]string{ | ||
| "FOO1": "bar", | ||
| "FOO2": "bar", | ||
| "FOO3": "bar", | ||
| "FOO4": "bar\"", | ||
| "FOO5": "bar", | ||
| "FOO6": "bar", | ||
| "FOO7": "bar", | ||
| "FOO8": "bar bar bar", | ||
| "FOO10": "$PATH", | ||
| "FOO11": "foo", | ||
| } | ||
|
|
||
| if len(got) != len(want) { | ||
| t.Fatalf("expected %d environment variables, got %d", len(want), len(got)) | ||
| } | ||
| for k, v := range want { | ||
| if got[k] != v { | ||
| t.Errorf("expected %q to be %q, got %q", k, v, got[k]) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestParseEnvironmentConfFile(t *testing.T) { | ||
| const fileContent = ` | ||
| TEST DEFAULT=@{HOME}/.config\ state OVERRIDE=./config\ s | ||
| FOO DEFAULT=@{HOME}/.config\ s | ||
| STRING DEFAULT="string" | ||
| STRINGOVERRIDE DEFAULT="string" OVERRIDE="string2" | ||
| FOO11="foo#bar" | ||
| ` | ||
|
|
||
| // create a temporary file with the content | ||
| tempFile := filepath.Join(t.TempDir(), "pam_env_conf") | ||
| if err := os.WriteFile(tempFile, []byte(fileContent), 0644); err != nil { | ||
| t.Fatalf("failed to write file: %v", err) | ||
| } | ||
|
|
||
| // parse the file | ||
| got, err := pamparse.ParseEnvironmentConfFile(tempFile) | ||
| if err != nil { | ||
| t.Fatalf("failed to parse pam environment conf file: %v", err) | ||
| } | ||
|
|
||
| want := map[string]string{ | ||
| "TEST": "./config\\ s:@{HOME}/.config\\ state", | ||
| "FOO": "@{HOME}/.config\\ s", | ||
| "STRING": "string", | ||
| "STRINGOVERRIDE": "string2:string", | ||
| "FOO11": "foo", | ||
| } | ||
|
|
||
| if len(got) != len(want) { | ||
| t.Fatalf("expected %d environment variables, got %d", len(want), len(got)) | ||
| } | ||
| for k, v := range want { | ||
| if got[k] != v { | ||
| t.Errorf("expected %q to be %q, got %q", k, v, got[k]) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.