whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
// Copyright 2020 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.
|
||||
|
||||
// The txtar command writes or extracts a text-based file archive in the format
|
||||
// provided by the golang.org/x/tools/txtar package.
|
||||
//
|
||||
// The default behavior is to read a comment from stdin and write the archive
|
||||
// file containing the recursive contents of the named files and directories,
|
||||
// including hidden files, to stdout. Any non-flag arguments to the command name
|
||||
// the files and/or directories to include, with the contents of directories
|
||||
// included recursively. An empty argument list is equivalent to ".".
|
||||
//
|
||||
// The --extract (or -x) flag instructs txtar to instead read the archive file
|
||||
// from stdin and extract all of its files to corresponding locations relative
|
||||
// to the current, writing the archive's comment to stdout.
|
||||
//
|
||||
// The --list flag instructs txtar to instead read the archive file from stdin
|
||||
// and list all of its files to stdout. Note that shell variables in paths are
|
||||
// not expanded in this mode.
|
||||
//
|
||||
// Archive files are by default extracted only to the current directory or its
|
||||
// subdirectories. To allow extracting outside the current directory, use the
|
||||
// --unsafe flag.
|
||||
//
|
||||
// When extracting, shell variables in paths are expanded (using os.Expand) if
|
||||
// the corresponding variable is set in the process environment. When writing an
|
||||
// archive, the variables (before expansion) are preserved in the archived paths.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// txtar *.go <README >testdata/example.txt
|
||||
//
|
||||
// txtar --extract <playground_example.txt >main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
var (
|
||||
extractFlag = flag.Bool("extract", false, "if true, extract files from the archive instead of writing to it")
|
||||
listFlag = flag.Bool("list", false, "if true, list files from the archive instead of writing to it")
|
||||
unsafeFlag = flag.Bool("unsafe", false, "allow extraction of files outside the current directory")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(extractFlag, "x", *extractFlag, "short alias for --extract")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var err error
|
||||
switch {
|
||||
case *extractFlag:
|
||||
if len(flag.Args()) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "Usage: txtar --extract <archive.txt")
|
||||
os.Exit(2)
|
||||
}
|
||||
err = extract()
|
||||
case *listFlag:
|
||||
if len(flag.Args()) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "Usage: txtar --list <archive.txt")
|
||||
os.Exit(2)
|
||||
}
|
||||
err = list()
|
||||
default:
|
||||
paths := flag.Args()
|
||||
if len(paths) == 0 {
|
||||
paths = []string{"."}
|
||||
}
|
||||
err = archive(paths)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func extract() (err error) {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ar := txtar.Parse(b)
|
||||
|
||||
if !*unsafeFlag {
|
||||
// Check that no files are extracted outside the current directory
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add trailing separator to terminate wd.
|
||||
// This prevents extracting to outside paths which prefix wd,
|
||||
// e.g. extracting to /home/foobar when wd is /home/foo
|
||||
if !strings.HasSuffix(wd, string(filepath.Separator)) {
|
||||
wd += string(filepath.Separator)
|
||||
}
|
||||
|
||||
for _, f := range ar.Files {
|
||||
fileName := filepath.Clean(expand(f.Name))
|
||||
|
||||
if strings.HasPrefix(fileName, "..") ||
|
||||
(filepath.IsAbs(fileName) && !strings.HasPrefix(fileName, wd)) {
|
||||
return fmt.Errorf("file path '%s' is outside the current directory", f.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range ar.Files {
|
||||
fileName := filepath.FromSlash(path.Clean(expand(f.Name)))
|
||||
if err := os.MkdirAll(filepath.Dir(fileName), 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(fileName, f.Data, 0666); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(ar.Comment) > 0 {
|
||||
os.Stdout.Write(ar.Comment)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func list() (err error) {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ar := txtar.Parse(b)
|
||||
for _, f := range ar.Files {
|
||||
fmt.Println(f.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func archive(paths []string) (err error) {
|
||||
txtarHeader := regexp.MustCompile(`(?m)^-- .* --$`)
|
||||
|
||||
ar := new(txtar.Archive)
|
||||
for _, p := range paths {
|
||||
root := filepath.Clean(expand(p))
|
||||
prefix := root + string(filepath.Separator)
|
||||
err := filepath.Walk(root, func(fileName string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return err
|
||||
}
|
||||
|
||||
suffix := ""
|
||||
if fileName != root {
|
||||
suffix = strings.TrimPrefix(fileName, prefix)
|
||||
}
|
||||
name := filepath.ToSlash(filepath.Join(p, suffix))
|
||||
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if txtarHeader.Match(data) {
|
||||
return fmt.Errorf("cannot archive %s: file contains a txtar header", name)
|
||||
}
|
||||
|
||||
ar.Files = append(ar.Files, txtar.File{Name: name, Data: data})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// After we have read all of the source files, read the comment from stdin.
|
||||
//
|
||||
// Wait until the read has been blocked for a while before prompting the user
|
||||
// to enter it: if they are piping the comment in from some other file, the
|
||||
// read should complete very quickly and there is no need for a prompt.
|
||||
// (200ms is typically long enough to read a reasonable comment from the local
|
||||
// machine, but short enough that humans don't notice it.)
|
||||
//
|
||||
// Don't prompt until we have successfully read the other files:
|
||||
// if we encountered an error, we don't need to ask for a comment.
|
||||
timer := time.AfterFunc(200*time.Millisecond, func() {
|
||||
fmt.Fprintln(os.Stderr, "Enter comment:")
|
||||
})
|
||||
comment, err := io.ReadAll(os.Stdin)
|
||||
timer.Stop()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading comment from %s: %v", os.Stdin.Name(), err)
|
||||
}
|
||||
ar.Comment = bytes.TrimSpace(comment)
|
||||
|
||||
_, err = os.Stdout.Write(txtar.Format(ar))
|
||||
return err
|
||||
}
|
||||
|
||||
// expand is like os.ExpandEnv, but preserves unescaped variables (instead
|
||||
// of escaping them to the empty string) if the variable is not set.
|
||||
func expand(p string) string {
|
||||
return os.Expand(p, func(key string) string {
|
||||
v, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
return "$" + key
|
||||
}
|
||||
return v
|
||||
})
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
// Copyright 2020 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_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const comment = "This is a txtar archive.\n"
|
||||
|
||||
const testdata = `This is a txtar archive.
|
||||
-- one.txt --
|
||||
one
|
||||
-- dir/two.txt --
|
||||
two
|
||||
-- $SPECIAL_LOCATION/three.txt --
|
||||
three
|
||||
`
|
||||
|
||||
var filelist = `
|
||||
one.txt
|
||||
dir/two.txt
|
||||
$SPECIAL_LOCATION/three.txt
|
||||
`[1:]
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := m.Run()
|
||||
txtarBin.once.Do(func() {})
|
||||
if txtarBin.name != "" {
|
||||
os.Remove(txtarBin.name)
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
os.Setenv("SPECIAL_LOCATION", "special")
|
||||
defer os.Unsetenv("SPECIAL_LOCATION")
|
||||
|
||||
// Expand the testdata archive into a temporary directory.
|
||||
parentDir, err := os.MkdirTemp("", "txtar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(parentDir)
|
||||
dir := filepath.Join(parentDir, "dir")
|
||||
if err := os.Mkdir(dir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if out, err := txtar(t, dir, testdata, "--list"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if out != filelist {
|
||||
t.Fatalf("txtar --list: stdout:\n%s\nwant:\n%s", out, filelist)
|
||||
}
|
||||
if entries, err := os.ReadDir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(entries) > 0 {
|
||||
t.Fatalf("txtar --list: did not expect any extracted files")
|
||||
}
|
||||
|
||||
if out, err := txtar(t, dir, testdata, "--extract"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if out != comment {
|
||||
t.Fatalf("txtar --extract: stdout:\n%s\nwant:\n%s", out, comment)
|
||||
}
|
||||
|
||||
// Now, re-archive its contents explicitly and ensure that the result matches
|
||||
// the original.
|
||||
args := []string{"one.txt", "dir", "$SPECIAL_LOCATION"}
|
||||
if out, err := txtar(t, dir, comment, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if out != testdata {
|
||||
t.Fatalf("txtar %s: archive:\n%s\n\nwant:\n%s", strings.Join(args, " "), out, testdata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsafePaths(t *testing.T) {
|
||||
// Set up temporary directories for test archives.
|
||||
parentDir, err := os.MkdirTemp("", "txtar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(parentDir)
|
||||
dir := filepath.Join(parentDir, "dir")
|
||||
if err := os.Mkdir(dir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test --unsafe option for both absolute and relative paths
|
||||
testcases := []struct{ name, path string }{
|
||||
{"Absolute", filepath.Join(parentDir, "dirSpecial")},
|
||||
{"Relative", "../special"},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Set SPECIAL_LOCATION outside the current directory
|
||||
t.Setenv("SPECIAL_LOCATION", tc.path)
|
||||
|
||||
// Expand the testdata archive into a temporary directory.
|
||||
|
||||
// Should fail without the --unsafe flag
|
||||
if _, err := txtar(t, dir, testdata, "--extract"); err == nil {
|
||||
t.Fatalf("txtar --extract: extracts to unsafe paths")
|
||||
}
|
||||
|
||||
// Should allow paths outside the current dir with the --unsafe flags
|
||||
out, err := txtar(t, dir, testdata, "--extract", "--unsafe")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != comment {
|
||||
t.Fatalf("txtar --extract --unsafe: stdout:\n%s\nwant:\n%s", out, comment)
|
||||
}
|
||||
|
||||
// Now, re-archive its contents explicitly and ensure that the result matches
|
||||
// the original.
|
||||
args := []string{"one.txt", "dir", "$SPECIAL_LOCATION"}
|
||||
out, err = txtar(t, dir, comment, args...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != testdata {
|
||||
t.Fatalf("txtar %s: archive:\n%s\n\nwant:\n%s", strings.Join(args, " "), out, testdata)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// txtar runs the txtar command in the given directory with the given input and
|
||||
// arguments.
|
||||
func txtar(t *testing.T, dir, input string, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(txtarName(t), args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "PWD="+dir)
|
||||
cmd.Stdin = strings.NewReader(input)
|
||||
stderr := new(strings.Builder)
|
||||
cmd.Stderr = stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, stderr)
|
||||
}
|
||||
if stderr.String() != "" {
|
||||
t.Logf("OK: %s\n%s", strings.Join(cmd.Args, " "), stderr)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
var txtarBin struct {
|
||||
once sync.Once
|
||||
name string
|
||||
err error
|
||||
}
|
||||
|
||||
// txtarName returns the name of the txtar executable, building it if needed.
|
||||
func txtarName(t *testing.T) string {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skipf("cannot build txtar binary: %v", err)
|
||||
}
|
||||
|
||||
txtarBin.once.Do(func() {
|
||||
exe, err := os.CreateTemp("", "txtar-*.exe")
|
||||
if err != nil {
|
||||
txtarBin.err = err
|
||||
return
|
||||
}
|
||||
exe.Close()
|
||||
txtarBin.name = exe.Name()
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", txtarBin.name, ".")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
txtarBin.err = fmt.Errorf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, out)
|
||||
}
|
||||
})
|
||||
|
||||
if txtarBin.err != nil {
|
||||
if runtime.GOOS == "android" {
|
||||
t.Skipf("skipping test after failing to build txtar binary: go_android_exec may have failed to copy needed dependencies (see https://golang.org/issue/37088)")
|
||||
}
|
||||
t.Fatal(txtarBin.err)
|
||||
}
|
||||
return txtarBin.name
|
||||
}
|
||||
Reference in New Issue
Block a user