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 < ' ' }