diff --git a/id/id.go b/id/id.go index d8b602197c9a624332874c059e56fb13fdf9ec1e..4f984e1de952134e05270353d796be0cdb5fe8ff 100644 --- a/id/id.go +++ b/id/id.go @@ -220,6 +220,62 @@ func NewIdFromString(idString string, idType Type, x interface{}) *ID { return newID } +var ( + // nonBase64Regex matches any character that are not base 64 compatible + // except strings. + nonBase64Regex = regexp.MustCompile(`[^a-zA-Z0-9+/\s]+`) + + // whitespaceRegex matches one or more whitespace characters. + whitespaceRegex = regexp.MustCompile(`\s+`) +) + +// NewIdFromBase64String creates a new ID that when base 64 encoded, looks like +// the passed in base64String. This function is for testing purposes only. +// +// If the string is longer than the data portion of the base 64 string, then it +// is truncated. If it is shorter, then the remaining bytes are filled with +// zeroes. The string is made to be base 64 compatible by replacing one or more +// consecutive white spaces with a plus "+" and stripping all other invalid +// characters (any character that is not an upper- and lower-case alphabet +// character (A–Z, a–z), numeral (0–9), or the plus "+" and slash "/" symbols). +func NewIdFromBase64String(base64String string, idType Type, x interface{}) *ID { + // Ensure that this function is only run in testing environments + switch x.(type) { + case *testing.T, *testing.M, *testing.B: + break + default: + panic("NewIdFromBase64String can only be used for testing.") + } + + // Convert the string to bytes and create new ID from it + newID := NewIdFromBytes([]byte{}, x) + + // Set the ID type + newID.SetType(idType) + + // Escape ID string to be base 64 compatible by replacing all strings with + + // and stripping all other invalid characters + base64String = nonBase64Regex.ReplaceAllString(base64String, "") + base64String = whitespaceRegex.ReplaceAllString(base64String, "+") + + b64Str := base64.StdEncoding.EncodeToString(newID.Marshal()) + + // Trim the string if it is over the max length + if len(base64String) > len(b64Str)-1 { + base64String = base64String[:len(b64Str)-1] + } + + // Concatenate the string with the rest of the generated bytes and type + base64String = base64String + b64Str[len(base64String):] + + data, err := base64.StdEncoding.DecodeString(base64String) + if err != nil { + jww.FATAL.Panicf("Failed to decode string: %+v", err) + } + + return NewIdFromBytes(data, x) +} + // NewIdFromUInt converts the specified uint64 into bytes and returns a new ID // based off it with the specified ID type. The remaining space of the array is // filled with zeros. This function is for testing purposes only. diff --git a/id/id_test.go b/id/id_test.go index a5b27ee9c96a3c4120f729ba6afbec5f285dc91c..d7ff2dceeb116c81ecb677397dd92a450d52902c 100644 --- a/id/id_test.go +++ b/id/id_test.go @@ -547,6 +547,73 @@ func TestNewIdFromString_TestError(t *testing.T) { _ = NewIdFromString("test", Generic, nil) } +// Tests that NewIdFromBase64String creates an ID, that when base 64 encoded, +// looks similar to the passed in string. +func TestNewIdFromBase64String(t *testing.T) { + tests := []struct{ base64String, expected string }{ + {"Test 1", "Test+1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE"}, + {"[Test 2]", "Test+2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE"}, + {"$#%[T$%%est $#$ 3]$$%", "Test+3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE"}, + {"$#%[T$%%est $#$ 4+/]$$%", "Test+4+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE"}, + {"Test 55555555555555555555555555555555555555", "Test+55555555555555555555555555555555555555E"}, + {"Test 66666666666666666666666666666666666666666", "Test+66666666666666666666666666666666666666E"}, + {"", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE"}, + } + + for i, tt := range tests { + newID := NewIdFromBase64String(tt.base64String, Group, t) + + b64 := base64.StdEncoding.EncodeToString(newID.Marshal()) + if tt.expected != b64 { + t.Errorf("Incorrect base 64 encoding for string %q (%d)."+ + "\nexpected: %s\nreceived: %s", + tt.base64String, i, tt.expected, b64) + } + } + // + // + // // Test values + // expectedIdString := "TestIDStringOfCorrectLength" + // expectedType := Group + // expectedID := new(ID) + // copy(expectedID[:], append([]byte(expectedIdString), byte(expectedType))) + // + // strs := []string{ + // "test", + // `"test"`, + // `Question ?`, + // `open angle bracket <`, + // `close angle bracket >`, + // `slash /`, + // `slash \`, + // } + // for i, str := range strs { + // escaped := url.QueryEscape(str) + // escaped = whitespaceRegex.ReplaceAllString(str, "+") + // escaped = nonBase64Regex.ReplaceAllString(escaped, "") + // fmt.Printf("%2d. %s\n %s\n", i, str, escaped) + // } + // + // // Create the ID and check its contents + // newID := NewIdFromBase64String(expectedIdString, expectedType, t) + // + // // Check if the new ID matches the expected ID + // if !expectedID.Cmp(newID) { + // t.Errorf("NewIdFromString() produced an ID with the incorrect data."+ + // "\n\texpected: %v\n\treceived: %v", expectedID[:], newID[:]) + // } + // + // // Check if the original string is still in the first 32 bytes + // newIdString := string(newID.Bytes()[:ArrIDLen-1]) + // if expectedIdString != newIdString { + // t.Errorf("NewIdFromString() did not correctly convert the original "+ + // "string to bytes.\n\texpected string: %#v\n\treceived string: %#v"+ + // "\n\texpected bytes: %v\n\treceived bytes: %v", + // expectedIdString, newIdString, + // []byte(expectedIdString), newID.Bytes()[:ArrIDLen-1]) + // } +} + // Tests that NewIdFromUInt() creates a new ID with the correct contents by // converting the ID back into a uint and comparing it to the original. func TestNewIdFromUInt(t *testing.T) {