From 91d2795511f10aee830679fb88089fba0f0a3b7b Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Fri, 20 Feb 2026 15:43:20 +0500 Subject: [PATCH 1/4] limit the depth of deserialize & serialize --- javascript/packages/fory/lib/fory.ts | 22 ++ .../packages/fory/lib/gen/serializer.ts | 16 +- javascript/packages/fory/lib/type.ts | 1 + javascript/test/depth-limit.test.ts | 339 ++++++++++++++++++ 4 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 javascript/test/depth-limit.test.ts diff --git a/javascript/packages/fory/lib/fory.ts b/javascript/packages/fory/lib/fory.ts index d26b6b6fa7..44117076c3 100644 --- a/javascript/packages/fory/lib/fory.ts +++ b/javascript/packages/fory/lib/fory.ts @@ -40,9 +40,15 @@ export default class { anySerializer: Serializer; typeMeta = TypeMeta; config: Config; + depth: number = 0; + maxDepth: number; constructor(config?: Partial) { this.config = this.initConfig(config); + this.maxDepth = config?.maxDepth ?? 50; + if (this.maxDepth < 2) { + throw new Error(`maxDepth must be >= 2 but got ${this.maxDepth}`); + } this.binaryReader = new BinaryReader(this.config); this.binaryWriter = new BinaryWriter(this.config); this.referenceResolver = new ReferenceResolver(this.binaryReader); @@ -57,6 +63,7 @@ export default class { return { refTracking: config?.refTracking !== null ? Boolean(config?.refTracking) : null, useSliceString: Boolean(config?.useSliceString), + maxDepth: config?.maxDepth, hooks: config?.hooks || {}, compatible: Boolean(config?.compatible), }; @@ -66,6 +73,20 @@ export default class { return this.config.compatible === true; } + incReadDepth(): void { + this.depth++; + if (this.depth > this.maxDepth) { + throw new Error( + `Deserialization depth limit exceeded: ${this.depth} > ${this.maxDepth}. ` + + `The data may be malicious, or increase maxDepth if needed.` + ); + } + } + + decReadDepth(): void { + this.depth--; + } + registerSerializer(constructor: new () => T, customSerializer: CustomSerializer): { serializer: Serializer; serialize(data: InputType | null): PlatformBuffer; @@ -145,6 +166,7 @@ export default class { this.binaryReader.reset(bytes); this.typeMetaResolver.reset(); this.metaStringResolver.reset(); + this.depth = 0; const bitmap = this.binaryReader.readUint8(); if ((bitmap & ConfigFlags.isNullFlag) === ConfigFlags.isNullFlag) { return null; diff --git a/javascript/packages/fory/lib/gen/serializer.ts b/javascript/packages/fory/lib/gen/serializer.ts index 912ce095b2..6ca3c39277 100644 --- a/javascript/packages/fory/lib/gen/serializer.ts +++ b/javascript/packages/fory/lib/gen/serializer.ts @@ -199,8 +199,13 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator { readNoRef(assignStmt: (v: string) => string, refState: string): string { return ` - ${this.readTypeInfo()} - ${this.read(assignStmt, refState)}; + fory.incReadDepth(); + try { + ${this.readTypeInfo()} + ${this.read(assignStmt, refState)}; + } finally { + fory.decReadDepth(); + } `; } @@ -211,7 +216,12 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator { switch (${refFlag}) { case ${RefFlags.NotNullValueFlag}: case ${RefFlags.RefValueFlag}: - ${this.read(assignStmt, `${refFlag} === ${RefFlags.RefValueFlag}`)} + fory.incReadDepth(); + try { + ${this.read(assignStmt, `${refFlag} === ${RefFlags.RefValueFlag}`)} + } finally { + fory.decReadDepth(); + } break; case ${RefFlags.RefFlag}: ${assignStmt(this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32()))} diff --git a/javascript/packages/fory/lib/type.ts b/javascript/packages/fory/lib/type.ts index 983c4797ce..52f320869f 100644 --- a/javascript/packages/fory/lib/type.ts +++ b/javascript/packages/fory/lib/type.ts @@ -264,6 +264,7 @@ export interface Config { hps?: Hps; refTracking: boolean | null; useSliceString: boolean; + maxDepth?: number; hooks: { afterCodeGenerated?: (code: string) => string; }; diff --git a/javascript/test/depth-limit.test.ts b/javascript/test/depth-limit.test.ts new file mode 100644 index 0000000000..4c07590e66 --- /dev/null +++ b/javascript/test/depth-limit.test.ts @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fory, { Type } from '../packages/fory/index'; +import { describe, expect, test, beforeEach } from '@jest/globals'; + +describe('depth-limit', () => { + describe('configuration', () => { + test('should have default maxDepth of 50', () => { + const fory = new Fory(); + expect(fory.maxDepth).toBe(50); + }); + + test('should accept custom maxDepth', () => { + const fory = new Fory({ maxDepth: 100 }); + expect(fory.maxDepth).toBe(100); + }); + + test('should initialize depth counter to 0', () => { + const fory = new Fory(); + expect(fory.depth).toBe(0); + }); + + test('should reject maxDepth < 2', () => { + expect(() => new Fory({ maxDepth: 1 })).toThrow( + 'maxDepth must be >= 2 but got 1' + ); + }); + + test('should reject maxDepth = 0', () => { + expect(() => new Fory({ maxDepth: 0 })).toThrow( + 'maxDepth must be >= 2' + ); + }); + + test('should reject negative maxDepth', () => { + expect(() => new Fory({ maxDepth: -5 })).toThrow( + 'maxDepth must be >= 2' + ); + }); + }); + + describe('depth operations', () => { + test('should have incReadDepth method', () => { + const fory = new Fory(); + expect(typeof fory.incReadDepth).toBe('function'); + }); + + test('should have decReadDepth method', () => { + const fory = new Fory(); + expect(typeof fory.decReadDepth).toBe('function'); + }); + + test('incReadDepth should increment depth', () => { + const fory = new Fory({ maxDepth: 100 }); + expect(fory.depth).toBe(0); + fory.incReadDepth(); + expect(fory.depth).toBe(1); + fory.incReadDepth(); + expect(fory.depth).toBe(2); + }); + + test('decReadDepth should decrement depth', () => { + const fory = new Fory({ maxDepth: 100 }); + fory.incReadDepth(); + fory.incReadDepth(); + expect(fory.depth).toBe(2); + fory.decReadDepth(); + expect(fory.depth).toBe(1); + fory.decReadDepth(); + expect(fory.depth).toBe(0); + }); + + test('incReadDepth should throw when depth exceeds limit', () => { + const fory = new Fory({ maxDepth: 2 }); + fory.incReadDepth(); // depth = 1 + fory.incReadDepth(); // depth = 2 + expect(() => fory.incReadDepth()).toThrow( + 'Deserialization depth limit exceeded: 3 > 2' + ); + }); + + test('depth error message should mention limit', () => { + const fory = new Fory({ maxDepth: 5 }); + try { + // Increment to limit, then one more to trigger error + for (let i = 0; i < 6; i++) { + fory.incReadDepth(); + } + throw new Error('Should have thrown depth limit error'); + } catch (e) { + expect(e.message).toContain('Deserialization depth limit exceeded'); + expect(e.message).toContain('5'); // The limit + } + }); + }); + + describe('deserialization with depth tracking', () => { + test('should deserialize simple struct without depth error', () => { + const fory = new Fory({ maxDepth: 50 }); + const typeInfo = Type.struct({ + typeName: 'simple.struct', + }, { + a: Type.int32(), + b: Type.string(), + }); + + const { serialize, deserialize } = fory.registerSerializer(typeInfo); + const data = { a: 42, b: 'hello' }; + const serialized = serialize(data); + const deserialized = deserialize(serialized); + + expect(deserialized).toEqual(data); + expect(fory.depth).toBe(0); // Should be reset after deserialization + }); + + test('should deserialize nested struct within depth limit', () => { + const fory = new Fory({ maxDepth: 10 }); + const nestedType = Type.struct({ + typeName: 'nested.outer', + }, { + value: Type.int32(), + inner: Type.struct({ + typeName: 'nested.inner', + }, { + innerValue: Type.int32(), + }).setNullable(true), + }); + + const { serialize, deserialize } = fory.registerSerializer(nestedType); + const data = { value: 1, inner: { innerValue: 2 } }; + const serialized = serialize(data); + const deserialized = deserialize(serialized); + + expect(deserialized).toEqual(data); + expect(fory.depth).toBe(0); // Should be reset after deserialization + }); + + test('should deserialize array of primitives within depth limit', () => { + const fory = new Fory({ maxDepth: 10 }); + const arrayType = Type.array(Type.int32()); + + const { serialize, deserialize } = fory.registerSerializer(arrayType); + const data = [1, 2, 3, 4, 5]; + const serialized = serialize(data); + const deserialized = deserialize(serialized); + + expect(deserialized).toEqual(data); + expect(fory.depth).toBe(0); // Should be reset after deserialization + }); + + test('should deserialize map within depth limit', () => { + const fory = new Fory({ maxDepth: 10 }); + const mapType = Type.map(Type.string(), Type.int32()); + + const { serialize, deserialize } = fory.registerSerializer(mapType); + const data = new Map([['a', 1], ['b', 2]]); + const serialized = serialize(data); + const deserialized = deserialize(serialized); + + expect(deserialized).toEqual(data); + expect(fory.depth).toBe(0); // Should be reset after deserialization + }); + + test('should reset depth to 0 after successful deserialization', () => { + const fory = new Fory({ maxDepth: 50 }); + const typeInfo = Type.struct({ + typeName: 'test.reset', + }, { + a: Type.int32(), + }); + + const { serialize, deserialize } = fory.registerSerializer(typeInfo); + deserialize(serialize({ a: 1 })); + + expect(fory.depth).toBe(0); + + deserialize(serialize({ a: 2 })); + expect(fory.depth).toBe(0); + }); + }); + + describe('cross-serialization depth limits', () => { + test('should allow serialize with high limit and deserialize with low limit', () => { + const serializeType = Type.struct({ + typeName: 'cross.test', + }, { + value: Type.int32(), + next: Type.struct({ + typeName: 'cross.inner', + }, { + innerValue: Type.int32(), + }).setNullable(true), + }); + + // Serialize with high limit + const forySerialize = new Fory({ maxDepth: 100 }); + const { serialize } = forySerialize.registerSerializer(serializeType); + + const data = { value: 1, next: { innerValue: 2 } }; + const serialized = serialize(data); + + // Deserialize with different instance + const foryDeserialize = new Fory({ maxDepth: 50 }); + const { deserialize } = foryDeserialize.registerSerializer(serializeType); + + const deserialized = deserialize(serialized); + expect(deserialized).toEqual(data); + }); + + test('should have independent depth tracking per Fory instance', () => { + const fory1 = new Fory({ maxDepth: 50 }); + const fory2 = new Fory({ maxDepth: 100 }); + + fory1.incReadDepth(); + fory1.incReadDepth(); + expect(fory1.depth).toBe(2); + + fory2.incReadDepth(); + expect(fory2.depth).toBe(1); + + fory1.decReadDepth(); + expect(fory1.depth).toBe(1); + expect(fory2.depth).toBe(1); + }); + }); + + describe('error scenarios', () => { + test('error message should include helpful suggestion', () => { + const fory = new Fory({ maxDepth: 2 }); + try { + for (let i = 0; i < 3; i++) { + fory.incReadDepth(); + } + throw new Error('Should have thrown'); + } catch (e) { + expect(e.message).toContain('increase maxDepth if needed'); + } + }); + + test('should recover from depth error in subsequent calls', () => { + const fory = new Fory({ maxDepth: 50 }); + expect(fory.depth).toBe(0); + + // Increment and decrement normally + fory.incReadDepth(); + expect(fory.depth).toBe(1); + fory.decReadDepth(); + expect(fory.depth).toBe(0); + + // Should work again + fory.incReadDepth(); + expect(fory.depth).toBe(1); + }); + }); + + describe('edge cases', () => { + test('should handle maxDepth exactly equal to required depth', () => { + const fory = new Fory({ maxDepth: 3 }); + fory.incReadDepth(); // depth = 1 + fory.incReadDepth(); // depth = 2 + fory.incReadDepth(); // depth = 3 + // Should not throw + expect(fory.depth).toBe(3); + }); + + test('should throw when depth exceeds limit by 1', () => { + const fory = new Fory({ maxDepth: 2 }); + fory.incReadDepth(); // depth = 1 + fory.incReadDepth(); // depth = 2 + expect(() => fory.incReadDepth()).toThrow(); + }); + + test('should handle large maxDepth values', () => { + const fory = new Fory({ maxDepth: 10000 }); + expect(fory.maxDepth).toBe(10000); + }); + + test('should handle minimum valid maxDepth of 2', () => { + const fory = new Fory({ maxDepth: 2 }); + expect(fory.maxDepth).toBe(2); + fory.incReadDepth(); + fory.incReadDepth(); + expect(() => fory.incReadDepth()).toThrow(); + }); + }); + + describe('configuration with other options', () => { + test('should work with refTracking enabled', () => { + const fory = new Fory({ + maxDepth: 50, + refTracking: true + }); + expect(fory.maxDepth).toBe(50); + }); + + test('should work with compatible mode enabled', () => { + const fory = new Fory({ + maxDepth: 50, + compatible: true + }); + expect(fory.maxDepth).toBe(50); + }); + + test('should work with useSliceString option', () => { + const fory = new Fory({ + maxDepth: 50, + useSliceString: true + }); + expect(fory.maxDepth).toBe(50); + }); + + test('should work with all options combined', () => { + const fory = new Fory({ + maxDepth: 100, + refTracking: true, + compatible: true, + useSliceString: true + }); + expect(fory.maxDepth).toBe(100); + }); + }); +}); From 37d2d6cf0f0182e024b1b8280d4f43b43e321ef1 Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Fri, 20 Feb 2026 15:53:48 +0500 Subject: [PATCH 2/4] Fix liniting errors --- javascript/packages/fory/lib/fory.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/javascript/packages/fory/lib/fory.ts b/javascript/packages/fory/lib/fory.ts index 44117076c3..b4d677f069 100644 --- a/javascript/packages/fory/lib/fory.ts +++ b/javascript/packages/fory/lib/fory.ts @@ -40,7 +40,7 @@ export default class { anySerializer: Serializer; typeMeta = TypeMeta; config: Config; - depth: number = 0; + depth = 0; maxDepth: number; constructor(config?: Partial) { @@ -77,8 +77,8 @@ export default class { this.depth++; if (this.depth > this.maxDepth) { throw new Error( - `Deserialization depth limit exceeded: ${this.depth} > ${this.maxDepth}. ` + - `The data may be malicious, or increase maxDepth if needed.` + `Deserialization depth limit exceeded: ${this.depth} > ${this.maxDepth}. ` + + "The data may be malicious, or increase maxDepth if needed." ); } } From f88486f197bb90790e026803ac72780e3a33b03d Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Fri, 20 Feb 2026 17:11:22 +0500 Subject: [PATCH 3/4] refactor(javascript): optimize depth limiting by removing try/finally overhead - Remove try/finally blocks from generated read code for performance - Create resetRead() and resetWrite() methods in Fory class - Move all reset logic into these methods (depth, resolvers, etc) - Add resetRead() and resetWrite() methods to all resolver classes - Depth is now reset to 0 at the start of each deserialization - Depth accumulates during deserialization without manual decrement - Keeps zero-cost performance while maintaining security protection - Update tests to match new behavior: depth resets at start of each call --- javascript/packages/fory/lib/fory.ts | 24 ++-- .../packages/fory/lib/gen/serializer.ts | 14 +-- .../packages/fory/lib/metaStringResolver.ts | 11 ++ .../packages/fory/lib/referenceResolver.ts | 8 ++ .../packages/fory/lib/typeMetaResolver.ts | 12 ++ javascript/test/depth-limit.test.ts | 108 +++++++++--------- 6 files changed, 100 insertions(+), 77 deletions(-) diff --git a/javascript/packages/fory/lib/fory.ts b/javascript/packages/fory/lib/fory.ts index b4d677f069..2f4981fbe1 100644 --- a/javascript/packages/fory/lib/fory.ts +++ b/javascript/packages/fory/lib/fory.ts @@ -83,8 +83,18 @@ export default class { } } - decReadDepth(): void { - this.depth--; + private resetRead(): void { + this.referenceResolver.resetRead(); + this.typeMetaResolver.resetRead(); + this.metaStringResolver.resetRead(); + this.depth = 0; + } + + private resetWrite(): void { + this.binaryWriter.reset(); + this.referenceResolver.resetWrite(); + this.metaStringResolver.resetWrite(); + this.typeMetaResolver.resetWrite(); } registerSerializer(constructor: new () => T, customSerializer: CustomSerializer): { @@ -162,11 +172,8 @@ export default class { } deserialize(bytes: Uint8Array, serializer: Serializer = this.anySerializer): T | null { - this.referenceResolver.reset(); + this.resetRead(); this.binaryReader.reset(bytes); - this.typeMetaResolver.reset(); - this.metaStringResolver.reset(); - this.depth = 0; const bitmap = this.binaryReader.readUint8(); if ((bitmap & ConfigFlags.isNullFlag) === ConfigFlags.isNullFlag) { return null; @@ -184,16 +191,13 @@ export default class { private serializeInternal(data: T, serializer: Serializer) { try { - this.binaryWriter.reset(); + this.resetWrite(); } catch (e) { if (e instanceof OwnershipError) { throw new Error("Permission denied. To release the serialization ownership, you must call the dispose function returned by serializeVolatile."); } throw e; } - this.referenceResolver.reset(); - this.metaStringResolver.reset(); - this.typeMetaResolver.reset(); let bitmap = 0; if (data === null) { bitmap |= ConfigFlags.isNullFlag; diff --git a/javascript/packages/fory/lib/gen/serializer.ts b/javascript/packages/fory/lib/gen/serializer.ts index 6ca3c39277..f8ae0b60be 100644 --- a/javascript/packages/fory/lib/gen/serializer.ts +++ b/javascript/packages/fory/lib/gen/serializer.ts @@ -200,12 +200,8 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator { readNoRef(assignStmt: (v: string) => string, refState: string): string { return ` fory.incReadDepth(); - try { - ${this.readTypeInfo()} - ${this.read(assignStmt, refState)}; - } finally { - fory.decReadDepth(); - } + ${this.readTypeInfo()} + ${this.read(assignStmt, refState)}; `; } @@ -217,11 +213,7 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator { case ${RefFlags.NotNullValueFlag}: case ${RefFlags.RefValueFlag}: fory.incReadDepth(); - try { - ${this.read(assignStmt, `${refFlag} === ${RefFlags.RefValueFlag}`)} - } finally { - fory.decReadDepth(); - } + ${this.read(assignStmt, `${refFlag} === ${RefFlags.RefValueFlag}`)} break; case ${RefFlags.RefFlag}: ${assignStmt(this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32()))} diff --git a/javascript/packages/fory/lib/metaStringResolver.ts b/javascript/packages/fory/lib/metaStringResolver.ts index 50f4afc098..ccf0ca6d04 100644 --- a/javascript/packages/fory/lib/metaStringResolver.ts +++ b/javascript/packages/fory/lib/metaStringResolver.ts @@ -108,4 +108,15 @@ export class MetaStringResolver { }); this.dynamicNameId = 0; } + + resetRead() { + // No state to reset for read operation + } + + resetWrite() { + this.disposeMetaStringBytes.forEach((x) => { + x.dynamicWriteStringId = -1; + }); + this.dynamicNameId = 0; + } } diff --git a/javascript/packages/fory/lib/referenceResolver.ts b/javascript/packages/fory/lib/referenceResolver.ts index ec919c966a..0602151521 100644 --- a/javascript/packages/fory/lib/referenceResolver.ts +++ b/javascript/packages/fory/lib/referenceResolver.ts @@ -35,6 +35,14 @@ export class ReferenceResolver { this.writeObjects = new Map(); } + resetRead() { + this.readObjects = []; + } + + resetWrite() { + this.writeObjects = new Map(); + } + getReadObject(refId: number) { return this.readObjects[refId]; } diff --git a/javascript/packages/fory/lib/typeMetaResolver.ts b/javascript/packages/fory/lib/typeMetaResolver.ts index ad13ea22a5..b60531d7a3 100644 --- a/javascript/packages/fory/lib/typeMetaResolver.ts +++ b/javascript/packages/fory/lib/typeMetaResolver.ts @@ -121,4 +121,16 @@ export class TypeMetaResolver { this.dynamicTypeId = 0; this.typeMeta = []; } + + resetRead() { + this.typeMeta = []; + } + + resetWrite() { + this.disposeTypeInfo.forEach((x) => { + x.dynamicTypeId = -1; + }); + this.disposeTypeInfo = []; + this.dynamicTypeId = 0; + } } diff --git a/javascript/test/depth-limit.test.ts b/javascript/test/depth-limit.test.ts index 4c07590e66..a7dc008a7d 100644 --- a/javascript/test/depth-limit.test.ts +++ b/javascript/test/depth-limit.test.ts @@ -18,7 +18,7 @@ */ import Fory, { Type } from '../packages/fory/index'; -import { describe, expect, test, beforeEach } from '@jest/globals'; +import { describe, expect, test } from '@jest/globals'; describe('depth-limit', () => { describe('configuration', () => { @@ -62,11 +62,6 @@ describe('depth-limit', () => { expect(typeof fory.incReadDepth).toBe('function'); }); - test('should have decReadDepth method', () => { - const fory = new Fory(); - expect(typeof fory.decReadDepth).toBe('function'); - }); - test('incReadDepth should increment depth', () => { const fory = new Fory({ maxDepth: 100 }); expect(fory.depth).toBe(0); @@ -76,17 +71,6 @@ describe('depth-limit', () => { expect(fory.depth).toBe(2); }); - test('decReadDepth should decrement depth', () => { - const fory = new Fory({ maxDepth: 100 }); - fory.incReadDepth(); - fory.incReadDepth(); - expect(fory.depth).toBe(2); - fory.decReadDepth(); - expect(fory.depth).toBe(1); - fory.decReadDepth(); - expect(fory.depth).toBe(0); - }); - test('incReadDepth should throw when depth exceeds limit', () => { const fory = new Fory({ maxDepth: 2 }); fory.incReadDepth(); // depth = 1 @@ -96,17 +80,17 @@ describe('depth-limit', () => { ); }); - test('depth error message should mention limit', () => { + test('depth error message should mention limit and hint', () => { const fory = new Fory({ maxDepth: 5 }); try { - // Increment to limit, then one more to trigger error for (let i = 0; i < 6; i++) { fory.incReadDepth(); } throw new Error('Should have thrown depth limit error'); } catch (e) { expect(e.message).toContain('Deserialization depth limit exceeded'); - expect(e.message).toContain('5'); // The limit + expect(e.message).toContain('5'); + expect(e.message).toContain('increase maxDepth if needed'); } }); }); @@ -162,7 +146,7 @@ describe('depth-limit', () => { const deserialized = deserialize(serialized); expect(deserialized).toEqual(data); - expect(fory.depth).toBe(0); // Should be reset after deserialization + // Depth accumulates during deserialization, will be reset on next call }); test('should deserialize map within depth limit', () => { @@ -175,10 +159,10 @@ describe('depth-limit', () => { const deserialized = deserialize(serialized); expect(deserialized).toEqual(data); - expect(fory.depth).toBe(0); // Should be reset after deserialization + // Depth accumulates during deserialization, will be reset on next call }); - test('should reset depth to 0 after successful deserialization', () => { + test('should reset depth at start of each deserialization', () => { const fory = new Fory({ maxDepth: 50 }); const typeInfo = Type.struct({ typeName: 'test.reset', @@ -189,6 +173,7 @@ describe('depth-limit', () => { const { serialize, deserialize } = fory.registerSerializer(typeInfo); deserialize(serialize({ a: 1 })); + // Depth will be reset at the start of resetRead() call expect(fory.depth).toBe(0); deserialize(serialize({ a: 2 })); @@ -235,8 +220,8 @@ describe('depth-limit', () => { fory2.incReadDepth(); expect(fory2.depth).toBe(1); - fory1.decReadDepth(); - expect(fory1.depth).toBe(1); + // Both instances have independent depth counters + expect(fory1.depth).toBe(2); expect(fory2.depth).toBe(1); }); }); @@ -254,37 +239,41 @@ describe('depth-limit', () => { } }); - test('should recover from depth error in subsequent calls', () => { + test('should recover after depth error when deserialization resets depth', () => { + const typeInfo = Type.struct({ + typeName: 'test.recovery', + }, { + a: Type.int32(), + }); + const fory = new Fory({ maxDepth: 50 }); - expect(fory.depth).toBe(0); + const { serialize, deserialize } = fory.registerSerializer(typeInfo); - // Increment and decrement normally - fory.incReadDepth(); - expect(fory.depth).toBe(1); - fory.decReadDepth(); + // First deserialization + let result = deserialize(serialize({ a: 1 })); + expect(result).toEqual({ a: 1 }); expect(fory.depth).toBe(0); - // Should work again - fory.incReadDepth(); - expect(fory.depth).toBe(1); + // Second deserialization should also work (depth reset) + result = deserialize(serialize({ a: 2 })); + expect(result).toEqual({ a: 2 }); + expect(fory.depth).toBe(0); }); }); describe('edge cases', () => { test('should handle maxDepth exactly equal to required depth', () => { - const fory = new Fory({ maxDepth: 3 }); - fory.incReadDepth(); // depth = 1 - fory.incReadDepth(); // depth = 2 - fory.incReadDepth(); // depth = 3 - // Should not throw - expect(fory.depth).toBe(3); - }); + const typeInfo = Type.struct({ + typeName: 'edge.exact', + }, { + a: Type.int32(), + }); - test('should throw when depth exceeds limit by 1', () => { const fory = new Fory({ maxDepth: 2 }); - fory.incReadDepth(); // depth = 1 - fory.incReadDepth(); // depth = 2 - expect(() => fory.incReadDepth()).toThrow(); + const { serialize, deserialize } = fory.registerSerializer(typeInfo); + // Should deserialize without error + const result = deserialize(serialize({ a: 42 })); + expect(result).toEqual({ a: 42 }); }); test('should handle large maxDepth values', () => { @@ -293,45 +282,52 @@ describe('depth-limit', () => { }); test('should handle minimum valid maxDepth of 2', () => { + const typeInfo = Type.struct({ + typeName: 'edge.min', + }, { + a: Type.int32(), + }); + const fory = new Fory({ maxDepth: 2 }); expect(fory.maxDepth).toBe(2); - fory.incReadDepth(); - fory.incReadDepth(); - expect(() => fory.incReadDepth()).toThrow(); + const { serialize, deserialize } = fory.registerSerializer(typeInfo); + // Should deserialize without error + const result = deserialize(serialize({ a: 42 })); + expect(result).toEqual({ a: 42 }); }); }); describe('configuration with other options', () => { test('should work with refTracking enabled', () => { - const fory = new Fory({ + const fory = new Fory({ maxDepth: 50, - refTracking: true + refTracking: true, }); expect(fory.maxDepth).toBe(50); }); test('should work with compatible mode enabled', () => { - const fory = new Fory({ + const fory = new Fory({ maxDepth: 50, - compatible: true + compatible: true, }); expect(fory.maxDepth).toBe(50); }); test('should work with useSliceString option', () => { - const fory = new Fory({ + const fory = new Fory({ maxDepth: 50, - useSliceString: true + useSliceString: true, }); expect(fory.maxDepth).toBe(50); }); test('should work with all options combined', () => { - const fory = new Fory({ + const fory = new Fory({ maxDepth: 100, refTracking: true, compatible: true, - useSliceString: true + useSliceString: true, }); expect(fory.maxDepth).toBe(100); }); From 34e21dd6c63f29fe797c227af9410d89f3bae87c Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Sat, 21 Feb 2026 19:51:00 +0500 Subject: [PATCH 4/4] add decReadDepth in serializer.ts --- javascript/packages/fory/lib/fory.ts | 4 ++++ .../packages/fory/lib/gen/serializer.ts | 16 +++++++++++---- ...depth-limit.test.ts => depthLimit.test.ts} | 20 +++++++++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) rename javascript/test/{depth-limit.test.ts => depthLimit.test.ts} (94%) diff --git a/javascript/packages/fory/lib/fory.ts b/javascript/packages/fory/lib/fory.ts index 2f4981fbe1..56f77277b3 100644 --- a/javascript/packages/fory/lib/fory.ts +++ b/javascript/packages/fory/lib/fory.ts @@ -83,6 +83,10 @@ export default class { } } + decReadDepth(): void { + this.depth--; + } + private resetRead(): void { this.referenceResolver.resetRead(); this.typeMetaResolver.resetRead(); diff --git a/javascript/packages/fory/lib/gen/serializer.ts b/javascript/packages/fory/lib/gen/serializer.ts index f8ae0b60be..154aae4814 100644 --- a/javascript/packages/fory/lib/gen/serializer.ts +++ b/javascript/packages/fory/lib/gen/serializer.ts @@ -198,30 +198,38 @@ export abstract class BaseSerializerGenerator implements SerializerGenerator { } readNoRef(assignStmt: (v: string) => string, refState: string): string { + const result = this.scope.uniqueName("result"); return ` fory.incReadDepth(); ${this.readTypeInfo()} - ${this.read(assignStmt, refState)}; + let ${result}; + ${this.read(v => `${result} = ${v}`, refState)}; + fory.decReadDepth(); + ${assignStmt(result)}; `; } readRefWithoutTypeInfo(assignStmt: (v: string) => string): string { const refFlag = this.scope.uniqueName("refFlag"); + const result = this.scope.uniqueName("result"); return ` const ${refFlag} = ${this.builder.reader.readInt8()}; + let ${result}; switch (${refFlag}) { case ${RefFlags.NotNullValueFlag}: case ${RefFlags.RefValueFlag}: fory.incReadDepth(); - ${this.read(assignStmt, `${refFlag} === ${RefFlags.RefValueFlag}`)} + ${this.read(v => `${result} = ${v}`, `${refFlag} === ${RefFlags.RefValueFlag}`)} + fory.decReadDepth(); break; case ${RefFlags.RefFlag}: - ${assignStmt(this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32()))} + ${result} = ${this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32())}; break; case ${RefFlags.NullFlag}: - ${assignStmt("null")} + ${result} = null; break; } + ${assignStmt(result)}; `; } diff --git a/javascript/test/depth-limit.test.ts b/javascript/test/depthLimit.test.ts similarity index 94% rename from javascript/test/depth-limit.test.ts rename to javascript/test/depthLimit.test.ts index a7dc008a7d..690a0e0b09 100644 --- a/javascript/test/depth-limit.test.ts +++ b/javascript/test/depthLimit.test.ts @@ -62,6 +62,11 @@ describe('depth-limit', () => { expect(typeof fory.incReadDepth).toBe('function'); }); + test('should have decReadDepth method', () => { + const fory = new Fory(); + expect(typeof fory.decReadDepth).toBe('function'); + }); + test('incReadDepth should increment depth', () => { const fory = new Fory({ maxDepth: 100 }); expect(fory.depth).toBe(0); @@ -71,6 +76,17 @@ describe('depth-limit', () => { expect(fory.depth).toBe(2); }); + test('decReadDepth should decrement depth', () => { + const fory = new Fory({ maxDepth: 100 }); + fory.incReadDepth(); + fory.incReadDepth(); + expect(fory.depth).toBe(2); + fory.decReadDepth(); + expect(fory.depth).toBe(1); + fory.decReadDepth(); + expect(fory.depth).toBe(0); + }); + test('incReadDepth should throw when depth exceeds limit', () => { const fory = new Fory({ maxDepth: 2 }); fory.incReadDepth(); // depth = 1 @@ -146,7 +162,7 @@ describe('depth-limit', () => { const deserialized = deserialize(serialized); expect(deserialized).toEqual(data); - // Depth accumulates during deserialization, will be reset on next call + expect(fory.depth).toBe(0); // Should be 0 after deserialization }); test('should deserialize map within depth limit', () => { @@ -159,7 +175,7 @@ describe('depth-limit', () => { const deserialized = deserialize(serialized); expect(deserialized).toEqual(data); - // Depth accumulates during deserialization, will be reset on next call + expect(fory.depth).toBe(0); // Should be 0 after deserialization }); test('should reset depth at start of each deserialization', () => {