blob: a08de6ddc306ed6d865420272cff89accf7d99d5 [file]
/*
* 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";
import { describe, expect, test } from "@jest/globals";
class EmptyExtension {}
class PartialExtension {}
Type.ext(8101)(EmptyExtension);
Type.ext(8105)(PartialExtension);
const emptyCodec = {
write() {},
read() {},
};
function emptyContainerFory(limit: number) {
const fory = new Fory({
compatible: false,
ref: false,
maxUnbackedContainerItems: limit,
});
fory.register(EmptyExtension, emptyCodec);
return fory;
}
function partialContainerFory(limit: number) {
let index = 0;
const fory = new Fory({
compatible: false,
ref: false,
maxUnbackedContainerItems: limit,
});
fory.register(PartialExtension, {
write(writeContext) {
if ((index++ & 1) === 0) {
writeContext.writeUint8(1);
}
},
read(readContext) {
if ((index++ & 1) === 0) {
readContext.readUint8();
}
},
});
return fory;
}
describe("unbacked container budget", () => {
test("validates configuration", () => {
expect(() => new Fory({ maxUnbackedContainerItems: -1 })).toThrow("maxUnbackedContainerItems");
expect(() => new Fory({ maxUnbackedContainerItems: 1.5 })).toThrow("maxUnbackedContainerItems");
expect(new Fory().config.maxUnbackedContainerItems).toBe(8192);
expect(new Fory({ maxUnbackedContainerItems: 0 }).config.maxUnbackedContainerItems).toBe(0);
});
test("bounds custom collection bodies and resets after failure", () => {
const writer = emptyContainerFory(8192);
const writerList = writer.register(Type.list(Type.ext(8101)));
const rejected = Uint8Array.from(
writerList.serialize([new EmptyExtension(), new EmptyExtension(), new EmptyExtension()]),
);
const accepted = Uint8Array.from(
writerList.serialize([new EmptyExtension(), new EmptyExtension()]),
);
const reader = emptyContainerFory(2);
const readerList = reader.register(Type.list(Type.ext(8101)));
expect(() => readerList.deserialize(rejected)).toThrow();
expect(readerList.deserialize(accepted)).toHaveLength(2);
});
test("settles the final partial collection window", () => {
const writer = emptyContainerFory(8192);
const writerList = writer.register(Type.list(Type.ext(8101)));
const bytes = Uint8Array.from([
...writerList.serialize(Array.from({ length: 1025 }, () => new EmptyExtension())),
0x7f,
]);
const reader = emptyContainerFory(1024);
const readerList = reader.register(Type.list(Type.ext(8101)));
expect(() => readerList.deserialize(bytes)).toThrow();
});
test("updates item and byte checkpoints together", () => {
const writer = partialContainerFory(8192);
const writerList = writer.register(Type.list(Type.ext(8105)));
const values = Array.from({ length: 2048 }, () => new PartialExtension());
const encoded = writerList.serialize(values);
const exactReader = partialContainerFory(1024);
const exactList = exactReader.register(Type.list(Type.ext(8105)));
expect(exactList.deserialize(encoded)).toHaveLength(values.length);
const rejected = Uint8Array.from([...encoded, 0x7f]);
const strictReader = partialContainerFory(1023);
const strictList = strictReader.register(Type.list(Type.ext(8105)));
expect(() => strictList.deserialize(rejected)).toThrow();
});
test("settles map entries at protocol chunk boundaries", () => {
const writer = emptyContainerFory(8192);
const writerMap = writer.register(Type.map(Type.ext(8101), Type.ext(8101)));
const values = new Map([
[new EmptyExtension(), new EmptyExtension()],
[new EmptyExtension(), new EmptyExtension()],
[new EmptyExtension(), new EmptyExtension()],
]);
const bytes = writerMap.serialize(values);
const reader = emptyContainerFory(2);
const readerMap = reader.register(Type.map(Type.ext(8101), Type.ext(8101)));
expect(() => readerMap.deserialize(bytes)).toThrow();
});
test("keeps generated positive collection loops unguarded", () => {
let generated = "";
const fory = new Fory({
compatible: true,
ref: false,
maxUnbackedContainerItems: 0,
hooks: {
afterCodeGenerated(code) {
generated += code;
return code;
},
},
});
const serializer = fory.register(
Type.struct(8106, {
values: Type.list(Type.int32({ encoding: "fixed" })),
mapping: Type.map(Type.int32({ encoding: "fixed" }), Type.string()),
}),
);
const value = {
values: [1, 2, 3],
mapping: new Map([[1, "one"]]),
};
const bytes = serializer.serialize(value);
expect(serializer.deserialize(bytes)).toEqual(value);
expect(generated).not.toContain("settleUnbackedContainerItems");
});
test("guards generated empty Struct collections", () => {
const emptyType = Type.struct(8102, {});
class EmptyStruct {}
emptyType(EmptyStruct);
const writer = new Fory({ compatible: true, ref: false });
writer.register(EmptyStruct);
const wrapperType = Type.struct(8107, { values: Type.list(emptyType) });
const writerWrapper = writer.register(wrapperType);
const bytes = writerWrapper.serialize({
values: [new EmptyStruct(), new EmptyStruct(), new EmptyStruct()],
});
const reader = new Fory({
compatible: true,
ref: false,
maxUnbackedContainerItems: 2,
});
reader.register(EmptyStruct);
const readerWrapper = reader.register(wrapperType);
expect(() => readerWrapper.deserialize(bytes)).toThrow();
});
test("charges compatible missing-field collection reads", () => {
const childType = Type.struct(8104, {});
class RemovedChild {}
childType(RemovedChild);
const writer = new Fory({ compatible: true, ref: false });
writer.register(RemovedChild);
const writerRoot = writer.register(
Type.struct(8103, {
removed: Type.list(childType),
}),
);
const rejected = Uint8Array.from([
...writerRoot.serialize({
removed: [new RemovedChild(), new RemovedChild(), new RemovedChild()],
}),
0x7f,
]);
const accepted = Uint8Array.from(
writerRoot.serialize({ removed: [new RemovedChild(), new RemovedChild()] }),
);
const reader = new Fory({
compatible: true,
ref: false,
maxUnbackedContainerItems: 2,
});
reader.register(RemovedChild);
const readerRoot = reader.register(Type.struct(8103, {}));
expect(() => readerRoot.deserialize(rejected)).toThrow();
expect(readerRoot.deserialize(accepted)).toEqual({});
});
});