github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/cmd/test/info/info.go (about)

     1  // Package info provides the info test command.
     2  package info
     3  
     4  // FIXME once translations are implemented will need a no-escape
     5  // option for Put so we can make these tests work again
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"log"
    14  	"os"
    15  	"path"
    16  	"regexp"
    17  	"sort"
    18  	"strconv"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/rclone/rclone/cmd"
    24  	"github.com/rclone/rclone/cmd/test"
    25  	"github.com/rclone/rclone/cmd/test/info/internal"
    26  	"github.com/rclone/rclone/fs"
    27  	"github.com/rclone/rclone/fs/config/flags"
    28  	"github.com/rclone/rclone/fs/hash"
    29  	"github.com/rclone/rclone/fs/object"
    30  	"github.com/rclone/rclone/fs/operations"
    31  	"github.com/rclone/rclone/lib/random"
    32  	"github.com/spf13/cobra"
    33  )
    34  
    35  var (
    36  	writeJSON          string
    37  	keepTestFiles      bool
    38  	checkNormalization bool
    39  	checkControl       bool
    40  	checkLength        bool
    41  	checkStreaming     bool
    42  	checkBase32768     bool
    43  	all                bool
    44  	uploadWait         time.Duration
    45  	positionLeftRe     = regexp.MustCompile(`(?s)^(.*)-position-left-([[:xdigit:]]+)$`)
    46  	positionMiddleRe   = regexp.MustCompile(`(?s)^position-middle-([[:xdigit:]]+)-(.*)-$`)
    47  	positionRightRe    = regexp.MustCompile(`(?s)^position-right-([[:xdigit:]]+)-(.*)$`)
    48  )
    49  
    50  func init() {
    51  	test.Command.AddCommand(commandDefinition)
    52  	cmdFlags := commandDefinition.Flags()
    53  	flags.StringVarP(cmdFlags, &writeJSON, "write-json", "", "", "Write results to file", "")
    54  	flags.BoolVarP(cmdFlags, &checkNormalization, "check-normalization", "", false, "Check UTF-8 Normalization", "")
    55  	flags.BoolVarP(cmdFlags, &checkControl, "check-control", "", false, "Check control characters", "")
    56  	flags.DurationVarP(cmdFlags, &uploadWait, "upload-wait", "", 0, "Wait after writing a file", "")
    57  	flags.BoolVarP(cmdFlags, &checkLength, "check-length", "", false, "Check max filename length", "")
    58  	flags.BoolVarP(cmdFlags, &checkStreaming, "check-streaming", "", false, "Check uploads with indeterminate file size", "")
    59  	flags.BoolVarP(cmdFlags, &checkBase32768, "check-base32768", "", false, "Check can store all possible base32768 characters", "")
    60  	flags.BoolVarP(cmdFlags, &all, "all", "", false, "Run all tests", "")
    61  	flags.BoolVarP(cmdFlags, &keepTestFiles, "keep-test-files", "", false, "Keep test files after execution", "")
    62  }
    63  
    64  var commandDefinition = &cobra.Command{
    65  	Use:   "info [remote:path]+",
    66  	Short: `Discovers file name or other limitations for paths.`,
    67  	Long: `rclone info discovers what filenames and upload methods are possible
    68  to write to the paths passed in and how long they can be.  It can take some
    69  time.  It will write test files into the remote:path passed in.  It outputs
    70  a bit of go code for each one.
    71  
    72  **NB** this can create undeletable files and other hazards - use with care
    73  `,
    74  	Annotations: map[string]string{
    75  		"versionIntroduced": "v1.55",
    76  	},
    77  	Run: func(command *cobra.Command, args []string) {
    78  		cmd.CheckArgs(1, 1e6, command, args)
    79  		if !checkNormalization && !checkControl && !checkLength && !checkStreaming && !checkBase32768 && !all {
    80  			log.Fatalf("no tests selected - select a test or use --all")
    81  		}
    82  		if all {
    83  			checkNormalization = true
    84  			checkControl = true
    85  			checkLength = true
    86  			checkStreaming = true
    87  			checkBase32768 = true
    88  		}
    89  		for i := range args {
    90  			tempDirName := "rclone-test-info-" + random.String(8)
    91  			tempDirPath := path.Join(args[i], tempDirName)
    92  			f := cmd.NewFsDir([]string{tempDirPath})
    93  			fs.Infof(f, "Created temporary directory for test files: %s", tempDirPath)
    94  			err := f.Mkdir(context.Background(), "")
    95  			if err != nil {
    96  				log.Fatalf("couldn't create temporary directory: %v", err)
    97  			}
    98  
    99  			cmd.Run(false, false, command, func() error {
   100  				return readInfo(context.Background(), f)
   101  			})
   102  		}
   103  	},
   104  }
   105  
   106  type results struct {
   107  	ctx                  context.Context
   108  	f                    fs.Fs
   109  	mu                   sync.Mutex
   110  	stringNeedsEscaping  map[string]internal.Position
   111  	controlResults       map[string]internal.ControlResult
   112  	maxFileLength        [4]int
   113  	canWriteUnnormalized bool
   114  	canReadUnnormalized  bool
   115  	canReadRenormalized  bool
   116  	canStream            bool
   117  	canBase32768         bool
   118  }
   119  
   120  func newResults(ctx context.Context, f fs.Fs) *results {
   121  	return &results{
   122  		ctx:                 ctx,
   123  		f:                   f,
   124  		stringNeedsEscaping: make(map[string]internal.Position),
   125  		controlResults:      make(map[string]internal.ControlResult),
   126  	}
   127  }
   128  
   129  // Print the results to stdout
   130  func (r *results) Print() {
   131  	fmt.Printf("// %s\n", r.f.Name())
   132  	if checkControl {
   133  		escape := []string{}
   134  		for c, needsEscape := range r.stringNeedsEscaping {
   135  			if needsEscape != internal.PositionNone {
   136  				k := strconv.Quote(c)
   137  				k = k[1 : len(k)-1]
   138  				escape = append(escape, fmt.Sprintf("'%s'", k))
   139  			}
   140  		}
   141  		sort.Strings(escape)
   142  		fmt.Printf("stringNeedsEscaping = []rune{\n")
   143  		fmt.Printf("\t%s\n", strings.Join(escape, ", "))
   144  		fmt.Printf("}\n")
   145  	}
   146  	if checkLength {
   147  		for i := range r.maxFileLength {
   148  			fmt.Printf("maxFileLength = %d // for %d byte unicode characters\n", r.maxFileLength[i], i+1)
   149  		}
   150  	}
   151  	if checkNormalization {
   152  		fmt.Printf("canWriteUnnormalized = %v\n", r.canWriteUnnormalized)
   153  		fmt.Printf("canReadUnnormalized   = %v\n", r.canReadUnnormalized)
   154  		fmt.Printf("canReadRenormalized   = %v\n", r.canReadRenormalized)
   155  	}
   156  	if checkStreaming {
   157  		fmt.Printf("canStream = %v\n", r.canStream)
   158  	}
   159  	if checkBase32768 {
   160  		fmt.Printf("base32768isOK = %v // make sure maxFileLength for 2 byte unicode chars is the same as for 1 byte characters\n", r.canBase32768)
   161  	}
   162  }
   163  
   164  // WriteJSON writes the results to a JSON file when requested
   165  func (r *results) WriteJSON() {
   166  	if writeJSON == "" {
   167  		return
   168  	}
   169  
   170  	report := internal.InfoReport{
   171  		Remote: r.f.Name(),
   172  	}
   173  	if checkControl {
   174  		report.ControlCharacters = &r.controlResults
   175  	}
   176  	if checkLength {
   177  		report.MaxFileLength = &r.maxFileLength[0]
   178  	}
   179  	if checkNormalization {
   180  		report.CanWriteUnnormalized = &r.canWriteUnnormalized
   181  		report.CanReadUnnormalized = &r.canReadUnnormalized
   182  		report.CanReadRenormalized = &r.canReadRenormalized
   183  	}
   184  	if checkStreaming {
   185  		report.CanStream = &r.canStream
   186  	}
   187  
   188  	if f, err := os.Create(writeJSON); err != nil {
   189  		fs.Errorf(r.f, "Creating JSON file failed: %s", err)
   190  	} else {
   191  		defer fs.CheckClose(f, &err)
   192  		enc := json.NewEncoder(f)
   193  		enc.SetIndent("", "  ")
   194  		err := enc.Encode(report)
   195  		if err != nil {
   196  			fs.Errorf(r.f, "Writing JSON file failed: %s", err)
   197  		}
   198  	}
   199  	fs.Infof(r.f, "Wrote JSON file: %s", writeJSON)
   200  }
   201  
   202  // writeFile writes a file with some random contents
   203  func (r *results) writeFile(path string) (fs.Object, error) {
   204  	contents := random.String(50)
   205  	src := object.NewStaticObjectInfo(path, time.Now(), int64(len(contents)), true, nil, r.f)
   206  	obj, err := r.f.Put(r.ctx, bytes.NewBufferString(contents), src)
   207  	if uploadWait > 0 {
   208  		time.Sleep(uploadWait)
   209  	}
   210  	return obj, err
   211  }
   212  
   213  // check whether normalization is enforced and check whether it is
   214  // done on the files anyway
   215  func (r *results) checkUTF8Normalization() {
   216  	unnormalized := "Héroique"
   217  	normalized := "Héroique"
   218  	_, err := r.writeFile(unnormalized)
   219  	if err != nil {
   220  		r.canWriteUnnormalized = false
   221  		return
   222  	}
   223  	r.canWriteUnnormalized = true
   224  	_, err = r.f.NewObject(r.ctx, unnormalized)
   225  	if err == nil {
   226  		r.canReadUnnormalized = true
   227  	}
   228  	_, err = r.f.NewObject(r.ctx, normalized)
   229  	if err == nil {
   230  		r.canReadRenormalized = true
   231  	}
   232  }
   233  
   234  func (r *results) checkStringPositions(k, s string) {
   235  	fs.Infof(r.f, "Writing position file 0x%0X", s)
   236  	positionError := internal.PositionNone
   237  	res := internal.ControlResult{
   238  		Text:       s,
   239  		WriteError: make(map[internal.Position]string, 3),
   240  		GetError:   make(map[internal.Position]string, 3),
   241  		InList:     make(map[internal.Position]internal.Presence, 3),
   242  	}
   243  
   244  	for _, pos := range internal.PositionList {
   245  		path := ""
   246  		switch pos {
   247  		case internal.PositionMiddle:
   248  			path = fmt.Sprintf("position-middle-%0X-%s-", s, s)
   249  		case internal.PositionLeft:
   250  			path = fmt.Sprintf("%s-position-left-%0X", s, s)
   251  		case internal.PositionRight:
   252  			path = fmt.Sprintf("position-right-%0X-%s", s, s)
   253  		default:
   254  			panic("invalid position: " + pos.String())
   255  		}
   256  		_, writeError := r.writeFile(path)
   257  		if writeError != nil {
   258  			res.WriteError[pos] = writeError.Error()
   259  			fs.Infof(r.f, "Writing %s position file 0x%0X Error: %s", pos.String(), s, writeError)
   260  		} else {
   261  			fs.Infof(r.f, "Writing %s position file 0x%0X OK", pos.String(), s)
   262  		}
   263  		obj, getErr := r.f.NewObject(r.ctx, path)
   264  		if getErr != nil {
   265  			res.GetError[pos] = getErr.Error()
   266  			fs.Infof(r.f, "Getting %s position file 0x%0X Error: %s", pos.String(), s, getErr)
   267  		} else {
   268  			if obj.Size() != 50 {
   269  				res.GetError[pos] = fmt.Sprintf("invalid size %d", obj.Size())
   270  				fs.Infof(r.f, "Getting %s position file 0x%0X Invalid Size: %d", pos.String(), s, obj.Size())
   271  			} else {
   272  				fs.Infof(r.f, "Getting %s position file 0x%0X OK", pos.String(), s)
   273  			}
   274  		}
   275  		if writeError != nil || getErr != nil {
   276  			positionError += pos
   277  		}
   278  	}
   279  
   280  	r.mu.Lock()
   281  	r.stringNeedsEscaping[k] = positionError
   282  	r.controlResults[k] = res
   283  	r.mu.Unlock()
   284  }
   285  
   286  // check we can write a file with the control chars
   287  func (r *results) checkControls() {
   288  	fs.Infof(r.f, "Trying to create control character file names")
   289  	ci := fs.GetConfig(context.Background())
   290  
   291  	// Concurrency control
   292  	tokens := make(chan struct{}, ci.Checkers)
   293  	for i := 0; i < ci.Checkers; i++ {
   294  		tokens <- struct{}{}
   295  	}
   296  	var wg sync.WaitGroup
   297  	for i := rune(0); i < 128; i++ {
   298  		s := string(i)
   299  		if i == 0 || i == '/' {
   300  			// We're not even going to check NULL or /
   301  			r.stringNeedsEscaping[s] = internal.PositionAll
   302  			continue
   303  		}
   304  		wg.Add(1)
   305  		go func(s string) {
   306  			defer wg.Done()
   307  			token := <-tokens
   308  			k := s
   309  			r.checkStringPositions(k, s)
   310  			tokens <- token
   311  		}(s)
   312  	}
   313  	for _, s := range []string{"\", "\u00A0", "\xBF", "\xFE"} {
   314  		wg.Add(1)
   315  		go func(s string) {
   316  			defer wg.Done()
   317  			token := <-tokens
   318  			k := s
   319  			r.checkStringPositions(k, s)
   320  			tokens <- token
   321  		}(s)
   322  	}
   323  	wg.Wait()
   324  	r.checkControlsList()
   325  	fs.Infof(r.f, "Done trying to create control character file names")
   326  }
   327  
   328  func (r *results) checkControlsList() {
   329  	l, err := r.f.List(context.TODO(), "")
   330  	if err != nil {
   331  		fs.Errorf(r.f, "Listing control character file names failed: %s", err)
   332  		return
   333  	}
   334  
   335  	namesMap := make(map[string]struct{}, len(l))
   336  	for _, s := range l {
   337  		namesMap[path.Base(s.Remote())] = struct{}{}
   338  	}
   339  
   340  	for path := range namesMap {
   341  		var pos internal.Position
   342  		var hex, value string
   343  		if g := positionLeftRe.FindStringSubmatch(path); g != nil {
   344  			pos, hex, value = internal.PositionLeft, g[2], g[1]
   345  		} else if g := positionMiddleRe.FindStringSubmatch(path); g != nil {
   346  			pos, hex, value = internal.PositionMiddle, g[1], g[2]
   347  		} else if g := positionRightRe.FindStringSubmatch(path); g != nil {
   348  			pos, hex, value = internal.PositionRight, g[1], g[2]
   349  		} else {
   350  			fs.Infof(r.f, "Unknown path %q", path)
   351  			continue
   352  		}
   353  		var hexValue []byte
   354  		for ; len(hex) >= 2; hex = hex[2:] {
   355  			if b, err := strconv.ParseUint(hex[:2], 16, 8); err != nil {
   356  				fs.Infof(r.f, "Invalid path %q: %s", path, err)
   357  				continue
   358  			} else {
   359  				hexValue = append(hexValue, byte(b))
   360  			}
   361  		}
   362  		if hex != "" {
   363  			fs.Infof(r.f, "Invalid path %q", path)
   364  			continue
   365  		}
   366  
   367  		hexStr := string(hexValue)
   368  		k := hexStr
   369  		switch r.controlResults[k].InList[pos] {
   370  		case internal.Absent:
   371  			if hexStr == value {
   372  				r.controlResults[k].InList[pos] = internal.Present
   373  			} else {
   374  				r.controlResults[k].InList[pos] = internal.Renamed
   375  			}
   376  		case internal.Present:
   377  			r.controlResults[k].InList[pos] = internal.Multiple
   378  		case internal.Renamed:
   379  			r.controlResults[k].InList[pos] = internal.Multiple
   380  		}
   381  		delete(namesMap, path)
   382  	}
   383  
   384  	if len(namesMap) > 0 {
   385  		fs.Infof(r.f, "Found additional control character file names:")
   386  		for name := range namesMap {
   387  			fs.Infof(r.f, "%q", name)
   388  		}
   389  	}
   390  }
   391  
   392  // find the max file name size we can use
   393  func (r *results) findMaxLength(characterLength int) {
   394  	var character rune
   395  	switch characterLength {
   396  	case 1:
   397  		character = 'a'
   398  	case 2:
   399  		character = 'á'
   400  	case 3:
   401  		character = '世'
   402  	case 4:
   403  		character = '🙂'
   404  	default:
   405  		panic("Bad characterLength")
   406  	}
   407  	if characterLength != len(string(character)) {
   408  		panic(fmt.Sprintf("Chose the wrong character length %q is %d not %d", character, len(string(character)), characterLength))
   409  	}
   410  	const maxLen = 16 * 1024
   411  	name := make([]rune, maxLen)
   412  	for i := range name {
   413  		name[i] = character
   414  	}
   415  	// Find the first size of filename we can't write
   416  	i := sort.Search(len(name), func(i int) (fail bool) {
   417  		defer func() {
   418  			if err := recover(); err != nil {
   419  				fs.Infof(r.f, "Couldn't write file with name length %d: %v", i, err)
   420  				fail = true
   421  			}
   422  		}()
   423  
   424  		path := string(name[:i])
   425  		o, err := r.writeFile(path)
   426  		if err != nil {
   427  			fs.Infof(r.f, "Couldn't write file with name length %d: %v", i, err)
   428  			return true
   429  		}
   430  		fs.Infof(r.f, "Wrote file with name length %d", i)
   431  		err = o.Remove(context.Background())
   432  		if err != nil {
   433  			fs.Errorf(o, "Failed to remove test file")
   434  		}
   435  		return false
   436  	})
   437  	r.maxFileLength[characterLength-1] = i - 1
   438  	fs.Infof(r.f, "Max file length is %d when writing %d byte characters %q", r.maxFileLength[characterLength-1], characterLength, character)
   439  }
   440  
   441  func (r *results) checkStreaming() {
   442  	putter := r.f.Put
   443  	if r.f.Features().PutStream != nil {
   444  		fs.Infof(r.f, "Given remote has specialized streaming function. Using that to test streaming.")
   445  		putter = r.f.Features().PutStream
   446  	}
   447  
   448  	contents := "thinking of test strings is hard"
   449  	buf := bytes.NewBufferString(contents)
   450  	hashIn := hash.NewMultiHasher()
   451  	in := io.TeeReader(buf, hashIn)
   452  
   453  	objIn := object.NewStaticObjectInfo("checkStreamingTest", time.Now(), -1, true, nil, r.f)
   454  	objR, err := putter(r.ctx, in, objIn)
   455  	if err != nil {
   456  		fs.Infof(r.f, "Streamed file failed to upload (%v)", err)
   457  		r.canStream = false
   458  		return
   459  	}
   460  
   461  	hashes := hashIn.Sums()
   462  	types := objR.Fs().Hashes().Array()
   463  	for _, Hash := range types {
   464  		sum, err := objR.Hash(r.ctx, Hash)
   465  		if err != nil {
   466  			fs.Infof(r.f, "Streamed file failed when getting hash %v (%v)", Hash, err)
   467  			r.canStream = false
   468  			return
   469  		}
   470  		if !hash.Equals(hashes[Hash], sum) {
   471  			fs.Infof(r.f, "Streamed file has incorrect hash %v: expecting %q got %q", Hash, hashes[Hash], sum)
   472  			r.canStream = false
   473  			return
   474  		}
   475  	}
   476  	if int64(len(contents)) != objR.Size() {
   477  		fs.Infof(r.f, "Streamed file has incorrect file size: expecting %d got %d", len(contents), objR.Size())
   478  		r.canStream = false
   479  		return
   480  	}
   481  	r.canStream = true
   482  }
   483  
   484  func readInfo(ctx context.Context, f fs.Fs) error {
   485  	// Ensure cleanup unless --keep-test-files is specified
   486  	if !keepTestFiles {
   487  		defer func() {
   488  			err := operations.Purge(ctx, f, "")
   489  			if err != nil {
   490  				fs.Errorf(f, "Failed to purge temporary directory: %v", err)
   491  			} else {
   492  				fs.Infof(f, "Removed temporary directory for test files: %s", f.Root())
   493  			}
   494  		}()
   495  	}
   496  
   497  	r := newResults(ctx, f)
   498  	if checkControl {
   499  		r.checkControls()
   500  	}
   501  	if checkLength {
   502  		for i := range r.maxFileLength {
   503  			r.findMaxLength(i + 1)
   504  		}
   505  	}
   506  	if checkNormalization {
   507  		r.checkUTF8Normalization()
   508  	}
   509  	if checkStreaming {
   510  		r.checkStreaming()
   511  	}
   512  	if checkBase32768 {
   513  		r.checkBase32768()
   514  	}
   515  	r.Print()
   516  	r.WriteJSON()
   517  	return nil
   518  }