import { describe, it, expect } from 'vitest'; import protobuf from 'protobufjs'; import { decodeValue, encodeValue, decodeDouble, decodeFloat, decodeSint32, decodeUint32, decodeUint64, decodeBool, decodeString, decodeBytes, decodeList, decodeTuple, decodeDictionary, decodeKrpcList, } from '../src/decoder.js'; import { TypeCode, type KrpcType } from '../src/types.js'; import { MESSAGE_TYPES, KRPC } from '../src/schema.js'; const T = (t: Partial): KrpcType => ({ code: t.code ?? 0, service: t.service ?? '', name: t.name ?? '', types: t.types ?? [], }); // We need a small fake "TYPE" registry for the tests so the decoder // can find List / Tuple / Dictionary / Status by name. We can use the // real MESSAGE_TYPES for the actual system messages. const REG = MESSAGE_TYPES; describe('primitive decoders', () => { it('decodes a double (8-byte little-endian)', () => { expect(decodeDouble(new Uint8Array(new Float64Array([3.14]).buffer))).toBeCloseTo(3.14, 10); expect(decodeDouble(new Uint8Array(new Float64Array([-0]).buffer))).toBe(-0); expect(decodeDouble(new Uint8Array(new Float64Array([0]).buffer))).toBe(0); expect(decodeDouble(new Uint8Array(new Float64Array([1e30]).buffer))).toBe(1e30); }); it('decodes a float (4-byte little-endian)', () => { expect(decodeFloat(new Uint8Array(new Float32Array([2.5]).buffer))).toBeCloseTo(2.5, 5); }); it('decodes a sint32 (zigzag varint)', () => { // 0 -> 0, 1 -> 2, -1 -> 1, 2 -> 4, -2 -> 3 expect(decodeSint32(new Uint8Array([0]))).toBe(0); expect(decodeSint32(new Uint8Array([2]))).toBe(1); expect(decodeSint32(new Uint8Array([1]))).toBe(-1); expect(decodeSint32(new Uint8Array([4]))).toBe(2); expect(decodeSint32(new Uint8Array([3]))).toBe(-2); }); it('decodes a uint32 (varint)', () => { expect(decodeUint32(new Uint8Array([0]))).toBe(0); expect(decodeUint32(new Uint8Array([0x7f]))).toBe(127); expect(decodeUint32(new Uint8Array([0x80, 0x01]))).toBe(128); }); it('decodes a uint64 to BigInt (preserves > 2^53)', () => { // 2^53 + 1 exceeds Number.MAX_SAFE_INTEGER. Verify the decoder // produces the exact BigInt. Hand-varint for 2^53+1: // 7-bit groups, LSB first: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x10 // (bit 53 = position 4 in the last group = 0b0010000 = 0x10) const big = 0x20_0000_0000_0001n; // 2^53 + 1 const bytes = new Uint8Array([0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10]); expect(decodeUint64(bytes)).toBe(big); }); it('decodes a bool', () => { expect(decodeBool(new Uint8Array([0]))).toBe(false); expect(decodeBool(new Uint8Array([1]))).toBe(true); expect(decodeBool(new Uint8Array([0xff]))).toBe(true); }); it('decodes a string', () => { const utf8 = new TextEncoder().encode('Kerbin'); const buf = new Uint8Array([utf8.length, ...utf8]); expect(decodeString(buf)).toBe('Kerbin'); }); it('decodes a string with non-ASCII characters', () => { const utf8 = new TextEncoder().encode('日本語'); const buf = new Uint8Array([utf8.length, ...utf8]); expect(decodeString(buf)).toBe('日本語'); }); it('decodes bytes', () => { const payload = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); const buf = new Uint8Array([payload.length, ...payload]); expect(decodeBytes(buf)).toEqual(payload); }); }); describe('decodeValue dispatcher', () => { it('decodes DOUBLE', () => { const bytes = new Uint8Array(new Float64Array([2.71828]).buffer); const out = decodeValue(T({ code: TypeCode.DOUBLE }), bytes); expect(out).toBeCloseTo(2.71828, 5); }); it('decodes STRING', () => { const utf8 = new TextEncoder().encode('Mun'); const bytes = new Uint8Array([utf8.length, ...utf8]); expect(decodeValue(T({ code: TypeCode.STRING }), bytes)).toBe('Mun'); }); it('decodes CLASS object id (non-null)', () => { // 42 as varint const id = decodeValue(T({ code: TypeCode.CLASS, service: 'SpaceCenter', name: 'Vessel' }), new Uint8Array([42])); expect(id).toBe(42n); }); it('decodes CLASS object id (null when 0)', () => { expect( decodeValue( T({ code: TypeCode.CLASS, service: 'SpaceCenter', name: 'Vessel' }), new Uint8Array([0]), ), ).toBeNull(); }); it('decodes ENUMERATION as a sint32', () => { expect( decodeValue( T({ code: TypeCode.ENUMERATION, service: 'SpaceCenter', name: 'VesselType' }), new Uint8Array([0]), // Ship ), ).toBe(0); }); it('decodes NONE as null', () => { expect(decodeValue(T({ code: TypeCode.NONE }), new Uint8Array(0))).toBeNull(); }); it('throws on unknown type code', () => { expect(() => decodeValue(T({ code: 9999 }), new Uint8Array(0))).toThrow(/unknown TypeCode/); }); }); describe('collection decoders', () => { it('decodes a list of doubles', () => { // Construct a KRPC.List message manually: // items[0] = 8 bytes for double 1.0 // items[1] = 8 bytes for double 2.5 const d1 = new Uint8Array(new Float64Array([1.0]).buffer); const d2 = new Uint8Array(new Float64Array([2.5]).buffer); const listMsg = KRPC.List.create({ items: [d1, d2] }); const listBytes = KRPC.List.encode(listMsg).finish(); const out = decodeList( T({ code: TypeCode.LIST, types: [T({ code: TypeCode.DOUBLE })] }), listBytes, REG, ); expect(out).toEqual([1.0, 2.5]); }); it('decodes a list of strings', () => { const enc = (s: string): Uint8Array => { const utf8 = new TextEncoder().encode(s); return new Uint8Array([utf8.length, ...utf8]); }; const listMsg = KRPC.List.create({ items: [enc('Kerbin'), enc('Mun')] }); const listBytes = KRPC.List.encode(listMsg).finish(); const out = decodeList( T({ code: TypeCode.LIST, types: [T({ code: TypeCode.STRING })] }), listBytes, REG, ); expect(out).toEqual(['Kerbin', 'Mun']); }); it('decodes an empty list', () => { const listMsg = KRPC.List.create({ items: [] }); const listBytes = KRPC.List.encode(listMsg).finish(); const out = decodeList( T({ code: TypeCode.LIST, types: [T({ code: TypeCode.STRING })] }), listBytes, REG, ); expect(out).toEqual([]); }); it('decodes a null list as null', () => { const out = decodeList( T({ code: TypeCode.LIST, types: [T({ code: TypeCode.STRING })] }), new Uint8Array([0]), REG, ); expect(out).toBeNull(); }); it('decodes a list of CLASS as a bigint array', () => { // items[0] = 7, items[1] = 0 (null) const listMsg = KRPC.List.create({ items: [new Uint8Array([7]), new Uint8Array([0])] }); const listBytes = KRPC.List.encode(listMsg).finish(); const out = decodeList( T({ code: TypeCode.LIST, types: [T({ code: TypeCode.CLASS, service: 'SpaceCenter', name: 'CelestialBody' })], }), listBytes, REG, ); expect(out).toEqual([7n, null]); }); it('decodes a tuple of mixed types', () => { // tuple: (string "Kerbin", double 0.5) const utf8 = new TextEncoder().encode('Kerbin'); const sBytes = new Uint8Array([utf8.length, ...utf8]); const dBytes = new Uint8Array(new Float64Array([0.5]).buffer); const tupleMsg = KRPC.Tuple.create({ items: [sBytes, dBytes] }); const tupleBytes = KRPC.Tuple.encode(tupleMsg).finish(); const out = decodeTuple( T({ code: TypeCode.TUPLE, types: [T({ code: TypeCode.STRING }), T({ code: TypeCode.DOUBLE })], }), tupleBytes, REG, ); expect(out).toEqual(['Kerbin', 0.5]); }); it('decodes a dictionary of string -> double', () => { const utf8 = new TextEncoder().encode('mu'); const sBytes = new Uint8Array([utf8.length, ...utf8]); const dBytes = new Uint8Array(new Float64Array([3.53e12]).buffer); const dictMsg = KRPC.Dictionary.create({ entries: [{ key: sBytes, value: dBytes }], }); const dictBytes = KRPC.Dictionary.encode(dictMsg).finish(); const out = decodeDictionary( T({ code: TypeCode.DICTIONARY, types: [T({ code: TypeCode.STRING }), T({ code: TypeCode.DOUBLE })], }), dictBytes, REG, ); expect(out.get('mu')).toBe(3.53e12); }); it('decodes a null dictionary as null', () => { const out = decodeDictionary( T({ code: TypeCode.DICTIONARY, types: [T({ code: TypeCode.STRING }), T({ code: TypeCode.DOUBLE })], }), new Uint8Array([0]), REG, ); expect(out).toBeNull(); }); it('exposes a low-level decodeKrpcList for testing', () => { const listMsg = KRPC.List.create({ items: [new Uint8Array([1, 2, 3])] }); const bytes = KRPC.List.encode(listMsg).finish(); const out = decodeKrpcList(bytes, KRPC.List); // protobufjs returns Node Buffers (which extend Uint8Array). Compare // the underlying bytes so this works regardless of wrapper class. expect(out).toHaveLength(1); expect(Array.from(out[0] as Uint8Array)).toEqual([1, 2, 3]); }); }); describe('encodeValue (round-trips)', () => { it('encodes a double', () => { const bytes = encodeValue(T({ code: TypeCode.DOUBLE }), 1.5); expect(decodeValue(T({ code: TypeCode.DOUBLE }), bytes)).toBe(1.5); }); it('encodes a string', () => { const bytes = encodeValue(T({ code: TypeCode.STRING }), 'Kerbol'); expect(decodeValue(T({ code: TypeCode.STRING }), bytes)).toBe('Kerbol'); }); it('encodes a CLASS object id', () => { const bytes = encodeValue( T({ code: TypeCode.CLASS, service: 'SpaceCenter', name: 'Vessel' }), 42n, ); expect(decodeValue(T({ code: TypeCode.CLASS, service: 'SpaceCenter', name: 'Vessel' }), bytes)).toBe(42n); }); it('encodes a null CLASS as id=0', () => { const bytes = encodeValue( T({ code: TypeCode.CLASS, service: 'SpaceCenter', name: 'Vessel' }), null, ); expect(bytes).toEqual(new Uint8Array([0])); }); it('encodes an ENUMERATION', () => { const bytes = encodeValue( T({ code: TypeCode.ENUMERATION, service: 'SpaceCenter', name: 'VesselType' }), 3, // Probe ); expect( decodeValue( T({ code: TypeCode.ENUMERATION, service: 'SpaceCenter', name: 'VesselType' }), bytes, ), ).toBe(3); }); it('encodes a list of doubles', () => { const bytes = encodeValue( T({ code: TypeCode.LIST, types: [T({ code: TypeCode.DOUBLE })] }), [1.0, 2.0, 3.0], REG, ); const out = decodeValue( T({ code: TypeCode.LIST, types: [T({ code: TypeCode.DOUBLE })] }), bytes, REG, ); expect(out).toEqual([1.0, 2.0, 3.0]); }); it('encodes a uint64 BigInt', () => { const id = 0x1_0000_0000n; // 2^32 const bytes = encodeValue(T({ code: TypeCode.UINT64 }), id); expect(decodeValue(T({ code: TypeCode.UINT64 }), bytes)).toBe(id); }); it('encodes a bool', () => { expect(decodeValue(T({ code: TypeCode.BOOL }), encodeValue(T({ code: TypeCode.BOOL }), true))).toBe(true); expect(decodeValue(T({ code: TypeCode.BOOL }), encodeValue(T({ code: TypeCode.BOOL }), false))).toBe(false); }); it('rejects SET (not implemented)', () => { expect(() => encodeValue(T({ code: TypeCode.SET, types: [T({ code: TypeCode.STRING })] }), new Set(), REG), ).toThrow(/SET not implemented/); }); }); // Suppress unused-warning for the TYPE const helper by using it once. const _checkTypeHelper: KrpcType = T({ code: TypeCode.STRING }); void _checkTypeHelper; // Also pin the protobufjs default import to confirm we still have it. const _pb: typeof protobuf = protobuf; void _pb;