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]