diff --git a/cmd/testscript/help.go b/cmd/testscript/help.go new file mode 100644 index 00000000..e814f2c5 --- /dev/null +++ b/cmd/testscript/help.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "io" +) + +func mainUsage(f io.Writer) { + fmt.Fprint(f, mainHelp) +} + +var mainHelp = ` +The testscript command runs github.com/rogpeppe/go-internal/testscript scripts +in a fresh temporary work directory tree. + +Usage: + testscript [-v] files... + +The testscript command is designed to make it easy to create self-contained +reproductions of command sequences. + +Each file is opened as a script and run as described in the documentation for +github.com/rogpeppe/go-internal/testscript. The special filename "-" is +interpreted as the standard input. + +As a special case, supporting files/directories in the .gomodproxy subdirectory +will be served via a github.com/rogpeppe/go-internal/goproxytest server which +is available to each script via the GOPROXY environment variable. The contents +of the .gomodproxy subdirectory are not available to the script except via the +proxy server. See the documentation for +github.com/rogpeppe/go-internal/goproxytest for details on the format of these +files/directories. + +Examples +======== + +The following example, fruit.txt, shows a simple reproduction that includes +.gomodproxy supporting files: + + go get -m fruit.com + go list fruit.com/... + stdout 'fruit.com/fruit' + + -- go.mod -- + module mod + + -- .gomodproxy/fruit.com_v1.0.0/.mod -- + module fruit.com + + -- .gomodproxy/fruit.com_v1.0.0/.info -- + {"Version":"v1.0.0","Time":"2018-10-22T18:45:39Z"} + + -- .gomodproxy/fruit.com_v1.0.0/fruit/fruit.go -- + package fruit + + const Name = "Apple" + +Running testscript -v fruit.txt we get: + + ... + > go get -m fruit.com + [stderr] + go: finding fruit.com v1.0.0 + + > go list fruit.com/... + [stdout] + fruit.com/fruit + + [stderr] + go: downloading fruit.com v1.0.0 + + > stdout 'fruit.com/fruit' + PASS + + +The following example, goimports.txt, shows a simple reproduction involving +goimports: + + go install golang.org/x/tools/cmd/goimports + + # check goimports help information + exec goimports -d main.go + stdout 'import "math"' + + -- go.mod -- + module mod + + require golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372 + + -- main.go -- + package mod + + const Pi = math.Pi + +Running testscript -v goimports.txt we get: + + ... + > go install golang.org/x/tools/cmd/goimports + [stderr] + go: finding golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372 + go: downloading golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372 + + # check goimports help information (0.015s) + > exec goimports -d main.go + [stdout] + diff -u main.go.orig main.go + --- main.go.orig 2019-01-08 16:03:35.861907738 +0000 + +++ main.go 2019-01-08 16:03:35.861907738 +0000 + @@ -1,3 +1,5 @@ + package mod + + +import "math" + + + const Pi = math.Pi + > stdout 'import "math"' + PASS +`[1:] diff --git a/cmd/testscript/main.go b/cmd/testscript/main.go new file mode 100644 index 00000000..2587629e --- /dev/null +++ b/cmd/testscript/main.go @@ -0,0 +1,222 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/rogpeppe/go-internal/goproxytest" + "github.com/rogpeppe/go-internal/gotooltest" + "github.com/rogpeppe/go-internal/testscript" + "github.com/rogpeppe/go-internal/txtar" +) + +const ( + // goModProxyDir is the special subdirectory in a txtar script's supporting files + // within which we expect to find github.com/rogpeppe/go-internal/goproxytest + // directories. + goModProxyDir = ".gomodproxy" +) + +func main() { + os.Exit(main1()) +} + +func main1() int { + if err := mainerr(); err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + return 0 +} + +func mainerr() (retErr error) { + fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + fs.Usage = func() { + mainUsage(os.Stderr) + } + fWork := fs.Bool("work", false, "print temporary work directory and do not remove when done") + fVerbose := fs.Bool("v", false, "run tests verbosely") + if err := fs.Parse(os.Args[1:]); err != nil { + return err + } + + td, err := ioutil.TempDir("", "testscript") + if err != nil { + return fmt.Errorf("unable to create temp dir: %v", err) + } + fmt.Printf("temporary work directory: %v\n", td) + if !*fWork { + defer os.RemoveAll(td) + } + + files := fs.Args() + if len(files) == 0 { + files = []string{"-"} + } + + for i, fileName := range files { + // TODO make running files concurrent by default? If we do, note we'll need to do + // something smarter with the runner stdout and stderr below + runDir := filepath.Join(td, strconv.Itoa(i)) + if err := os.Mkdir(runDir, 0777); err != nil { + return fmt.Errorf("failed to create a run directory within %v for %v: %v", td, fileName, err) + } + if err := run(runDir, fileName, *fVerbose); err != nil { + return err + } + } + + return nil +} + +var ( + failedRun = errors.New("failed run") + skipRun = errors.New("skip") +) + +type runner struct { + verbose bool +} + +func (r runner) Skip(is ...interface{}) { + panic(skipRun) +} + +func (r runner) Fatal(is ...interface{}) { + r.Log(is...) + r.FailNow() +} + +func (r runner) Parallel() { + // No-op for now; we are currently only running a single script in a + // testscript instance. +} + +func (r runner) Log(is ...interface{}) { + fmt.Print(is...) +} + +func (r runner) FailNow() { + panic(failedRun) +} + +func (r runner) Run(n string, f func(t testscript.T)) { + // For now we we don't top/tail the run of a subtest. We are currently only + // running a single script in a testscript instance, which means that we + // will only have a single subtest. + f(r) +} + +func (r runner) Verbose() bool { + return r.verbose +} + +func run(runDir, fileName string, verbose bool) error { + var ar *txtar.Archive + var err error + + mods := filepath.Join(runDir, goModProxyDir) + + if err := os.MkdirAll(mods, 0777); err != nil { + return fmt.Errorf("failed to create goModProxy dir: %v", err) + } + + if fileName == "-" { + fileName = "" + byts, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("failed to read from stdin: %v", err) + } + ar = txtar.Parse(byts) + } else { + ar, err = txtar.ParseFile(fileName) + } + + if err != nil { + return fmt.Errorf("failed to txtar parse %v: %v", fileName, err) + } + + var script, gomodProxy txtar.Archive + script.Comment = ar.Comment + + for _, f := range ar.Files { + fp := filepath.Clean(filepath.FromSlash(f.Name)) + parts := strings.Split(fp, string(os.PathSeparator)) + + if len(parts) > 1 && parts[0] == goModProxyDir { + gomodProxy.Files = append(gomodProxy.Files, f) + } else { + script.Files = append(script.Files, f) + } + } + + if txtar.Write(&gomodProxy, runDir); err != nil { + return fmt.Errorf("failed to write .gomodproxy files: %v", err) + } + + if err := ioutil.WriteFile(filepath.Join(runDir, "script.txt"), txtar.Format(&script), 0666); err != nil { + return fmt.Errorf("failed to write script for %v: %v", fileName, err) + } + + p := testscript.Params{ + Dir: runDir, + } + + if len(gomodProxy.Files) > 0 { + srv, err := goproxytest.NewServer(mods, "") + if err != nil { + return fmt.Errorf("cannot start proxy for %v: %v", fileName, err) + } + defer srv.Close() + + currSetup := p.Setup + + p.Setup = func(env *testscript.Env) error { + env.Vars = append(env.Vars, "GOPROXY="+srv.URL) + if currSetup != nil { + return currSetup(env) + } + return nil + } + } + + if _, err := exec.LookPath("go"); err == nil { + if err := gotooltest.Setup(&p); err != nil { + return fmt.Errorf("failed to setup go tool for %v run: %v", fileName, err) + } + } + + r := runner{ + verbose: verbose, + } + + func() { + defer func() { + switch recover() { + case nil, skipRun: + case failedRun: + err = failedRun + default: + panic(fmt.Errorf("unexpected panic: %v [%T]", err, err)) + } + }() + testscript.RunT(r, p) + }() + + if err != nil { + return fmt.Errorf("error running %v in %v\n", fileName, runDir) + } + + return nil +} diff --git a/cmd/testscript/main_test.go b/cmd/testscript/main_test.go new file mode 100644 index 00000000..93197215 --- /dev/null +++ b/cmd/testscript/main_test.go @@ -0,0 +1,71 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/rogpeppe/go-internal/gotooltest" + "github.com/rogpeppe/go-internal/testscript" +) + +func TestMain(m *testing.M) { + os.Exit(testscript.RunMain(m, map[string]func() int{ + "testscript": main1, + })) +} + +func TestScripts(t *testing.T) { + var stderr bytes.Buffer + cmd := exec.Command("go", "env", "GOMOD") + cmd.Stderr = &stderr + out, err := cmd.Output() + if err != nil { + t.Fatalf("failed to run %v: %v\n%s", strings.Join(cmd.Args, " "), err, stderr.String()) + } + gomod := string(out) + + if gomod == "" { + t.Fatalf("apparently we are not running in module mode?") + } + + p := testscript.Params{ + Dir: "testdata", + Setup: func(env *testscript.Env) error { + env.Vars = append(env.Vars, + "GOINTERNALMODPATH="+filepath.Dir(gomod), + ) + return nil + }, + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ + "unquote": unquote, + }, + } + if err := gotooltest.Setup(&p); err != nil { + t.Fatal(err) + } + testscript.Run(t, p) +} + +func unquote(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! unquote") + } + for _, arg := range args { + file := ts.MkAbs(arg) + data, err := ioutil.ReadFile(file) + ts.Check(err) + data = bytes.Replace(data, []byte("\n>"), []byte("\n"), -1) + data = bytes.TrimPrefix(data, []byte(">")) + err = ioutil.WriteFile(file, data, 0666) + ts.Check(err) + } +} diff --git a/cmd/testscript/testdata/error.txt b/cmd/testscript/testdata/error.txt new file mode 100644 index 00000000..b6fefcec --- /dev/null +++ b/cmd/testscript/testdata/error.txt @@ -0,0 +1,14 @@ +# should support skip +unquote file.txt + +# stdin +stdin file.txt +! testscript -v +stderr 'error running in' + +# file-based +! testscript -v file.txt +stderr 'error running file.txt in' + +-- file.txt -- +>exec false diff --git a/cmd/testscript/testdata/help.txt b/cmd/testscript/testdata/help.txt new file mode 100644 index 00000000..43b8e0e3 --- /dev/null +++ b/cmd/testscript/testdata/help.txt @@ -0,0 +1,5 @@ +# Simply sanity check on help output +! testscript -help +! stdout .+ +stderr 'The testscript command' +stderr 'Examples' diff --git a/cmd/testscript/testdata/nogo.txt b/cmd/testscript/testdata/nogo.txt new file mode 100644 index 00000000..e3dd684d --- /dev/null +++ b/cmd/testscript/testdata/nogo.txt @@ -0,0 +1,10 @@ +# should support skip +unquote file.txt + +env PATH= +! testscript -v file.txt +stdout 'unknown command "go"' +stderr 'error running file.txt in' + +-- file.txt -- +>go env diff --git a/cmd/testscript/testdata/noproxy.txt b/cmd/testscript/testdata/noproxy.txt new file mode 100644 index 00000000..a0fc0d4b --- /dev/null +++ b/cmd/testscript/testdata/noproxy.txt @@ -0,0 +1,8 @@ +# with no .gomodproxy supporting files we should not have a GOPROXY set +unquote file.txt +testscript -v file.txt + +-- file.txt -- +>go env +>[!windows] stdout '^GOPROXY=""$' +>[windows] stdout '^set GOPROXY=$' diff --git a/cmd/testscript/testdata/simple.txt b/cmd/testscript/testdata/simple.txt new file mode 100644 index 00000000..3084e106 --- /dev/null +++ b/cmd/testscript/testdata/simple.txt @@ -0,0 +1,36 @@ +unquote file.txt +testscript -v file.txt +stdout 'example.com/mod' +! stderr .+ + +-- file.txt -- +>go list -m +>stdout 'example.com/mod' +>go list fruit.com/... +>stdout 'fruit.com/fruit' +>stdout 'fruit.com/coretest' + +>-- go.mod -- +>module example.com/mod +> +>require fruit.com v1.0.0 + +>-- .gomodproxy/fruit.com_v1.0.0/.mod -- +>module fruit.com +> +>-- .gomodproxy/fruit.com_v1.0.0/.info -- +>{"Version":"v1.0.0","Time":"2018-10-22T18:45:39Z"} +> +>-- .gomodproxy/fruit.com_v1.0.0/go.mod -- +>module fruit.com +> +>-- .gomodproxy/fruit.com_v1.0.0/fruit/fruit.go -- +>package fruit +> +>const Apple = "apple" +>-- .gomodproxy/fruit.com_v1.0.0/coretest/coretest.go -- +>// package coretest becomes a candidate for the missing +>// core import in main above +>package coretest +> +>const Mandarin = "mandarin" diff --git a/cmd/testscript/testdata/skip.txt b/cmd/testscript/testdata/skip.txt new file mode 100644 index 00000000..53a83d0c --- /dev/null +++ b/cmd/testscript/testdata/skip.txt @@ -0,0 +1,9 @@ +# should support skip +unquote file.txt +testscript -v file.txt +stdout 'go version' +! stderr .+ + +-- file.txt -- +>go version +>skip diff --git a/cmd/txtar-x/extract.go b/cmd/txtar-x/extract.go index dff86c2d..38d1a9fd 100644 --- a/cmd/txtar-x/extract.go +++ b/cmd/txtar-x/extract.go @@ -19,8 +19,6 @@ import ( "io/ioutil" "log" "os" - "path/filepath" - "strings" "github.com/rogpeppe/go-internal/txtar" ) @@ -64,45 +62,9 @@ func main1() int { } a = a1 } - if err := extract(a); err != nil { + if err := txtar.Write(a, *extractDir); err != nil { log.Print(err) return 1 } return 0 } - -func extract(a *txtar.Archive) error { - for _, f := range a.Files { - if err := extractFile(f); err != nil { - return fmt.Errorf("cannot extract %q: %v", f.Name, err) - } - } - return nil -} - -func extractFile(f txtar.File) error { - path := filepath.Clean(filepath.FromSlash(f.Name)) - if isAbs(path) || strings.HasPrefix(path, ".."+string(filepath.Separator)) { - return fmt.Errorf("outside parent directory") - } - path = filepath.Join(*extractDir, path) - if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { - return err - } - // Avoid overwriting existing files by using O_EXCL. - out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) - if err != nil { - return err - } - defer out.Close() - if _, err := out.Write(f.Data); err != nil { - return err - } - return nil -} - -func isAbs(p string) bool { - // Note: under Windows, filepath.IsAbs(`\foo`) returns false, - // so we need to check for that case specifically. - return filepath.IsAbs(p) || strings.HasPrefix(p, string(filepath.Separator)) -} diff --git a/cmd/txtar-x/testdata/extract-out-of-bounds.txt b/cmd/txtar-x/testdata/extract-out-of-bounds.txt index d3c053bd..a05e7857 100644 --- a/cmd/txtar-x/testdata/extract-out-of-bounds.txt +++ b/cmd/txtar-x/testdata/extract-out-of-bounds.txt @@ -1,9 +1,9 @@ unquote file1.txtar file2.txtar ! txtar-x file1.txtar -stderr 'cannot extract "\.\./foo": outside parent directory' +stderr '"\.\./foo": outside parent directory' ! txtar-x file2.txtar -stderr 'cannot extract "/foo": outside parent directory' +stderr '"/foo": outside parent directory' -- file1.txtar -- >-- ../foo -- diff --git a/goproxytest/proxy.go b/goproxytest/proxy.go index 7ed5c93b..51eb7223 100644 --- a/goproxytest/proxy.go +++ b/goproxytest/proxy.go @@ -41,9 +41,9 @@ import ( ) type Server struct { + server *http.Server URL string dir string - listener net.Listener modList []module.Version zipCache par.Cache archiveCache par.Cache @@ -71,17 +71,22 @@ func NewServer(dir, addr string) (*Server, error) { if err != nil { return nil, fmt.Errorf("cannot listen on %q: %v", addr, err) } + srv.server = &http.Server{ + Handler: http.HandlerFunc(srv.handler), + } addr = l.Addr().String() srv.URL = "http://" + addr + "/mod" go func() { - log.Printf("go proxy: http.Serve: %v", http.Serve(l, http.HandlerFunc(srv.handler))) + if err := srv.server.Serve(l); err != nil && err != http.ErrServerClosed { + log.Printf("go proxy: http.Serve: %v", err) + } }() return &srv, nil } // Close shuts down the proxy. func (srv *Server) Close() { - srv.listener.Close() + srv.server.Close() } func (srv *Server) readModList() error { diff --git a/gotooltest/setup.go b/gotooltest/setup.go index 078bce7e..5a489310 100644 --- a/gotooltest/setup.go +++ b/gotooltest/setup.go @@ -7,6 +7,8 @@ package gotooltest import ( + "bytes" + "encoding/json" "fmt" "go/build" "os/exec" @@ -14,6 +16,7 @@ import ( "regexp" "runtime" "strings" + "sync" "github.com/rogpeppe/go-internal/imports" "github.com/rogpeppe/go-internal/testscript" @@ -21,11 +24,53 @@ import ( var ( goVersionRegex = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) + + goEnv struct { + GOROOT string + GOCACHE string + goversion string + releaseTags []string + once sync.Once + err error + } ) -type testContext struct { - goroot string - gocache string +// initGoEnv initialises goEnv. It should only be called using goEnv.once.Do, +// as in Setup. +func initGoEnv() error { + var err error + + run := func(args ...string) (*bytes.Buffer, *bytes.Buffer, error) { + var stdout, stderr bytes.Buffer + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + return &stdout, &stderr, cmd.Run() + } + + lout, stderr, err := run("go", "list", "-f={{context.ReleaseTags}}", "runtime") + if err != nil { + return fmt.Errorf("failed to determine release tags from go command: %v\n%v", err, stderr.String()) + } + tagStr := strings.TrimSpace(lout.String()) + tagStr = strings.Trim(tagStr, "[]") + goEnv.releaseTags = strings.Split(tagStr, " ") + + eout, stderr, err := run("go", "env", "-json", "GOROOT", "GOCACHE") + if err != nil { + return fmt.Errorf("failed to determine GOROOT and GOCACHE tags from go command: %v\n%v", err, stderr) + } + if err := json.Unmarshal(eout.Bytes(), &goEnv); err != nil { + return fmt.Errorf("failed to unmarshal GOROOT and GOCACHE tags from go command out: %v\n%v", err, eout) + } + + version := goEnv.releaseTags[len(goEnv.releaseTags)-1] + if !goVersionRegex.MatchString(version) { + return fmt.Errorf("invalid go version %q", version) + } + goEnv.goversion = version[2:] + + return nil } // Setup sets up the given test environment for tests that use the go @@ -36,13 +81,16 @@ type testContext struct { // It checks go command can run, but not that it can build or run // binaries. func Setup(p *testscript.Params) error { - var c testContext - if err := c.init(); err != nil { - return err + goEnv.once.Do(func() { + goEnv.err = initGoEnv() + }) + if goEnv.err != nil { + return goEnv.err } + origSetup := p.Setup p.Setup = func(e *testscript.Env) error { - e.Vars = c.goEnviron(e.Vars) + e.Vars = goEnviron(e.Vars) if origSetup != nil { return origSetup(e) } @@ -78,27 +126,7 @@ func Setup(p *testscript.Params) error { return nil } -func (c *testContext) init() error { - goEnv := func(name string) (string, error) { - out, err := exec.Command("go", "env", name).CombinedOutput() - if err != nil { - return "", fmt.Errorf("go env %s: %v (%s)", name, err, out) - } - return strings.TrimSpace(string(out)), nil - } - var err error - c.goroot, err = goEnv("GOROOT") - if err != nil { - return err - } - c.gocache, err = goEnv("GOCACHE") - if err != nil { - return err - } - return nil -} - -func (c *testContext) goEnviron(env0 []string) []string { +func goEnviron(env0 []string) []string { env := environ(env0) workdir := env.get("WORK") return append(env, []string{ @@ -106,8 +134,9 @@ func (c *testContext) goEnviron(env0 []string) []string { "CCACHE_DISABLE=1", // ccache breaks with non-existent HOME "GOARCH=" + runtime.GOARCH, "GOOS=" + runtime.GOOS, - "GOROOT=" + c.goroot, - "GOCACHE=" + c.gocache, + "GOROOT=" + goEnv.GOROOT, + "GOCACHE=" + goEnv.GOCACHE, + "goversion=" + goEnv.goversion, }...) } diff --git a/testscript/testscript.go b/testscript/testscript.go index 424eee23..faac0af7 100644 --- a/testscript/testscript.go +++ b/testscript/testscript.go @@ -12,7 +12,6 @@ import ( "context" "flag" "fmt" - "go/build" "io/ioutil" "os" "os/exec" @@ -195,7 +194,6 @@ func (ts *TestScript) setup() { homeEnvName() + "=/no-home", tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"), "devnull=" + os.DevNull, - "goversion=" + goVersion(ts), ":=" + string(os.PathListSeparator), }, WorkDir: ts.workdir, @@ -226,16 +224,6 @@ func (ts *TestScript) setup() { } } -// goVersion returns the current Go version. -func goVersion(ts *TestScript) string { - tags := build.Default.ReleaseTags - version := tags[len(tags)-1] - if !regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`).MatchString(version) { - ts.Fatalf("invalid go version %q", version) - } - return version[2:] -} - // run runs the test script. func (ts *TestScript) run() { // Truncate log at end of last phase marker, diff --git a/txtar/archive.go b/txtar/archive.go index 7cfcdfe9..e3c9415e 100644 --- a/txtar/archive.go +++ b/txtar/archive.go @@ -35,6 +35,8 @@ import ( "bytes" "fmt" "io/ioutil" + "os" + "path/filepath" "strings" ) @@ -141,3 +143,41 @@ func fixNL(data []byte) []byte { d[len(data)] = '\n' return d } + +// Write writes each File in an Archive to the given directory, returning any +// errors encountered. An error is also returned in the event a file would be +// written outside of dir. +func Write(a *Archive, dir string) error { + for _, f := range a.Files { + fp := filepath.Clean(filepath.FromSlash(f.Name)) + if isAbs(fp) || strings.HasPrefix(fp, ".."+string(filepath.Separator)) { + return fmt.Errorf("%q: outside parent directory", f.Name) + } + fp = filepath.Join(dir, fp) + + if err := os.MkdirAll(filepath.Dir(fp), 0777); err != nil { + return err + } + // Avoid overwriting existing files by using O_EXCL. + out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) + if err != nil { + return err + } + + _, err = out.Write(f.Data) + cerr := out.Close() + if err != nil { + return err + } + if cerr != nil { + return cerr + } + } + return nil +} + +func isAbs(p string) bool { + // Note: under Windows, filepath.IsAbs(`\foo`) returns false, + // so we need to check for that case specifically. + return filepath.IsAbs(p) || strings.HasPrefix(p, string(filepath.Separator)) +} diff --git a/txtar/archive_test.go b/txtar/archive_test.go index 9b90d867..2d63f750 100644 --- a/txtar/archive_test.go +++ b/txtar/archive_test.go @@ -7,6 +7,8 @@ package txtar import ( "bytes" "fmt" + "io/ioutil" + "os" "reflect" "testing" ) @@ -77,3 +79,28 @@ func shortArchive(a *Archive) string { } return buf.String() } + +func TestWrite(t *testing.T) { + td, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(td) + + good := &Archive{Files: []File{File{Name: "good.txt"}}} + if err := Write(good, td); err != nil { + t.Fatalf("expected no error; got %v", err) + } + + badRel := &Archive{Files: []File{File{Name: "../bad.txt"}}} + want := `"../bad.txt": outside parent directory` + if err := Write(badRel, td); err == nil || err.Error() != want { + t.Fatalf("expected %v; got %v", want, err) + } + + badAbs := &Archive{Files: []File{File{Name: "/bad.txt"}}} + want = `"/bad.txt": outside parent directory` + if err := Write(badAbs, td); err == nil || err.Error() != want { + t.Fatalf("expected %v; got %v", want, err) + } +}