mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-12 14:27:22 -04:00
187 lines
4.3 KiB
Go
187 lines
4.3 KiB
Go
package utils
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type AppChecker interface {
|
|
CommandExists(cmd string) bool
|
|
AnyCommandExists(cmds ...string) bool
|
|
FlatpakExists(name string) bool
|
|
AnyFlatpakExists(flatpaks ...string) bool
|
|
}
|
|
|
|
type DefaultAppChecker struct{}
|
|
|
|
func (DefaultAppChecker) CommandExists(cmd string) bool {
|
|
return CommandExists(cmd)
|
|
}
|
|
|
|
func (DefaultAppChecker) AnyCommandExists(cmds ...string) bool {
|
|
return AnyCommandExists(cmds...)
|
|
}
|
|
|
|
func (DefaultAppChecker) FlatpakExists(name string) bool {
|
|
return FlatpakExists(name)
|
|
}
|
|
|
|
func (DefaultAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
|
|
return AnyFlatpakExists(flatpaks...)
|
|
}
|
|
|
|
func CommandExists(cmd string) bool {
|
|
_, err := exec.LookPath(cmd)
|
|
if err == nil {
|
|
return true
|
|
}
|
|
if strings.ContainsRune(cmd, os.PathSeparator) {
|
|
return false
|
|
}
|
|
for _, dir := range userBinDirs() {
|
|
path := filepath.Join(dir, cmd)
|
|
info, statErr := os.Stat(path)
|
|
if statErr == nil && !info.IsDir() && info.Mode()&0o111 != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func AnyCommandExists(cmds ...string) bool {
|
|
return slices.ContainsFunc(cmds, CommandExists)
|
|
}
|
|
|
|
func EnvWithUserBinPath(env []string) []string {
|
|
if env == nil {
|
|
env = os.Environ()
|
|
}
|
|
|
|
out := append([]string(nil), env...)
|
|
pathIndex := -1
|
|
pathValue := ""
|
|
for i, entry := range out {
|
|
if strings.HasPrefix(entry, "PATH=") {
|
|
pathIndex = i
|
|
pathValue = strings.TrimPrefix(entry, "PATH=")
|
|
break
|
|
}
|
|
}
|
|
|
|
parts := filepath.SplitList(pathValue)
|
|
seen := make(map[string]struct{}, len(parts))
|
|
for _, part := range parts {
|
|
if part != "" {
|
|
seen[part] = struct{}{}
|
|
}
|
|
}
|
|
|
|
prepend := make([]string, 0, len(userBinDirs()))
|
|
for _, dir := range userBinDirs() {
|
|
if dir == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[dir]; ok {
|
|
continue
|
|
}
|
|
prepend = append(prepend, dir)
|
|
seen[dir] = struct{}{}
|
|
}
|
|
|
|
parts = append(prepend, parts...)
|
|
newPath := "PATH=" + strings.Join(parts, string(os.PathListSeparator))
|
|
if pathIndex >= 0 {
|
|
out[pathIndex] = newPath
|
|
} else {
|
|
out = append(out, newPath)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func userBinDirs() []string {
|
|
dirs := []string{}
|
|
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
|
dirs = append(dirs,
|
|
filepath.Join(home, ".local", "bin"),
|
|
filepath.Join(home, ".nix-profile", "bin"),
|
|
)
|
|
if user := filepath.Base(home); user != "" && user != string(filepath.Separator) {
|
|
dirs = append(dirs, filepath.Join("/etc/profiles/per-user", user, "bin"))
|
|
}
|
|
}
|
|
dirs = append(dirs, "/usr/local/bin")
|
|
return dirs
|
|
}
|
|
|
|
const maxJSONProcessBytes = 4 * 1024 * 1024
|
|
|
|
type boundedWriter struct {
|
|
bytes.Buffer
|
|
limit int
|
|
}
|
|
|
|
func (w *boundedWriter) Write(p []byte) (int, error) {
|
|
if len(p) > w.limit-w.Len() {
|
|
return 0, errors.New("process output exceeds size bound")
|
|
}
|
|
return w.Buffer.Write(p)
|
|
}
|
|
|
|
func RunJSON(ctx context.Context, executable string, args []string, input []byte, result any) error {
|
|
if len(input) > maxJSONProcessBytes {
|
|
return errors.New("request exceeds size bound")
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, executable, args...)
|
|
cmd.WaitDelay = time.Second
|
|
stdout := &boundedWriter{limit: maxJSONProcessBytes + 1}
|
|
stderr := &boundedWriter{limit: 16 * 1024}
|
|
cmd.Stdout, cmd.Stderr = stdout, stderr
|
|
if input != nil {
|
|
cmd.Stdin = bytes.NewReader(input)
|
|
}
|
|
err := cmd.Run()
|
|
if ctx.Err() != nil {
|
|
return fmt.Errorf("unavailable: command completion uncertain: %w", ctx.Err())
|
|
}
|
|
if stdout.Len() != 0 {
|
|
if decodeErr := DecodeJSON(bytes.TrimSuffix(stdout.Bytes(), []byte{'\n'}), result); decodeErr != nil {
|
|
return errors.Join(err, fmt.Errorf("invalid process JSON: %w", decodeErr))
|
|
}
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", executable, err)
|
|
}
|
|
if stdout.Len() == 0 {
|
|
return errors.New("missing command acknowledgement")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DecodeJSON(data []byte, target any) error {
|
|
if len(data) > maxJSONProcessBytes || !utf8.Valid(data) {
|
|
return errors.New("invalid UTF-8 or oversized JSON record")
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(new(any)); err != io.EOF {
|
|
return errors.New("trailing JSON data")
|
|
}
|
|
return nil
|
|
}
|