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.
Mental model
Section titled “Mental model”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)returnsbuffer.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)returnsEncodingService: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 raisesbase64 input contains an invalid characterfor 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 bufferlocal Again = Serde.decode(Base64.decode(AsText))
-- Text from outside (another publisher, an old record) can be malformed: decode raiseslocal Ok, Decoded = pcall(Base64.decode, "not base64!")API reference
Section titled “API reference”| 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. |
Consumers
Section titled “Consumers”| 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. |
Design notes
Section titled “Design notes”- 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.