Skip to content

Base64

RFC 4648 base64 between buffers and text, through EncodingService where the engine has it, for stores and transports that only carry strings.

Realm
Shared
Require
ReplicatedStorage.ChloeKernel.Base64
Since
v0.1.0
Source
Base64.luau

Base64 turns a buffer into RFC 4648 text with = padding and back. It exists for the places a Serde buffer cannot go as bytes: MessagingService carries only text, and some MemoryStore and custom backends reject buffer values. Since 0.9.0 both calls go through EncodingService when the engine provides it and fall back to a buffer-native Luau codec elsewhere (the standalone regression runners). Both paths produce the same text, and both raise on input that is not valid base64.

The first call resolves EncodingService inside a pcall and checks that it has Base64Encode. The result is cached for the life of the VM: the service, or false where the engine has none.

  • encode(buffer) returns buffer.tostring(EncodingService:Base64Encode(input)) on the engine path. The Luau path sizes one output buffer at 4 characters per 3 input bytes (rounded up) and fills it through lookup tables, with no string concatenation.
  • decode(text) returns EncodingService:Base64Decode(buffer.fromstring(input)) on the engine path. The Luau path asserts the length is a multiple of 4 (base64 input length must be a multiple of 4), then raises base64 input contains an invalid character for any byte outside the alphabet and for = anywhere except the last one or two positions. Before 0.9.0 the Luau decoder read unknown characters as zero bits and returned garbage.

Base64 has no state beyond that cache. When to use it (probing a store, forcing text, wrapping an envelope) is decided by the driver that calls it.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Base64 = require(ReplicatedStorage.ChloeKernel.Base64)
local Serde = require(ReplicatedStorage.ChloeKernel.Serde)
local Blob = Serde.encode({ Anything = true, Nested = { 1, 2.5, "x" } })
local AsText = Base64.encode(Blob) -- about 33% larger than the buffer
local Again = Serde.decode(Base64.decode(AsText))
-- Text from outside (another publisher, an old record) can be malformed: decode raises
local Ok, Decoded = pcall(Base64.decode, "not base64!")
Member Description
Base64.encode(input: buffer) -> string RFC 4648 with padding. 4 characters per 3 bytes, rounded up.
Base64.decode(input: string) -> buffer Raises on input that is not valid padded base64.
Base64._encodeLuau(input) / Base64._decodeLuau(input) The Luau codec, callable directly. Specs compare it with the engine path.
Base64._useNative(service?) Spec seam: installs a stand-in service, false forces the Luau codec, nil detects again.
Consumer Behavior
MemoryDriver The first write probes with a raw buffer. Only a type error proves buffers unsupported, so a throttle during the probe cannot lock the driver into text. Text values carry a CK1: prefix so strings written by other code are never misread. ForceBase64 = true skips the probe.
MessageDriver Every payload is packed with Serde (and Zstd with Compress), then base64’d, because MessagingService carries text. A packed payload over 950 base64 characters is refused with an error naming both sizes. Incoming text that fails to decode is reported as a decode failure.
DataDriver Codec profiles store as raw buffers on the DataStore backend. On backends without buffer support (Memory, Http) the save is an { F = "B64", P = <base64> } envelope. See Backends.
  • The size cost is about 33%. Drivers pay it only where a store has proven it needs text (MemoryDriver’s probe) or the transport is text-only (MessageDriver).
  • The Luau path does not accept unpadded input or whitespace. Anything producing text for these drivers should use Base64.encode.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.