summaryrefslogtreecommitdiffstats
path: root/dependencies/pkg/mod/golang.org/x/exp@v0.0.0-20220613132600-b0d781184e0d/cmd/txtar/txtar.go
blob: 9beb163d686a888c3a8b1dd86be4977d8f2c5e84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// 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.
//
// 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.
//
// 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/ioutil"
	"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")
	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
	if *extractFlag {
		if len(flag.Args()) > 0 {
			fmt.Fprintln(os.Stderr, "Usage: txtar --extract <archive.txt")
			os.Exit(2)
		}
		err = extract()
	} else {
		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 := ioutil.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 := ioutil.WriteFile(fileName, f.Data, 0666); err != nil {
			return err
		}
	}

	if len(ar.Comment) > 0 {
		os.Stdout.Write(ar.Comment)
	}
	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 := ioutil.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 := ioutil.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
	})
}