blob: bb53938351500a329817a9c875348d6e423dfbe6 [file] [log] [blame]
/*
* 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/core/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);
});
});
});