From 3ff2be4b71f0023f0e01a4bd593f8efbb839fcc8 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Thu, 8 Oct 2026 03:03:24 +0400 Subject: [PATCH] Remove the CodeQL findings in typegen and PortableRegistry - typegen formatType: the inline-struct shortcut test /^{.+:.+}/ has two unbounded wildcards and backtracks quadratically on input such as '{{a:a:a:...' (370 ms at 20k repetitions). isInlineStruct checks the same thing with indexOf: an opening brace, a colon at index 2 or later and a closing brace at least two characters after it, all on the first line. 500k random inputs give the same answer as the regex; a test keeps it under 100 ms at 100k repetitions and goes red on the regex. - PortableRegistry: the type definition cache was a plain object written as typeDefs[lookupIndex][key]. lookupIndex is always a number, but nothing in the shape said so; it is now a Map, and the definition is filled through its own reference. --- packages/typegen/src/util/formatting.spec.ts | 28 ++++++- packages/typegen/src/util/formatting.ts | 15 +++- .../PortableRegistry/PortableRegistry.ts | 73 ++++++++++--------- 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/packages/typegen/src/util/formatting.spec.ts b/packages/typegen/src/util/formatting.spec.ts index 759749e..e7102a3 100644 --- a/packages/typegen/src/util/formatting.spec.ts +++ b/packages/typegen/src/util/formatting.spec.ts @@ -5,7 +5,7 @@ import { TypeRegistry } from '@pezkuwi/types'; -import { formatType } from './formatting.js'; +import { formatType, isInlineStruct } from './formatting.js'; describe('formatType', (): void => { const registry = new TypeRegistry(); @@ -28,3 +28,29 @@ describe('formatType', (): void => { ).toEqual('ITuple<[AccountId, ITuple<[Balance, u32]>, u64]>'); }); }); + +describe('isInlineStruct', (): void => { + it('matches what /^{.+:.+}/ matched', (): void => { + for (const [type, expected] of [ + ['{ a: u32 }', true], + ['{"a":"u32"}', true], + ['{a:}', false], + ['{:a}', false], + ['{a:b', false], + ['{a:b}c', true], + ['x{a:b}', false], + ['{a\n:b}', false], + ['{a:b\n}', false], + ['(a: b)', false] + ] as const) { + expect([type, isInlineStruct(type)]).toEqual([type, expected]); + } + }); + + it('is linear on input that made the regex backtrack', (): void => { + const start = Date.now(); + + expect(isInlineStruct(`{{${'a:'.repeat(100_000)}`)).toBe(false); + expect(Date.now() - start < 100).toBe(true); + }); +}); diff --git a/packages/typegen/src/util/formatting.ts b/packages/typegen/src/util/formatting.ts index c8a72a6..a89bdec 100644 --- a/packages/typegen/src/util/formatting.ts +++ b/packages/typegen/src/util/formatting.ts @@ -301,6 +301,19 @@ const formatters: Record; #names: Record; #params: TypeInfoParams; - #typeDefs: Record = {}; + readonly #typeDefs = new Map(); #types: Record; constructor (registry: Registry, value?: Uint8Array, isContract?: boolean) { @@ -613,43 +613,46 @@ export class PortableRegistry extends Struct implements ILookup { */ public getTypeDef (lookupId: SiLookupTypeId | LookupString | number): TypeDef { const lookupIndex = this.#getLookupId(lookupId); + const cached = this.#typeDefs.get(lookupIndex); - if (!this.#typeDefs[lookupIndex]) { - const lookupName = this.#names[lookupIndex]; - const empty = { - info: TypeDefInfo.DoNotConstruct, - lookupIndex, - lookupName, - type: this.registry.createLookupType(lookupIndex) - }; - - // Set named items since we will get into circular lookups along the way - if (lookupName) { - this.#typeDefs[lookupIndex] = empty; - } - - const extracted = this.#extract(this.getSiType(lookupId), lookupIndex); - - // For non-named items, we only set this right at the end - if (!lookupName) { - this.#typeDefs[lookupIndex] = empty; - } - - Object.keys(extracted).forEach((k): void => { - if (k !== 'lookupName' || extracted[k]) { - // these are safe since we are looking through the keys as set - this.#typeDefs[lookupIndex][k as 'info'] = extracted[k as 'info']; - } - }); - - // don't set lookupName on lower-level, we want to always direct to the type - if (extracted.info === TypeDefInfo.Plain) { - this.#typeDefs[lookupIndex].lookupNameRoot = this.#typeDefs[lookupIndex].lookupName; - delete this.#typeDefs[lookupIndex].lookupName; - } + if (cached) { + return cached; } - return this.#typeDefs[lookupIndex]; + const lookupName = this.#names[lookupIndex]; + const typeDef: TypeDef = { + info: TypeDefInfo.DoNotConstruct, + lookupIndex, + lookupName, + type: this.registry.createLookupType(lookupIndex) + }; + + // Set named items since we will get into circular lookups along the way + if (lookupName) { + this.#typeDefs.set(lookupIndex, typeDef); + } + + const extracted = this.#extract(this.getSiType(lookupId), lookupIndex); + + // For non-named items, we only set this right at the end + if (!lookupName) { + this.#typeDefs.set(lookupIndex, typeDef); + } + + Object.keys(extracted).forEach((k): void => { + if (k !== 'lookupName' || extracted[k]) { + // these are safe since we are looking through the keys as set + typeDef[k as 'info'] = extracted[k as 'info']; + } + }); + + // don't set lookupName on lower-level, we want to always direct to the type + if (extracted.info === TypeDefInfo.Plain) { + typeDef.lookupNameRoot = typeDef.lookupName; + delete typeDef.lookupName; + } + + return typeDef; } /**