180 lines
4.8 KiB
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
|
|
}
|