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 2febfddf3 fix: keep skip reference ids aligned (#3823)
2febfddf3 is described below
commit 2febfddf3c8f03bbcbe6efc3b34ea83f6c78d2ee
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jul 8 10:55:09 2026 +0530
fix: keep skip reference ids aligned (#3823)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
AGENTS.md | 13 --
cpp/fory/serialization/ref_resolver.h | 32 +++-
cpp/fory/serialization/skip.cc | 29 ++--
cpp/fory/serialization/struct_compatible_test.cc | 178 ++++++++++++++++++++
dart/packages/fory/lib/src/context/ref_reader.dart | 2 +
go/fory/ref_resolver.go | 40 ++++-
go/fory/ref_resolver_test.go | 28 +++-
go/fory/skip.go | 84 +++++-----
go/fory/skip_test.go | 60 +++++++
javascript/packages/core/lib/context.ts | 2 +
rust/fory-core/src/resolver/ref_resolver.rs | 3 +
rust/fory-core/src/serializer/collection.rs | 2 +-
rust/fory-core/src/serializer/map.rs | 4 +-
rust/fory-core/src/serializer/skip.rs | 185 ++++++++++++++++-----
rust/tests/tests/test_ref_resolver.rs | 17 ++
rust/tests/tests/test_skip_fields.rs | 157 +++++++++++++++++
16 files changed, 705 insertions(+), 131 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 83e6aa0c1..9039a2369 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -147,19 +147,6 @@ This is the entry point for AI guidance in Apache Fory.
Read this file first, th
- `docs/DEVELOPMENT.md` plus updates under `docs/guide/` and
`docs/benchmarks/` are synced to `apache/fory-site`; other website content
belongs there.
- When benchmark logic, scripts, configuration, or compared serializers
change, rerun the relevant benchmarks and refresh the artifacts under
`docs/benchmarks/**`.
-## Network Error Command Log
-
-- 2026-06-26: `cargo check` from `rust/` failed while updating crates.io
through
- `127.0.0.1:7890`; retried as
- `env -u all_proxy -u http_proxy -u https_proxy -u ALL_PROXY -u HTTP_PROXY -u
HTTPS_PROXY cargo check`,
- which still used the configured proxy. `cargo check --offline` succeeded
using the local Cargo
- cache.
-- 2026-06-26: `cmake -S . -B ../tasks/cpp-cmake-build -DFORY_BUILD_TESTS=ON
--DFORY_BUILD_SHARED=OFF -DFORY_BUILD_STATIC=ON` from `cpp/` failed while
FetchContent tried to
- clone googletest through `127.0.0.1:7890`; retried as
- `env -u all_proxy -u http_proxy -u https_proxy -u ALL_PROXY -u HTTP_PROXY -u
HTTPS_PROXY cmake -S . -B ../tasks/cpp-cmake-build -DFORY_BUILD_TESTS=ON
-DFORY_BUILD_SHARED=OFF -DFORY_BUILD_STATIC=ON`,
- which still used the configured proxy in the nested clone.
-
## Shared Engineering Expectations
- Favor zero-copy techniques, JIT or codegen opportunities, and cache-friendly
memory access patterns in performance-critical paths.
diff --git a/cpp/fory/serialization/ref_resolver.h
b/cpp/fory/serialization/ref_resolver.h
index 01c2b8eec..1a6322ad0 100644
--- a/cpp/fory/serialization/ref_resolver.h
+++ b/cpp/fory/serialization/ref_resolver.h
@@ -102,8 +102,7 @@ public:
RefReader() = default;
template <typename T> uint32_t store_shared_ref(std::shared_ptr<T> ptr) {
- // Store as shared_ptr<void> for type erasure, maintaining reference count
- refs_.emplace_back(std::shared_ptr<void>(ptr, ptr.get()));
+ refs_.push_back(RefSlot{std::shared_ptr<void>(ptr, ptr.get()),
&typeid(T)});
return static_cast<uint32_t>(refs_.size() - 1);
}
@@ -112,8 +111,7 @@ public:
if (ref_id >= refs_.size()) {
refs_.resize(ref_id + 1);
}
- // Store as shared_ptr<void> for type erasure
- refs_[ref_id] = std::shared_ptr<void>(ptr, ptr.get());
+ refs_[ref_id] = RefSlot{std::shared_ptr<void>(ptr, ptr.get()), &typeid(T)};
}
template <typename T>
@@ -123,15 +121,26 @@ public:
std::to_string(ref_id)));
}
- const std::shared_ptr<void> &stored = refs_[ref_id];
+ const RefSlot &slot = refs_[ref_id];
+ const std::shared_ptr<void> &stored = slot.ptr;
if (!stored) {
return Unexpected(Error::invalid_ref("Reference not resolved: " +
std::to_string(ref_id)));
}
- // Alias constructor: create shared_ptr<T> that shares ownership with
stored
- // This works for polymorphic types because the void* points to the actual
- // derived object
+ const std::type_info &requested_type = typeid(T);
+ // The stored void* is only safe to reinterpret as the same C++ static type
+ // that materialized the slot. Reject mismatches before a forged ref id can
+ // turn one shared_ptr<T> owner into an unrelated shared_ptr<U>.
+ if (slot.type == nullptr ||
+ (slot.type != &requested_type && !(*slot.type == requested_type))) {
+ std::string stored_type =
+ slot.type == nullptr ? "<unknown>" : slot.type->name();
+ return Unexpected(Error::invalid_ref(
+ "Reference type mismatch for ID " + std::to_string(ref_id) +
+ ": stored " + stored_type + ", requested " + requested_type.name()));
+ }
+
return std::shared_ptr<T>(stored, static_cast<T *>(stored.get()));
}
@@ -188,7 +197,12 @@ public:
}
private:
- std::vector<std::shared_ptr<void>> refs_;
+ struct RefSlot {
+ std::shared_ptr<void> ptr;
+ const std::type_info *type = nullptr;
+ };
+
+ std::vector<RefSlot> refs_;
std::vector<UpdateCallback> callbacks_;
};
diff --git a/cpp/fory/serialization/skip.cc b/cpp/fory/serialization/skip.cc
index ec75d88e0..1f652261b 100644
--- a/cpp/fory/serialization/skip.cc
+++ b/cpp/fory/serialization/skip.cc
@@ -61,7 +61,15 @@ bool consume_ref_flag(ReadContext &ctx, bool tracking_ref,
bool null_only) {
(void)ctx.read_var_uint32(ctx.error());
return false;
}
- if (ref_flag == NOT_NULL_VALUE_FLAG || ref_flag == REF_VALUE_FLAG) {
+ if (ref_flag == REF_VALUE_FLAG) {
+ // A skipped first occurrence still consumes a producer ref id. Reserve an
+ // empty slot so later Ref ids keep the same numbering as the wire stream.
+ if (tracking_ref) {
+ ctx.ref_reader().reserve_ref_id();
+ }
+ return true;
+ }
+ if (ref_flag == NOT_NULL_VALUE_FLAG) {
return true;
}
ctx.set_error(Error::invalid_data(
@@ -605,22 +613,9 @@ void skip_union(ReadContext &ctx) {
if (FORY_PREDICT_FALSE(ctx.has_error())) {
return;
}
- // Read ref flag for the union value (Any-style)
- int8_t ref_flag = ctx.read_int8(ctx.error());
- if (FORY_PREDICT_FALSE(ctx.has_error())) {
- return;
- }
- if (ref_flag == NULL_FLAG) {
- return;
- }
- if (ref_flag == REF_FLAG) {
- (void)ctx.read_var_uint32(ctx.error());
- return;
- }
- if (ref_flag != NOT_NULL_VALUE_FLAG && ref_flag != REF_VALUE_FLAG) {
- ctx.set_error(
- Error::invalid_data("Unknown reference flag: " +
- std::to_string(static_cast<int>(ref_flag))));
+ // Read ref flag for the union value (Any-style).
+ bool has_value = consume_ref_flag(ctx, true, false);
+ if (FORY_PREDICT_FALSE(ctx.has_error()) || !has_value) {
return;
}
diff --git a/cpp/fory/serialization/struct_compatible_test.cc
b/cpp/fory/serialization/struct_compatible_test.cc
index b062191ca..562e98626 100644
--- a/cpp/fory/serialization/struct_compatible_test.cc
+++ b/cpp/fory/serialization/struct_compatible_test.cc
@@ -277,6 +277,75 @@ struct CompatibleUnsignedExactV2 {
FORY_STRUCT(CompatibleUnsignedExactV2, (id, fory::F(1)), (count,
fory::F(2)));
};
+struct SkippedRefItem {
+ int32_t value = 0;
+ FORY_STRUCT(SkippedRefItem, (value, fory::F(1)));
+};
+
+struct SkippedFieldRefWriter {
+ std::shared_ptr<SkippedRefItem> skipped;
+ std::shared_ptr<SkippedRefItem> kept;
+ std::shared_ptr<SkippedRefItem> alias;
+ FORY_STRUCT(SkippedFieldRefWriter, (skipped, fory::F(1)), (kept, fory::F(2)),
+ (alias, fory::F(3)));
+};
+
+struct SkippedFieldRefReader {
+ std::shared_ptr<SkippedRefItem> kept;
+ std::shared_ptr<SkippedRefItem> alias;
+ FORY_STRUCT(SkippedFieldRefReader, (kept, fory::F(2)), (alias, fory::F(3)));
+};
+
+struct SkippedListRefWriter {
+ std::vector<std::shared_ptr<SkippedRefItem>> skipped;
+ std::shared_ptr<SkippedRefItem> kept;
+ std::shared_ptr<SkippedRefItem> alias;
+ FORY_STRUCT(SkippedListRefWriter, (skipped, fory::F(1)), (kept, fory::F(2)),
+ (alias, fory::F(3)));
+};
+
+struct SkippedListRefReader {
+ std::shared_ptr<SkippedRefItem> kept;
+ std::shared_ptr<SkippedRefItem> alias;
+ FORY_STRUCT(SkippedListRefReader, (kept, fory::F(2)), (alias, fory::F(3)));
+};
+
+struct SkippedMapRefWriter {
+ std::map<int32_t, std::shared_ptr<SkippedRefItem>> skipped;
+ std::shared_ptr<SkippedRefItem> kept;
+ std::shared_ptr<SkippedRefItem> alias;
+ FORY_STRUCT(SkippedMapRefWriter, (skipped, fory::F(1)), (kept, fory::F(2)),
+ (alias, fory::F(3)));
+};
+
+struct SkippedMapRefReader {
+ std::shared_ptr<SkippedRefItem> kept;
+ std::shared_ptr<SkippedRefItem> alias;
+ FORY_STRUCT(SkippedMapRefReader, (kept, fory::F(2)), (alias, fory::F(3)));
+};
+
+struct RefTypeMismatchA {
+ int32_t value = 0;
+ FORY_STRUCT(RefTypeMismatchA, (value, fory::F(1)));
+};
+
+struct RefTypeMismatchB {
+ int32_t value = 0;
+ FORY_STRUCT(RefTypeMismatchB, (value, fory::F(1)));
+};
+
+struct RefTypeMismatchWriter {
+ std::shared_ptr<RefTypeMismatchA> first;
+ std::shared_ptr<RefTypeMismatchA> alias;
+ FORY_STRUCT(RefTypeMismatchWriter, (first, fory::F(1)), (alias, fory::F(2)));
+};
+
+struct RefTypeMismatchReader {
+ std::shared_ptr<RefTypeMismatchA> first;
+ std::shared_ptr<RefTypeMismatchB> alias;
+ FORY_STRUCT(RefTypeMismatchReader, (first, fory::F(1)), (alias, fory::F(2)));
+};
+
struct ScalarBoolField {
bool value = false;
FORY_STRUCT(ScalarBoolField, (value, fory::F(1)));
@@ -731,6 +800,115 @@ TEST(SchemaEvolutionTest,
ChangedSchemaReadsExactUnsignedFields) {
EXPECT_EQ(decoded.value().count, value.count);
}
+TEST(SchemaEvolutionTest, SkippedTrackedFieldKeepsRefIdsAligned) {
+ auto writer = Fory::builder().compatible(true).xlang(true).build();
+ auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+ constexpr uint32_t ITEM_TYPE_ID = 1010;
+ constexpr uint32_t WRAPPER_TYPE_ID = 1011;
+ ASSERT_TRUE(writer.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+ ASSERT_TRUE(reader.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+ ASSERT_TRUE(
+ writer.register_struct<SkippedFieldRefWriter>(WRAPPER_TYPE_ID).ok());
+ ASSERT_TRUE(
+ reader.register_struct<SkippedFieldRefReader>(WRAPPER_TYPE_ID).ok());
+
+ auto kept = std::make_shared<SkippedRefItem>();
+ kept->value = 42;
+ auto skipped = std::make_shared<SkippedRefItem>();
+ skipped->value = 7;
+ auto bytes = writer.serialize(SkippedFieldRefWriter{skipped, kept, kept});
+ ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+ auto decoded = reader.deserialize<SkippedFieldRefReader>(
+ bytes.value().data(), bytes.value().size());
+
+ ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+ ASSERT_NE(decoded.value().kept, nullptr);
+ EXPECT_EQ(decoded.value().kept->value, 42);
+ EXPECT_EQ(decoded.value().kept, decoded.value().alias);
+}
+
+TEST(SchemaEvolutionTest, SkippedListElementsKeepRefIdsAligned) {
+ auto writer = Fory::builder().compatible(true).xlang(true).build();
+ auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+ constexpr uint32_t ITEM_TYPE_ID = 1012;
+ constexpr uint32_t WRAPPER_TYPE_ID = 1013;
+ ASSERT_TRUE(writer.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+ ASSERT_TRUE(reader.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+ ASSERT_TRUE(
+ writer.register_struct<SkippedListRefWriter>(WRAPPER_TYPE_ID).ok());
+ ASSERT_TRUE(
+ reader.register_struct<SkippedListRefReader>(WRAPPER_TYPE_ID).ok());
+
+ auto kept = std::make_shared<SkippedRefItem>();
+ kept->value = 43;
+ auto skipped = std::make_shared<SkippedRefItem>();
+ skipped->value = 8;
+ auto bytes = writer.serialize(SkippedListRefWriter{{skipped}, kept, kept});
+ ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+ auto decoded = reader.deserialize<SkippedListRefReader>(bytes.value().data(),
+
bytes.value().size());
+
+ ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+ ASSERT_NE(decoded.value().kept, nullptr);
+ EXPECT_EQ(decoded.value().kept->value, 43);
+ EXPECT_EQ(decoded.value().kept, decoded.value().alias);
+}
+
+TEST(SchemaEvolutionTest, SkippedMapValuesKeepRefIdsAligned) {
+ auto writer = Fory::builder().compatible(true).xlang(true).build();
+ auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+ constexpr uint32_t ITEM_TYPE_ID = 1014;
+ constexpr uint32_t WRAPPER_TYPE_ID = 1015;
+ ASSERT_TRUE(writer.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+ ASSERT_TRUE(reader.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+ ASSERT_TRUE(
+ writer.register_struct<SkippedMapRefWriter>(WRAPPER_TYPE_ID).ok());
+ ASSERT_TRUE(
+ reader.register_struct<SkippedMapRefReader>(WRAPPER_TYPE_ID).ok());
+
+ auto kept = std::make_shared<SkippedRefItem>();
+ kept->value = 44;
+ auto skipped = std::make_shared<SkippedRefItem>();
+ skipped->value = 9;
+ auto bytes =
+ writer.serialize(SkippedMapRefWriter{{{1, skipped}}, kept, kept});
+ ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+ auto decoded = reader.deserialize<SkippedMapRefReader>(bytes.value().data(),
+ bytes.value().size());
+
+ ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+ ASSERT_NE(decoded.value().kept, nullptr);
+ EXPECT_EQ(decoded.value().kept->value, 44);
+ EXPECT_EQ(decoded.value().kept, decoded.value().alias);
+}
+
+TEST(SchemaEvolutionTest, ReferenceTypeMismatchIsRejected) {
+ auto writer = Fory::builder().compatible(true).xlang(true).build();
+ auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+ ASSERT_TRUE(writer.register_struct<RefTypeMismatchA>(1016).ok());
+ ASSERT_TRUE(reader.register_struct<RefTypeMismatchA>(1016).ok());
+ ASSERT_TRUE(reader.register_struct<RefTypeMismatchB>(1017).ok());
+ ASSERT_TRUE(writer.register_struct<RefTypeMismatchWriter>(1018).ok());
+ ASSERT_TRUE(reader.register_struct<RefTypeMismatchReader>(1018).ok());
+
+ auto shared = std::make_shared<RefTypeMismatchA>();
+ shared->value = 45;
+ auto bytes = writer.serialize(RefTypeMismatchWriter{shared, shared});
+ ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+
+ auto decoded = reader.deserialize<RefTypeMismatchReader>(
+ bytes.value().data(), bytes.value().size());
+
+ ASSERT_FALSE(decoded.ok());
+ EXPECT_EQ(decoded.error().code(), ErrorCode::InvalidRef);
+ EXPECT_NE(decoded.error().message().find("Reference type mismatch"),
+ std::string::npos);
+}
+
TEST(SchemaEvolutionTest, PimplPropertyNestedNamedStruct) {
auto writer = Fory::builder().compatible(true).xlang(true).build();
auto reader = Fory::builder().compatible(true).xlang(true).build();
diff --git a/dart/packages/fory/lib/src/context/ref_reader.dart
b/dart/packages/fory/lib/src/context/ref_reader.dart
index 5b524bc2e..dafb8b695 100644
--- a/dart/packages/fory/lib/src/context/ref_reader.dart
+++ b/dart/packages/fory/lib/src/context/ref_reader.dart
@@ -21,6 +21,8 @@ import 'package:fory/src/memory/buffer.dart';
import 'package:fory/src/context/ref_writer.dart';
final class RefReader {
+ // Missing compatible structs may surface as null field values, but they are
+ // not published as reference targets; keep ref reads as direct lookups.
final List<Object?> _refs = <Object?>[];
final List<int> _preservedIds = <int>[];
Object? _resolved;
diff --git a/go/fory/ref_resolver.go b/go/fory/ref_resolver.go
index 155c16791..1eb0d138b 100644
--- a/go/fory/ref_resolver.go
+++ b/go/fory/ref_resolver.go
@@ -202,8 +202,19 @@ func (r *RefResolver) ReadRefOrNull(buffer *ByteBuffer,
ctxErr *Error) int8 {
}
if refTag == RefFlag {
// read ref id and get object from ref resolver
- refId := buffer.ReadVarUint32(ctxErr)
- r.readObject = r.GetReadObject(int32(refId))
+ refId := int32(buffer.ReadVarUint32(ctxErr))
+ if refId < 0 || int(refId) >= len(r.readObjects) {
+ ctxErr.SetError(InvalidRefIdError(refId))
+ r.readObject = reflect.Value{}
+ return RefFlag
+ }
+ object := r.readObjects[refId]
+ if !object.IsValid() {
+ ctxErr.SetError(InvalidRefIdError(refId))
+ r.readObject = reflect.Value{}
+ return RefFlag
+ }
+ r.readObject = object
return RefFlag
} else {
r.readObject = reflect.Value{}
@@ -228,6 +239,18 @@ func (r *RefResolver) PreserveRefId() (int32, error) {
return nextReadRefId, nil
}
+func (r *RefResolver) ReserveSkippedRefId() error {
+ if !r.refTracking {
+ return nil
+ }
+ nextReadRefId := len(r.readObjects)
+ if nextReadRefId > MaxInt32 {
+ return fmt.Errorf("referencable objects exceeds max int32")
+ }
+ r.readObjects = append(r.readObjects, reflect.Value{})
+ return nil
+}
+
func (r *RefResolver) TryPreserveRefId(buffer *ByteBuffer) (int32, error) {
var ctxErr Error
headFlag := buffer.ReadInt8(&ctxErr)
@@ -236,11 +259,20 @@ func (r *RefResolver) TryPreserveRefId(buffer
*ByteBuffer) (int32, error) {
}
if headFlag == RefFlag {
// read ref id and get object from ref resolver
- refId := buffer.ReadVarUint32(&ctxErr)
+ refId := int32(buffer.ReadVarUint32(&ctxErr))
if ctxErr.HasError() {
return 0, ctxErr
}
- r.readObject = r.GetReadObject(int32(refId))
+ if refId < 0 || int(refId) >= len(r.readObjects) {
+ r.readObject = reflect.Value{}
+ return 0, InvalidRefIdError(refId)
+ }
+ object := r.readObjects[refId]
+ if !object.IsValid() {
+ r.readObject = reflect.Value{}
+ return 0, InvalidRefIdError(refId)
+ }
+ r.readObject = object
} else {
r.readObject = reflect.Value{}
if headFlag == RefValueFlag {
diff --git a/go/fory/ref_resolver_test.go b/go/fory/ref_resolver_test.go
index 2e5157c2c..3b475c30f 100644
--- a/go/fory/ref_resolver_test.go
+++ b/go/fory/ref_resolver_test.go
@@ -44,9 +44,11 @@ func TestReferenceResolver(t *testing.T) {
}
refResolver.readObjects = make([]reflect.Value,
len(refResolver.writtenObjects))
ctxErr := &Error{}
- for range values {
+ for i, data := range values {
require.Equal(t, refResolver.ReadRefOrNull(buf, ctxErr),
RefValueFlag)
+ refResolver.SetReadObject(int32(i), reflect.ValueOf(data))
require.Equal(t, refResolver.ReadRefOrNull(buf, ctxErr),
RefFlag)
+ require.False(t, ctxErr.HasError())
}
{
s := []int{1, 2, 3}
@@ -200,6 +202,30 @@ func TestRefResolverOOBPanic(t *testing.T) {
require.NotPanics(t, func() {
resolver.ReadRefOrNull(buffer, &ctxErr)
}, "RefResolver.GetReadObject should not panic on OOB index")
+ require.True(t, ctxErr.HasError())
+}
+
+func TestRefResolverRejectsSkippedSlotRef(t *testing.T) {
+ resolver := newRefResolver(true)
+ require.NoError(t, resolver.ReserveSkippedRefId())
+ keptID, err := resolver.PreserveRefId()
+ require.NoError(t, err)
+ kept := reflect.ValueOf("kept")
+ resolver.SetReadObject(keptID, kept)
+
+ skippedRef := NewByteBuffer(nil)
+ skippedRef.WriteInt8(RefFlag)
+ skippedRef.WriteVarUint32(0)
+ _, err = resolver.TryPreserveRefId(skippedRef)
+ require.Error(t, err)
+
+ keptRef := NewByteBuffer(nil)
+ keptRef.WriteInt8(RefFlag)
+ keptRef.WriteVarUint32(uint32(keptID))
+ refID, err := resolver.TryPreserveRefId(keptRef)
+ require.NoError(t, err)
+ require.Equal(t, int32(RefFlag), refID)
+ require.Equal(t, kept.String(),
resolver.GetCurrentReadObject().String())
}
// TestRefResolverBoundaryRegression verifies that valid boundary indices
diff --git a/go/fory/skip.go b/go/fory/skip.go
index 82b03a505..23508a3cc 100644
--- a/go/fory/skip.go
+++ b/go/fory/skip.go
@@ -30,6 +30,37 @@ func skipSizedBytes(ctx *ReadContext, size uint64) {
ctx.buffer.Skip(int(size), ctx.Err())
}
+func consumeSkippedRefFlag(ctx *ReadContext, readRefFlag bool) bool {
+ if !readRefFlag {
+ return true
+ }
+ err := ctx.Err()
+ refFlag := ctx.buffer.ReadInt8(err)
+ if ctx.HasError() {
+ return false
+ }
+ switch refFlag {
+ case NullFlag:
+ return false
+ case RefFlag:
+ _ = ctx.buffer.ReadVarUint32(err)
+ return false
+ case RefValueFlag:
+ // A skipped first occurrence still consumes a producer ref id.
Keep
+ // numbering aligned without adding a pending ref for the next
materialized value.
+ if reserveErr := ctx.RefResolver().ReserveSkippedRefId();
reserveErr != nil {
+ ctx.SetError(FromError(reserveErr))
+ return false
+ }
+ return true
+ case NotNullValueFlag:
+ return true
+ default:
+ ctx.SetError(DeserializationErrorf("invalid ref flag %d",
refFlag))
+ return false
+ }
+}
+
// SkipFieldValue skips a field value in compatible mode when the field
doesn't exist
// or is incompatible with the local type.
// Uses context error state for deferred error checking.
@@ -45,17 +76,8 @@ func SkipFieldValueWithTypeFlag(ctx *ReadContext, fieldDef
FieldDef, readRefFlag
if readTypeInfo {
// Type info was written for this field (struct fields in
compatible mode)
// Read ref flag first if needed
- if readRefFlag {
- refFlag := ctx.buffer.ReadInt8(err)
- if refFlag == NullFlag {
- return
- }
- if refFlag == RefFlag {
- // Reference to already-seen object, skip the
reference index
- _ = ctx.buffer.ReadVarUint32(err)
- return
- }
- // RefValueFlag (0) or NotNullValueFlag (-1) means we
need to read the actual object
+ if !consumeSkippedRefFlag(ctx, readRefFlag) {
+ return
}
// Read type info (typeID + meta_index)
@@ -135,20 +157,8 @@ func isStructTypeId(id TypeId) bool {
func SkipAnyValue(ctx *ReadContext, readRefFlag bool) {
err := ctx.Err()
// Handle ref flag first if needed
- if readRefFlag {
- refFlag := ctx.buffer.ReadInt8(err)
- if ctx.HasError() {
- return
- }
- if refFlag == NullFlag {
- return
- }
- if refFlag == RefFlag {
- // Reference to already-seen object, skip the reference
index
- _ = ctx.buffer.ReadVarUint32(err)
- return
- }
- // RefValueFlag (0) or NotNullValueFlag (-1) means we need to
read the actual object
+ if !consumeSkippedRefFlag(ctx, readRefFlag) {
+ return
}
// ReadData type_id first
@@ -265,6 +275,7 @@ func skipCollection(ctx *ReadContext, fieldDef FieldDef) {
elemDef = FieldDef{
typeSpec:
NewSimpleTypeSpec(TypeId(elemTypeInfo.TypeID)),
nullable: hasNull,
+ trackRef: trackRef,
}
} else if isDeclared {
// Use declared element type from the collection's field type
@@ -272,19 +283,22 @@ func skipCollection(ctx *ReadContext, fieldDef FieldDef) {
elemDef = FieldDef{
typeSpec: fieldDef.typeSpec.elementType,
nullable: hasNull,
+ trackRef: trackRef,
}
} else {
// Fallback: use unknown type
elemDef = FieldDef{
typeSpec: NewSimpleTypeSpec(UNKNOWN),
nullable: true,
+ trackRef: trackRef,
}
}
} else {
// Not same type - each element has its own type info, use
unknown
elemDef = FieldDef{
typeSpec: NewSimpleTypeSpec(UNKNOWN),
- nullable: true,
+ nullable: hasNull,
+ trackRef: trackRef,
}
}
@@ -297,7 +311,7 @@ func skipCollection(ctx *ReadContext, fieldDef FieldDef) {
for i := uint32(0); i < length; i++ {
// Read ref flag if collection has ref tracking enabled
- skipValue(ctx, elemDef, trackRef, false, elemTypeInfo)
+ skipValue(ctx, elemDef, trackRef || hasNull, false,
elemTypeInfo)
if ctx.HasError() {
return
}
@@ -547,20 +561,8 @@ func skipStruct(ctx *ReadContext, info *TypeInfo) {
// Uses context error state for deferred error checking.
func skipValue(ctx *ReadContext, fieldDef FieldDef, readRefFlag bool, isField
bool, typeInfo *TypeInfo) {
err := ctx.Err()
- if readRefFlag {
- refFlag := ctx.buffer.ReadInt8(err)
- if ctx.HasError() {
- return
- }
- if refFlag == NullFlag {
- return
- }
- if refFlag == RefFlag {
- // Reference to already-seen object, skip the reference
index
- _ = ctx.buffer.ReadVarUint32(err)
- return
- }
- // RefValueFlag (0) or NotNullValueFlag (-1) means we need to
read the actual object
+ if !consumeSkippedRefFlag(ctx, readRefFlag) {
+ return
}
typeIDNum := uint32(fieldDef.typeSpec.TypeId())
diff --git a/go/fory/skip_test.go b/go/fory/skip_test.go
index 1552a6ceb..3bae47359 100644
--- a/go/fory/skip_test.go
+++ b/go/fory/skip_test.go
@@ -130,3 +130,63 @@ func TestSkipMapRejectsInvalidChunkSize(t *testing.T) {
)
require.Error(t, f.readCtx.CheckError())
}
+
+func TestSkipTrackedValueReservesRefId(t *testing.T) {
+ f := New(WithXlang(true), WithCompatible(true), WithTrackRef(true))
+ buf := NewByteBuffer(nil)
+ buf.WriteInt8(RefValueFlag)
+ buf.WriteLength(0)
+
+ f.readCtx.SetData(buf.Bytes())
+ skipValue(
+ f.readCtx,
+ FieldDef{
+ typeSpec: NewCollectionTypeSpec(LIST,
NewSimpleTypeSpec(STRING)),
+ trackRef: true,
+ },
+ true,
+ false,
+ nil,
+ )
+ require.NoError(t, f.readCtx.CheckError())
+
+ resolver := f.readCtx.RefResolver()
+ require.Len(t, resolver.readObjects, 1)
+ require.Empty(t, resolver.readRefIds)
+
+ nextRefId, err := resolver.PreserveRefId()
+ require.NoError(t, err)
+ require.Equal(t, int32(1), nextRefId)
+}
+
+func TestSkipCollectionConsumesNullElementFlag(t *testing.T) {
+ tests := []struct {
+ name string
+ header byte
+ }{
+ {name: "same_type_declared", header: CollectionDeclSameType |
CollectionHasNull},
+ {name: "element_type_per_value", header: CollectionHasNull},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ f := New(WithXlang(true), WithCompatible(false))
+ buf := NewByteBuffer(nil)
+ buf.WriteLength(1)
+ buf.WriteByte(tc.header)
+ buf.WriteInt8(NullFlag)
+ buf.WriteByte(0x7f)
+
+ f.readCtx.SetData(buf.Bytes())
+ skipCollection(
+ f.readCtx,
+ FieldDef{
+ typeSpec: NewCollectionTypeSpec(LIST,
NewSimpleTypeSpec(INT32)),
+ nullable: true,
+ },
+ )
+ require.NoError(t, f.readCtx.CheckError())
+ require.Equal(t, byte(0x7f),
f.readCtx.Buffer().ReadByte(f.readCtx.Err()))
+ })
+ }
+}
diff --git a/javascript/packages/core/lib/context.ts
b/javascript/packages/core/lib/context.ts
index a4cebfb35..f2613c7bc 100644
--- a/javascript/packages/core/lib/context.ts
+++ b/javascript/packages/core/lib/context.ts
@@ -241,6 +241,8 @@ export class RefReader {
}
getReadRef(refId: number) {
+ // Missing compatible structs may surface as null field values, but they
are
+ // not published as reference targets; keep this hot path as a direct
lookup.
return this.readObjects[refId];
}
diff --git a/rust/fory-core/src/resolver/ref_resolver.rs
b/rust/fory-core/src/resolver/ref_resolver.rs
index 14ca87c58..963354da2 100644
--- a/rust/fory-core/src/resolver/ref_resolver.rs
+++ b/rust/fory-core/src/resolver/ref_resolver.rs
@@ -272,6 +272,9 @@ impl RefReader {
#[inline(always)]
pub fn reserve_ref_id(&mut self) -> u32 {
let ref_id = self.refs.len() as u32;
+ // Skipped values reserve their producer ref id but intentionally stay
+ // unresolved. A later Ref to this slot must fail downcast instead of
+ // aliasing the next materialized object.
self.refs.push(Box::new(()));
ref_id
}
diff --git a/rust/fory-core/src/serializer/collection.rs
b/rust/fory-core/src/serializer/collection.rs
index eb3fc2ad8..2ca2342b5 100644
--- a/rust/fory-core/src/serializer/collection.rs
+++ b/rust/fory-core/src/serializer/collection.rs
@@ -26,7 +26,7 @@ use crate::serializer::{ForyDefault, Serializer};
use crate::type_id::{self, need_to_write_type_for_field, TypeId,
PRIMITIVE_ARRAY_TYPES};
use crate::types::{bfloat16::bfloat16, float16::float16};
-const TRACKING_REF: u8 = 0b1;
+pub const TRACKING_REF: u8 = 0b1;
pub const HAS_NULL: u8 = 0b10;
diff --git a/rust/fory-core/src/serializer/map.rs
b/rust/fory-core/src/serializer/map.rs
index 8275cccbc..eb1b9089b 100644
--- a/rust/fory-core/src/serializer/map.rs
+++ b/rust/fory-core/src/serializer/map.rs
@@ -28,10 +28,10 @@ use std::rc::Rc;
const MAX_CHUNK_SIZE: u8 = 255;
-const TRACKING_KEY_REF: u8 = 0b1;
+pub const TRACKING_KEY_REF: u8 = 0b1;
pub const KEY_NULL: u8 = 0b10;
pub const DECL_KEY_TYPE: u8 = 0b100;
-const TRACKING_VALUE_REF: u8 = 0b1000;
+pub const TRACKING_VALUE_REF: u8 = 0b1000;
pub const VALUE_NULL: u8 = 0b10000;
pub const DECL_VALUE_TYPE: u8 = 0b100000;
diff --git a/rust/fory-core/src/serializer/skip.rs
b/rust/fory-core/src/serializer/skip.rs
index f4df4bf11..3a267429b 100644
--- a/rust/fory-core/src/serializer/skip.rs
+++ b/rust/fory-core/src/serializer/skip.rs
@@ -19,7 +19,7 @@ use crate::context::ReadContext;
use crate::ensure;
use crate::error::Error;
use crate::meta::FieldType;
-use crate::serializer::collection::{DECL_ELEMENT_TYPE, HAS_NULL, IS_SAME_TYPE};
+use crate::serializer::collection::{DECL_ELEMENT_TYPE, HAS_NULL, IS_SAME_TYPE,
TRACKING_REF};
use crate::serializer::util;
use crate::type_id as types;
use crate::util::ENABLE_FORY_DEBUG_OUTPUT;
@@ -34,7 +34,14 @@ pub fn skip_field_value(
field_type: &FieldType,
read_ref_flag: bool,
) -> Result<(), Error> {
- skip_value(context, field_type, read_ref_flag, true, &None)
+ skip_value(
+ context,
+ field_type,
+ field_type.track_ref,
+ read_ref_flag && !field_type.track_ref,
+ true,
+ &None,
+ )
}
#[inline(always)]
@@ -42,6 +49,21 @@ fn unknown_field_type() -> FieldType {
FieldType::new(types::UNKNOWN, true, Vec::new())
}
+#[inline(always)]
+fn has_unknown_element_type(field_type: &FieldType) -> bool {
+ field_type
+ .generics
+ .first()
+ .is_some_and(|elem_type| elem_type.type_id == types::UNKNOWN)
+}
+
+#[inline(always)]
+fn next_byte_is_null_only_ref_flag(context: &mut ReadContext) -> Result<bool,
Error> {
+ let value = context.reader.read_i8()?;
+ context.reader.move_back(1);
+ Ok(value == RefFlag::Null as i8 || value == RefFlag::NotNullValue as i8)
+}
+
#[inline(always)]
fn skip_bytes(context: &mut ReadContext, len: usize) -> Result<(), Error> {
context.reader.check_bound(len)?;
@@ -49,6 +71,39 @@ fn skip_bytes(context: &mut ReadContext, len: usize) ->
Result<(), Error> {
Ok(())
}
+#[inline(always)]
+fn consume_ref_flag(
+ context: &mut ReadContext,
+ tracking_ref: bool,
+ null_only: bool,
+) -> Result<bool, Error> {
+ if !tracking_ref && !null_only {
+ return Ok(true);
+ }
+ let ref_flag = context.reader.read_i8()?;
+ if ref_flag == RefFlag::Null as i8 {
+ return Ok(false);
+ }
+ if ref_flag == RefFlag::Ref as i8 {
+ let _ref_index = context.reader.read_var_u32()?;
+ return Ok(false);
+ }
+ if ref_flag == RefFlag::RefValue as i8 {
+ // A skipped first occurrence still consumes a producer ref id.
Reserve an
+ // empty slot so later Ref ids keep the same numbering as the wire
stream.
+ if tracking_ref {
+ context.ref_reader.reserve_ref_id();
+ }
+ return Ok(true);
+ }
+ if ref_flag == RefFlag::NotNullValue as i8 {
+ return Ok(true);
+ }
+ Err(Error::invalid_ref(format!(
+ "Invalid reference flag: {ref_flag}"
+ )))
+}
+
#[cold]
#[inline(never)]
fn skip_sized_bytes(context: &mut ReadContext, element_size: Option<usize>) ->
Result<(), Error> {
@@ -117,18 +172,8 @@ fn skip_decimal(context: &mut ReadContext) -> Result<(),
Error> {
#[cold]
#[inline(never)]
pub fn skip_any_value(context: &mut ReadContext, read_ref_flag: bool) ->
Result<(), Error> {
- // Handle ref flag first if needed
- if read_ref_flag {
- let ref_flag = context.reader.read_i8()?;
- if ref_flag == (RefFlag::Null as i8) {
- return Ok(());
- }
- if ref_flag == (RefFlag::Ref as i8) {
- // Reference to already-seen object, skip the reference index
- let _ref_index = context.reader.read_var_u32()?;
- return Ok(());
- }
- // RefValue (0) or NotNullValue (-1) means we need to read the actual
object
+ if !consume_ref_flag(context, read_ref_flag, false)? {
+ return Ok(());
}
// Read type_id first
@@ -209,7 +254,7 @@ pub fn skip_any_value(context: &mut ReadContext,
read_ref_flag: bool) -> Result<
};
// Don't read ref flag again in skip_value since we already handled it.
// Pass type_info so skip_struct doesn't try to read type_id/meta_index
again.
- skip_value(context, &field_type, false, false, &type_info_opt)
+ skip_value(context, &field_type, false, false, false, &type_info_opt)
}
#[cold]
@@ -220,16 +265,31 @@ fn skip_collection(context: &mut ReadContext, field_type:
&FieldType) -> Result<
return Ok(());
}
let header = context.reader.read_u8()?;
+ let track_ref = (header & TRACKING_REF) != 0;
let has_null = (header & HAS_NULL) != 0;
let is_same_type = (header & IS_SAME_TYPE) != 0;
- let skip_ref_flag = is_same_type && !has_null;
let is_declared = (header & DECL_ELEMENT_TYPE) != 0;
if field_type.generics.is_empty() {
return Err(Error::invalid_data("empty generics"));
}
let default_elem_type = field_type.generics.first().unwrap();
+ if !is_same_type {
+ // Tuple fields use LIST<UNKNOWN> metadata and a collection-shaped
prefix, but their
+ // compatible elements are still written as NullOnly values with
inline type info.
+ let tuple_elements = header == 0
+ && has_unknown_element_type(field_type)
+ && next_byte_is_null_only_ref_flag(context)?;
+ let read_ref_flag = tuple_elements || track_ref || has_null;
+ context.inc_depth()?;
+ for _ in 0..length {
+ skip_any_value(context, read_ref_flag)?;
+ }
+ context.dec_depth();
+ return Ok(());
+ }
+
let (type_info, elem_field_type);
- let elem_type = if is_same_type && !is_declared {
+ let elem_type = if !is_declared {
let type_info_rc = context.read_any_type_info()?;
elem_field_type = FieldType::new_with_user_type_id(
type_info_rc.get_type_id() as u32,
@@ -245,8 +305,9 @@ fn skip_collection(context: &mut ReadContext, field_type:
&FieldType) -> Result<
default_elem_type
};
context.inc_depth()?;
+ let null_only = has_null && !track_ref;
for _ in 0..length {
- skip_value(context, elem_type, !skip_ref_flag, false, &type_info)?;
+ skip_value(context, elem_type, track_ref, null_only, false,
&type_info)?;
}
context.dec_depth();
Ok(())
@@ -279,6 +340,7 @@ fn skip_map(context: &mut ReadContext, field_type:
&FieldType) -> Result<(), Err
if header & crate::serializer::map::KEY_NULL != 0 {
// Read value type info if not declared
let value_declared = (header &
crate::serializer::map::DECL_VALUE_TYPE) != 0;
+ let value_track_ref = (header &
crate::serializer::map::TRACKING_VALUE_REF) != 0;
let (value_type_info, value_field_type);
let value_type = if !value_declared {
let type_info = context.read_any_type_info()?;
@@ -296,7 +358,14 @@ fn skip_map(context: &mut ReadContext, field_type:
&FieldType) -> Result<(), Err
default_value_type
};
context.inc_depth()?;
- skip_value(context, value_type, false, false, &value_type_info)?;
+ skip_value(
+ context,
+ value_type,
+ value_track_ref,
+ false,
+ false,
+ &value_type_info,
+ )?;
context.dec_depth();
len_counter += 1;
continue;
@@ -304,6 +373,7 @@ fn skip_map(context: &mut ReadContext, field_type:
&FieldType) -> Result<(), Err
if header & crate::serializer::map::VALUE_NULL != 0 {
// Read key type info if not declared
let key_declared = (header &
crate::serializer::map::DECL_KEY_TYPE) != 0;
+ let key_track_ref = (header &
crate::serializer::map::TRACKING_KEY_REF) != 0;
let (key_type_info, key_field_type);
let key_type = if !key_declared {
let type_info = context.read_any_type_info()?;
@@ -321,7 +391,14 @@ fn skip_map(context: &mut ReadContext, field_type:
&FieldType) -> Result<(), Err
default_key_type
};
context.inc_depth()?;
- skip_value(context, key_type, false, false, &key_type_info)?;
+ skip_value(
+ context,
+ key_type,
+ key_track_ref,
+ false,
+ false,
+ &key_type_info,
+ )?;
context.dec_depth();
len_counter += 1;
continue;
@@ -330,6 +407,8 @@ fn skip_map(context: &mut ReadContext, field_type:
&FieldType) -> Result<(), Err
let chunk_size = context.reader.read_u8()?;
let key_declared = (header & crate::serializer::map::DECL_KEY_TYPE) !=
0;
let value_declared = (header &
crate::serializer::map::DECL_VALUE_TYPE) != 0;
+ let key_track_ref = (header &
crate::serializer::map::TRACKING_KEY_REF) != 0;
+ let value_track_ref = (header &
crate::serializer::map::TRACKING_VALUE_REF) != 0;
// Read key type info if not declared
let (key_type_info, key_field_type);
@@ -369,8 +448,22 @@ fn skip_map(context: &mut ReadContext, field_type:
&FieldType) -> Result<(), Err
context.inc_depth()?;
for _ in 0..chunk_size {
- skip_value(context, key_type, false, false, &key_type_info)?;
- skip_value(context, value_type, false, false, &value_type_info)?;
+ skip_value(
+ context,
+ key_type,
+ key_track_ref,
+ false,
+ false,
+ &key_type_info,
+ )?;
+ skip_value(
+ context,
+ value_type,
+ value_track_ref,
+ false,
+ false,
+ &value_type_info,
+ )?;
}
context.dec_depth();
len_counter += chunk_size as u32;
@@ -417,11 +510,20 @@ fn skip_struct(
field_info.field_name, field_info.field_type.type_id,
field_info.field_type.type_id
);
}
- let read_ref_flag = util::field_need_write_ref_into(
- field_info.field_type.type_id,
- field_info.field_type.nullable,
- );
- skip_value(context, &field_info.field_type, read_ref_flag, true,
&None)?;
+ let tracking_ref = field_info.field_type.track_ref;
+ let null_only = !tracking_ref
+ && util::field_need_write_ref_into(
+ field_info.field_type.type_id,
+ field_info.field_type.nullable,
+ );
+ skip_value(
+ context,
+ &field_info.field_type,
+ tracking_ref,
+ null_only,
+ true,
+ &None,
+ )?;
}
context.dec_depth();
Ok(())
@@ -458,21 +560,13 @@ fn skip_ext(
fn skip_value(
context: &mut ReadContext,
field_type: &FieldType,
- read_ref_flag: bool,
+ tracking_ref: bool,
+ null_only: bool,
_is_field: bool,
type_info: &Option<Rc<crate::TypeInfo>>,
) -> Result<(), Error> {
- if read_ref_flag {
- let ref_flag = context.reader.read_i8()?;
- if ref_flag == (RefFlag::Null as i8) {
- return Ok(());
- }
- if ref_flag == (RefFlag::Ref as i8) {
- // Reference to already-seen object, skip the reference index
- let _ref_index = context.reader.read_var_u32()?;
- return Ok(());
- }
- // RefValue (0) or NotNullValue (-1) means we need to read the actual
object
+ if !consume_ref_flag(context, tracking_ref, null_only)? {
+ return Ok(());
}
let type_id_num = field_type.type_id;
@@ -858,10 +952,15 @@ pub fn skip_enum_variant(
Ok(())
}
0b1 => {
- // Unnamed variant, skip tuple data (which is serialized as a
collection)
- // Tuple uses collection format but doesn't write type info, so
skip directly
- let field_type = FieldType::new(types::LIST, false,
vec![unknown_field_type()]);
- skip_collection(context, &field_type)
+ let len = context.reader.read_var_u32()? as usize;
+ let _header = context.reader.read_u8()?;
+ // Compatible tuple variants use a collection-shaped prefix, but
each element is
+ // encoded as a NullOnly field with inline type info. The list
skip path would read a
+ // shared element TypeInfo from the header and shift the stream by
one byte.
+ for _ in 0..len {
+ skip_any_value(context, true)?;
+ }
+ Ok(())
}
0b10 => {
// Named variant, skip struct-like data using skip_struct
diff --git a/rust/tests/tests/test_ref_resolver.rs
b/rust/tests/tests/test_ref_resolver.rs
index 23c693947..a0842a90e 100644
--- a/rust/tests/tests/test_ref_resolver.rs
+++ b/rust/tests/tests/test_ref_resolver.rs
@@ -68,6 +68,23 @@ fn test_rc_storage_and_retrieval() {
assert!(Rc::ptr_eq(&rc, &retrieved));
}
+#[test]
+fn test_reserved_ref_slot_stays_unresolved() {
+ let mut ref_reader = RefReader::new();
+
+ let skipped_ref_id = ref_reader.reserve_ref_id();
+ let rc = Rc::new(String::from("kept"));
+ let kept_ref_id = ref_reader.store_rc_ref(rc.clone());
+
+ assert_eq!(skipped_ref_id, 0);
+ assert_eq!(kept_ref_id, 1);
+ assert!(ref_reader.get_rc_ref::<String>(skipped_ref_id).is_none());
+
+ let retrieved = ref_reader.get_rc_ref::<String>(kept_ref_id).unwrap();
+ assert_eq!(*retrieved, "kept");
+ assert!(Rc::ptr_eq(&rc, &retrieved));
+}
+
#[test]
fn test_arc_storage_and_retrieval() {
let mut ref_reader = RefReader::new();
diff --git a/rust/tests/tests/test_skip_fields.rs
b/rust/tests/tests/test_skip_fields.rs
index bb4a1f7dc..3f5b6114e 100644
--- a/rust/tests/tests/test_skip_fields.rs
+++ b/rust/tests/tests/test_skip_fields.rs
@@ -17,6 +17,9 @@
use fory_core::{Fory, Reader};
use fory_derive::{ForyEnum, ForyStruct};
+use std::any::Any;
+use std::collections::HashMap;
+use std::rc::Rc;
#[derive(ForyStruct, Debug, PartialEq)]
struct TestSkipFields {
@@ -70,6 +73,66 @@ struct ComplexNestedSkip {
skipped_nested: TestSkipFields,
}
+#[derive(ForyStruct, Debug, PartialEq)]
+struct RefSkipItem {
+ #[fory(id = 0)]
+ value: i32,
+}
+
+#[derive(ForyStruct)]
+struct SkippedAnyFieldWriter {
+ #[fory(id = 0)]
+ skipped: Rc<dyn Any>,
+ #[fory(id = 1)]
+ kept: Rc<dyn Any>,
+ #[fory(id = 2)]
+ alias: Rc<dyn Any>,
+}
+
+#[derive(ForyStruct)]
+struct SkippedAnyFieldReader {
+ #[fory(id = 1)]
+ kept: Rc<dyn Any>,
+ #[fory(id = 2)]
+ alias: Rc<dyn Any>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefListWriter {
+ #[fory(id = 0)]
+ skipped: Vec<Rc<RefSkipItem>>,
+ #[fory(id = 1)]
+ kept: Rc<RefSkipItem>,
+ #[fory(id = 2)]
+ alias: Rc<RefSkipItem>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefListReader {
+ #[fory(id = 1)]
+ kept: Rc<RefSkipItem>,
+ #[fory(id = 2)]
+ alias: Rc<RefSkipItem>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefMapWriter {
+ #[fory(id = 0)]
+ skipped: HashMap<i32, Rc<RefSkipItem>>,
+ #[fory(id = 1)]
+ kept: Rc<RefSkipItem>,
+ #[fory(id = 2)]
+ alias: Rc<RefSkipItem>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefMapReader {
+ #[fory(id = 1)]
+ kept: Rc<RefSkipItem>,
+ #[fory(id = 2)]
+ alias: Rc<RefSkipItem>,
+}
+
#[derive(ForyEnum, Debug, PartialEq)]
enum TestEnumSkip {
Pending,
@@ -204,6 +267,100 @@ fn test_complex_nested_skip() {
assert_eq!(decoded.skipped_nested, TestSkipFields::fory_default());
}
+#[test]
+fn skipped_ref_field_keeps_ref_ids_aligned() {
+ let mut writer = Fory::builder()
+ .xlang(false)
+ .compatible(true)
+ .track_ref(true)
+ .build();
+ writer.register::<SkippedAnyFieldWriter>(1001).unwrap();
+ let mut reader = Fory::builder()
+ .xlang(false)
+ .compatible(true)
+ .track_ref(true)
+ .build();
+ reader.register::<SkippedAnyFieldReader>(1001).unwrap();
+
+ let skipped: Rc<dyn Any> = Rc::new("skip".to_string());
+ let kept: Rc<dyn Any> = Rc::new("kept".to_string());
+ let bytes = writer
+ .serialize(&SkippedAnyFieldWriter {
+ skipped,
+ kept: kept.clone(),
+ alias: kept,
+ })
+ .unwrap();
+ let decoded: SkippedAnyFieldReader = reader.deserialize(&bytes).unwrap();
+
+ assert_eq!(decoded.kept.downcast_ref::<String>().unwrap(), "kept");
+ assert!(Rc::ptr_eq(&decoded.kept, &decoded.alias));
+}
+
+#[test]
+fn skipped_ref_list_keeps_ref_ids_aligned() {
+ let mut writer = Fory::builder()
+ .xlang(false)
+ .compatible(true)
+ .track_ref(true)
+ .build();
+ writer.register::<RefSkipItem>(1002).unwrap();
+ writer.register::<SkippedRefListWriter>(1003).unwrap();
+ let mut reader = Fory::builder()
+ .xlang(false)
+ .compatible(true)
+ .track_ref(true)
+ .build();
+ reader.register::<RefSkipItem>(1002).unwrap();
+ reader.register::<SkippedRefListReader>(1003).unwrap();
+
+ let skipped = Rc::new(RefSkipItem { value: 8 });
+ let kept = Rc::new(RefSkipItem { value: 43 });
+ let bytes = writer
+ .serialize(&SkippedRefListWriter {
+ skipped: vec![skipped],
+ kept: kept.clone(),
+ alias: kept,
+ })
+ .unwrap();
+ let decoded: SkippedRefListReader = reader.deserialize(&bytes).unwrap();
+
+ assert_eq!(decoded.kept.value, 43);
+ assert!(Rc::ptr_eq(&decoded.kept, &decoded.alias));
+}
+
+#[test]
+fn skipped_ref_map_keeps_ref_ids_aligned() {
+ let mut writer = Fory::builder()
+ .xlang(false)
+ .compatible(true)
+ .track_ref(true)
+ .build();
+ writer.register::<RefSkipItem>(1004).unwrap();
+ writer.register::<SkippedRefMapWriter>(1005).unwrap();
+ let mut reader = Fory::builder()
+ .xlang(false)
+ .compatible(true)
+ .track_ref(true)
+ .build();
+ reader.register::<RefSkipItem>(1004).unwrap();
+ reader.register::<SkippedRefMapReader>(1005).unwrap();
+
+ let skipped = Rc::new(RefSkipItem { value: 9 });
+ let kept = Rc::new(RefSkipItem { value: 44 });
+ let bytes = writer
+ .serialize(&SkippedRefMapWriter {
+ skipped: HashMap::from([(1, skipped)]),
+ kept: kept.clone(),
+ alias: kept,
+ })
+ .unwrap();
+ let decoded: SkippedRefMapReader = reader.deserialize(&bytes).unwrap();
+
+ assert_eq!(decoded.kept.value, 44);
+ assert!(Rc::ptr_eq(&decoded.kept, &decoded.alias));
+}
+
#[test]
fn test_enum_skip() {
let mut fory = Fory::builder().xlang(false).compatible(false).build();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]