whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -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
})
}
@@ -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
}