ulid/crockford32/crockford32.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)
}