3076ea72创建于 2025年12月1日历史提交
/** 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
  }
}