433 lines
9.5 KiB
Go
433 lines
9.5 KiB
Go
package encoder
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/pflag"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// Check it satisfies the interfaces
|
|
var (
|
|
_ pflag.Value = (*MultiEncoder)(nil)
|
|
_ fmt.Scanner = (*MultiEncoder)(nil)
|
|
)
|
|
|
|
func TestEncodeString(t *testing.T) {
|
|
for _, test := range []struct {
|
|
mask MultiEncoder
|
|
want string
|
|
}{
|
|
{EncodeRaw, "Raw"},
|
|
{EncodeZero, "None"},
|
|
{EncodeDoubleQuote, "DoubleQuote"},
|
|
{EncodeDot, "Dot"},
|
|
{EncodeWin, "LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe"},
|
|
{EncodeHashPercent, "Hash,Percent"},
|
|
{EncodeSlash | EncodeDollar | EncodeColon, "Slash,Dollar,Colon"},
|
|
{EncodeSlash | (1 << 31), "Slash,0x80000000"},
|
|
} {
|
|
got := test.mask.String()
|
|
assert.Equal(t, test.want, got)
|
|
}
|
|
|
|
}
|
|
|
|
func TestEncodeSet(t *testing.T) {
|
|
for _, test := range []struct {
|
|
in string
|
|
want MultiEncoder
|
|
wantErr bool
|
|
}{
|
|
{"", 0, true},
|
|
{"Raw", EncodeRaw, false},
|
|
{"None", EncodeZero, false},
|
|
{"DoubleQuote", EncodeDoubleQuote, false},
|
|
{"Dot", EncodeDot, false},
|
|
{"LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe", EncodeWin, false},
|
|
{"Hash,Percent", EncodeHashPercent, false},
|
|
{"Slash,Dollar,Colon", EncodeSlash | EncodeDollar | EncodeColon, false},
|
|
{"Slash,0x80000000", EncodeSlash | (1 << 31), false},
|
|
{"Blerp", 0, true},
|
|
{"0xFGFFF", 0, true},
|
|
} {
|
|
var got MultiEncoder
|
|
err := got.Set(test.in)
|
|
assert.Equal(t, test.wantErr, err != nil, err)
|
|
assert.Equal(t, test.want, got, test.in)
|
|
}
|
|
|
|
}
|
|
|
|
type testCase struct {
|
|
mask MultiEncoder
|
|
in string
|
|
out string
|
|
}
|
|
|
|
func TestEncodeSingleMask(t *testing.T) {
|
|
for i, tc := range testCasesSingle {
|
|
e := tc.mask
|
|
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
|
|
got := e.Encode(tc.in)
|
|
if got != tc.out {
|
|
t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
|
|
}
|
|
got2 := e.Decode(got)
|
|
if got2 != tc.in {
|
|
t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeSingleMaskEdge(t *testing.T) {
|
|
for i, tc := range testCasesSingleEdge {
|
|
e := tc.mask
|
|
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
|
|
got := e.Encode(tc.in)
|
|
if got != tc.out {
|
|
t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
|
|
}
|
|
got2 := e.Decode(got)
|
|
if got2 != tc.in {
|
|
t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeDoubleMaskEdge(t *testing.T) {
|
|
for i, tc := range testCasesDoubleEdge {
|
|
e := tc.mask
|
|
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
|
|
got := e.Encode(tc.in)
|
|
if got != tc.out {
|
|
t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
|
|
}
|
|
got2 := e.Decode(got)
|
|
if got2 != tc.in {
|
|
t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeInvalidUnicode(t *testing.T) {
|
|
for i, tc := range []testCase{
|
|
{
|
|
mask: EncodeInvalidUtf8,
|
|
in: "\xBF",
|
|
out: "‛BF",
|
|
}, {
|
|
mask: EncodeInvalidUtf8,
|
|
in: "\xBF\xFE",
|
|
out: "‛BF‛FE",
|
|
}, {
|
|
mask: EncodeInvalidUtf8,
|
|
in: "a\xBF\xFEb",
|
|
out: "a‛BF‛FEb",
|
|
}, {
|
|
mask: EncodeInvalidUtf8,
|
|
in: "a\xBFξ\xFEb",
|
|
out: "a‛BFξ‛FEb",
|
|
}, {
|
|
mask: EncodeInvalidUtf8 | EncodeBackSlash,
|
|
in: "a\xBF\\\xFEb",
|
|
out: "a‛BF\‛FEb",
|
|
}, {
|
|
mask: 0,
|
|
in: "\xBF",
|
|
out: "\xBF",
|
|
}, {
|
|
mask: 0,
|
|
in: "\xBF\xFE",
|
|
out: "\xBF\xFE",
|
|
}, {
|
|
mask: 0,
|
|
in: "a\xBF\xFEb",
|
|
out: "a\xBF\xFEb",
|
|
}, {
|
|
mask: 0,
|
|
in: "a\xBFξ\xFEb",
|
|
out: "a\xBFξ\xFEb",
|
|
}, {
|
|
mask: EncodeBackSlash,
|
|
in: "a\xBF\\\xFEb",
|
|
out: "a\xBF\\xFEb",
|
|
},
|
|
} {
|
|
e := tc.mask
|
|
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
|
|
got := e.Encode(tc.in)
|
|
if got != tc.out {
|
|
t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
|
|
}
|
|
got2 := e.Decode(got)
|
|
if got2 != tc.in {
|
|
t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeDot(t *testing.T) {
|
|
for i, tc := range []testCase{
|
|
{
|
|
mask: EncodeZero,
|
|
in: ".",
|
|
out: ".",
|
|
}, {
|
|
mask: EncodeDot,
|
|
in: ".",
|
|
out: ".",
|
|
}, {
|
|
mask: EncodeZero,
|
|
in: "..",
|
|
out: "..",
|
|
}, {
|
|
mask: EncodeDot,
|
|
in: "..",
|
|
out: "..",
|
|
}, {
|
|
mask: EncodeDot,
|
|
in: "...",
|
|
out: "...",
|
|
}, {
|
|
mask: EncodeDot,
|
|
in: ". .",
|
|
out: ". .",
|
|
},
|
|
} {
|
|
e := tc.mask
|
|
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
|
|
got := e.Encode(tc.in)
|
|
if got != tc.out {
|
|
t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got)
|
|
}
|
|
got2 := e.Decode(got)
|
|
if got2 != tc.in {
|
|
t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeHalf(t *testing.T) {
|
|
for i, tc := range []testCase{
|
|
{
|
|
mask: 0,
|
|
in: "‛",
|
|
out: "‛",
|
|
}, {
|
|
mask: EncodeZero,
|
|
in: "‛‛",
|
|
out: "‛",
|
|
}, {
|
|
mask: 0,
|
|
in: "‛a‛",
|
|
out: "‛a‛",
|
|
}, {
|
|
mask: EncodeInvalidUtf8,
|
|
in: "a‛B‛Eg",
|
|
out: "a‛B‛Eg",
|
|
}, {
|
|
mask: EncodeInvalidUtf8,
|
|
in: "a‛B\‛Eg",
|
|
out: "a‛B\‛Eg",
|
|
}, {
|
|
mask: EncodeInvalidUtf8 | EncodeBackSlash,
|
|
in: "a‛B\‛Eg",
|
|
out: "a‛B\\‛Eg",
|
|
},
|
|
} {
|
|
e := tc.mask
|
|
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
|
|
got := e.Decode(tc.in)
|
|
if got != tc.out {
|
|
t.Errorf("Decode(%q) want %q got %q", tc.in, tc.out, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const oneDrive = (Standard |
|
|
EncodeWin |
|
|
EncodeBackSlash |
|
|
EncodeHashPercent |
|
|
EncodeDel |
|
|
EncodeCtl |
|
|
EncodeLeftTilde |
|
|
EncodeRightSpace |
|
|
EncodeRightPeriod)
|
|
|
|
var benchTests = []struct {
|
|
in string
|
|
outOld string
|
|
outNew string
|
|
}{
|
|
{
|
|
"",
|
|
"",
|
|
"",
|
|
},
|
|
{
|
|
"abc 123",
|
|
"abc 123",
|
|
"abc 123",
|
|
},
|
|
{
|
|
`\*<>?:|#%".~`,
|
|
`\*<>?:|#%".~`,
|
|
`\*<>?:|#%".~`,
|
|
},
|
|
{
|
|
`\*<>?:|#%".~/\*<>?:|#%".~`,
|
|
`\*<>?:|#%".~/\*<>?:|#%".~`,
|
|
`\*<>?:|#%".~/\*<>?:|#%".~`,
|
|
},
|
|
{
|
|
" leading space",
|
|
" leading space",
|
|
" leading space",
|
|
},
|
|
{
|
|
"~leading tilde",
|
|
"~leading tilde",
|
|
"~leading tilde",
|
|
},
|
|
{
|
|
"trailing dot.",
|
|
"trailing dot.",
|
|
"trailing dot.",
|
|
},
|
|
{
|
|
" leading space/ leading space/ leading space",
|
|
" leading space/ leading space/ leading space",
|
|
" leading space/ leading space/ leading space",
|
|
},
|
|
{
|
|
"~leading tilde/~leading tilde/~leading tilde",
|
|
"~leading tilde/~leading tilde/~leading tilde",
|
|
"~leading tilde/~leading tilde/~leading tilde",
|
|
},
|
|
{
|
|
"leading tilde/~leading tilde",
|
|
"leading tilde/~leading tilde",
|
|
"leading tilde/~leading tilde",
|
|
},
|
|
{
|
|
"trailing dot./trailing dot./trailing dot.",
|
|
"trailing dot./trailing dot./trailing dot.",
|
|
"trailing dot./trailing dot./trailing dot.",
|
|
},
|
|
}
|
|
|
|
func benchReplace(b *testing.B, f func(string) string, old bool) {
|
|
for range make([]struct{}, b.N) {
|
|
for _, test := range benchTests {
|
|
got := f(test.in)
|
|
out := test.outNew
|
|
if old {
|
|
out = test.outOld
|
|
}
|
|
if got != out {
|
|
b.Errorf("Encode(%q) want %q got %q", test.in, out, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func benchRestore(b *testing.B, f func(string) string, old bool) {
|
|
for range make([]struct{}, b.N) {
|
|
for _, test := range benchTests {
|
|
out := test.outNew
|
|
if old {
|
|
out = test.outOld
|
|
}
|
|
got := f(out)
|
|
if got != test.in {
|
|
b.Errorf("Decode(%q) want %q got %q", out, test.in, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkOneDriveReplaceNew(b *testing.B) {
|
|
benchReplace(b, oneDrive.Encode, false)
|
|
}
|
|
|
|
func BenchmarkOneDriveReplaceOld(b *testing.B) {
|
|
benchReplace(b, replaceReservedChars, true)
|
|
}
|
|
|
|
func BenchmarkOneDriveRestoreNew(b *testing.B) {
|
|
benchRestore(b, oneDrive.Decode, false)
|
|
}
|
|
|
|
func BenchmarkOneDriveRestoreOld(b *testing.B) {
|
|
benchRestore(b, restoreReservedChars, true)
|
|
}
|
|
|
|
var (
|
|
charMap = map[rune]rune{
|
|
'\\': '\', // FULLWIDTH REVERSE SOLIDUS
|
|
'*': '*', // FULLWIDTH ASTERISK
|
|
'<': '<', // FULLWIDTH LESS-THAN SIGN
|
|
'>': '>', // FULLWIDTH GREATER-THAN SIGN
|
|
'?': '?', // FULLWIDTH QUESTION MARK
|
|
':': ':', // FULLWIDTH COLON
|
|
'|': '|', // FULLWIDTH VERTICAL LINE
|
|
'#': '#', // FULLWIDTH NUMBER SIGN
|
|
'%': '%', // FULLWIDTH PERCENT SIGN
|
|
'"': '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
|
|
'.': '.', // FULLWIDTH FULL STOP
|
|
'~': '~', // FULLWIDTH TILDE
|
|
' ': '␠', // SYMBOL FOR SPACE
|
|
}
|
|
invCharMap map[rune]rune
|
|
fixEndingInPeriod = regexp.MustCompile(`\.(/|$)`)
|
|
fixEndingWithSpace = regexp.MustCompile(` (/|$)`)
|
|
fixStartingWithTilde = regexp.MustCompile(`(/|^)~`)
|
|
)
|
|
|
|
func init() {
|
|
// Create inverse charMap
|
|
invCharMap = make(map[rune]rune, len(charMap))
|
|
for k, v := range charMap {
|
|
invCharMap[v] = k
|
|
}
|
|
}
|
|
|
|
// replaceReservedChars takes a path and substitutes any reserved
|
|
// characters in it
|
|
func replaceReservedChars(in string) string {
|
|
// Folder names can't end with a period '.'
|
|
in = fixEndingInPeriod.ReplaceAllString(in, string(charMap['.'])+"$1")
|
|
// OneDrive for Business file or folder names cannot begin with a tilde '~'
|
|
in = fixStartingWithTilde.ReplaceAllString(in, "$1"+string(charMap['~']))
|
|
// Apparently file names can't start with space either
|
|
in = fixEndingWithSpace.ReplaceAllString(in, string(charMap[' '])+"$1")
|
|
// Encode reserved characters
|
|
return strings.Map(func(c rune) rune {
|
|
if replacement, ok := charMap[c]; ok && c != '.' && c != '~' && c != ' ' {
|
|
return replacement
|
|
}
|
|
return c
|
|
}, in)
|
|
}
|
|
|
|
// restoreReservedChars takes a path and undoes any substitutions
|
|
// made by replaceReservedChars
|
|
func restoreReservedChars(in string) string {
|
|
return strings.Map(func(c rune) rune {
|
|
if replacement, ok := invCharMap[c]; ok {
|
|
return replacement
|
|
}
|
|
return c
|
|
}, in)
|
|
}
|