From af386ca384eeaebb51a6d40bfd5617888fa0629a Mon Sep 17 00:00:00 2001 From: Ihar Lichko <98809665+ihar-orca@users.noreply.github.com> Date: Mon, 9 May 2022 12:05:24 +0200 Subject: [PATCH] interp: add custom handler for os.Stat/os.Lstat operations The interpreter has interfaces for all OS interactions except stat-ing files. Add a new handler, noting that we need both Stat and Lstat. See #630. --- interp/api.go | 14 ++++++++++++++ interp/builtin.go | 12 ++++++------ interp/handler.go | 15 +++++++++++++++ interp/runner.go | 10 ++++++++-- interp/test.go | 44 ++++++++++++++++++++++---------------------- 5 files changed, 65 insertions(+), 30 deletions(-) diff --git a/interp/api.go b/interp/api.go index 0180394d5..0648bd9db 100644 --- a/interp/api.go +++ b/interp/api.go @@ -74,6 +74,9 @@ type Runner struct { // glob expansion. It must be non-nil. readDirHandler ReadDirHandlerFunc + // statHandler is a function responsible for getting file stat. It must be non-nil. + statHandler StatHandlerFunc + stdin io.Reader stdout io.Writer stderr io.Writer @@ -174,6 +177,7 @@ func New(opts ...RunnerOption) (*Runner, error) { execHandler: DefaultExecHandler(2 * time.Second), openHandler: DefaultOpenHandler(), readDirHandler: DefaultReadDirHandler(), + statHandler: DefaultStatHandler(), } r.dirStack = r.dirBootstrap[:0] for _, opt := range opts { @@ -336,6 +340,14 @@ func ReadDirHandler(f ReadDirHandlerFunc) RunnerOption { } } +// StatHandler sets the stat handler. See StatHandlerFunc for more info. +func StatHandler(f StatHandlerFunc) RunnerOption { + return func(r *Runner) error { + r.statHandler = f + return nil + } +} + // StdIO configures an interpreter's standard input, standard output, and // standard error. If out or err are nil, they default to a writer that discards // the output. @@ -437,6 +449,7 @@ func (r *Runner) Reset() { execHandler: r.execHandler, openHandler: r.openHandler, readDirHandler: r.readDirHandler, + statHandler: r.statHandler, // These can be set by functions like Dir or Params, but // builtins can overwrite them; reset the fields to whatever the @@ -590,6 +603,7 @@ func (r *Runner) Subshell() *Runner { execHandler: r.execHandler, openHandler: r.openHandler, readDirHandler: r.readDirHandler, + statHandler: r.statHandler, stdin: r.stdin, stdout: r.stdout, stderr: r.stderr, diff --git a/interp/builtin.go b/interp/builtin.go index 7ca36e34e..f8161998e 100644 --- a/interp/builtin.go +++ b/interp/builtin.go @@ -225,7 +225,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.errf("usage: cd [dir]\n") return 2 } - return r.changeDir(path) + return r.changeDir(ctx, path) case "wait": if len(args) > 0 { panic("wait with args not handled yet") @@ -493,13 +493,13 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a return 1 } newtop := swap() - if code := r.changeDir(newtop); code != 0 { + if code := r.changeDir(ctx, newtop); code != 0 { return code } r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) case 1: if change { - if code := r.changeDir(args[0]); code != 0 { + if code := r.changeDir(ctx, args[0]); code != 0 { return code } r.dirStack = append(r.dirStack, r.Dir) @@ -528,7 +528,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.dirStack = r.dirStack[:len(r.dirStack)-1] if change { newtop := r.dirStack[len(r.dirStack)-1] - if code := r.changeDir(newtop); code != 0 { + if code := r.changeDir(ctx, newtop); code != 0 { return code } } else { @@ -852,12 +852,12 @@ func (r *Runner) readLine(raw bool) ([]byte, error) { } } -func (r *Runner) changeDir(path string) int { +func (r *Runner) changeDir(ctx context.Context, path string) int { if path == "" { path = "." } path = r.absPath(path) - info, err := r.stat(path) + info, err := r.stat(ctx, path) if err != nil || !info.IsDir() { return 1 } diff --git a/interp/handler.go b/interp/handler.go index 32e3f01d1..881ed83b3 100644 --- a/interp/handler.go +++ b/interp/handler.go @@ -315,3 +315,18 @@ func DefaultReadDirHandler() ReadDirHandlerFunc { return ioutil.ReadDir(path) } } + +// StatHandlerFunc is a handler which gets the file stat. the first argument provides directory to use as +// basedir if name is relative path +type StatHandlerFunc func(ctx context.Context, name string, followSymlinks bool) (os.FileInfo, error) + +// DefaultStatHandler returns a StatHandlerFunc used by default. It uses os.Stat() +func DefaultStatHandler() StatHandlerFunc { + return func(ctx context.Context, path string, followSymlinks bool) (os.FileInfo, error) { + if !followSymlinks { + return os.Lstat(path) + } else { + return os.Stat(path) + } + } +} diff --git a/interp/runner.go b/interp/runner.go index 06134836c..1ce661c21 100644 --- a/interp/runner.go +++ b/interp/runner.go @@ -915,6 +915,12 @@ func (r *Runner) open(ctx context.Context, path string, flags int, mode os.FileM return f, err } -func (r *Runner) stat(name string) (os.FileInfo, error) { - return os.Stat(r.absPath(name)) +func (r *Runner) stat(ctx context.Context, name string) (os.FileInfo, error) { + path := absPath(r.Dir, name) + return r.statHandler(ctx, path, true) +} + +func (r *Runner) lstat(ctx context.Context, name string) (os.FileInfo, error) { + path := absPath(r.Dir, name) + return r.statHandler(ctx, path, false) } diff --git a/interp/test.go b/interp/test.go index 156370196..fb45699c4 100644 --- a/interp/test.go +++ b/interp/test.go @@ -46,7 +46,7 @@ func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic boo } return "" } - if r.binTest(x.Op, r.bashTest(ctx, x.X, classic), r.bashTest(ctx, x.Y, classic)) { + if r.binTest(ctx, x.Op, r.bashTest(ctx, x.X, classic), r.bashTest(ctx, x.Y, classic)) { return "1" } return "" @@ -59,7 +59,7 @@ func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic boo return "" } -func (r *Runner) binTest(op syntax.BinTestOperator, x, y string) bool { +func (r *Runner) binTest(ctx context.Context, op syntax.BinTestOperator, x, y string) bool { switch op { case syntax.TsReMatch: re, err := regexp.Compile(y) @@ -69,22 +69,22 @@ func (r *Runner) binTest(op syntax.BinTestOperator, x, y string) bool { } return re.MatchString(x) case syntax.TsNewer: - info1, err1 := r.stat(x) - info2, err2 := r.stat(y) + info1, err1 := r.stat(ctx, x) + info2, err2 := r.stat(ctx, y) if err1 != nil || err2 != nil { return false } return info1.ModTime().After(info2.ModTime()) case syntax.TsOlder: - info1, err1 := r.stat(x) - info2, err2 := r.stat(y) + info1, err1 := r.stat(ctx, x) + info2, err2 := r.stat(ctx, y) if err1 != nil || err2 != nil { return false } return info1.ModTime().Before(info2.ModTime()) case syntax.TsDevIno: - info1, err1 := r.stat(x) - info2, err2 := r.stat(y) + info1, err1 := r.stat(ctx, x) + info2, err2 := r.stat(ctx, y) if err1 != nil || err2 != nil { return false } @@ -112,40 +112,40 @@ func (r *Runner) binTest(op syntax.BinTestOperator, x, y string) bool { } } -func (r *Runner) statMode(name string, mode os.FileMode) bool { - info, err := r.stat(name) +func (r *Runner) statMode(ctx context.Context, name string, mode os.FileMode) bool { + info, err := r.stat(ctx, name) return err == nil && info.Mode()&mode != 0 } func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) bool { switch op { case syntax.TsExists: - _, err := r.stat(x) + _, err := r.stat(ctx, x) return err == nil case syntax.TsRegFile: - info, err := r.stat(x) + info, err := r.stat(ctx, x) return err == nil && info.Mode().IsRegular() case syntax.TsDirect: - return r.statMode(x, os.ModeDir) + return r.statMode(ctx, x, os.ModeDir) case syntax.TsCharSp: - return r.statMode(x, os.ModeCharDevice) + return r.statMode(ctx, x, os.ModeCharDevice) case syntax.TsBlckSp: - info, err := r.stat(x) + info, err := r.stat(ctx, x) return err == nil && info.Mode()&os.ModeDevice != 0 && info.Mode()&os.ModeCharDevice == 0 case syntax.TsNmPipe: - return r.statMode(x, os.ModeNamedPipe) + return r.statMode(ctx, x, os.ModeNamedPipe) case syntax.TsSocket: - return r.statMode(x, os.ModeSocket) + return r.statMode(ctx, x, os.ModeSocket) case syntax.TsSmbLink: - info, err := os.Lstat(r.absPath(x)) + info, err := r.lstat(ctx, x) return err == nil && info.Mode()&os.ModeSymlink != 0 case syntax.TsSticky: - return r.statMode(x, os.ModeSticky) + return r.statMode(ctx, x, os.ModeSticky) case syntax.TsUIDSet: - return r.statMode(x, os.ModeSetuid) + return r.statMode(ctx, x, os.ModeSetuid) case syntax.TsGIDSet: - return r.statMode(x, os.ModeSetgid) + return r.statMode(ctx, x, os.ModeSetgid) // case syntax.TsGrpOwn: // case syntax.TsUsrOwn: // case syntax.TsModif: @@ -165,7 +165,7 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) _, err := exec.LookPath(r.absPath(x)) return err == nil case syntax.TsNoEmpty: - info, err := r.stat(x) + info, err := r.stat(ctx, x) return err == nil && info.Size() > 0 case syntax.TsFdTerm: fd := atoi(x)