106 lines
3.0 KiB
Go
106 lines
3.0 KiB
Go
package crockford32
|
|
|
|
import "slices"
|
|
|
|
// CrockfordEncode takes a byte array and encodes it into a character
|
|
// string according to Crockford's base 32 encoding. Every 5-bits
|
|
// corresponds to a character. This specific implementation uses Big
|
|
// Endian byte order to fit the ULID spec ("network byte ordering")
|
|
//
|
|
// https://www.crockford.com/base32.html
|
|
// https://git.wisellama.rocks/Mirrors/ulid-spec
|
|
func CrockfordEncode(bytes []byte) string {
|
|
// Crockford is a base 32 encoding.
|
|
// 2^5 = 32, so every 5 bits will give us a character.
|
|
//
|
|
// Each byte is 8 bits, so we'll have to smoosh bytes together to
|
|
// get values divisible by 5. Any remainder will be padded with
|
|
// zeros.
|
|
//
|
|
// For ULIDs, we have 128 bits which doesn't evenly divide by 5.
|
|
// Technically we'll be encoding 130 bits of information
|
|
// (divisible by 5), but the timestamp will should always start
|
|
// with some zero padding.
|
|
//
|
|
// According to the spec, this is why the maximum ULID value is
|
|
// `7ZZZZZZZZZZZZZZZZZZZZZZZZZ` instead of all Z's.
|
|
// The largest supported timestamp is 2^48 - 1.
|
|
|
|
if len(bytes) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Split our byte array up into 5-bit sections and determine how
|
|
// much of a remainder we have.
|
|
splitSize := len(bytes) * 8 / 5
|
|
remainder := len(bytes) * 8 % 5
|
|
|
|
// Then determine how many uint8's we need to represent these
|
|
// bits.
|
|
numInts := splitSize
|
|
if remainder > 0 {
|
|
numInts += 1
|
|
}
|
|
intList := make([]uint8, 0, numInts)
|
|
|
|
// Go right to left across the bits grabbing each 5-bit chunk
|
|
byteIndex := len(bytes) - 1
|
|
bitsRemaining := 8
|
|
bitsNeeded := 5
|
|
currentByte := uint8(bytes[byteIndex])
|
|
for byteIndex >= 0 {
|
|
mask := uint8(0b11111)
|
|
|
|
// We have all the bits we need
|
|
if bitsRemaining > bitsNeeded {
|
|
// Just grab what we need and shift down
|
|
bitsRemaining -= 5
|
|
newInt := currentByte & mask
|
|
currentByte = currentByte >> 5
|
|
intList = append(intList, newInt)
|
|
} else {
|
|
// Take our remaining bits and them fill in the rest from the next byte
|
|
bitsNeeded -= bitsRemaining
|
|
oldB := currentByte
|
|
byteIndex--
|
|
|
|
// Grab the next byte and shift it upwards
|
|
tempB := byte(0)
|
|
if byteIndex >= 0 {
|
|
tempB = uint8(bytes[byteIndex])
|
|
}
|
|
tempB = tempB << bitsRemaining
|
|
|
|
// Merge its bits with our remaining bits.
|
|
merged := tempB | oldB
|
|
newInt := merged & mask
|
|
intList = append(intList, newInt)
|
|
|
|
// Finally grab the next byte and shift it downwards to
|
|
// discard the bits we already used.
|
|
if byteIndex >= 0 {
|
|
currentByte = uint8(bytes[byteIndex])
|
|
currentByte = currentByte >> bitsNeeded
|
|
}
|
|
|
|
// Update our tracking values
|
|
bitsRemaining = 8 - bitsNeeded
|
|
bitsNeeded = 5
|
|
}
|
|
}
|
|
|
|
// Since we went right-to-left, reverse the list to get our values
|
|
// in the correct order.
|
|
slices.Reverse(intList)
|
|
|
|
// Encode those ints into strings 5-bits at a time.
|
|
output := make([]rune, 0, len(intList))
|
|
for _, i := range intList {
|
|
lookup := i & 0b11111
|
|
character := crockfordEncodeMap[lookup]
|
|
output = append(output, character)
|
|
}
|
|
|
|
return string(output)
|
|
}
|