This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 3100df2a8 feat(javascript): limit the depth of deserialize & serialize 
(#3382)
3100df2a8 is described below

commit 3100df2a8b9ad7778c24c9735ad068b809889bf0
Author: Talha Amjad <[email protected]>
AuthorDate: Wed Mar 11 14:50:35 2026 +0500

    feat(javascript): limit the depth of deserialize & serialize (#3382)
    
    ## What does this PR do?
    
    Adds depth limiting for deserialization to prevent stack overflow and
    denial-of-service attacks from maliciously crafted deeply nested data
    structures.
    
    ## Why is this needed?
    
    Without depth limits, an attacker could send deeply nested serialized
    data that causes stack overflow during deserialization, crashing the
    application or causing resource exhaustion.
    
    ## Implementation
    
    - Added `maxDepth` config option (default: 50, minimum: 2)
    - Depth tracked only during deserialization (security-focused)
    - Integrated into code generator with try/finally for proper cleanup
    - Comprehensive test coverage (29 tests)
    
    ## Usage
    
    ```typescript
    const fory = new Fory({ maxDepth: 100 });
    ```
    
    ## Consistency
    Follows the same pattern as Java and Python implementations for
    cross-language alignment.
    
    Fixes #3335
    
    ---------
    
    Co-authored-by: chaokunyang <[email protected]>
---
 javascript/packages/fory/lib/fory.ts               |  48 ++-
 javascript/packages/fory/lib/gen/collection.ts     |  23 +-
 javascript/packages/fory/lib/gen/ext.ts            |   7 +-
 javascript/packages/fory/lib/gen/map.ts            |  25 +-
 javascript/packages/fory/lib/gen/serializer.ts     |  23 +-
 javascript/packages/fory/lib/gen/struct.ts         |  11 +-
 javascript/packages/fory/lib/metaStringResolver.ts |  11 +
 javascript/packages/fory/lib/referenceResolver.ts  |   8 +
 javascript/packages/fory/lib/type.ts               |   1 +
 javascript/packages/fory/lib/typeMetaResolver.ts   |  12 +
 javascript/test/depthLimit.test.ts                 | 400 +++++++++++++++++++++
 11 files changed, 537 insertions(+), 32 deletions(-)

diff --git a/javascript/packages/fory/lib/fory.ts 
b/javascript/packages/fory/lib/fory.ts
index d26b6b6fa..2d039b169 100644
--- a/javascript/packages/fory/lib/fory.ts
+++ b/javascript/packages/fory/lib/fory.ts
@@ -30,6 +30,9 @@ import { PlatformBuffer } from "./platformBuffer";
 import { TypeMetaResolver } from "./typeMetaResolver";
 import { MetaStringResolver } from "./metaStringResolver";
 
+const DEFAULT_DEPTH_LIMIT = 50 as const;
+const MIN_DEPTH_LIMIT = 2 as const;
+
 export default class {
   binaryReader: BinaryReader;
   binaryWriter: BinaryWriter;
@@ -40,9 +43,16 @@ export default class {
   anySerializer: Serializer;
   typeMeta = TypeMeta;
   config: Config;
+  depth = 0;
+  maxDepth: number;
 
   constructor(config?: Partial<Config>) {
     this.config = this.initConfig(config);
+    const maxDepth = config?.maxDepth ?? DEFAULT_DEPTH_LIMIT;
+    if (!Number.isInteger(maxDepth) || maxDepth < MIN_DEPTH_LIMIT) {
+      throw new Error(`maxDepth must be an integer >= ${MIN_DEPTH_LIMIT} but 
got ${maxDepth}`);
+    }
+    this.maxDepth = maxDepth;
     this.binaryReader = new BinaryReader(this.config);
     this.binaryWriter = new BinaryWriter(this.config);
     this.referenceResolver = new ReferenceResolver(this.binaryReader);
@@ -57,6 +67,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 +77,34 @@ 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--;
+  }
+
+  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<T>(constructor: new () => T, customSerializer: 
CustomSerializer<T>): {
     serializer: Serializer;
     serialize(data: InputType<T> | null): PlatformBuffer;
@@ -141,10 +180,8 @@ export default class {
   }
 
   deserialize<T = any>(bytes: Uint8Array, serializer: Serializer = 
this.anySerializer): T | null {
-    this.referenceResolver.reset();
+    this.resetRead();
     this.binaryReader.reset(bytes);
-    this.typeMetaResolver.reset();
-    this.metaStringResolver.reset();
     const bitmap = this.binaryReader.readUint8();
     if ((bitmap & ConfigFlags.isNullFlag) === ConfigFlags.isNullFlag) {
       return null;
@@ -162,16 +199,13 @@ export default class {
 
   private serializeInternal<T = any>(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/collection.ts 
b/javascript/packages/fory/lib/gen/collection.ts
index 9278b6dcc..396885abb 100644
--- a/javascript/packages/fory/lib/gen/collection.ts
+++ b/javascript/packages/fory/lib/gen/collection.ts
@@ -45,6 +45,13 @@ class CollectionAnySerializer {
 
   }
 
+  private readSerializerWithDepth(serializer: Serializer, fromRef: boolean) {
+    this.fory.incReadDepth();
+    const result = serializer.read(fromRef);
+    this.fory.decReadDepth();
+    return result;
+  }
+
   protected writeElementsHeader(arr: any) {
     let flag = 0;
     let isSame = true;
@@ -161,7 +168,7 @@ class CollectionAnySerializer {
             const refId = this.fory.binaryReader.readVarUInt32();
             accessor(result, i, 
this.fory.referenceResolver.getReadObject(refId));
           } else if (refFlag === RefFlags.RefValueFlag) {
-            accessor(result, i, serializer!.read(true));
+            accessor(result, i, this.readSerializerWithDepth(serializer!, 
true));
           } else {
             accessor(result, i, null);
           }
@@ -172,12 +179,12 @@ class CollectionAnySerializer {
           if (flag === RefFlags.NullFlag) {
             accessor(result, i, null);
           } else {
-            accessor(result, i, serializer!.read(false));
+            accessor(result, i, this.readSerializerWithDepth(serializer!, 
false));
           }
         }
       } else {
         for (let i = 0; i < len; i++) {
-          accessor(result, i, serializer!.read(false));
+          accessor(result, i, this.readSerializerWithDepth(serializer!, 
false));
         }
       }
     } else {
@@ -193,13 +200,13 @@ class CollectionAnySerializer {
             accessor(result, i, null);
           } else {
             const itemSerializer = AnyHelper.detectSerializer(this.fory);
-            accessor(result, i, itemSerializer!.read(false));
+            accessor(result, i, this.readSerializerWithDepth(itemSerializer!, 
false));
           }
         }
       } else {
         for (let i = 0; i < len; i++) {
           const itemSerializer = AnyHelper.detectSerializer(this.fory);
-          accessor(result, i, itemSerializer!.read(false));
+          accessor(result, i, this.readSerializerWithDepth(itemSerializer!, 
false));
         }
       }
     }
@@ -306,7 +313,7 @@ export abstract class CollectionSerializerGenerator extends 
BaseSerializerGenera
                     switch (${refFlag}) {
                         case ${RefFlags.NotNullValueFlag}:
                         case ${RefFlags.RefValueFlag}:
-                            ${this.innerGenerator.readEmbed().read((x: any) => 
`${this.putAccessor(result, x, idx)}`, `${refFlag} === 
${RefFlags.RefValueFlag}`)}
+                            ${this.innerGenerator.readWithDepth((x: any) => 
`${this.putAccessor(result, x, idx)}`, `${refFlag} === 
${RefFlags.RefValueFlag}`)}
                             break;
                         case ${RefFlags.RefFlag}:
                             ${this.putAccessor(result, 
this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32()),
 idx)}
@@ -321,13 +328,13 @@ export abstract class CollectionSerializerGenerator 
extends BaseSerializerGenera
                     if (${this.builder.reader.readInt8()} == 
${RefFlags.NullFlag}) {
                         ${this.putAccessor(result, "null", idx)}
                     } else {
-                        ${this.innerGenerator.readEmbed().read((x: any) => 
`${this.putAccessor(result, x, idx)}`, "false")}
+                        ${this.innerGenerator.readWithDepth((x: any) => 
`${this.putAccessor(result, x, idx)}`, "false")}
                     }
                 }
 
             } else {
                 for (let ${idx} = 0; ${idx} < ${len}; ${idx}++) {
-                    ${this.innerGenerator.readEmbed().read((x: any) => 
`${this.putAccessor(result, x, idx)}`, "false")}
+                    ${this.innerGenerator.readWithDepth((x: any) => 
`${this.putAccessor(result, x, idx)}`, "false")}
                 }
             }
             ${accessor(result)}
diff --git a/javascript/packages/fory/lib/gen/ext.ts 
b/javascript/packages/fory/lib/gen/ext.ts
index c9c18133e..a708f15ec 100644
--- a/javascript/packages/fory/lib/gen/ext.ts
+++ b/javascript/packages/fory/lib/gen/ext.ts
@@ -61,9 +61,14 @@ class ExtSerializerGenerator extends BaseSerializerGenerator 
{
   }
 
   readNoRef(assignStmt: (v: string) => string, refState: string): string {
+    const result = this.scope.uniqueName("result");
     return `
       ${this.readTypeInfo()}
-      ${this.read(assignStmt, refState)};
+      fory.incReadDepth();
+      let ${result};
+      ${this.read(v => `${result} = ${v}`, refState)};
+      fory.decReadDepth();
+      ${assignStmt(result)};
     `;
   }
 
diff --git a/javascript/packages/fory/lib/gen/map.ts 
b/javascript/packages/fory/lib/gen/map.ts
index d94f05606..7f80bbc20 100644
--- a/javascript/packages/fory/lib/gen/map.ts
+++ b/javascript/packages/fory/lib/gen/map.ts
@@ -145,6 +145,13 @@ class MapAnySerializer {
 
   }
 
+  private readSerializerWithDepth(serializer: Serializer, fromRef: boolean) {
+    this.fory.incReadDepth();
+    const result = serializer.read(fromRef);
+    this.fory.decReadDepth();
+    return result;
+  }
+
   private writeFlag(header: number, v: any) {
     if (header & MapFlags.HAS_NULL) {
       return true;
@@ -215,21 +222,21 @@ class MapAnySerializer {
     }
     if (!trackingRef) {
       serializer = serializer == null ? AnyHelper.detectSerializer(this.fory) 
: serializer;
-      return serializer!.read(false);
+      return this.readSerializerWithDepth(serializer!, false);
     }
 
     const flag = this.fory.binaryReader.readInt8();
     switch (flag) {
       case RefFlags.RefValueFlag:
         serializer = serializer == null ? 
AnyHelper.detectSerializer(this.fory) : serializer;
-        return serializer!.read(true);
+        return this.readSerializerWithDepth(serializer!, true);
       case RefFlags.RefFlag:
         return 
this.fory.referenceResolver.getReadObject(this.fory.binaryReader.readVarUInt32());
       case RefFlags.NullFlag:
         return null;
       case RefFlags.NotNullValueFlag:
         serializer = serializer == null ? 
AnyHelper.detectSerializer(this.fory) : serializer;
-        return serializer!.read(false);
+        return this.readSerializerWithDepth(serializer!, false);
     }
   }
 
@@ -427,7 +434,7 @@ export class MapSerializerGenerator extends 
BaseSerializerGenerator {
             const flag = ${this.builder.reader.readInt8()};
             switch (flag) {
               case ${RefFlags.RefValueFlag}:
-                ${this.keyGenerator.read(x => `key = ${x}`, "true")}
+                ${this.keyGenerator.readWithDepth(x => `key = ${x}`, "true")}
                 break;
               case ${RefFlags.RefFlag}:
                 key = 
${this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32())}
@@ -436,11 +443,11 @@ export class MapSerializerGenerator extends 
BaseSerializerGenerator {
                 key = null;
                 break;
               case ${RefFlags.NotNullValueFlag}:
-                ${this.keyGenerator.read(x => `key = ${x}`, "false")}
+                ${this.keyGenerator.readWithDepth(x => `key = ${x}`, "false")}
                 break;
             }
           } else {
-              ${this.keyGenerator.read(x => `key = ${x}`, "false")}
+              ${this.keyGenerator.readWithDepth(x => `key = ${x}`, "false")}
           }
           
           if (valueIncludeNone) {
@@ -449,7 +456,7 @@ export class MapSerializerGenerator extends 
BaseSerializerGenerator {
             const flag = ${this.builder.reader.readInt8()};
             switch (flag) {
               case ${RefFlags.RefValueFlag}:
-                ${this.valueGenerator.read(x => `value = ${x}`, "true")}
+                ${this.valueGenerator.readWithDepth(x => `value = ${x}`, 
"true")}
                 break;
               case ${RefFlags.RefFlag}:
                 value = 
${this.builder.referenceResolver.getReadObject(this.builder.reader.readVarUInt32())}
@@ -458,11 +465,11 @@ export class MapSerializerGenerator extends 
BaseSerializerGenerator {
                 value = null;
                 break;
               case ${RefFlags.NotNullValueFlag}:
-                ${this.valueGenerator.read(x => `value = ${x}`, "false")}
+                ${this.valueGenerator.readWithDepth(x => `value = ${x}`, 
"false")}
                 break;
             }
           } else {
-            ${this.valueGenerator.read(x => `value = ${x}`, "false")}
+            ${this.valueGenerator.readWithDepth(x => `value = ${x}`, "false")}
           }
           
           ${result}.set(
diff --git a/javascript/packages/fory/lib/gen/serializer.ts 
b/javascript/packages/fory/lib/gen/serializer.ts
index 912ce095b..2f4c79ff1 100644
--- a/javascript/packages/fory/lib/gen/serializer.ts
+++ b/javascript/packages/fory/lib/gen/serializer.ts
@@ -47,6 +47,7 @@ export interface SerializerGenerator {
   readRef(assignStmt: (v: string) => string): string;
   readRefWithoutTypeInfo(assignStmt: (v: string) => string): string;
   readNoRef(assignStmt: (v: string) => string, refState: string): string;
+  readWithDepth(assignStmt: (v: string) => string, refState: string): string;
   readTypeInfo(): string;
   read(assignStmt: (v: string) => string, refState: string): string;
   readEmbed(): any;
@@ -186,6 +187,17 @@ export abstract class BaseSerializerGenerator implements 
SerializerGenerator {
 
   abstract read(assignStmt: (v: string) => string, refState: string): string;
 
+  readWithDepth(assignStmt: (v: string) => string, refState: string): string {
+    const result = this.scope.uniqueName("result");
+    return `
+      fory.incReadDepth();
+      let ${result};
+      ${this.read(v => `${result} = ${v}`, refState)};
+      fory.decReadDepth();
+      ${assignStmt(result)};
+    `;
+  }
+
   readTypeInfo(): string {
     const typeId = this.getTypeId();
     const readUserTypeStmt = TypeId.needsUserTypeId(typeId) && typeId !== 
TypeId.COMPATIBLE_STRUCT
@@ -200,26 +212,29 @@ export abstract class BaseSerializerGenerator implements 
SerializerGenerator {
   readNoRef(assignStmt: (v: string) => string, refState: string): string {
     return `
       ${this.readTypeInfo()}
-      ${this.read(assignStmt, refState)};
+      ${this.readWithDepth(assignStmt, refState)}
     `;
   }
 
   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}:
-                ${this.read(assignStmt, `${refFlag} === 
${RefFlags.RefValueFlag}`)}
+                ${this.readWithDepth(v => `${result} = ${v}`, `${refFlag} === 
${RefFlags.RefValueFlag}`)}
                 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/packages/fory/lib/gen/struct.ts 
b/javascript/packages/fory/lib/gen/struct.ts
index 61b4fda82..c0bbb08b0 100644
--- a/javascript/packages/fory/lib/gen/struct.ts
+++ b/javascript/packages/fory/lib/gen/struct.ts
@@ -83,7 +83,7 @@ class StructSerializerGenerator extends 
BaseSerializerGenerator {
           ${embedGenerator.readRefWithoutTypeInfo(assignStmt)}
         `;
       } else {
-        stmt = embedGenerator.read(assignStmt, "false");
+        stmt = embedGenerator.readWithDepth(assignStmt, "false");
       }
     } else {
       if (refMode == RefMode.TRACKING || refMode === RefMode.NULL_ONLY) {
@@ -207,13 +207,18 @@ class StructSerializerGenerator extends 
BaseSerializerGenerator {
   }
 
   readNoRef(assignStmt: (v: string) => string, refState: string): string {
+    const result = this.scope.uniqueName("result");
     return `
       ${this.readTypeInfo()}
+      fory.incReadDepth();
+      let ${result};
       if (${this.metaChangedSerializer} !== null) {
-        ${assignStmt(`${this.metaChangedSerializer}.read(${refState})`)}
+        ${result} = ${this.metaChangedSerializer}.read(${refState});
       } else {
-        ${this.read(assignStmt, refState)};
+        ${this.read(v => `${result} = ${v}`, refState)};
       }
+      fory.decReadDepth();
+      ${assignStmt(result)};
     `;
   }
 
diff --git a/javascript/packages/fory/lib/metaStringResolver.ts 
b/javascript/packages/fory/lib/metaStringResolver.ts
index 50f4afc09..ccf0ca6d0 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 ec919c966..060215152 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/type.ts 
b/javascript/packages/fory/lib/type.ts
index 983c4797c..52f320869 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/packages/fory/lib/typeMetaResolver.ts 
b/javascript/packages/fory/lib/typeMetaResolver.ts
index ad13ea22a..b60531d7a 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/depthLimit.test.ts 
b/javascript/test/depthLimit.test.ts
new file mode 100644
index 000000000..384794cc5
--- /dev/null
+++ b/javascript/test/depthLimit.test.ts
@@ -0,0 +1,400 @@
+/*
+ * 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 } 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 an integer >= 2 but got 1'
+      );
+    });
+
+    test('should reject maxDepth = 0', () => {
+      expect(() => new Fory({ maxDepth: 0 })).toThrow(
+        'maxDepth must be an integer >= 2'
+      );
+    });
+
+    test('should reject negative maxDepth', () => {
+      expect(() => new Fory({ maxDepth: -5 })).toThrow(
+        'maxDepth must be an integer >= 2'
+      );
+    });
+
+    test('should reject NaN maxDepth', () => {
+      expect(() => new Fory({ maxDepth: Number.NaN })).toThrow(
+        'maxDepth must be an integer >= 2'
+      );
+    });
+
+    test('should reject non-integer maxDepth', () => {
+      expect(() => new Fory({ maxDepth: 2.5 })).toThrow(
+        'maxDepth must be an integer >= 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 and hint', () => {
+      const fory = new Fory({ maxDepth: 5 });
+      try {
+        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');
+        expect(e.message).toContain('increase maxDepth if needed');
+      }
+    });
+  });
+
+  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 0 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 0 after deserialization
+    });
+
+    test('should throw when nested arrays exceed maxDepth', () => {
+      const fory = new Fory({ maxDepth: 2 });
+      const nestedArrayType = Type.array(Type.array(Type.array(Type.int32())));
+      const { serialize, deserialize } = 
fory.registerSerializer(nestedArrayType);
+      const serialized = serialize([[[1]]]);
+
+      expect(() => deserialize(serialized)).toThrow(
+        'Deserialization depth limit exceeded'
+      );
+    });
+
+    test('should throw when nested monomorphic struct fields exceed maxDepth', 
() => {
+      const fory = new Fory({ maxDepth: 2 });
+      const leaf = Type.struct({
+        typeName: 'depth.leaf',
+      }, {
+        value: Type.int32(),
+      });
+      const mid = Type.struct({
+        typeName: 'depth.mid',
+      }, {
+        leaf,
+      });
+      const root = Type.struct({
+        typeName: 'depth.root',
+      }, {
+        mid,
+      });
+
+      const { serialize, deserialize } = fory.registerSerializer(root);
+      const serialized = serialize({ mid: { leaf: { value: 7 } } });
+
+      expect(() => deserialize(serialized)).toThrow(
+        'Deserialization depth limit exceeded'
+      );
+    });
+
+    test('should reset depth at start of each 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 }));
+
+      // Depth will be reset at the start of resetRead() call
+      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);
+
+      // Both instances have independent depth counters
+      expect(fory1.depth).toBe(2);
+      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 after depth error when deserialization resets depth', 
() => {
+      const typeInfo = Type.struct({
+        typeName: 'test.recovery',
+      }, {
+        a: Type.int32(),
+      });
+
+      const fory = new Fory({ maxDepth: 50 });
+      const { serialize, deserialize } = fory.registerSerializer(typeInfo);
+
+      // First deserialization
+      let result = deserialize(serialize({ a: 1 }));
+      expect(result).toEqual({ a: 1 });
+      expect(fory.depth).toBe(0);
+
+      // 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 typeInfo = Type.struct({
+        typeName: 'edge.exact',
+      }, {
+        a: Type.int32(),
+      });
+
+      const fory = new Fory({ maxDepth: 2 });
+      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', () => {
+      const fory = new Fory({ maxDepth: 10000 });
+      expect(fory.maxDepth).toBe(10000);
+    });
+
+    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);
+      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({
+        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);
+    });
+  });
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to