/** UTF-8 encode string to bytes */
export function encodeUTF8(s: string): Uint8Array {
const out: number[] = []
let i = 0
while (i < s.length) {
let codePoint = s.charCodeAt(i++)
if (codePoint >= 0xd800 && codePoint <= 0xdbff && i < s.length) {
const next = s.charCodeAt(i)
if ((next & 0xfc00) === 0xdc00) {
i++
codePoint = ((codePoint & 0x3ff) << 10) + (next & 0x3ff) + 0x10000
}
}
if (codePoint <= 0x7f) {
out.push(codePoint)
} else if (codePoint <= 0x7ff) {
out.push(0xc0 | (codePoint >> 6))
out.push(0x80 | (codePoint & 0x3f))
} else if (codePoint <= 0xffff) {
out.push(0xe0 | (codePoint >> 12))
out.push(0x80 | ((codePoint >> 6) & 0x3f))
out.push(0x80 | (codePoint & 0x3f))
} else {
out.push(0xf0 | (codePoint >> 18))
out.push(0x80 | ((codePoint >> 12) & 0x3f))
out.push(0x80 | ((codePoint >> 6) & 0x3f))
out.push(0x80 | (codePoint & 0x3f))
}
}
return new Uint8Array(out)
}
/** UTF-8 decode bytes to string */
export function decodeUTF8(b: Uint8Array): string {
let out = ''
let i = 0
while (i < b.length) {
const byte1 = b[i++]
if (byte1 < 0x80) {
out += String.fromCharCode(byte1)
} else if (byte1 >= 0xc0 && byte1 < 0xe0) {
const byte2 = b[i++]
const codePoint = ((byte1 & 0x1f) << 6) | (byte2 & 0x3f)
out += String.fromCharCode(codePoint)
} else if (byte1 >= 0xe0 && byte1 < 0xf0) {
const byte2 = b[i++], byte3 = b[i++]
const codePoint = ((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
out += String.fromCharCode(codePoint)
} else {
const byte2 = b[i++], byte3 = b[i++], byte4 = b[i++]
let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3f) << 12) | ((byte3 & 0x3f) << 6) | (byte4 & 0x3f)
codePoint -= 0x10000
out += String.fromCharCode(0xd800 | ((codePoint >> 10) & 0x3ff))
out += String.fromCharCode(0xdc00 | (codePoint & 0x3ff))
}
}
return out
}
const b64abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
const b64lookup: Record<string, number> = {}
for (let i = 0; i < b64abc.length; i++) {
const ch = b64abc.charAt(i)
b64lookup[ch] = i
}
/** Encode bytes to base64 string (unpadded standard alphabet) */
export function encodeBase64(bytes: Uint8Array): string {
let out = ''
let i = 0
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
const n = bytes.byteLength
while (i < n) {
const b0 = dv.getUint8(i++)
const b1 = i < n ? dv.getUint8(i++) : 0
const b2 = i < n ? dv.getUint8(i++) : 0
out += b64abc.charAt(b0 >> 2)
out += b64abc.charAt(((b0 & 0x03) << 4) | (b1 >> 4))
out += i - 1 < n ? b64abc.charAt(((b1 & 0x0F) << 2) | (b2 >> 6)) : '='
out += i < n ? b64abc.charAt(b2 & 0x3F) : '='
}
return out
}
/** Decode base64 string to bytes; throws on invalid length/alphabet */
export function decodeBase64(str: string): Uint8Array {
const s = str.replace(/\s+/g, '')
if (s.length % 4 !== 0) throw new Error('invalid base64')
const len = s.endsWith('==') ? (s.length / 4) * 3 - 2 : s.endsWith('=') ? (s.length / 4) * 3 - 1 : (s.length / 4) * 3
const out = new Uint8Array(len)
let o = 0
for (let i = 0; i < s.length; i += 4) {
const c0 = s.charAt(i), c1 = s.charAt(i+1), c2 = s.charAt(i+2), c3 = s.charAt(i+3)
const v0 = b64lookup[c0], v1 = b64lookup[c1]
const v2 = c2 === '=' ? 0 : b64lookup[c2]
const v3 = c3 === '=' ? 0 : b64lookup[c3]
const b0 = (v0 << 2) | (v1 >> 4)
out[o++] = b0 & 0xFF
if (c2 !== '=') {
const b1 = ((v1 & 0x0F) << 2) | (v2 >> 4)
out[o++] = b1 & 0xFF
}
if (c3 !== '=') {
const b2 = ((v2 & 0x0F) << 6) | v3
out[o++] = b2 & 0xFF
}
}
return out
}
/** ZigZag encode 32-bit signed integer */
export function zigZag32(n: number): number {
return (n << 1) ^ (n >> 31)
}
/** ZigZag decode 32-bit signed integer */
export function unZigZag32(n: number): number {
return (n >>> 1) ^ -(n & 1)
}
/** ZigZag encode 64-bit bigint */
export function zigZag64(n: bigint): bigint {
return (n << 1n) ^ (n >> 63n)
}
/** ZigZag decode 64-bit bigint */
export function unZigZag64(n: bigint): bigint {
return (n >> 1n) ^ (-(n & 1n))
}
/**
* RFC3339 serialize google.protobuf.Timestamp
* seconds as bigint, nanos as number (0..999,999,999)
*/
export function timestampToJson(seconds: bigint, nanos: number): string {
const ms = Number(seconds) * 1000
if (ms < Date.parse('0001-01-01T00:00:00Z') || ms > Date.parse('9999-12-31T23:59:59Z')) {
throw new Error('google.protobuf.Timestamp out of range')
}
if (nanos < 0) throw new Error('google.protobuf.Timestamp nanos must not be negative')
let z = 'Z'
if (nanos > 0) {
const nanosStr = (nanos + 1000000000).toString().substring(1)
if (nanosStr.substring(3) === '000000') z = '.' + nanosStr.substring(0,3) + 'Z'
else if (nanosStr.substring(6) === '000') z = '.' + nanosStr.substring(0,6) + 'Z'
else z = '.' + nanosStr + 'Z'
}
return new Date(ms).toISOString().replace('.000Z', z)
}
/** RFC3339 parse to google.protobuf.Timestamp */
export function timestampFromJson(json: string): TimestampParts {
if (typeof json !== 'string') throw new Error('invalid RFC3339 string')
const matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z|\.([0-9]{3,9})Z|([+-][0-9][0-9]:[0-9][0-9]))$/)
if (!matches) throw new Error('invalid RFC3339 string')
const ms = Date.parse(matches[1] + '-' + matches[2] + '-' + matches[3] + 'T' + matches[4] + ':' + matches[5] + ':' + matches[6] + (matches[8] ? matches[8] : 'Z'))
if (Number.isNaN(ms)) throw new Error('invalid RFC3339 string')
if (ms < Date.parse('0001-01-01T00:00:00Z') || ms > Date.parse('9999-12-31T23:59:59Z')) throw new Error('timestamp out of range')
let nanos = 0
if (matches[7]) nanos = parseInt('1' + matches[7] + '0'.repeat(9 - matches[7].length)) - 1000000000
return new TimestampParts(BigInt(Math.floor(ms / 1000)), nanos)
}
/**
* Serialize google.protobuf.Duration to string: "Xs" or "X.Ys"
*/
export function durationToJson(seconds: bigint, nanos: number): string {
const sign = seconds < 0n || nanos < 0 ? '-' : ''
const absSec = seconds < 0n ? -seconds : seconds
const absNanos = nanos < 0 ? -nanos : nanos
let frac = ''
if (absNanos > 0) {
const nanosStr = (absNanos + 1000000000).toString().substring(1)
// trim trailing zeros in 3/6/9 groups per spec
if (nanosStr.substring(6) === '000') frac = '.' + nanosStr.substring(0,6)
else if (nanosStr.substring(3) === '000000') frac = '.' + nanosStr.substring(0,3)
else frac = '.' + nanosStr
}
return `${sign}${absSec.toString()}${frac}s`
}
/** Parse google.protobuf.Duration from string */
export function durationFromJson(str: string): DurationParts {
if (typeof str !== 'string' || !str.endsWith('s')) throw new Error('invalid duration')
const body = str.slice(0, -1)
const m = body.match(/^(-?)(\d+)(?:\.(\d{1,9}))?$/)
if (!m) throw new Error('invalid duration')
const neg = m[1] === '-'
const sec = BigInt(m[2])
let nanos = 0
if (m[3]) {
const frac = m[3]
const pad = frac + '0'.repeat(9 - frac.length)
nanos = parseInt(pad, 10)
}
if (neg) {
return new DurationParts(-sec, -nanos)
}
return new DurationParts(sec, nanos)
}
export class TimestampParts {
seconds: bigint
nanos: number
constructor(seconds: bigint, nanos: number) {
this.seconds = seconds
this.nanos = nanos
}
}
export class DurationParts {
seconds: bigint
nanos: number
constructor(seconds: bigint, nanos: number) {
this.seconds = seconds
this.nanos = nanos
}
}