ulid/crockford32/crockford32.go

180 lines
4.8 KiB
Go

package crockford32
import (
"fmt"
"slices"
)
// Encode 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 Encode(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 := encodeMap[lookup]
output = append(output, character)
}
return string(output)
}
// Decode takes in a Crockford base-32 encoded string and parses the
// bytes out of it. Each character corresponds to 5 bits.
//
// The string may contain optional dashes to make it more readable,
// similar to a UUID.
//
// This implementation does not handle the additional check symbols
// mentioned in the spec. It intentionally ignores them.
func Decode(input string) ([]byte, error) {
// Decode each character into an 8-bit uint
intList := make([]uint8, 0, len(input))
for _, r := range input {
_, ignored := ignoredSymbols[r]
if ignored {
continue
}
value, ok := decodeMap[r]
if !ok {
return nil, fmt.Errorf("invalid character: %c", r)
}
intList = append(intList, value)
}
slices.Reverse(intList)
// Go right to left across the bits placing each 5-bit chunk into
// the byte array
output := make([]byte, 0, len(input))
bitsRemaining := uint8(8)
currentByte := byte(0)
for _, value := range intList {
mask := uint8(0b11111)
// We have all the space we need
if bitsRemaining > 5 {
// Just use what we need
shift := 8 - bitsRemaining
shifted := value << shift
currentByte = currentByte | shifted
bitsRemaining -= 5
} else {
// Take our remaining bits and them fill in the rest from the next value
oldV := value
// Shift this value up to fill in the remainder of this byte
oldV = oldV & (mask >> (5 - bitsRemaining))
shift := 8 - bitsRemaining
oldV = oldV << shift
currentByte = currentByte | oldV
// Start the next byte with the remainder of the this value
output = append(output, currentByte)
currentByte = byte(0)
value = value >> bitsRemaining
currentByte = currentByte | value
bitsUsed := 5 - bitsRemaining
bitsRemaining = 8 - bitsUsed
}
}
if currentByte != 0 {
output = append(output, currentByte)
}
slices.Reverse(output)
return output, nil
}