github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/lib/encoder/encoder_test.go (about)

     1  package encoder
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strconv"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/spf13/pflag"
    11  	"github.com/stretchr/testify/assert"
    12  )
    13  
    14  // Check it satisfies the interfaces
    15  var (
    16  	_ pflag.Value = (*MultiEncoder)(nil)
    17  	_ fmt.Scanner = (*MultiEncoder)(nil)
    18  )
    19  
    20  func TestEncodeString(t *testing.T) {
    21  	for _, test := range []struct {
    22  		mask MultiEncoder
    23  		want string
    24  	}{
    25  		{0, "None"},
    26  		{EncodeZero, "None"},
    27  		{EncodeDoubleQuote, "DoubleQuote"},
    28  		{EncodeDot, "Dot"},
    29  		{EncodeWin, "LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe"},
    30  		{EncodeHashPercent, "Hash,Percent"},
    31  		{EncodeSlash | EncodeDollar | EncodeColon, "Slash,Dollar,Colon"},
    32  		{EncodeSlash | (1 << 31), "Slash,0x80000000"},
    33  	} {
    34  		got := test.mask.String()
    35  		assert.Equal(t, test.want, got)
    36  	}
    37  
    38  }
    39  
    40  func TestEncodeSet(t *testing.T) {
    41  	for _, test := range []struct {
    42  		in      string
    43  		want    MultiEncoder
    44  		wantErr bool
    45  	}{
    46  		{"", 0, true},
    47  		{"None", 0, false},
    48  		{"None", EncodeZero, false},
    49  		{"DoubleQuote", EncodeDoubleQuote, false},
    50  		{"Dot", EncodeDot, false},
    51  		{"LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe", EncodeWin, false},
    52  		{"Hash,Percent", EncodeHashPercent, false},
    53  		{"Slash,Dollar,Colon", EncodeSlash | EncodeDollar | EncodeColon, false},
    54  		{"Slash,0x80000000", EncodeSlash | (1 << 31), false},
    55  		{"Blerp", 0, true},
    56  		{"0xFGFFF", 0, true},
    57  	} {
    58  		var got MultiEncoder
    59  		err := got.Set(test.in)
    60  		assert.Equal(t, test.wantErr, err != nil, err)
    61  		assert.Equal(t, test.want, got, test.in)
    62  	}
    63  
    64  }
    65  
    66  type testCase struct {
    67  	mask MultiEncoder
    68  	in   string
    69  	out  string
    70  }
    71  
    72  func TestEncodeSingleMask(t *testing.T) {
    73  	for i, tc := range testCasesSingle {
    74  		e := tc.mask
    75  		t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
    76  			got := e.Encode(tc.in)
    77  			if got != tc.out {
    78  				t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
    79  			}
    80  			got2 := e.Decode(got)
    81  			if got2 != tc.in {
    82  				t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
    83  			}
    84  		})
    85  	}
    86  }
    87  
    88  func TestEncodeSingleMaskEdge(t *testing.T) {
    89  	for i, tc := range testCasesSingleEdge {
    90  		e := tc.mask
    91  		t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
    92  			got := e.Encode(tc.in)
    93  			if got != tc.out {
    94  				t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
    95  			}
    96  			got2 := e.Decode(got)
    97  			if got2 != tc.in {
    98  				t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
    99  			}
   100  		})
   101  	}
   102  }
   103  
   104  func TestEncodeDoubleMaskEdge(t *testing.T) {
   105  	for i, tc := range testCasesDoubleEdge {
   106  		e := tc.mask
   107  		t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
   108  			got := e.Encode(tc.in)
   109  			if got != tc.out {
   110  				t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
   111  			}
   112  			got2 := e.Decode(got)
   113  			if got2 != tc.in {
   114  				t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
   115  			}
   116  		})
   117  	}
   118  }
   119  
   120  func TestEncodeInvalidUnicode(t *testing.T) {
   121  	for i, tc := range []testCase{
   122  		{
   123  			mask: EncodeInvalidUtf8,
   124  			in:   "\xBF",
   125  			out:  "‛BF",
   126  		}, {
   127  			mask: EncodeInvalidUtf8,
   128  			in:   "\xBF\xFE",
   129  			out:  "‛BF‛FE",
   130  		}, {
   131  			mask: EncodeInvalidUtf8,
   132  			in:   "a\xBF\xFEb",
   133  			out:  "a‛BF‛FEb",
   134  		}, {
   135  			mask: EncodeInvalidUtf8,
   136  			in:   "a\xBFξ\xFEb",
   137  			out:  "a‛BFξ‛FEb",
   138  		}, {
   139  			mask: EncodeInvalidUtf8 | EncodeBackSlash,
   140  			in:   "a\xBF\\\xFEb",
   141  			out:  "a‛BF\‛FEb",
   142  		}, {
   143  			mask: 0,
   144  			in:   "\xBF",
   145  			out:  "\xBF",
   146  		}, {
   147  			mask: 0,
   148  			in:   "\xBF\xFE",
   149  			out:  "\xBF\xFE",
   150  		}, {
   151  			mask: 0,
   152  			in:   "a\xBF\xFEb",
   153  			out:  "a\xBF\xFEb",
   154  		}, {
   155  			mask: 0,
   156  			in:   "a\xBFξ\xFEb",
   157  			out:  "a\xBFξ\xFEb",
   158  		}, {
   159  			mask: EncodeBackSlash,
   160  			in:   "a\xBF\\\xFEb",
   161  			out:  "a\xBF\\xFEb",
   162  		},
   163  	} {
   164  		e := tc.mask
   165  		t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
   166  			got := e.Encode(tc.in)
   167  			if got != tc.out {
   168  				t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
   169  			}
   170  			got2 := e.Decode(got)
   171  			if got2 != tc.in {
   172  				t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
   173  			}
   174  		})
   175  	}
   176  }
   177  
   178  func TestEncodeDot(t *testing.T) {
   179  	for i, tc := range []testCase{
   180  		{
   181  			mask: 0,
   182  			in:   ".",
   183  			out:  ".",
   184  		}, {
   185  			mask: EncodeDot,
   186  			in:   ".",
   187  			out:  ".",
   188  		}, {
   189  			mask: 0,
   190  			in:   "..",
   191  			out:  "..",
   192  		}, {
   193  			mask: EncodeDot,
   194  			in:   "..",
   195  			out:  "..",
   196  		}, {
   197  			mask: EncodeDot,
   198  			in:   "...",
   199  			out:  "...",
   200  		}, {
   201  			mask: EncodeDot,
   202  			in:   ". .",
   203  			out:  ". .",
   204  		},
   205  	} {
   206  		e := tc.mask
   207  		t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
   208  			got := e.Encode(tc.in)
   209  			if got != tc.out {
   210  				t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
   211  			}
   212  			got2 := e.Decode(got)
   213  			if got2 != tc.in {
   214  				t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
   215  			}
   216  		})
   217  	}
   218  }
   219  
   220  func TestDecodeHalf(t *testing.T) {
   221  	for i, tc := range []testCase{
   222  		{
   223  			mask: 0,
   224  			in:   "‛",
   225  			out:  "‛",
   226  		}, {
   227  			mask: 0,
   228  			in:   "‛‛",
   229  			out:  "‛",
   230  		}, {
   231  			mask: 0,
   232  			in:   "‛a‛",
   233  			out:  "‛a‛",
   234  		}, {
   235  			mask: EncodeInvalidUtf8,
   236  			in:   "a‛B‛Eg",
   237  			out:  "a‛B‛Eg",
   238  		}, {
   239  			mask: EncodeInvalidUtf8,
   240  			in:   "a‛B\‛Eg",
   241  			out:  "a‛B\‛Eg",
   242  		}, {
   243  			mask: EncodeInvalidUtf8 | EncodeBackSlash,
   244  			in:   "a‛B\‛Eg",
   245  			out:  "a‛B\\‛Eg",
   246  		},
   247  	} {
   248  		e := tc.mask
   249  		t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
   250  			got := e.Decode(tc.in)
   251  			if got != tc.out {
   252  				t.Errorf("Decode(%q) want %q got %q", tc.in, tc.out, got)
   253  			}
   254  		})
   255  	}
   256  }
   257  
   258  const oneDrive = (Standard |
   259  	EncodeWin |
   260  	EncodeBackSlash |
   261  	EncodeHashPercent |
   262  	EncodeDel |
   263  	EncodeCtl |
   264  	EncodeLeftTilde |
   265  	EncodeRightSpace |
   266  	EncodeRightPeriod)
   267  
   268  var benchTests = []struct {
   269  	in  string
   270  	out string
   271  }{
   272  	{"", ""},
   273  	{"abc 123", "abc 123"},
   274  	{`\*<>?:|#%".~`, `\*<>?:|#%".~`},
   275  	{`\*<>?:|#%".~/\*<>?:|#%".~`, `\*<>?:|#%".~/\*<>?:|#%".~`},
   276  	{" leading space", " leading space"},
   277  	{"~leading tilde", "~leading tilde"},
   278  	{"trailing dot.", "trailing dot."},
   279  	{" leading space/ leading space/ leading space", " leading space/ leading space/ leading space"},
   280  	{"~leading tilde/~leading tilde/~leading tilde", "~leading tilde/~leading tilde/~leading tilde"},
   281  	{"leading tilde/~leading tilde", "leading tilde/~leading tilde"},
   282  	{"trailing dot./trailing dot./trailing dot.", "trailing dot./trailing dot./trailing dot."},
   283  }
   284  
   285  func benchReplace(b *testing.B, f func(string) string) {
   286  	for range make([]struct{}, b.N) {
   287  		for _, test := range benchTests {
   288  			got := f(test.in)
   289  			if got != test.out {
   290  				b.Errorf("Encode(%q) want %q got %q", test.in, test.out, got)
   291  			}
   292  		}
   293  	}
   294  }
   295  
   296  func benchRestore(b *testing.B, f func(string) string) {
   297  	for range make([]struct{}, b.N) {
   298  		for _, test := range benchTests {
   299  			got := f(test.out)
   300  			if got != test.in {
   301  				b.Errorf("Decode(%q) want %q got %q", got, test.in, got)
   302  			}
   303  		}
   304  	}
   305  }
   306  func BenchmarkOneDriveReplaceNew(b *testing.B) {
   307  	benchReplace(b, oneDrive.Encode)
   308  }
   309  func BenchmarkOneDriveReplaceOld(b *testing.B) {
   310  	benchReplace(b, replaceReservedChars)
   311  }
   312  func BenchmarkOneDriveRestoreNew(b *testing.B) {
   313  	benchRestore(b, oneDrive.Decode)
   314  }
   315  func BenchmarkOneDriveRestoreOld(b *testing.B) {
   316  	benchRestore(b, restoreReservedChars)
   317  }
   318  
   319  var (
   320  	charMap = map[rune]rune{
   321  		'\\': '\', // FULLWIDTH REVERSE SOLIDUS
   322  		'*':  '*', // FULLWIDTH ASTERISK
   323  		'<':  '<', // FULLWIDTH LESS-THAN SIGN
   324  		'>':  '>', // FULLWIDTH GREATER-THAN SIGN
   325  		'?':  '?', // FULLWIDTH QUESTION MARK
   326  		':':  ':', // FULLWIDTH COLON
   327  		'|':  '|', // FULLWIDTH VERTICAL LINE
   328  		'#':  '#', // FULLWIDTH NUMBER SIGN
   329  		'%':  '%', // FULLWIDTH PERCENT SIGN
   330  		'"':  '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
   331  		'.':  '.', // FULLWIDTH FULL STOP
   332  		'~':  '~', // FULLWIDTH TILDE
   333  		' ':  '␠', // SYMBOL FOR SPACE
   334  	}
   335  	invCharMap           map[rune]rune
   336  	fixEndingInPeriod    = regexp.MustCompile(`\.(/|$)`)
   337  	fixEndingWithSpace   = regexp.MustCompile(` (/|$)`)
   338  	fixStartingWithTilde = regexp.MustCompile(`(/|^)~`)
   339  )
   340  
   341  func init() {
   342  	// Create inverse charMap
   343  	invCharMap = make(map[rune]rune, len(charMap))
   344  	for k, v := range charMap {
   345  		invCharMap[v] = k
   346  	}
   347  }
   348  
   349  // replaceReservedChars takes a path and substitutes any reserved
   350  // characters in it
   351  func replaceReservedChars(in string) string {
   352  	// Folder names can't end with a period '.'
   353  	in = fixEndingInPeriod.ReplaceAllString(in, string(charMap['.'])+"$1")
   354  	// OneDrive for Business file or folder names cannot begin with a tilde '~'
   355  	in = fixStartingWithTilde.ReplaceAllString(in, "$1"+string(charMap['~']))
   356  	// Apparently file names can't start with space either
   357  	in = fixEndingWithSpace.ReplaceAllString(in, string(charMap[' '])+"$1")
   358  	// Encode reserved characters
   359  	return strings.Map(func(c rune) rune {
   360  		if replacement, ok := charMap[c]; ok && c != '.' && c != '~' && c != ' ' {
   361  			return replacement
   362  		}
   363  		return c
   364  	}, in)
   365  }
   366  
   367  // restoreReservedChars takes a path and undoes any substitutions
   368  // made by replaceReservedChars
   369  func restoreReservedChars(in string) string {
   370  	return strings.Map(func(c rune) rune {
   371  		if replacement, ok := invCharMap[c]; ok {
   372  			return replacement
   373  		}
   374  		return c
   375  	}, in)
   376  }