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  }