golang.org/x/playground@v0.0.0-20230418134305-14ebe15bcd59/txtar.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "path" 12 "strings" 13 14 "golang.org/x/tools/txtar" 15 ) 16 17 // fileSet is a set of files. 18 // The zero value for fileSet is an empty set ready to use. 19 type fileSet struct { 20 files []string // filenames in user-provided order 21 m map[string][]byte // filename -> source 22 noHeader bool // whether the prog.go entry was implicit 23 } 24 25 // Data returns the content of the named file. 26 // The fileSet retains ownership of the returned slice. 27 func (fs *fileSet) Data(filename string) []byte { return fs.m[filename] } 28 29 // Num returns the number of files in the set. 30 func (fs *fileSet) Num() int { return len(fs.m) } 31 32 // Contains reports whether fs contains the given filename. 33 func (fs *fileSet) Contains(filename string) bool { 34 _, ok := fs.m[filename] 35 return ok 36 } 37 38 // AddFile adds a file to fs. If fs already contains filename, its 39 // contents are replaced. 40 func (fs *fileSet) AddFile(filename string, src []byte) { 41 had := fs.Contains(filename) 42 if fs.m == nil { 43 fs.m = make(map[string][]byte) 44 } 45 fs.m[filename] = src 46 if !had { 47 fs.files = append(fs.files, filename) 48 } 49 } 50 51 func (fs *fileSet) Update(filename string, src []byte) { 52 if fs.Contains(filename) { 53 fs.m[filename] = src 54 } 55 } 56 57 func (fs *fileSet) MvFile(source, target string) { 58 if fs.m == nil { 59 return 60 } 61 data, ok := fs.m[source] 62 if !ok { 63 return 64 } 65 fs.m[target] = data 66 delete(fs.m, source) 67 for i := range fs.files { 68 if fs.files[i] == source { 69 fs.files[i] = target 70 break 71 } 72 } 73 } 74 75 // Format returns fs formatted as a txtar archive. 76 func (fs *fileSet) Format() []byte { 77 a := new(txtar.Archive) 78 if fs.noHeader { 79 a.Comment = fs.m[progName] 80 } 81 for i, f := range fs.files { 82 if i == 0 && f == progName && fs.noHeader { 83 continue 84 } 85 a.Files = append(a.Files, txtar.File{Name: f, Data: fs.m[f]}) 86 } 87 return txtar.Format(a) 88 } 89 90 // splitFiles splits the user's input program src into 1 or more 91 // files, splitting it based on boundaries as specified by the "txtar" 92 // format. It returns an error if any filenames are bogus or 93 // duplicates. The implicit filename for the txtar comment (the lines 94 // before any txtar separator line) are named "prog.go". It is an 95 // error to have an explicit file named "prog.go" in addition to 96 // having the implicit "prog.go" file (non-empty comment section). 97 // 98 // The filenames are validated to only be relative paths, not too 99 // long, not too deep, not have ".." elements, not have backslashes or 100 // low ASCII binary characters, and to be in path.Clean canonical 101 // form. 102 // 103 // splitFiles takes ownership of src. 104 func splitFiles(src []byte) (*fileSet, error) { 105 fs := new(fileSet) 106 a := txtar.Parse(src) 107 if v := bytes.TrimSpace(a.Comment); len(v) > 0 { 108 fs.noHeader = true 109 fs.AddFile(progName, a.Comment) 110 } 111 const limitNumFiles = 20 // arbitrary 112 numFiles := len(a.Files) + fs.Num() 113 if numFiles > limitNumFiles { 114 return nil, fmt.Errorf("too many files in txtar archive (%v exceeds limit of %v)", numFiles, limitNumFiles) 115 } 116 for _, f := range a.Files { 117 if len(f.Name) > 200 { // arbitrary limit 118 return nil, errors.New("file name too long") 119 } 120 if strings.IndexFunc(f.Name, isBogusFilenameRune) != -1 { 121 return nil, fmt.Errorf("invalid file name %q", f.Name) 122 } 123 if f.Name != path.Clean(f.Name) || path.IsAbs(f.Name) { 124 return nil, fmt.Errorf("invalid file name %q", f.Name) 125 } 126 parts := strings.Split(f.Name, "/") 127 if len(parts) > 10 { // arbitrary limit 128 return nil, fmt.Errorf("file name %q too deep", f.Name) 129 } 130 for _, part := range parts { 131 if part == "." || part == ".." { 132 return nil, fmt.Errorf("invalid file name %q", f.Name) 133 } 134 } 135 if fs.Contains(f.Name) { 136 return nil, fmt.Errorf("duplicate file name %q", f.Name) 137 } 138 fs.AddFile(f.Name, f.Data) 139 } 140 return fs, nil 141 } 142 143 // isBogusFilenameRune reports whether r should be rejected if it 144 // appears in a txtar section's filename. 145 func isBogusFilenameRune(r rune) bool { return r == '\\' || r < ' ' }