github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/identifier/identifier.go (about) 1 package identifier 2 3 import ( 4 "errors" 5 "regexp" 6 "strings" 7 8 "github.com/mutagen-io/mutagen/pkg/encoding" 9 "github.com/mutagen-io/mutagen/pkg/random" 10 ) 11 12 const ( 13 // PrefixSynchronization is the prefix used for synchronization session 14 // identifiers. 15 PrefixSynchronization = "sync" 16 // PrefixForwarding is the prefix used for forwarding session identifiers. 17 PrefixForwarding = "fwrd" 18 // PrefixProject is the prefix used for project identifiers. 19 PrefixProject = "proj" 20 // PrefixPrompter is the prefix used for prompter identifiers. 21 PrefixPrompter = "pmtr" 22 23 // requiredPrefixLength is the required length for identifier prefixes. 24 requiredPrefixLength = 4 25 // collisionResistantLength is the number of random bytes needed to ensure 26 // collision-resistance in an identifier. 27 collisionResistantLength = 32 28 // targetBase62Length is the target length for the Base62-encoded portion of 29 // the identifier. This is set to the maximum possible length that a byte 30 // array of collisionResistantLength bytes will take to encode in Base62 31 // encoding. This length can be computed for n bytes using the formula 32 // ceil(n*8*ln(2)/ln(62))). 33 targetBase62Length = 43 34 ) 35 36 // legacyMatcher is a regular expression that matches Mutagen's legacy 37 // identifiers (which are lowercase UUIDs). 38 var legacyMatcher = regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") 39 40 // matcher is a regular expression that matches Mutagen's identifiers. 41 var matcher = regexp.MustCompile("^[a-z]{4}_[0-9a-zA-Z]{43}$") 42 43 // New generates a new collision-resistant identifier with the specified prefix. 44 // The prefix should have a length of RequiredPrefixLength. 45 func New(prefix string) (string, error) { 46 // Ensure that the prefix length is correct. 47 if len(prefix) != requiredPrefixLength { 48 return "", errors.New("incorrect prefix length") 49 } 50 51 // Ensure that each prefix character is allowed. 52 for _, r := range prefix { 53 if !('a' <= r && r <= 'z') { 54 return "", errors.New("invalid prefix character") 55 } 56 } 57 58 // Create the random value. 59 random, err := random.New(collisionResistantLength) 60 if err != nil { 61 return "", err 62 } 63 64 // Encode the random value using a Base62 encoding scheme. As a sanity 65 // check, ensure that the encoded value doesn't exceed the target length. 66 encoded := encoding.EncodeBase62(random) 67 if len(encoded) > targetBase62Length { 68 panic("encoded random data length longer than expected") 69 } 70 71 // Create a string builder. 72 builder := &strings.Builder{} 73 74 // Add the identifier prefix. 75 builder.WriteString(prefix) 76 77 // Add the separator. 78 builder.WriteRune('_') 79 80 // If the encoded value has a length less than the target length, then 81 // left-pad it with 0s. Actually, we technically pad it using whatever the 82 // zero value is in our Base62 alphabet, but that happens to be '0'. 83 for i := targetBase62Length - len(encoded); i > 0; i-- { 84 builder.WriteByte(encoding.Base62Alphabet[0]) 85 } 86 87 // Write the encoded value. 88 builder.WriteString(encoded) 89 90 // Success. 91 return builder.String(), nil 92 } 93 94 // IsValid determines whether or not a string is a valid identifier. 95 func IsValid(value string) bool { 96 return matcher.MatchString(value) || legacyMatcher.MatchString(value) 97 } 98 99 // Truncated returns a truncated version of an identifier. The truncated version 100 // is not a valid identifier on its own, but it should be suitably unique for 101 // identification purposes (e.g. in logging). If the identifier is invalid, then 102 // an empty string is returned. 103 func Truncated(identifier string) string { 104 if matcher.MatchString(identifier) { 105 return identifier[:requiredPrefixLength+1+8] 106 } else if legacyMatcher.MatchString(identifier) { 107 return identifier[:8] 108 } 109 return "" 110 }