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 }