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 477105cb7 feat(rust): forbid late type registration after resolver 
snapshot ini… (#3435)
477105cb7 is described below

commit 477105cb73785093222d27d13c3ae6cbde4eeb49
Author: Geethapranay1 <[email protected]>
AuthorDate: Thu Mar 5 18:32:54 2026 +0530

    feat(rust): forbid late type registration after resolver snapshot ini… 
(#3435)
    
    Add a lifecycle guard to all registration entry points in fory. After
    the first serialize/deserialize call initializes the
    `final_type_resolver` (`OnceLock`), any subsequent `register*` call
    returns `Error::NotAllowed` with a descriptive message.
    
    this prevents a silent footgun where late registrations appear to
    succeed but are never reflected in the frozen resolver snapshot.
    
    ## Guarded Methods
    
    - `register`, `register_union`
    - `register_by_name`, `register_union_by_name`
    - `register_by_namespace`, `register_union_by_namespace`
    - `register_serializer`, `register_serializer_by_name`,
    `register_serializer_by_namespace`
    - `register_generic_trait`
    
    
    
    ## Why?
    
    
    
    
    After the first serialize/deserialize call, Fory's `final_type_resolver`
    (`OnceLock`) is frozen. Any subsequent type registration calls
    (`register*`) silently succeed but do not affect the frozen resolver,
    leading to confusing runtime errors.
    this PR introduces a **lifecycle guard** to prevent late registration
    and make the lifecycle explicit and fail-fast.
    
    ## What does this PR do?
    
    
    
    * adds a lifecycle guard (`check_registration_allowed`) to all type
    registration entry points in fory.
    * after the first serialize/deserialize call, any further registration
    attempts return `Error::NotAllowed` with a descriptive message.
    * prevents silent bugs where late registrations appear to succeed but
    are ignored at runtime.
    * adds comprehensive tests to verify both positive and negative cases
    for all registration methods.
    
    ## Guarded Methods
    
    * `register`, `register_union`
    * `register_by_name`, `register_union_by_name`
    * `register_by_namespace`, `register_union_by_namespace`
    * `register_serializer`, `register_serializer_by_name`,
    `register_serializer_by_namespace`
    * `register_generic_trait`
    
    ## Related issues
    Closes #3417
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    Yes. users will now receive a clear error if they attempt to register
    types after the resolver snapshot is initialized, instead of silent
    failure.
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    
    after updating code changes cargo build successful
    <img width="974" height="112" alt="Screenshot From 2026-02-28 14-59-24"
    
src="https://github.com/user-attachments/assets/db3b9bb3-4f04-4339-aeed-4ad0351522ac";
    />
    
    testing with new test file successfully passed all testcases
    <img width="978" height="525" alt="Screenshot From 2026-02-28 15-00-21"
    
src="https://github.com/user-attachments/assets/e9f20643-f84d-4f1a-a694-c80567fde0b8";
    />
---
 rust/fory-core/src/fory.rs               |  31 ++++
 rust/tests/tests/test_lifecycle_guard.rs | 273 +++++++++++++++++++++++++++++++
 2 files changed, 304 insertions(+)

diff --git a/rust/fory-core/src/fory.rs b/rust/fory-core/src/fory.rs
index 9d3f82694..d4e2359b3 100644
--- a/rust/fory-core/src/fory.rs
+++ b/rust/fory-core/src/fory.rs
@@ -372,6 +372,27 @@ impl Fory {
         &self.config
     }
 
+    /// Checks whether the final type resolver has already been initialized.
+    ///
+    /// If it has, further type registrations would be silently ignored (the 
frozen
+    /// snapshot is what serialize/deserialize actually use),so we fail fast 
with
+    /// a clear error instead.
+    ///
+    /// # errors
+    ///
+    /// returns [`Error::NotAllowed`] when the resolver snapshot has already 
been
+    /// built (i.e after the first `serialize` / `deserialize` call).
+    fn check_registration_allowed(&self) -> Result<(), Error> {
+        if self.final_type_resolver.get().is_some() {
+            return Err(Error::not_allowed(
+                "Type registration is not allowed after the first 
serialize/deserialize call. \
+                 The type resolver snapshot has already been finalized. \
+                 Please complete all type registrations before performing any 
serialization or deserialization.",
+            ));
+        }
+        Ok(())
+    }
+
     /// Serializes a value of type `T` into a byte vector.
     ///
     /// # Type Parameters
@@ -655,6 +676,7 @@ impl Fory {
         &mut self,
         id: u32,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.type_resolver.register_by_id::<T>(id)
     }
 
@@ -665,6 +687,7 @@ impl Fory {
         &mut self,
         id: u32,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.type_resolver.register_union_by_id::<T>(id)
     }
 
@@ -703,6 +726,7 @@ impl Fory {
         namespace: &str,
         type_name: &str,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.type_resolver
             .register_by_namespace::<T>(namespace, type_name)
     }
@@ -715,6 +739,7 @@ impl Fory {
         namespace: &str,
         type_name: &str,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.type_resolver
             .register_union_by_namespace::<T>(namespace, type_name)
     }
@@ -749,6 +774,7 @@ impl Fory {
         &mut self,
         type_name: &str,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.register_by_namespace::<T>("", type_name)
     }
 
@@ -757,6 +783,7 @@ impl Fory {
         &mut self,
         type_name: &str,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.register_union_by_namespace::<T>("", type_name)
     }
 
@@ -791,6 +818,7 @@ impl Fory {
         &mut self,
         id: u32,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.type_resolver.register_serializer_by_id::<T>(id)
     }
 
@@ -815,6 +843,7 @@ impl Fory {
         namespace: &str,
         type_name: &str,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.type_resolver
             .register_serializer_by_namespace::<T>(namespace, type_name)
     }
@@ -836,6 +865,7 @@ impl Fory {
         &mut self,
         type_name: &str,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.register_serializer_by_namespace::<T>("", type_name)
     }
 
@@ -845,6 +875,7 @@ impl Fory {
     pub fn register_generic_trait<T: 'static + Serializer + ForyDefault>(
         &mut self,
     ) -> Result<(), Error> {
+        self.check_registration_allowed()?;
         self.type_resolver.register_generic_trait::<T>()
     }
 
diff --git a/rust/tests/tests/test_lifecycle_guard.rs 
b/rust/tests/tests/test_lifecycle_guard.rs
new file mode 100644
index 000000000..81d4cc7a9
--- /dev/null
+++ b/rust/tests/tests/test_lifecycle_guard.rs
@@ -0,0 +1,273 @@
+// 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.
+
+//! tests for the lifecycle guard that forbids type registrations
+//! after the resolver snapshot has been initialized (i.e after the
+//! first serialize or deserialize call).
+
+use fory_core::error::Error;
+use fory_core::fory::Fory;
+use fory_derive::ForyObject;
+
+/// helper struct used across multiple tests.
+#[derive(ForyObject, Debug, PartialEq)]
+struct Point {
+    x: i32,
+    y: i32,
+}
+
+/// A second type used for late-registration attempts.
+#[derive(ForyObject, Debug, PartialEq)]
+struct Color {
+    r: u8,
+    g: u8,
+    b: u8,
+}
+
+// Postive tests
+#[test]
+fn test_register_before_serialize_succeeds() {
+    let mut fory = Fory::default();
+    // registration before any serialize/deserialize should succeed.
+    assert!(fory.register::<Point>(100).is_ok());
+
+    let point = Point { x: 1, y: 2 };
+    let bytes = fory.serialize(&point).unwrap();
+    let result: Point = fory.deserialize(&bytes).unwrap();
+    assert_eq!(point, result);
+}
+
+#[test]
+fn test_multiple_registrations_before_serialize_succeed() {
+    let mut fory = Fory::default();
+    assert!(fory.register::<Point>(100).is_ok());
+    assert!(fory.register::<Color>(101).is_ok());
+
+    let point = Point { x: 10, y: 20 };
+    let bytes = fory.serialize(&point).unwrap();
+    let result: Point = fory.deserialize(&bytes).unwrap();
+    assert_eq!(point, result);
+}
+
+// Negative tests
+
+/// ensures `register()` is forbidden after `serialize()` triggers snapshot 
init.
+#[test]
+fn test_register_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+
+    // first serialize, this initializes the final_type_resolver snapshot.
+    let point = Point { x: 1, y: 2 };
+    let _bytes = fory.serialize(&point).unwrap();
+
+    // now any registration must fail with NotAllowed.
+    let err = fory
+        .register::<Color>(101)
+        .expect_err("register after serialize should fail");
+    assert!(
+        matches!(err, Error::NotAllowed(_)),
+        "expected NotAllowed, got: {:?}",
+        err
+    );
+    let msg = format!("{}", err);
+    assert!(
+        msg.contains("not allowed"),
+        "error message should explain the restriction, got: {}",
+        msg
+    );
+}
+
+/// Ensures `register()` is forbidden after `deserialize()` triggers snapshot 
init.
+#[test]
+fn test_register_after_deserialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+
+    let point = Point { x: 5, y: 10 };
+    let bytes = fory.serialize(&point).unwrap();
+
+    // Deserialize — also initializes the snapshot if not already done.
+    let _result: Point = fory.deserialize(&bytes).unwrap();
+
+    let err = fory
+        .register::<Color>(101)
+        .expect_err("register after deserialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_by_name()` is forbidden after snapshot init.
+#[test]
+fn test_register_by_name_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_by_name::<Color>("Color")
+        .expect_err("register_by_name after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_by_namespace()` is forbidden after snapshot init.
+#[test]
+fn test_register_by_namespace_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_by_namespace::<Color>("com.example", "Color")
+        .expect_err("register_by_namespace after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_serializer()` is forbidden after snapshot init.
+#[test]
+fn test_register_serializer_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_serializer::<Color>(102)
+        .expect_err("register_serializer after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_serializer_by_name()` is forbidden after snapshot init.
+#[test]
+fn test_register_serializer_by_name_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_serializer_by_name::<Color>("Color")
+        .expect_err("register_serializer_by_name after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_serializer_by_namespace()` is forbidden after snapshot 
init.
+#[test]
+fn test_register_serializer_by_namespace_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_serializer_by_namespace::<Color>("com.example", "Color")
+        .expect_err("register_serializer_by_namespace after serialize should 
fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_generic_trait()` is forbidden after snapshot init.
+#[test]
+fn test_register_generic_trait_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_generic_trait::<Vec<i32>>()
+        .expect_err("register_generic_trait after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_union()` is forbidden after snapshot init.
+#[test]
+fn test_register_union_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_union::<Color>(103)
+        .expect_err("register_union after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_union_by_name()` is forbidden after snapshot init.
+#[test]
+fn test_register_union_by_name_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_union_by_name::<Color>("Color")
+        .expect_err("register_union_by_name after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+/// Ensures `register_union_by_namespace()` is forbidden after snapshot init.
+#[test]
+fn test_register_union_by_namespace_after_serialize_fails() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory
+        .register_union_by_namespace::<Color>("com.example", "Color")
+        .expect_err("register_union_by_namespace after serialize should fail");
+    assert!(matches!(err, Error::NotAllowed(_)));
+}
+
+// Edge-case
+#[test]
+fn test_late_registration_error_message_is_descriptive() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+    let _bytes = fory.serialize(&Point { x: 0, y: 0 }).unwrap();
+
+    let err = fory.register::<Color>(101).unwrap_err();
+    let msg = format!("{}", err);
+    assert!(
+        msg.contains("not allowed"),
+        "should mention 'not allowed', got: {}",
+        msg
+    );
+    assert!(
+        msg.contains("serialize/deserialize"),
+        "should mention serialize/deserialize, got: {}",
+        msg
+    );
+    assert!(
+        msg.contains("finalized"),
+        "should mention the snapshot is finalized, got: {}",
+        msg
+    );
+}
+
+// Positive edge-case
+
+#[test]
+fn test_serialize_multiple_times_after_registration_succeeds() {
+    let mut fory = Fory::default();
+    fory.register::<Point>(100).unwrap();
+
+    let p1 = Point { x: 1, y: 2 };
+    let p2 = Point { x: 3, y: 4 };
+
+    let bytes1 = fory.serialize(&p1).unwrap();
+    let bytes2 = fory.serialize(&p2).unwrap();
+
+    let r1: Point = fory.deserialize(&bytes1).unwrap();
+    let r2: Point = fory.deserialize(&bytes2).unwrap();
+    assert_eq!(p1, r1);
+    assert_eq!(p2, r2);
+}


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

Reply via email to