mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
547 lines
15 KiB
Go
547 lines
15 KiB
Go
package intercept
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"mvdan.cc/sh/v3/expand"
|
|
"mvdan.cc/sh/v3/syntax"
|
|
)
|
|
|
|
const maxInlineVariants = 64
|
|
const maxInlineParses = 256
|
|
|
|
// Inline code is a shell string, not a filename. Accept escaped source text
|
|
// and simple quoted parameters proven by finite, unmodified enclosing loops.
|
|
// Expansion has no host environment, subprocess or filesystem callbacks.
|
|
func inlineShellWords(w *syntax.Word, bindings map[string][]string) ([]string, bool) {
|
|
if w == nil {
|
|
return nil, false
|
|
}
|
|
var names []string
|
|
seen := map[string]bool{}
|
|
var validParts func([]syntax.WordPart, bool) bool
|
|
validParts = func(parts []syntax.WordPart, quoted bool) bool {
|
|
for _, part := range parts {
|
|
switch p := part.(type) {
|
|
case *syntax.Lit:
|
|
if !quoted && strings.ContainsAny(p.Value, "*?[]~") {
|
|
return false
|
|
}
|
|
case *syntax.SglQuoted:
|
|
case *syntax.DblQuoted:
|
|
if !validParts(p.Parts, true) {
|
|
return false
|
|
}
|
|
case *syntax.ParamExp:
|
|
if !quoted || p.Excl || p.Length || p.Width || p.Index != nil || p.Slice != nil || p.Repl != nil || p.Names != 0 || p.Exp != nil || len(bindings[p.Param.Value]) == 0 {
|
|
return false
|
|
}
|
|
if !seen[p.Param.Value] {
|
|
names = append(names, p.Param.Value)
|
|
seen[p.Param.Value] = true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
if !validParts(w.Parts, false) {
|
|
return nil, false
|
|
}
|
|
envs := [][]string{nil}
|
|
for _, name := range names {
|
|
if len(envs) > maxInlineVariants/len(bindings[name]) {
|
|
return nil, false
|
|
}
|
|
var next [][]string
|
|
for _, env := range envs {
|
|
for _, value := range bindings[name] {
|
|
pairs := append([]string(nil), env...)
|
|
next = append(next, append(pairs, name+"="+value))
|
|
}
|
|
}
|
|
envs = next
|
|
}
|
|
var words []string
|
|
for _, env := range envs {
|
|
fields, err := expand.Fields(&expand.Config{Env: expand.ListEnviron(env...), NoUnset: true}, w)
|
|
if err != nil || len(fields) != 1 || len(fields[0]) > maxReviewScriptBytes {
|
|
return nil, false
|
|
}
|
|
words = append(words, fields[0])
|
|
}
|
|
return words, true
|
|
}
|
|
|
|
// Avoid guessing loop values in the presence of assignments or commands which
|
|
// can mutate shell variables. The analysis intentionally prefers incomplete
|
|
// capture to interpreting eval, source, arithmetic or arbitrary builtins.
|
|
func loopBindingWrites(tree *syntax.File, constants map[string]shellConstant) (map[string]bool, bool) {
|
|
writes := map[string]bool{}
|
|
unknown := false
|
|
loops := map[string]int{}
|
|
var parents []syntax.Node
|
|
syntax.Walk(tree, func(node syntax.Node) bool {
|
|
if node == nil {
|
|
parent := parents[len(parents)-1]
|
|
parents = parents[:len(parents)-1]
|
|
if loop, ok := parent.(*syntax.ForClause); ok {
|
|
if iter, ok := loop.Loop.(*syntax.WordIter); ok {
|
|
loops[iter.Name.Value]--
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
parents = append(parents, node)
|
|
switch n := node.(type) {
|
|
case *syntax.Assign:
|
|
if n.Name != nil {
|
|
writes[n.Name.Value] = true
|
|
}
|
|
case *syntax.ForClause:
|
|
if iter, ok := n.Loop.(*syntax.WordIter); ok {
|
|
if loops[iter.Name.Value] > 0 {
|
|
writes[iter.Name.Value] = true
|
|
}
|
|
loops[iter.Name.Value]++
|
|
}
|
|
case *syntax.FuncDecl:
|
|
unknown = true
|
|
case *syntax.ArithmExp, *syntax.ArithmCmd, *syntax.CStyleLoop:
|
|
unknown = true
|
|
case *syntax.ParamExp:
|
|
if n.Exp != nil && (n.Exp.Op == syntax.AssignUnset || n.Exp.Op == syntax.AssignUnsetOrNull) {
|
|
writes[n.Param.Value] = true
|
|
}
|
|
case *syntax.CallExpr:
|
|
if len(n.Args) == 0 {
|
|
break
|
|
}
|
|
resolved, known := constantCallArgs(n.Args, constants)
|
|
if !known {
|
|
unknown = true
|
|
return true
|
|
}
|
|
args, name, ok := unwrapScriptCall(resolved)
|
|
if !ok {
|
|
unknown = true
|
|
return true
|
|
}
|
|
switch name {
|
|
case "eval", "source", ".", "read", "readarray", "mapfile", "unset", "declare", "typeset", "local", "export", "let", "getopts", "trap", "alias":
|
|
unknown = true
|
|
case "printf":
|
|
if len(args) > 1 {
|
|
values, known := inlineShellWords(args[1], nil)
|
|
if !known || strings.HasPrefix(values[0], "-v") {
|
|
unknown = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return writes, unknown
|
|
}
|
|
|
|
func forLoopBindings(loop *syntax.ForClause, bindings map[string][]string, writes map[string]bool, unknown bool) map[string][]string {
|
|
next := make(map[string][]string, len(bindings)+1)
|
|
for name, values := range bindings {
|
|
next[name] = values
|
|
}
|
|
iter, ok := loop.Loop.(*syntax.WordIter)
|
|
if !ok {
|
|
return next
|
|
}
|
|
name := iter.Name.Value
|
|
delete(next, name)
|
|
if unknown || writes[name] || loop.Select || !iter.InPos.IsValid() || len(iter.Items) == 0 || len(iter.Items) > maxInlineVariants {
|
|
return next
|
|
}
|
|
var values []string
|
|
for _, item := range iter.Items {
|
|
value, known := staticWord(item)
|
|
if !known {
|
|
return next
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
next[name] = values
|
|
return next
|
|
}
|
|
|
|
// Unwrap only options whose argument layout is known. Treat unsupported layouts
|
|
// as opaque instead of mistaking an option's value for the executed command.
|
|
func unwrapScriptCall(args []*syntax.Word) ([]*syntax.Word, string, bool) {
|
|
for len(args) > 0 {
|
|
name, ok := staticCommandName(args[0])
|
|
if !ok {
|
|
return args, "", false
|
|
}
|
|
wrapper := filepath.Base(name)
|
|
switch wrapper {
|
|
case "env", "sudo", "exec", "timeout", "nohup", "command", "builtin", "time":
|
|
default:
|
|
return args, name, true
|
|
}
|
|
original := args
|
|
if len(args) == 1 && wrapper != "timeout" {
|
|
return args, name, true
|
|
}
|
|
args = args[1:]
|
|
for len(args) > 0 {
|
|
value, known := staticWord(args[0])
|
|
if !known {
|
|
return args, "", false
|
|
}
|
|
if value == "--" {
|
|
args = args[1:]
|
|
break
|
|
}
|
|
if wrapper == "env" && strings.Contains(value, "=") && !strings.HasPrefix(value, "-") {
|
|
args = args[1:]
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(value, "-") || value == "-" {
|
|
break
|
|
}
|
|
consume := 1
|
|
switch wrapper {
|
|
case "timeout":
|
|
switch value {
|
|
case "-s", "--signal", "-k", "--kill-after":
|
|
consume = 2
|
|
case "--foreground", "--preserve-status", "--verbose", "-v":
|
|
default:
|
|
if !strings.HasPrefix(value, "--signal=") && !strings.HasPrefix(value, "--kill-after=") {
|
|
return args, "", false
|
|
}
|
|
}
|
|
case "time":
|
|
switch value {
|
|
case "-f", "--format", "-o", "--output":
|
|
consume = 2
|
|
case "-a", "--append", "-p", "--portability", "-q", "--quiet", "-v", "--verbose":
|
|
case "--help", "--version":
|
|
return original, name, true
|
|
default:
|
|
if !strings.HasPrefix(value, "--format=") && !strings.HasPrefix(value, "--output=") && !strings.HasPrefix(value, "-f") && !strings.HasPrefix(value, "-o") {
|
|
return args, "", false
|
|
}
|
|
}
|
|
case "sudo":
|
|
switch value {
|
|
case "-u", "--user", "-g", "--group", "-h", "--host", "-p", "--prompt", "-C", "--close-from", "-T", "--command-timeout":
|
|
consume = 2
|
|
case "-n", "--non-interactive", "-E", "--preserve-env", "-H", "--set-home", "-S", "--stdin", "-k", "--reset-timestamp":
|
|
default:
|
|
if !strings.HasPrefix(value, "--user=") && !strings.HasPrefix(value, "--group=") && !strings.HasPrefix(value, "--preserve-env=") {
|
|
return args, "", false
|
|
}
|
|
}
|
|
case "env":
|
|
switch value {
|
|
case "-u", "--unset":
|
|
consume = 2
|
|
case "-i", "--ignore-environment", "-0", "--null", "-v", "--debug":
|
|
default:
|
|
if !strings.HasPrefix(value, "--unset=") {
|
|
return args, "", false
|
|
}
|
|
}
|
|
case "exec":
|
|
if value == "-a" {
|
|
consume = 2
|
|
} else if value != "-c" && value != "-l" {
|
|
return args, "", false
|
|
}
|
|
case "command":
|
|
if value == "-v" || value == "-V" {
|
|
return original, name, true
|
|
}
|
|
if value != "-p" {
|
|
return args, "", false
|
|
}
|
|
default:
|
|
return args, "", false
|
|
}
|
|
if len(args) < consume {
|
|
return args, "", false
|
|
}
|
|
args = args[consume:]
|
|
}
|
|
if len(args) == 0 && wrapper != "timeout" {
|
|
return original, name, true
|
|
}
|
|
if wrapper == "timeout" {
|
|
if len(args) == 0 {
|
|
return args, "", false
|
|
}
|
|
if _, known := staticWord(args[0]); !known {
|
|
return args, "", false
|
|
}
|
|
args = args[1:]
|
|
}
|
|
}
|
|
return args, "", false
|
|
}
|
|
|
|
// Capture literal files created by a top-level `cat > file <<'EOF'` before a
|
|
// later interpreter call. These files do not exist yet during approval, and an
|
|
// existing on-disk version would be stale. Other writes remain explicitly opaque.
|
|
func generatedScriptWrites(stmt *syntax.Stmt, args []*syntax.Word, dir string, topLevel bool, generated map[string]scriptReference, allowSed bool) []scriptReference {
|
|
var writes []scriptReference
|
|
var input *syntax.Redirect
|
|
appendOutput := false
|
|
inputs, outputs := 0, 0
|
|
for _, r := range stmt.Redirs {
|
|
stdin := r.N == nil || r.N.Value == "0"
|
|
stdout := r.N == nil || r.N.Value == "1"
|
|
switch r.Op {
|
|
case syntax.Hdoc, syntax.DashHdoc, syntax.WordHdoc, syntax.RdrIn, syntax.DplIn:
|
|
if stdin {
|
|
inputs++
|
|
input = r
|
|
}
|
|
case syntax.RdrOut, syntax.ClbOut, syntax.AppOut, syntax.RdrAll:
|
|
if !stdout && r.Op != syntax.RdrAll {
|
|
continue
|
|
}
|
|
outputs++
|
|
appendOutput = appendOutput || r.Op == syntax.AppOut
|
|
path, known := staticWord(r.Word)
|
|
if !known || path == "" {
|
|
continue
|
|
}
|
|
if !filepath.IsAbs(path) {
|
|
path = filepath.Join(dir, path)
|
|
}
|
|
if filepath.IsAbs(path) || dir != "" {
|
|
absolute, err := filepath.Abs(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
path = absolute
|
|
}
|
|
writes = append(writes, scriptReference{path: path, generated: true, problem: "命令内脚本写入内容或执行路径不确定"})
|
|
}
|
|
}
|
|
if allowSed && len(writes) == 1 && outputs == 1 && inputs == 0 && !appendOutput && topLevel && !stmt.Background {
|
|
content, problem, known := generatedSedSnapshot(stmt, args, dir, writes[0].path, generated)
|
|
if known {
|
|
writes[0].problem = ""
|
|
writes[0].inline = true
|
|
writes[0].content = content
|
|
return writes
|
|
}
|
|
if problem != "" {
|
|
writes[0].problem = problem
|
|
}
|
|
}
|
|
if len(writes) != 1 || outputs != 1 || inputs != 1 || input == nil || (input.Op != syntax.Hdoc && input.Op != syntax.DashHdoc) || !topLevel || stmt.Background || len(args) != 1 {
|
|
return writes
|
|
}
|
|
name, known := staticCommandName(args[0])
|
|
if !known || filepath.Base(name) != "cat" {
|
|
return writes
|
|
}
|
|
// Only a cat's stderr duplication is transparent. Replacing stdout (>&2),
|
|
// closing descriptors, or combining output from other producers is opaque.
|
|
for _, r := range stmt.Redirs {
|
|
if r.Op == syntax.RdrAll {
|
|
return writes
|
|
}
|
|
if r.Op == syntax.DplOut {
|
|
target, known := staticWord(r.Word)
|
|
if r.N == nil || r.N.Value != "2" || !known || target != "1" {
|
|
return writes
|
|
}
|
|
}
|
|
}
|
|
quoted := false
|
|
syntax.Walk(input.Word, func(node syntax.Node) bool {
|
|
switch n := node.(type) {
|
|
case *syntax.SglQuoted, *syntax.DblQuoted:
|
|
quoted = true
|
|
case *syntax.Lit:
|
|
quoted = quoted || strings.Contains(n.Value, "\\")
|
|
}
|
|
return true
|
|
})
|
|
body := input.Hdoc
|
|
if body == nil {
|
|
body = &syntax.Word{}
|
|
}
|
|
var content string
|
|
if quoted {
|
|
var b strings.Builder
|
|
for _, part := range body.Parts {
|
|
lit, ok := part.(*syntax.Lit)
|
|
if !ok {
|
|
return writes
|
|
}
|
|
b.WriteString(lit.Value)
|
|
}
|
|
content = b.String()
|
|
} else {
|
|
// An unquoted heredoc performs shell expansion. Accept literal bodies
|
|
// only, and use heredoc quote/escape semantics without any execution.
|
|
for _, part := range body.Parts {
|
|
if _, ok := part.(*syntax.Lit); !ok {
|
|
return writes
|
|
}
|
|
}
|
|
var err error
|
|
content, err = expand.Document(&expand.Config{NoUnset: true}, body)
|
|
if err != nil {
|
|
return writes
|
|
}
|
|
}
|
|
if input.Op == syntax.DashHdoc {
|
|
lines := strings.Split(content, "\n")
|
|
for i := range lines {
|
|
lines[i] = strings.TrimLeft(lines[i], "\t")
|
|
}
|
|
content = strings.Join(lines, "\n")
|
|
}
|
|
if appendOutput {
|
|
previous := generated[writes[0].path]
|
|
if previous.path != "" {
|
|
if !previous.inline || previous.problem != "" {
|
|
return writes
|
|
}
|
|
content = previous.content + content
|
|
} else {
|
|
if !filepath.IsAbs(writes[0].path) {
|
|
return writes
|
|
}
|
|
old, err := readScript(writes[0].path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
writes[0].problem = "脚本追加前内容无法读取:" + err.Error()
|
|
return writes
|
|
}
|
|
content = string(old) + content
|
|
}
|
|
}
|
|
writes[0].problem = ""
|
|
writes[0].inline = true
|
|
writes[0].content = content
|
|
return writes
|
|
}
|
|
|
|
// These preparations have a statically known success path and do not transform
|
|
// a following quoted heredoc. Arbitrary guards, pipelines and transforms are
|
|
// intentionally excluded.
|
|
func staticScriptPreparation(stmt *syntax.Stmt) bool {
|
|
if stmt.Background {
|
|
return false
|
|
}
|
|
if binary, ok := stmt.Cmd.(*syntax.BinaryCmd); ok {
|
|
return binary.Op == syntax.AndStmt && staticScriptPreparation(binary.X) && staticScriptPreparation(binary.Y)
|
|
}
|
|
call, ok := stmt.Cmd.(*syntax.CallExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if len(call.Args) == 0 {
|
|
if len(call.Assigns) == 0 {
|
|
return false
|
|
}
|
|
for _, assign := range call.Assigns {
|
|
if assign.Name == nil || assign.Array != nil || assign.Index != nil {
|
|
return false
|
|
}
|
|
if _, known := staticWord(assign.Value); !known {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
name, known := staticCommandName(call.Args[0])
|
|
if !known {
|
|
return false
|
|
}
|
|
if name == "cd" && len(call.Args) == 2 {
|
|
path, known := staticWord(call.Args[1])
|
|
return known && path != "-"
|
|
}
|
|
if name != "mkdir" || len(call.Args) < 3 {
|
|
return false
|
|
}
|
|
flag, known := staticWord(call.Args[1])
|
|
if !known || (flag != "-p" && flag != "--parents") {
|
|
return false
|
|
}
|
|
for _, arg := range call.Args[2:] {
|
|
if _, known := staticWord(arg); !known {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Shell quote removal for literal words, including escaped spaces and ANSI-C
|
|
// quotes. Unquoted expansion syntax stays unresolved; no environment or code
|
|
// is consulted by the expander.
|
|
func literalShellParts(parts []syntax.WordPart) (string, bool) {
|
|
var known func([]syntax.WordPart, bool) bool
|
|
known = func(parts []syntax.WordPart, quoted bool) bool {
|
|
for _, part := range parts {
|
|
switch p := part.(type) {
|
|
case *syntax.Lit:
|
|
if !quoted && unquotedExpansion(p.Value) {
|
|
return false
|
|
}
|
|
case *syntax.SglQuoted:
|
|
case *syntax.DblQuoted:
|
|
if !known(p.Parts, true) {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
if !known(parts, false) {
|
|
return "", false
|
|
}
|
|
fields, err := expand.Fields(&expand.Config{NoUnset: true}, &syntax.Word{Parts: parts})
|
|
if err != nil || len(fields) != 1 {
|
|
return "", false
|
|
}
|
|
return fields[0], true
|
|
}
|
|
|
|
func unquotedExpansion(value string) bool {
|
|
for i := 0; i < len(value); i++ {
|
|
if value[i] == '\\' {
|
|
i++
|
|
continue
|
|
}
|
|
if strings.ContainsRune("*?[]~", rune(value[i])) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func interpreterInfoOnly(name, option string) bool {
|
|
if strings.HasPrefix(name, "python") {
|
|
switch option {
|
|
case "-V", "--version", "-h", "--help", "--help-all", "--help-env", "--help-xoptions":
|
|
return true
|
|
}
|
|
}
|
|
if name == "node" || name == "nodejs" {
|
|
switch option {
|
|
case "-v", "--version", "-h", "--help":
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|