// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package config import ( "errors" "fmt" "io" "os" "path/filepath" ) // WriteGreeterSettings installs the effective saved profile at path. The caller // must run as the destination account; no privileged filesystem writes are needed. func WriteGreeterSettings(path string, settings Settings) error { contents, err := GreeterSettings(settings) if err != nil { return err } if settings.Path != "" { sourcePath, err := filepath.Abs(settings.Path) if err != nil { return err } targetPath, err := filepath.Abs(path) if err != nil { return err } source, sourceErr := os.Stat(settings.Path) target, targetErr := os.Stat(path) if sourcePath == targetPath || sourceErr == nil && targetErr == nil && os.SameFile(source, target) { return errors.New("greeter copy cannot replace the source settings") } } if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return fmt.Errorf("create greeter settings directory: %w", err) } temporary, err := os.CreateTemp(filepath.Dir(path), ".greeter-settings-*.toml") if err != nil { return fmt.Errorf("create greeter settings copy: %w", err) } defer os.Remove(temporary.Name()) if _, err := temporary.Write(contents); err != nil { temporary.Close() return err } if err := temporary.Sync(); err != nil { temporary.Close() return err } if err := temporary.Close(); err != nil { return err } if err := os.Rename(temporary.Name(), path); err != nil { return fmt.Errorf("replace greeter settings copy: %w", err) } return syncDirectory(filepath.Dir(path)) } // GreeterSettings encodes a complete active profile without application mappings // or other profiles. Tentacle registries are stored separately and are not copied. func GreeterSettings(settings Settings) ([]byte, error) { if err := validate(settings); err != nil { return nil, err } return encodeDocument(writableDocument{ SchemaVersion: CurrentSchemaVersion, ActiveProfile: "greeter", Profiles: map[string]writableProfile{"greeter": profileFromSettings(settings)}, }) } // ReadGreeterSettings reads the bounded snapshot from an already-open pipe. // Reopening /dev/stdin after switching users can fail its ownership checks. func ReadGreeterSettings(reader io.Reader) (Settings, error) { contents, err := io.ReadAll(io.LimitReader(reader, maxConfigSize+1)) if err != nil { return Settings{}, fmt.Errorf("read greeter settings: %w", err) } if len(contents) > maxConfigSize { return Settings{}, fmt.Errorf("greeter settings exceed %d bytes", maxConfigSize) } return decodeWritable(contents) }