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 package main
36
37 import (
38 "bytes"
39 "flag"
40 "fmt"
41 "io"
42 "os"
43 "path"
44 "path/filepath"
45 "regexp"
46 "strings"
47 "time"
48
49 "golang.org/x/tools/txtar"
50 )
51
52 var (
53 extractFlag = flag.Bool("extract", false, "if true, extract files from the archive instead of writing to it")
54 listFlag = flag.Bool("list", false, "if true, list files from the archive instead of writing to it")
55 unsafeFlag = flag.Bool("unsafe", false, "allow extraction of files outside the current directory")
56 )
57
58 func init() {
59 flag.BoolVar(extractFlag, "x", *extractFlag, "short alias for --extract")
60 }
61
62 func main() {
63 flag.Parse()
64
65 var err error
66 switch {
67 case *extractFlag:
68 if len(flag.Args()) > 0 {
69 fmt.Fprintln(os.Stderr, "Usage: txtar --extract <archive.txt")
70 os.Exit(2)
71 }
72 err = extract()
73 case *listFlag:
74 if len(flag.Args()) > 0 {
75 fmt.Fprintln(os.Stderr, "Usage: txtar --list <archive.txt")
76 os.Exit(2)
77 }
78 err = list()
79 default:
80 paths := flag.Args()
81 if len(paths) == 0 {
82 paths = []string{"."}
83 }
84 err = archive(paths)
85 }
86
87 if err != nil {
88 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
89 os.Exit(1)
90 }
91 }
92
93 func extract() (err error) {
94 b, err := io.ReadAll(os.Stdin)
95 if err != nil {
96 return err
97 }
98
99 ar := txtar.Parse(b)
100
101 if !*unsafeFlag {
102
103 wd, err := os.Getwd()
104 if err != nil {
105 return err
106 }
107
108
109
110 if !strings.HasSuffix(wd, string(filepath.Separator)) {
111 wd += string(filepath.Separator)
112 }
113
114 for _, f := range ar.Files {
115 fileName := filepath.Clean(expand(f.Name))
116
117 if strings.HasPrefix(fileName, "..") ||
118 (filepath.IsAbs(fileName) && !strings.HasPrefix(fileName, wd)) {
119 return fmt.Errorf("file path '%s' is outside the current directory", f.Name)
120 }
121 }
122 }
123
124 for _, f := range ar.Files {
125 fileName := filepath.FromSlash(path.Clean(expand(f.Name)))
126 if err := os.MkdirAll(filepath.Dir(fileName), 0777); err != nil {
127 return err
128 }
129 if err := os.WriteFile(fileName, f.Data, 0666); err != nil {
130 return err
131 }
132 }
133
134 if len(ar.Comment) > 0 {
135 os.Stdout.Write(ar.Comment)
136 }
137 return nil
138 }
139
140 func list() (err error) {
141 b, err := io.ReadAll(os.Stdin)
142 if err != nil {
143 return err
144 }
145
146 ar := txtar.Parse(b)
147 for _, f := range ar.Files {
148 fmt.Println(f.Name)
149 }
150 return nil
151 }
152
153 func archive(paths []string) (err error) {
154 txtarHeader := regexp.MustCompile(`(?m)^-- .* --$`)
155
156 ar := new(txtar.Archive)
157 for _, p := range paths {
158 root := filepath.Clean(expand(p))
159 prefix := root + string(filepath.Separator)
160 err := filepath.Walk(root, func(fileName string, info os.FileInfo, err error) error {
161 if err != nil || info.IsDir() {
162 return err
163 }
164
165 suffix := ""
166 if fileName != root {
167 suffix = strings.TrimPrefix(fileName, prefix)
168 }
169 name := filepath.ToSlash(filepath.Join(p, suffix))
170
171 data, err := os.ReadFile(fileName)
172 if err != nil {
173 return err
174 }
175 if txtarHeader.Match(data) {
176 return fmt.Errorf("cannot archive %s: file contains a txtar header", name)
177 }
178
179 ar.Files = append(ar.Files, txtar.File{Name: name, Data: data})
180 return nil
181 })
182 if err != nil {
183 return err
184 }
185 }
186
187
188
189
190
191
192
193
194
195
196
197 timer := time.AfterFunc(200*time.Millisecond, func() {
198 fmt.Fprintln(os.Stderr, "Enter comment:")
199 })
200 comment, err := io.ReadAll(os.Stdin)
201 timer.Stop()
202 if err != nil {
203 return fmt.Errorf("reading comment from %s: %v", os.Stdin.Name(), err)
204 }
205 ar.Comment = bytes.TrimSpace(comment)
206
207 _, err = os.Stdout.Write(txtar.Format(ar))
208 return err
209 }
210
211
212
213 func expand(p string) string {
214 return os.Expand(p, func(key string) string {
215 v, ok := os.LookupEnv(key)
216 if !ok {
217 return "$" + key
218 }
219 return v
220 })
221 }
222
View as plain text