SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,242 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { run, imports } from '../upstream/compile';
|
||||
import { bundle } from '../upstream/project';
|
||||
import { schemaCodecs, schemaRegistry } from '../upstream/schema';
|
||||
import { generateCode } from '../../src/codegen/generator';
|
||||
import { protobuf_getUnknownFields } from '../../src/runtime';
|
||||
import { ProtobufCompiler } from '../../src/compiler/compiler';
|
||||
import { withProject } from '../upstream/project';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('SnowLuma compatibility', () => {
|
||||
it('leaves generic data unions and nested business types outside codec discovery', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Json = string | Json[];
|
||||
interface Field<T> {value:T; metadata:Json;}
|
||||
type Wrapper<T> = Field<T> & { extra?:Json };
|
||||
function identity<T>(value:Field<T>) {return value;}
|
||||
function generic<T>(field:Field<Wrapper<T>>) {return identity(field);}
|
||||
function result<T>(ok:boolean, value:T) {if(ok) return {ok:true, value}; return {ok:false, value};}
|
||||
const field:Field<Wrapper<string>> = {value:{value:'ok', metadata:[]}, metadata:[]};
|
||||
export const output = result(true, generic(field)).value.value.value;
|
||||
`).output,
|
||||
).toBe('ok');
|
||||
});
|
||||
|
||||
it('keeps unrelated callback aliases and inferred containers lazy', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Json = string | Json[];
|
||||
type Handler<T> = (value:T) => void;
|
||||
interface Field<T> {value:T; metadata:Json;}
|
||||
const field:Field<Json> = {value:'ok', metadata:[]};
|
||||
function wrap<T>(value:Field<T>) {return {get(){return value.value;}};}
|
||||
const handler:Handler<Json> = (value) => {};
|
||||
handler('x');
|
||||
export const result = wrap(field).get();
|
||||
`).result,
|
||||
).toBe('ok');
|
||||
});
|
||||
|
||||
it('propagates composite class bindings through generic factories', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Box<T> { value:pb<1,T>; }
|
||||
class Codec<T> { encode(value:T) { return protobuf_encode<T>(value); } }
|
||||
function create<T>() { return new Codec<Box<T>>(); }
|
||||
export const result = [...create<uint_32>().encode({value:7})];
|
||||
`).result,
|
||||
).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it('defers unrelated class type arguments but validates codec class schemas', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Json = string | Json[];
|
||||
class Field<T> { value(): null { return null; } }
|
||||
function field() { return new Field<Json>(); }
|
||||
export const result = field().value();
|
||||
`).result,
|
||||
).toBeNull();
|
||||
expect(() =>
|
||||
run(`
|
||||
class Codec<T> { encode(value:T) {return protobuf_encode<T>(value);} }
|
||||
new Codec<Missing>().encode({});
|
||||
`),
|
||||
).toThrow(/Unresolved type Missing/);
|
||||
});
|
||||
|
||||
it('infers matching generic containers without expanding unrelated recursive data', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Json = string | Json[];
|
||||
interface Field<T> { value: T; metadata: Json; }
|
||||
function arrayField<E>(field: Field<E>) { return Object.assign({}, { get: () => field.value }); }
|
||||
function wrap<E>(field: Field<E>) { return arrayField(field); }
|
||||
const field: Field<string> = { value:'ok', metadata:[] };
|
||||
export const result = wrap(field).get();
|
||||
`).result,
|
||||
).toBe('ok');
|
||||
});
|
||||
|
||||
it('does not resolve recursive data aliases while looking for class receivers', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Data<T> = { children: T[]; name: string };
|
||||
type Element = Data<Element>;
|
||||
type Wrapped<T> = T;
|
||||
function data(): Wrapped<Element> { return { children: [], name: 'ok' }; }
|
||||
export const result = data().name.toUpperCase();
|
||||
`).result,
|
||||
).toBe('OK');
|
||||
});
|
||||
|
||||
it('does not resolve ordinary generic return values or data alias members as callbacks', () => {
|
||||
expect(
|
||||
run(`
|
||||
type Id<T> = T;
|
||||
interface Data { value: string; children: Id<Data>[]; }
|
||||
const data: Id<Data> = {value:'ok', children:[]};
|
||||
function decode<T>(raw: Uint8Array, callback: () => T): T | null { return callback(); }
|
||||
const raw: Uint8Array = new Uint8Array();
|
||||
const decoded = decode(raw, () => data);
|
||||
export const result = [decoded.value, data.value];
|
||||
`).result,
|
||||
).toEqual(['ok', 'ok']);
|
||||
});
|
||||
|
||||
it('does not specialize ambient library functions', async () => {
|
||||
const source = `import { expect } from './external';
|
||||
const data: Record<string, unknown> = {};
|
||||
expect(data).check();`;
|
||||
await withProject(
|
||||
{
|
||||
'external.d.ts': 'export declare function expect<T>(value: T): { check(): void };',
|
||||
'entry.ts': source,
|
||||
},
|
||||
(directory) => {
|
||||
expect(
|
||||
new ProtobufCompiler().compile(source, join(directory, 'entry.ts')),
|
||||
).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves ordinary calls and data receivers outside schema resolution', () => {
|
||||
expect(
|
||||
run(`
|
||||
interface Options { meta?: Record<string, unknown>; }
|
||||
function options(value: Options): Options { return value; }
|
||||
const input: Options = { meta: { source: 'test' } };
|
||||
const copy = options(input);
|
||||
function data(flag: boolean) { if (flag) return { value: 7 }; return { value: 8 }; }
|
||||
function bytes(value: Uint8Array): Uint8Array { return value; }
|
||||
const raw: Uint8Array = new Uint8Array([1]);
|
||||
export const result = [copy.meta.source, data(true).value, bytes(raw).length];
|
||||
`).result,
|
||||
).toEqual(['test', 7, 1]);
|
||||
});
|
||||
|
||||
it('keeps TypeScript optionality separate from explicit wire presence', () => {
|
||||
const result = run(`
|
||||
interface Msg {
|
||||
plain?: pb<1, uint_32>;
|
||||
flag?: pb<2, bool>;
|
||||
text?: pb<3, string>;
|
||||
data?: pb<4, bytes>;
|
||||
big?: pb<5, uint_64>;
|
||||
forced?: pb_optional<6, uint_32>;
|
||||
}
|
||||
export const wire = [...protobuf_encode<Msg>({
|
||||
plain: 0, flag: false, text: '', data: new Uint8Array(), big: 0n, forced: 0,
|
||||
})];
|
||||
export const decoded = protobuf_decode<Msg>(new Uint8Array(wire));
|
||||
`);
|
||||
expect(result.wire).toEqual([48, 0]);
|
||||
expect(result.decoded).toEqual({
|
||||
plain: null,
|
||||
flag: null,
|
||||
text: null,
|
||||
data: null,
|
||||
big: null,
|
||||
forced: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('retains aliased markers and presence through exported generic wrappers', async () => {
|
||||
const result = await bundle({
|
||||
'schema.ts': `import type { pb as P, pb_optional as O, pb_repeated as R, uint_32 } from '@snowluma/proton';
|
||||
export interface Box<T> { value?: P<1, T>; }
|
||||
export interface Msg { plain?: P<1, uint_32>; forced?: O<2, uint_32>; items?: R<3, uint_32>; }
|
||||
export interface Outer<U> { inner?: P<1, Box<U>>; }`,
|
||||
'barrel.ts': `export * from './schema';`,
|
||||
'wrapper.ts': `${imports} import type {Outer} from './barrel';
|
||||
export function encode<T>(value:Outer<T>) { return protobuf_encode<Outer<T>>(value); }`,
|
||||
'forward.ts': `import {encode} from './wrapper'; import type {Outer} from './barrel';
|
||||
export function forward<T>(value:Outer<T>) { return encode<T>(value); }`,
|
||||
'entry.ts': `${imports} import type {Msg} from './barrel'; import {forward} from './forward';
|
||||
export const wire = [...forward<Msg>({inner:{value:{plain:0,forced:0,items:[0,7]}}})];`,
|
||||
});
|
||||
expect(result.wire).toEqual([10, 8, 10, 6, 16, 0, 24, 0, 24, 7]);
|
||||
});
|
||||
|
||||
it.each([true, false])('resolves package exports with tsconfig=%s', async (withConfig) => {
|
||||
const result = await bundle({
|
||||
...(withConfig
|
||||
? {
|
||||
'tsconfig.json': JSON.stringify({
|
||||
compilerOptions: { moduleResolution: 'bundler', module: 'esnext' },
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
'node_modules/@fake/schemas/package.json': JSON.stringify({
|
||||
name: '@fake/schemas',
|
||||
type: 'module',
|
||||
exports: { '.': { types: './index.ts', default: './index.ts' } },
|
||||
}),
|
||||
'node_modules/@fake/schemas/index.ts': `export * from './schema';`,
|
||||
'node_modules/@fake/schemas/schema.ts': `export interface Msg { value:pb<1,uint_32>; }`,
|
||||
'entry.ts': `${imports} import type {Msg} from '@fake/schemas';
|
||||
export const result = protobuf_decode<Msg>(protobuf_encode<Msg>({value:42}));`,
|
||||
});
|
||||
expect(result).toEqual({ result: { value: 42 } });
|
||||
});
|
||||
|
||||
const codec = schemaCodecs<{ value: number }>('interface Msg { value:pb<1,uint_32>; }');
|
||||
it.each([
|
||||
['unterminated group', [0x13, 8, 7]],
|
||||
['mismatched group', [0x13, 0x1c]],
|
||||
['unexpected group end', [0x14]],
|
||||
['invalid group tag', [0x13, 0]],
|
||||
['group payload exceeds frame', [0x13, 0x1a, 4, 0x14]],
|
||||
['group recursion limit', [...Array(66).fill(0x13), ...Array(66).fill(0x14)]],
|
||||
] as const)('rejects %s', (_name, bytes) => {
|
||||
expect(() => codec.decode(Uint8Array.from(bytes))).toThrow(/protobuf/);
|
||||
});
|
||||
|
||||
it('skips nested groups and retains non-enumerable metadata', () => {
|
||||
const decoded = codec.decode(Uint8Array.from([0x13, 0x1b, 8, 99, 0x1c, 0x14, 8, 7]));
|
||||
expect(decoded).toEqual({ value: 7 });
|
||||
expect(protobuf_getUnknownFields(decoded)).toEqual([
|
||||
{ fieldNumber: 2, wireType: 3, count: 1, totalByteLength: 5 },
|
||||
]);
|
||||
expect(JSON.stringify(decoded)).toBe('{"value":7}');
|
||||
expect([...codec.encode(decoded)]).toEqual([8, 7]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[NaN, 0],
|
||||
[0, NaN],
|
||||
[0.5, 1],
|
||||
[0, 0.5],
|
||||
[-1, 1],
|
||||
[1, 0],
|
||||
[0, 3],
|
||||
[0, Infinity],
|
||||
])('rejects invalid decoder bounds %s..%s', (offset, end) => {
|
||||
const generated = generateCode(schemaRegistry('interface Msg { value:pb<1,uint_32>; }'));
|
||||
const decode = new Function(generated + '\nreturn protobuf_decode_Msg;')();
|
||||
expect(() => decode(new Uint8Array([8, 1]), offset, end)).toThrow(/bounds/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,342 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { schemaRegistry as analyzeSource } from '../upstream/schema';
|
||||
import { generateCode } from '../../src/codegen/generator';
|
||||
import { protobuf_getUnknownFieldMetadata } from '../../src/runtime';
|
||||
import { loadFixture } from '../helpers';
|
||||
|
||||
/** Analyze fixture → generate → eval → return encode/decode pair */
|
||||
function makeRoundTrip(fixtureName: string, msgName: string) {
|
||||
return makeRoundTripFromSchema(loadFixture(fixtureName), msgName);
|
||||
}
|
||||
|
||||
function makeRoundTripFromSchema(schema: string, msgName: string) {
|
||||
const gen = generateCode(analyzeSource(schema, `${msgName}.ts`));
|
||||
return new Function(
|
||||
gen + `\nreturn { enc: protobuf_encode_${msgName}, dec: protobuf_decode_${msgName} };`,
|
||||
)() as {
|
||||
enc: (obj: unknown) => Uint8Array;
|
||||
dec: (data: Uint8Array) => any;
|
||||
};
|
||||
}
|
||||
|
||||
function encodeVarint32(value: number): number[] {
|
||||
const bytes: number[] = [];
|
||||
let remaining = value >>> 0;
|
||||
|
||||
do {
|
||||
let byte = remaining & 0x7f;
|
||||
remaining >>>= 7;
|
||||
|
||||
if (remaining !== 0) {
|
||||
byte |= 0x80;
|
||||
}
|
||||
|
||||
bytes.push(byte);
|
||||
} while (remaining !== 0);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
describe('code generator', () => {
|
||||
it.each([
|
||||
[0, []],
|
||||
[1, [0x08, 0x01]],
|
||||
[127, [0x08, 0x7f]],
|
||||
[128, [0x08, 0x80, 0x01]],
|
||||
[300, [0x08, 0xac, 0x02]],
|
||||
[16384, [0x08, 0x80, 0x80, 0x01]],
|
||||
[0xffffffff, [0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]],
|
||||
] as const)('encodes and decodes uint32 wire fixture %i', (value, bytes) => {
|
||||
const { enc, dec } = makeRoundTrip('simple.ts', 'SimpleMsg');
|
||||
expect([...enc({ value })]).toEqual(bytes);
|
||||
expect(dec(Uint8Array.from(bytes)).value).toBe(value);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0x10, 0xac, 0x02],
|
||||
[0x12, 0x03, 0x41, 0x42, 0x43],
|
||||
[0x15, 0, 0, 0, 0],
|
||||
[0x11, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
])('skips an unknown wire field before a known field (%j)', (...bytes) => {
|
||||
const { dec } = makeRoundTrip('simple.ts', 'SimpleMsg');
|
||||
expect(dec(Uint8Array.from([...bytes, 0x08, 42])).value).toBe(42);
|
||||
});
|
||||
|
||||
it('generates encode/decode for simple uint_32', () => {
|
||||
const gen = generateCode(analyzeSource(loadFixture('simple.ts'), 't.ts'));
|
||||
expect(gen).toContain('function protobuf_encode_SimpleMsg(obj)');
|
||||
expect(gen).toContain('function protobuf_decode_SimpleMsg(data');
|
||||
// Tag for field 1, varint should be pre-computed as 0x08
|
||||
expect(gen).toContain('buf[offset++] = 8;');
|
||||
});
|
||||
|
||||
it('generates nested message code', () => {
|
||||
const gen = generateCode(analyzeSource(loadFixture('nested.ts'), 't.ts'));
|
||||
expect(gen).toContain('__write_Inner(');
|
||||
expect(gen).toContain('protobuf_decode_Inner(data, offset, offset + _len)');
|
||||
});
|
||||
|
||||
it('round-trip uint_32', () => {
|
||||
const { enc, dec } = makeRoundTrip('simple.ts', 'SimpleMsg');
|
||||
expect(dec(enc({ value: 42 })).value).toBe(42);
|
||||
});
|
||||
|
||||
it('round-trip nested message', () => {
|
||||
const { enc, dec } = makeRoundTrip('nested.ts', 'Outer');
|
||||
expect(dec(enc({ inner: { value: 999 } })).inner.value).toBe(999);
|
||||
});
|
||||
|
||||
it('round-trip string', () => {
|
||||
const { enc, dec } = makeRoundTrip('string-msg.ts', 'StringMsg');
|
||||
expect(dec(enc({ text: 'hello world' })).text).toBe('hello world');
|
||||
});
|
||||
|
||||
it('round-trip unicode string', () => {
|
||||
const { enc, dec } = makeRoundTrip('string-msg.ts', 'StringMsg');
|
||||
expect(dec(enc({ text: '你好,protobuf🚀' })).text).toBe('你好,protobuf🚀');
|
||||
});
|
||||
|
||||
it('round-trip bool', () => {
|
||||
const { enc, dec } = makeRoundTrip('bool-msg.ts', 'BoolMsg');
|
||||
expect(dec(enc({ active: true })).active).toBe(true);
|
||||
const empty = enc({ active: false });
|
||||
expect(empty.length).toBe(0);
|
||||
expect(dec(empty).active).toBe(false);
|
||||
});
|
||||
|
||||
it('proto3 default encoding (0 → empty)', () => {
|
||||
const { enc } = makeRoundTrip('simple.ts', 'SimpleMsg');
|
||||
expect(enc({ value: 0 }).length).toBe(0);
|
||||
});
|
||||
|
||||
it('round-trip repeated uint_32', () => {
|
||||
const { enc, dec } = makeRoundTrip('repeated.ts', 'RepeatedMsg');
|
||||
const result = dec(enc({ ids: [10, 20, 30], names: [] }));
|
||||
expect(result.ids).toEqual([10, 20, 30]);
|
||||
expect(result.names).toEqual([]);
|
||||
});
|
||||
|
||||
it('round-trip repeated string', () => {
|
||||
const { enc, dec } = makeRoundTrip('repeated.ts', 'RepeatedMsg');
|
||||
const result = dec(enc({ ids: [], names: ['hello', 'world'] }));
|
||||
expect(result.ids).toEqual([]);
|
||||
expect(result.names).toEqual(['hello', 'world']);
|
||||
});
|
||||
|
||||
it('repeated empty arrays round-trip', () => {
|
||||
const { enc, dec } = makeRoundTrip('repeated.ts', 'RepeatedMsg');
|
||||
const result = dec(enc({ ids: [], names: [] }));
|
||||
expect(result.ids).toEqual([]);
|
||||
expect(result.names).toEqual([]);
|
||||
});
|
||||
|
||||
it('round-trip bigint-backed 64-bit integers', () => {
|
||||
const schema = `
|
||||
interface BigIntMsg {
|
||||
unsigned: pb<1, uint_64>;
|
||||
signed: pb<2, int_64>;
|
||||
zigzag: pb<3, sint_64>;
|
||||
fixed: pb<4, fixed_64>;
|
||||
sfixed: pb<5, sfixed_64>;
|
||||
ids: pb_repeated<6, uint_64>;
|
||||
}`;
|
||||
const { enc, dec } = makeRoundTripFromSchema(schema, 'BigIntMsg');
|
||||
const input = {
|
||||
unsigned: 18446744073709551615n,
|
||||
signed: -1n,
|
||||
zigzag: -1234567890123456789n,
|
||||
fixed: 0x0102030405060708n,
|
||||
sfixed: -0x0102030405060708n,
|
||||
ids: [1n, 2n, 9007199254740993n],
|
||||
};
|
||||
expect(dec(enc(input))).toEqual(input);
|
||||
});
|
||||
|
||||
it('round-trip sint_32 with negative values', () => {
|
||||
const schema = `
|
||||
interface Sint32Msg {
|
||||
value: pb<1, sint_32>;
|
||||
values: pb_repeated<2, sint_32>;
|
||||
}`;
|
||||
const { enc, dec } = makeRoundTripFromSchema(schema, 'Sint32Msg');
|
||||
const input = { value: -789, values: [-1, 0, 1, -789, 2147483647, -2147483648] };
|
||||
expect(dec(enc(input))).toEqual(input);
|
||||
});
|
||||
|
||||
it('preserves legacy outbound int_32 bytes while accepting canonical negative varints', () => {
|
||||
const schema = `
|
||||
interface Int32Msg {
|
||||
value: pb<1, int_32>;
|
||||
values: pb_repeated<2, int_32>;
|
||||
forced: pb_optional<3, int_32>;
|
||||
}`;
|
||||
const { enc, dec } = makeRoundTripFromSchema(schema, 'Int32Msg');
|
||||
const input = {
|
||||
value: -1,
|
||||
values: [-2147483648, -1, 0, 1, 2147483647],
|
||||
forced: -2147483648,
|
||||
};
|
||||
const encoded = enc(input);
|
||||
|
||||
// Keep SnowLuma's established five-byte negative int32 encoding on the
|
||||
// outbound path until QQ interoperability is verified on a live account.
|
||||
// The decoder remains liberal and accepts protobuf's canonical ten-byte
|
||||
// negative form below.
|
||||
expect(Array.from(encoded.slice(0, 6))).toEqual([0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]);
|
||||
expect(dec(encoded)).toEqual(input);
|
||||
expect(
|
||||
dec(Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]))
|
||||
.value,
|
||||
).toBe(-1);
|
||||
});
|
||||
|
||||
it('retains the low 32 bits of extended uint_32 field varints', () => {
|
||||
const { dec } = makeRoundTripFromSchema(
|
||||
`interface Uint32Msg { value: pb<1, uint_32>; }`,
|
||||
'Uint32Msg',
|
||||
);
|
||||
|
||||
// QQ sometimes serializes a signed 32-bit source into a schema slot that
|
||||
// clients expose as uint32. Protobuf readers consume the full ten-byte
|
||||
// varint and retain its low 32 bits for wire compatibility.
|
||||
expect(
|
||||
dec(Uint8Array.from([0x08, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]))
|
||||
.value,
|
||||
).toBe(0xfffffffe);
|
||||
|
||||
// QQ-compatible uint32 readers consume up to a full 64-bit varint and
|
||||
// retain its low 32 bits. Some push-envelope sequences use that wire
|
||||
// representation, so values with non-zero high bits must remain decodable.
|
||||
expect(dec(Uint8Array.from([0x08, 0x80, 0x80, 0x80, 0x80, 0x10])).value).toBe(0);
|
||||
expect(dec(Uint8Array.from([0x08, 0x87, 0x80, 0x80, 0x80, 0x10])).value).toBe(7);
|
||||
|
||||
// Compatibility applies only to complete uint64 varints. Truncated input
|
||||
// and values wider than 64 bits must still fail loudly.
|
||||
expect(() => dec(Uint8Array.from([0x08, 0x80]))).toThrow(/[Tt]runcated.*varint/);
|
||||
expect(() =>
|
||||
dec(
|
||||
Uint8Array.from([0x08, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02]),
|
||||
),
|
||||
).toThrow(/Invalid protobuf varint/);
|
||||
});
|
||||
|
||||
it('rejects malformed length-delimited fields without leaving parent bounds', () => {
|
||||
const schema = `interface BytesMsg { data: pb<1, bytes>; }`;
|
||||
const { dec } = makeRoundTripFromSchema(schema, 'BytesMsg');
|
||||
|
||||
expect(() => dec(Uint8Array.from([0x0a, 0x80]))).toThrow(/[Tt]runcated.*varint/);
|
||||
expect(() => dec(Uint8Array.from([0x0a, 0x80, 0x80, 0x80, 0x80, 0x10]))).toThrow(
|
||||
/Invalid protobuf varint/,
|
||||
);
|
||||
});
|
||||
|
||||
it('skips unknown groups without decoding their fields into the parent', () => {
|
||||
const schema = `
|
||||
interface Text { str: pb<1, string>; }
|
||||
interface Elem { text: pb<1, Text>; }
|
||||
`;
|
||||
const { dec } = makeRoundTripFromSchema(schema, 'Elem');
|
||||
const decoded = dec(
|
||||
Uint8Array.from([
|
||||
0x7b, 0x0a, 0x08, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x64, 0x7c,
|
||||
]),
|
||||
);
|
||||
|
||||
expect(decoded.text).toBeNull();
|
||||
expect(protobuf_getUnknownFieldMetadata(decoded).fields).toMatchObject([
|
||||
{ fieldNumber: 15, wireType: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('aggregates repeated unknown fields instead of retaining every occurrence', () => {
|
||||
const { dec } = makeRoundTripFromSchema(
|
||||
`interface UnknownMsg { known: pb<1, uint_32>; }`,
|
||||
'UnknownMsg',
|
||||
);
|
||||
const wire: number[] = [];
|
||||
const tag = encodeVarint32((60 << 3) | 0);
|
||||
|
||||
for (let index = 0; index < 1_000; index++) {
|
||||
wire.push(...tag, 0);
|
||||
}
|
||||
|
||||
const metadata = protobuf_getUnknownFieldMetadata(dec(Uint8Array.from(wire)));
|
||||
expect(metadata).toEqual({
|
||||
fields: [{ fieldNumber: 60, wireType: 0, count: 1_000, totalByteLength: 1_000 }],
|
||||
totalOccurrences: 1_000,
|
||||
omittedOccurrences: 0,
|
||||
omittedByteLength: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('caps retained unknown field kinds and summarizes overflow', () => {
|
||||
const { dec } = makeRoundTripFromSchema(
|
||||
`interface UnknownKindsMsg { known: pb<1, uint_32>; }`,
|
||||
'UnknownKindsMsg',
|
||||
);
|
||||
const wire: number[] = [];
|
||||
|
||||
for (let fieldNumber = 2; fieldNumber <= 66; fieldNumber++) {
|
||||
wire.push(...encodeVarint32((fieldNumber << 3) | 0), 0);
|
||||
}
|
||||
|
||||
const metadata = protobuf_getUnknownFieldMetadata(dec(Uint8Array.from(wire)));
|
||||
expect(metadata.fields).toHaveLength(64);
|
||||
expect(metadata.totalOccurrences).toBe(65);
|
||||
expect(metadata.omittedOccurrences).toBe(1);
|
||||
expect(metadata.omittedByteLength).toBe(1);
|
||||
});
|
||||
|
||||
it('round-trip double as ieee754 number', () => {
|
||||
const schema = `
|
||||
interface DoubleMsg {
|
||||
value: pb<1, double>;
|
||||
values: pb_repeated<2, double>;
|
||||
}`;
|
||||
const { enc, dec } = makeRoundTripFromSchema(schema, 'DoubleMsg');
|
||||
const result = dec(enc({ value: Math.PI, values: [Math.E, -0.5] }));
|
||||
expect(result.value).toBeCloseTo(Math.PI);
|
||||
expect(result.values[0]).toBeCloseTo(Math.E);
|
||||
expect(result.values[1]).toBeCloseTo(-0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pb_optional explicit presence', () => {
|
||||
it('serialises a zero scalar (where pb<> would omit it)', () => {
|
||||
const { enc, dec } = makeRoundTripFromSchema(
|
||||
`interface OptMsg { forced: pb_optional<1, uint_32>; }`,
|
||||
'OptMsg',
|
||||
);
|
||||
// field 1, varint, value 0 → tag 0x08 then payload 0x00 (NOT empty).
|
||||
expect(Array.from(enc({ forced: 0 }))).toEqual([0x08, 0x00]);
|
||||
expect(dec(enc({ forced: 0 })).forced).toBe(0);
|
||||
});
|
||||
|
||||
it('still omits the field when the value is absent (undefined)', () => {
|
||||
const { enc } = makeRoundTripFromSchema(
|
||||
`interface OptMsg2 { forced: pb_optional<1, uint_32>; }`,
|
||||
'OptMsg2',
|
||||
);
|
||||
expect(enc({}).length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not change a sibling plain pb<> field (it still omits its zero)', () => {
|
||||
const { enc, dec } = makeRoundTripFromSchema(
|
||||
`interface MixMsg { plain: pb<1, uint_32>; forced: pb_optional<2, uint_32>; }`,
|
||||
'MixMsg',
|
||||
);
|
||||
// plain=0 omitted (unchanged); only forced=0 is emitted (field 2 → tag 0x10).
|
||||
expect(Array.from(enc({ plain: 0, forced: 0 }))).toEqual([0x10, 0x00]);
|
||||
expect(dec(enc({ plain: 7, forced: 9 }))).toEqual({ plain: 7, forced: 9 });
|
||||
});
|
||||
|
||||
it('forces a false bool onto the wire', () => {
|
||||
const { enc, dec } = makeRoundTripFromSchema(
|
||||
`interface BoolOpt { flag: pb_optional<1, bool>; }`,
|
||||
'BoolOpt',
|
||||
);
|
||||
expect(Array.from(enc({ flag: false }))).toEqual([0x08, 0x00]);
|
||||
expect(dec(enc({ flag: false })).flag).toBe(false);
|
||||
expect(Array.from(enc({ flag: true }))).toEqual([0x08, 0x01]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user