From 50aa3b0bede1b109ac1c5406d972c7e37e6f3196 Mon Sep 17 00:00:00 2001 From: Release Validation Test Date: Thu, 27 Aug 2026 11:26:54 +0800 Subject: [PATCH 1/6] refactor: remove redundant codec hardening layers --- .agents/languages/cpp.md | 6 - .agents/languages/go.md | 6 - .agents/languages/java.md | 16 - AGENTS.md | 40 +-- cpp/fory/serialization/array_serializer.h | 18 +- .../serialization/collection_serializer.h | 85 +++--- cpp/fory/serialization/serialization_test.cc | 2 +- cpp/fory/serialization/stream_test.cc | 34 --- cpp/fory/serialization/string_serializer.h | 23 +- cpp/fory/serialization/struct_serializer.h | 69 ++--- cpp/fory/serialization/unsigned_serializer.h | 44 +-- cpp/fory/util/buffer.h | 286 +++++++----------- cpp/fory/util/buffer_test.cc | 53 +--- .../fory/lib/src/memory/buffer_mixin.dart | 54 +--- docs/security/deserialization.md | 8 - .../src/main/java/org/apache/fory/Fory.java | 14 +- .../apache/fory/memory/MemoryAllocator.java | 5 +- .../org/apache/fory/memory/MemoryBuffer.java | 92 +----- .../org/apache/fory/memory/MemoryBuffer.java | 19 +- rust/fory-core/src/buffer.rs | 30 +- swift/Sources/Fory/ByteBuffer.swift | 11 +- 21 files changed, 255 insertions(+), 660 deletions(-) diff --git a/.agents/languages/cpp.md b/.agents/languages/cpp.md index 1b3780eba4..aae2f0399b 100644 --- a/.agents/languages/cpp.md +++ b/.agents/languages/cpp.md @@ -13,12 +13,6 @@ Load this file when changing `cpp/`, Cython build plumbing, or C++ xlang behavio - When invoking a method that returns `Result`, use `FORY_TRY` unless you are in control-flow logic that cannot use it cleanly. - Wrap error checks with `FORY_PREDICT_FALSE` for branch prediction. - Continue on trivial errors; return early only for critical errors such as buffer overflow. -- `ReadContext` and related codec owners intentionally accumulate non-critical - errors and inspect them at established serializer or root safepoints. Bounds-safe - work may continue after an error is recorded. Do not add eager per-field checks, - cursor rollback, or tests that pin the first detection point unless deferral can - cause out-of-bounds access, undefined behavior, resource amplification, state - pollution, or success past the required safepoint. - Put private methods last in class definitions, immediately before private fields. - Do not redesign alias-based or low-level public type shapes to add convenience methods unless the user explicitly asks for that API change. - For cross-language feature ports, match protocol behavior but use idiomatic C++ ownership and layering instead of mirroring Java structure literally. diff --git a/.agents/languages/go.md b/.agents/languages/go.md index a8256d74df..829383509a 100644 --- a/.agents/languages/go.md +++ b/.agents/languages/go.md @@ -7,12 +7,6 @@ Load this file when changing `go/fory/` or Go xlang behavior. - Run Go commands from within `go/fory/`. - Changes under `go/` must pass formatting and tests. - The Go implementation focuses on fast serializers. -- Go codec paths may intentionally record an error and continue bounds-safe work - until an established serializer or root safepoint checks it. Do not add eager - per-field or per-element error branches, cursor rollback, or tests that pin the - first detection point unless deferral can cause a panic, out-of-bounds access, - disproportionate work or allocation, persistent state pollution, or success - past the required safepoint. - Root deserialization graph memory budget state belongs to `ReadContext`. `WithMaxGraphMemoryBytes` uses a fixed `128 MiB` default; positive explicit values override it, and explicit non-positive values are invalid at config creation. diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 00d5553c04..93d058fea7 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -159,22 +159,6 @@ Load this file when changing anything under `java/` or when Java drives a cross- - In `MemoryBuffer` and `MemoryOps` hot paths, duplicate small straight-line copy/read/write logic when that keeps control flow direct. Do not add private helper indirection to hot paths just to reduce local code duplication; keep helpers for slow, cold, or error paths. -- Add a Java `MemoryBuffer` check only when its absence can cause a JVM or native crash, OOM, or - attacker-controlled memory amplification. Delayed, masked, less precise, or differently typed - failures do not justify a check, and neither does an incorrect decoded result without one of - those crash or memory consequences. Do not duplicate an array, `ByteBuffer`, VarHandle, stream, - or other existing bounds owner merely to move or normalize an error. -- The JDK 25 `MemoryBuffer` overlay is intentionally unchecked at its logical buffer boundary. - Indexed array accesses, absolute `ByteBuffer` accesses, and VarHandle accesses own bounds - enforcement and already provide a controlled failure; its exact exception type, message, and - detection point are not contracts. Do not copy the JDK 8-24 Unsafe-path range checks into this - overlay. Keep an explicit check only before allocation, capacity growth, or another side effect - when it is required to prevent a crash, OOM, or attacker-controlled memory amplification that the - access owner cannot contain. -- `MemoryAllocator.grow` owns the postcondition that a successful return has supplied at least the - requested capacity. `MemoryBuffer` must not recheck that postcondition after calling `grow` or - `ensure`; an allocator that returns without satisfying it violates the allocator contract and - must be fixed in the allocator implementation rather than burdening every buffer hot path. - In JDK 25 Fory JSON C2-sensitive code, preserve measured, naturally large hot-method boundaries. A method that exceeds HotSpot's 325-byte hot-inline limit through real representation, scalar, array, collection, or generated-schema work is an independent subtree owner. Generated group diff --git a/AGENTS.md b/AGENTS.md index 680f992a56..e985c28e4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,13 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th - Preserve architecture. Do not introduce new layers, parallel flows, or public APIs unless explicitly requested; prefer local repair in the existing owner over shared-infra expansion, and stop if a fix conflicts with an ADR, spec, or invariant. - Do not change an existing `RefReader`/`RefWriter` architecture or API to support compatible skip. Compatible skip must not add alternate reference slots or tables, alternate reference lookup or publication methods, or forwarding APIs in read/write contexts, builders, serializers, or generated-code plumbing. Keep ordinary reference publication and lookup unchanged and resolve the case in the existing compatible generated owner. For an authorized removed-field read of an unregistered Struct, the empty object created by the skip reader is that path's final owner: publish that same object for `RefValue`, consume the Struct fields, and let later `RefFlag` values resolve to it. This preserves reference numbering and identity without registering the Struct; an independent dynamic root still requires normal registration. Do not add parallel reference state, a sentinel, a rejection, or a common-path branch for this case. - Respect ownership. Keep logic, state, and helpers in their natural owner, and do not move serializer-local, context-local, runtime-type-local, or protocol-local problems into global utilities. +- Low-level APIs explicitly named `unsafe` or `unchecked` may be public and may omit local bounds or + capacity checks. Their callers own the proof required by the operation. Do not hide these APIs + behind private access, checked forwarding wrappers, friend adapters, or duplicate implementations + solely to prevent misuse; add validation only at the caller or owner that lacks the required proof. +- Keep hardening changes causal. A shared wire defect may require aligned runtime fixes, but it does + not justify unrelated buffer checks, performance rewrites, or cleanup without their own concrete + consequence and evidence. - Check the spec before implementation. For wire behavior and xlang mapping, use the specs as the source of truth and never copy one runtime's bug into another runtime just to make tests pass. - `foryc` is a build-time compiler for trusted schema inputs and is never invoked by runtime serialization or deserialization. Schema provenance, package/namespace options, output-path options, and generated-source review belong to the application or build owner. Do not classify hostile-schema source injection or path traversal as a Fory runtime security vulnerability; `foryc` does not promise to sandbox untrusted schemas. - Row format accepts only trusted input and is outside Fory's untrusted binary-deserialization security boundary. Rust `check_string_read(false)` is likewise an explicit trusted-input mode that disables UTF-8 validation; its caller owns the validity guarantee. Classify issues in those paths as correctness, soundness, or hardening bugs when applicable, not as attacker-controlled deserialization vulnerabilities under the default security model. @@ -45,20 +52,6 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th hot-path branches, helper APIs, allocations, or generated-code expansion solely to make an error earlier, more specific, or more uniform, and do not write tests that force such error normalization. -- Non-strict, non-precise, delayed, masked, differently typed, or differently layered controlled - errors are not security findings. Security remediation must not add checks solely to normalize, - sharpen, advance, or otherwise make such errors deterministic. Security tests must not require a - particular error type, message, layer, offset, or detection point unless an explicit public - contract makes that precision part of the policy boundary. -- Runtimes with an established lazy-error accumulator, including C++ and Go, - may keep executing bounds-safe codec work after recording an error and inspect - it at an existing serializer or root-operation safepoint. Deferred inspection - is an intentional hot-path design, not by itself a correctness or security - defect. Do not add per-field or per-element error branches, cursor rollback, - or tests that require immediate propagation solely because an earlier check is - possible. Treat the behavior as a defect only when the deferred path has a - concrete consequence listed below or an established safepoint can return - success instead of a controlled root error. - Never add a reader-side check solely to produce a more precise malformed-input error. Retain or add a check only when the unchecked path has a concrete consequence such as a crash, panic, undefined behavior, out-of-bounds access, @@ -69,15 +62,6 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th helper when the language supports it. A bounds-safe downstream operation that already raises a controlled root error is sufficient; do not duplicate it for error precision. -- Buffer and memory-buffer checks require a stricter consequence test. Security remediation may add - a check only when its absence can cause a process crash or panic, undefined memory access, OOM, or - attacker-controlled memory amplification. Delayed, masked, less precise, or differently typed - errors are not sufficient reasons. Neither is an incorrect decoded result when the unchecked - path cannot cause one of those crash or memory consequences. A downstream bounds owner that - already fails in a controlled way is sufficient; do not duplicate its check in a hotter wrapper. - Apply this admission rule prospectively: do not remove an already completed and validated check - solely by reclassifying it when it has no demonstrated performance cost. Preserve that check as - a correctness fix unless an explicit task requires the behavioral rollback. - Before reporting or fixing a robustness finding, prove that the current path causes at least one concrete consequence: crash, panic, undefined behavior, or out-of-bounds access; disproportionate allocation, CPU work, or stream @@ -88,16 +72,6 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th malformed or noncanonical flag, enum value, marker, length form, or reserved value is accepted, rejected late, decoded differently, or produces a less precise error. -- Keep security remediation scope frozen. Do not fix a standalone non-security - correctness, interoperability, API, or lifecycle issue merely because it is - discovered during security work. Include an adjacent non-security change only - when omitting it would leave the security fix incomplete or force the same - owner into an ugly, duplicated, or knowingly unsound design; otherwise record - the issue separately and leave production code unchanged. This rule prevents - new scope; it does not authorize rolling back a non-security fix that is - already implemented and validated. Preserve such fixes. If one may affect - performance, inspect and measure the affected path first, then optimize it on - evidence instead of removing it by classification. - Arbitrary-precision binary Decimal codecs accept only scales in `[-10_000, 10_000]` and an absolute unscaled magnitude of at most `10_000` binary bytes. The Java standalone `BigInteger` serializer uses the same diff --git a/cpp/fory/serialization/array_serializer.h b/cpp/fory/serialization/array_serializer.h index 25580f18a7..49d31456db 100644 --- a/cpp/fory/serialization/array_serializer.h +++ b/cpp/fory/serialization/array_serializer.h @@ -93,8 +93,8 @@ struct Serializer< buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); // write array length in bytes - writer_index += buffer.put_var_uint32(writer_index, - static_cast(N * sizeof(T))); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(N * sizeof(T))); // write data if constexpr (N > 0) { @@ -110,7 +110,7 @@ struct Serializer< } } } - buffer.writer_index(writer_index + N * sizeof(T)); + buffer.unsafe_set_writer_index(writer_index + N * sizeof(T)); } static inline void write_data_generic(const std::array &arr, @@ -221,14 +221,14 @@ template struct Serializer> { uint32_t writer_index = buffer.writer_index(); // write array length writer_index += - buffer.put_var_uint32(writer_index, static_cast(N)); + buffer.put_var_uint32_unchecked(writer_index, static_cast(N)); // write each boolean as a byte for (size_t i = 0; i < N; ++i) { buffer.unsafe_put_byte(writer_index + i, static_cast(arr[i] ? 1 : 0)); } - buffer.writer_index(writer_index + N); + buffer.unsafe_set_writer_index(writer_index + N); } static inline void write_data_generic(const std::array &arr, @@ -323,7 +323,7 @@ template struct Serializer> { constexpr size_t max_size = 8 + N * sizeof(float16_t); buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); - writer_index += buffer.put_var_uint32( + writer_index += buffer.put_var_uint32_unchecked( writer_index, static_cast(N * sizeof(float16_t))); if constexpr (N > 0) { if constexpr (FORY_LITTLE_ENDIAN) { @@ -336,7 +336,7 @@ template struct Serializer> { } } } - buffer.writer_index(writer_index + N * sizeof(float16_t)); + buffer.unsafe_set_writer_index(writer_index + N * sizeof(float16_t)); } static inline void write_data_generic(const std::array &arr, @@ -436,7 +436,7 @@ template struct Serializer> { constexpr size_t max_size = 8 + N * sizeof(bfloat16_t); buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); - writer_index += buffer.put_var_uint32( + writer_index += buffer.put_var_uint32_unchecked( writer_index, static_cast(N * sizeof(bfloat16_t))); if constexpr (N > 0) { if constexpr (FORY_LITTLE_ENDIAN) { @@ -449,7 +449,7 @@ template struct Serializer> { } } } - buffer.writer_index(writer_index + N * sizeof(bfloat16_t)); + buffer.unsafe_set_writer_index(writer_index + N * sizeof(bfloat16_t)); } static inline void write_data_generic(const std::array &arr, diff --git a/cpp/fory/serialization/collection_serializer.h b/cpp/fory/serialization/collection_serializer.h index 905f0fa293..bafe03a46e 100644 --- a/cpp/fory/serialization/collection_serializer.h +++ b/cpp/fory/serialization/collection_serializer.h @@ -50,36 +50,21 @@ constexpr uint8_t COLL_IS_SAME_TYPE = 0b1000; namespace detail { -/// Primitive vectors reserve their header and complete body before writing. -/// Commit that proven range directly so public random-access safety checks do -/// not expand every generated vector-field write. -struct PrimitiveVectorWriter { - FORY_ALWAYS_INLINE static bool reserve(WriteContext &ctx, - uint64_t body_size) { - Buffer &buffer = ctx.buffer(); - const uint64_t required_size = - static_cast(buffer.writer_index()) + 8 + body_size; - if (FORY_PREDICT_FALSE(required_size >= - std::numeric_limits::max())) { - ctx.set_error( - Error::invalid("Vector write exceeds uint32_t buffer range")); - return false; - } - // The uint32 encoder may make an eight-byte physical store for a five-byte - // value. Reserve that physical extent together with the complete body. - buffer.grow(static_cast(8 + body_size)); - return true; - } - - FORY_ALWAYS_INLINE static uint32_t put_size(Buffer &buffer, uint32_t offset, - uint32_t size) { - return buffer.put_var_uint32_unchecked(offset, size); - } - - FORY_ALWAYS_INLINE static void commit(Buffer &buffer, uint32_t offset) { - buffer.writer_index_ = offset; +FORY_ALWAYS_INLINE bool reserve_primitive_vector(WriteContext &ctx, + uint64_t body_size) { + Buffer &buffer = ctx.buffer(); + const uint64_t required_size = + static_cast(buffer.writer_index()) + 8 + body_size; + if (FORY_PREDICT_FALSE(required_size >= + std::numeric_limits::max())) { + ctx.set_error(Error::invalid("Vector write exceeds uint32_t buffer range")); + return false; } -}; + // The uint32 encoder may make an eight-byte physical store for a five-byte + // value. Reserve that physical extent together with the complete body. + buffer.grow(static_cast(8 + body_size)); + return true; +} } // namespace detail @@ -1019,19 +1004,19 @@ struct Serializer< static inline void write_data(const std::vector &vec, WriteContext &ctx) { uint64_t total_bytes = static_cast(vec.size()) * sizeof(T); - if (!detail::PrimitiveVectorWriter::reserve(ctx, total_bytes)) { + if (!detail::reserve_primitive_vector(ctx, total_bytes)) { return; } Buffer &buffer = ctx.buffer(); uint32_t writer_index = buffer.writer_index(); - writer_index += detail::PrimitiveVectorWriter::put_size( - buffer, writer_index, static_cast(total_bytes)); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(total_bytes)); if (total_bytes > 0) { buffer.unsafe_put(writer_index, vec.data(), static_cast(total_bytes)); } - detail::PrimitiveVectorWriter::commit( - buffer, writer_index + static_cast(total_bytes)); + buffer.unsafe_set_writer_index(writer_index + + static_cast(total_bytes)); } static inline void write_data_generic(const std::vector &vec, @@ -1132,19 +1117,19 @@ template struct Serializer> { WriteContext &ctx) { uint64_t total_bytes = static_cast(vec.size()) * sizeof(float16_t); - if (!detail::PrimitiveVectorWriter::reserve(ctx, total_bytes)) { + if (!detail::reserve_primitive_vector(ctx, total_bytes)) { return; } Buffer &buffer = ctx.buffer(); uint32_t writer_index = buffer.writer_index(); - writer_index += detail::PrimitiveVectorWriter::put_size( - buffer, writer_index, static_cast(total_bytes)); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(total_bytes)); if (total_bytes > 0) { buffer.unsafe_put(writer_index, vec.data(), static_cast(total_bytes)); } - detail::PrimitiveVectorWriter::commit( - buffer, writer_index + static_cast(total_bytes)); + buffer.unsafe_set_writer_index(writer_index + + static_cast(total_bytes)); } static inline void @@ -1237,19 +1222,19 @@ template struct Serializer> { WriteContext &ctx) { uint64_t total_bytes = static_cast(vec.size()) * sizeof(bfloat16_t); - if (!detail::PrimitiveVectorWriter::reserve(ctx, total_bytes)) { + if (!detail::reserve_primitive_vector(ctx, total_bytes)) { return; } Buffer &buffer = ctx.buffer(); uint32_t writer_index = buffer.writer_index(); - writer_index += detail::PrimitiveVectorWriter::put_size( - buffer, writer_index, static_cast(total_bytes)); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(total_bytes)); if (total_bytes > 0) { buffer.unsafe_put(writer_index, vec.data(), static_cast(total_bytes)); } - detail::PrimitiveVectorWriter::commit( - buffer, writer_index + static_cast(total_bytes)); + buffer.unsafe_set_writer_index(writer_index + + static_cast(total_bytes)); } static inline void @@ -1530,20 +1515,20 @@ template struct Serializer> { static inline void write_data(const std::vector &vec, WriteContext &ctx) { - if (!detail::PrimitiveVectorWriter::reserve( - ctx, static_cast(vec.size()))) { + if (!detail::reserve_primitive_vector(ctx, + static_cast(vec.size()))) { return; } Buffer &buffer = ctx.buffer(); uint32_t writer_index = buffer.writer_index(); - writer_index += detail::PrimitiveVectorWriter::put_size( - buffer, writer_index, static_cast(vec.size())); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(vec.size())); for (size_t i = 0; i < vec.size(); ++i) { buffer.unsafe_put_byte(writer_index + i, static_cast(vec[i] ? 1 : 0)); } - detail::PrimitiveVectorWriter::commit( - buffer, writer_index + static_cast(vec.size())); + buffer.unsafe_set_writer_index(writer_index + + static_cast(vec.size())); } static inline void write_data_generic(const std::vector &vec, diff --git a/cpp/fory/serialization/serialization_test.cc b/cpp/fory/serialization/serialization_test.cc index 95ee192287..e7ea4989a9 100644 --- a/cpp/fory/serialization/serialization_test.cc +++ b/cpp/fory/serialization/serialization_test.cc @@ -433,7 +433,7 @@ TEST(SerializationTest, PrimitiveVectorWriteChecksCompleteRange) { Fory::builder().xlang(true).compatible(false).track_ref(false).build(); WriteContext write_ctx(fory.config(), fory.type_resolver().clone()); - EXPECT_FALSE(detail::PrimitiveVectorWriter::reserve( + EXPECT_FALSE(detail::reserve_primitive_vector( write_ctx, std::numeric_limits::max())); EXPECT_TRUE(write_ctx.has_error()); EXPECT_EQ(write_ctx.buffer().writer_index(), 0U); diff --git a/cpp/fory/serialization/stream_test.cc b/cpp/fory/serialization/stream_test.cc index 67982cddd9..4d05d50a86 100644 --- a/cpp/fory/serialization/stream_test.cc +++ b/cpp/fory/serialization/stream_test.cc @@ -155,25 +155,6 @@ class OneByteOStream final : public std::ostream { OneByteOutputStreamBuf buf_; }; -class CountingOutputStream final : public OutputStream { -public: - Result write_to_stream(const uint8_t *src, - uint32_t length) override { - ++write_calls_; - data_.insert(data_.end(), src, src + length); - return Result(); - } - - Result flush_stream() override { return Result(); } - - const std::vector &data() const { return data_; } - uint32_t write_calls() const { return write_calls_; } - -private: - std::vector data_; - uint32_t write_calls_ = 0; -}; - static inline void register_stream_types(Fory &fory) { uint32_t type_id = 1; fory.register_struct(type_id++); @@ -379,21 +360,6 @@ TEST(StreamSerializationTest, SerializeToOutputStreamRoundTrip) { EXPECT_EQ(roundtrip.value(), original); } -TEST(StreamSerializationTest, LargeStringsFlushIncrementally) { - auto fory = - Fory::builder().xlang(true).compatible(false).track_ref(false).build(); - std::vector original{std::string(5000, 'a'), - std::string(5000, 'b')}; - auto expected = fory.serialize(original); - ASSERT_TRUE(expected.ok()) << expected.error().to_string(); - - CountingOutputStream writer; - auto streamed = fory.serialize(writer, original); - ASSERT_TRUE(streamed.ok()) << streamed.error().to_string(); - EXPECT_GE(writer.write_calls(), 2U); - EXPECT_EQ(writer.data(), expected.value()); -} - TEST(StreamSerializationTest, SerializeToOStreamOverloadParity) { auto fory = Fory::builder().xlang(true).compatible(false).track_ref(true).build(); diff --git a/cpp/fory/serialization/string_serializer.h b/cpp/fory/serialization/string_serializer.h index 45a6174835..80b5a9b306 100644 --- a/cpp/fory/serialization/string_serializer.h +++ b/cpp/fory/serialization/string_serializer.h @@ -47,20 +47,6 @@ enum class StringEncoding : uint8_t { namespace detail { -struct StringFieldWriter { - FORY_ALWAYS_INLINE static uint32_t put_size(Buffer &buffer, uint32_t offset, - uint64_t size) { - return buffer.put_var_uint64_unchecked(offset, size); - } - - FORY_ALWAYS_INLINE static void commit(Buffer &buffer, uint32_t offset) { - buffer.writer_index_ = offset; - if (FORY_PREDICT_FALSE(buffer.output_stream_ != nullptr && offset > 4096)) { - buffer.output_stream_->try_flush(); - } - } -}; - /// write string data with UTF-8 encoding inline void write_string_data(const char *data, size_t size, WriteContext &ctx) { @@ -75,15 +61,10 @@ inline void write_string_data(const char *data, size_t size, const uint64_t length = static_cast(size); const uint64_t size_with_encoding = (length << 2) | static_cast(StringEncoding::UTF8); - Buffer &buffer = ctx.buffer(); - buffer.grow(static_cast(size + 9)); - uint32_t writer_index = buffer.writer_index(); - writer_index += - StringFieldWriter::put_size(buffer, writer_index, size_with_encoding); + ctx.write_var_uint36_small(size_with_encoding); if (size > 0) { - buffer.unsafe_put(writer_index, data, static_cast(size)); + ctx.write_bytes(data, static_cast(size)); } - StringFieldWriter::commit(buffer, writer_index + static_cast(size)); } /// write UTF-16 string data, converting to UTF-8 or using native encoding diff --git a/cpp/fory/serialization/struct_serializer.h b/cpp/fory/serialization/struct_serializer.h index 4a237305ac..d04707935a 100644 --- a/cpp/fory/serialization/struct_serializer.h +++ b/cpp/fory/serialization/struct_serializer.h @@ -78,35 +78,6 @@ struct SerializationMeta(value); uint32_t zigzag = (static_cast(val) << 1) ^ static_cast(val >> 31); - return StructFieldWriter::put_var_uint32(buffer, offset, zigzag); + return buffer.put_var_uint32_unchecked(offset, zigzag); } else if constexpr (std::is_same_v || std::is_same_v) { - return StructFieldWriter::put_var_uint32(buffer, offset, - static_cast(value)); + return buffer.put_var_uint32_unchecked(offset, + static_cast(value)); } else if constexpr (std::is_same_v || std::is_same_v) { // varint64 with zigzag encoding int64_t val = static_cast(value); uint64_t zigzag = (static_cast(val) << 1) ^ static_cast(val >> 63); - return StructFieldWriter::put_var_uint64(buffer, offset, zigzag); + return buffer.put_var_uint64_unchecked(offset, zigzag); } else if constexpr (std::is_same_v || std::is_same_v) { - return StructFieldWriter::put_var_uint64(buffer, offset, - static_cast(value)); + return buffer.put_var_uint64_unchecked(offset, + static_cast(value)); } else if constexpr (std::is_same_v || std::is_same_v) { buffer.unsafe_put(offset, static_cast(value)); return 4; @@ -301,25 +272,25 @@ FORY_ALWAYS_INLINE uint32_t put_varint_at(T value, Buffer &buffer, int32_t val = static_cast(value); uint32_t zigzag = (static_cast(val) << 1) ^ static_cast(val >> 31); - return StructFieldWriter::put_var_uint32(buffer, offset, zigzag); + return buffer.put_var_uint32_unchecked(offset, zigzag); } else if constexpr (std::is_same_v || std::is_same_v) { // varint64 with zigzag encoding int64_t val = static_cast(value); uint64_t zigzag = (static_cast(val) << 1) ^ static_cast(val >> 63); - return StructFieldWriter::put_var_uint64(buffer, offset, zigzag); + return buffer.put_var_uint64_unchecked(offset, zigzag); } else if constexpr (std::is_same_v || std::is_same_v) { // Unsigned 32-bit varint (no zigzag) - return StructFieldWriter::put_var_uint32(buffer, offset, - static_cast(value)); + return buffer.put_var_uint32_unchecked(offset, + static_cast(value)); } else if constexpr (std::is_same_v || std::is_same_v) { // Unsigned 64-bit varint (no zigzag) - used for VAR_UINT64 and // TAGGED_UINT64 - return StructFieldWriter::put_var_uint64(buffer, offset, - static_cast(value)); + return buffer.put_var_uint64_unchecked(offset, + static_cast(value)); } else { static_assert(sizeof(T) == 0, "Unsupported varint type"); return 0; @@ -461,8 +432,8 @@ FORY_ALWAYS_INLINE uint32_t write_configurable_int_at(FieldType value, return 8; } if constexpr (enc == Encoding::Tagged) { - return StructFieldWriter::put_tagged_int64(buffer, offset, - static_cast(value)); + return buffer.put_tagged_int64_unchecked(offset, + static_cast(value)); } return put_varint_at(value, buffer, offset); } else { @@ -476,8 +447,8 @@ FORY_ALWAYS_INLINE uint32_t write_configurable_int_at(FieldType value, } if constexpr (enc == Encoding::Tagged) { if constexpr (is_configurable_int64_v) { - return StructFieldWriter::put_tagged_uint64( - buffer, offset, static_cast(value)); + return buffer.put_tagged_uint64_unchecked(offset, + static_cast(value)); } return put_varint_at(value, buffer, offset); } @@ -2568,8 +2539,8 @@ write_fixed_primitive_fields(const T &obj, Buffer &buffer, (write_single_fixed_field(obj, buffer, base_offset), ...); // Update writer_index once with total fixed bytes (compile-time constant) - StructFieldWriter::commit(buffer, - base_offset + Helpers::leading_fixed_size_bytes); + buffer.unsafe_set_writer_index(base_offset + + Helpers::leading_fixed_size_bytes); } /// Helper to write a single varint primitive field. @@ -2682,7 +2653,7 @@ write_primitive_fields_fast(const T &obj, Buffer &buffer, uint32_t offset = buffer.writer_index(); write_varint_primitive_fields( obj, buffer, offset, std::make_index_sequence{}); - StructFieldWriter::commit(buffer, offset); + buffer.unsafe_set_writer_index(offset); } // Phase 3: write remaining primitives (if any) using dedicated helper @@ -2692,7 +2663,7 @@ write_primitive_fields_fast(const T &obj, Buffer &buffer, write_remaining_primitive_fields( obj, buffer, offset, std::make_index_sequence{}); - StructFieldWriter::commit(buffer, offset); + buffer.unsafe_set_writer_index(offset); } } diff --git a/cpp/fory/serialization/unsigned_serializer.h b/cpp/fory/serialization/unsigned_serializer.h index 57c895b715..ac0cb8d172 100644 --- a/cpp/fory/serialization/unsigned_serializer.h +++ b/cpp/fory/serialization/unsigned_serializer.h @@ -333,11 +333,11 @@ template struct Serializer> { buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); writer_index += - buffer.put_var_uint32(writer_index, static_cast(N)); + buffer.put_var_uint32_unchecked(writer_index, static_cast(N)); if constexpr (N > 0) { buffer.unsafe_put(writer_index, arr.data(), N * sizeof(uint8_t)); } - buffer.writer_index(writer_index + N * sizeof(uint8_t)); + buffer.unsafe_set_writer_index(writer_index + N * sizeof(uint8_t)); } static inline void write_data_generic(const std::array &arr, @@ -422,11 +422,11 @@ template struct Serializer> { buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); writer_index += - buffer.put_var_uint32(writer_index, static_cast(N)); + buffer.put_var_uint32_unchecked(writer_index, static_cast(N)); if constexpr (N > 0) { buffer.unsafe_put(writer_index, arr.data(), N * sizeof(uint16_t)); } - buffer.writer_index(writer_index + N * sizeof(uint16_t)); + buffer.unsafe_set_writer_index(writer_index + N * sizeof(uint16_t)); } static inline void write_data_generic(const std::array &arr, @@ -511,11 +511,11 @@ template struct Serializer> { buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); writer_index += - buffer.put_var_uint32(writer_index, static_cast(N)); + buffer.put_var_uint32_unchecked(writer_index, static_cast(N)); if constexpr (N > 0) { buffer.unsafe_put(writer_index, arr.data(), N * sizeof(uint32_t)); } - buffer.writer_index(writer_index + N * sizeof(uint32_t)); + buffer.unsafe_set_writer_index(writer_index + N * sizeof(uint32_t)); } static inline void write_data_generic(const std::array &arr, @@ -600,11 +600,11 @@ template struct Serializer> { buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); writer_index += - buffer.put_var_uint32(writer_index, static_cast(N)); + buffer.put_var_uint32_unchecked(writer_index, static_cast(N)); if constexpr (N > 0) { buffer.unsafe_put(writer_index, arr.data(), N * sizeof(uint64_t)); } - buffer.writer_index(writer_index + N * sizeof(uint64_t)); + buffer.unsafe_set_writer_index(writer_index + N * sizeof(uint64_t)); } static inline void write_data_generic(const std::array &arr, @@ -693,12 +693,13 @@ template <> struct Serializer> { size_t max_size = 8 + vec.size(); buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); - writer_index += - buffer.put_var_uint32(writer_index, static_cast(vec.size())); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(vec.size())); if (!vec.empty()) { buffer.unsafe_put(writer_index, vec.data(), vec.size()); } - buffer.writer_index(writer_index + static_cast(vec.size())); + buffer.unsafe_set_writer_index(writer_index + + static_cast(vec.size())); } static inline void write_data_generic(const std::vector &vec, @@ -786,13 +787,14 @@ template <> struct Serializer> { size_t max_size = 8 + static_cast(total_bytes); buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); - writer_index += - buffer.put_var_uint32(writer_index, static_cast(total_bytes)); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(total_bytes)); if (total_bytes > 0) { buffer.unsafe_put(writer_index, vec.data(), static_cast(total_bytes)); } - buffer.writer_index(writer_index + static_cast(total_bytes)); + buffer.unsafe_set_writer_index(writer_index + + static_cast(total_bytes)); } static inline void write_data_generic(const std::vector &vec, @@ -891,13 +893,14 @@ template <> struct Serializer> { size_t max_size = 8 + static_cast(total_bytes); buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); - writer_index += - buffer.put_var_uint32(writer_index, static_cast(total_bytes)); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(total_bytes)); if (total_bytes > 0) { buffer.unsafe_put(writer_index, vec.data(), static_cast(total_bytes)); } - buffer.writer_index(writer_index + static_cast(total_bytes)); + buffer.unsafe_set_writer_index(writer_index + + static_cast(total_bytes)); } static inline void write_data_generic(const std::vector &vec, @@ -996,13 +999,14 @@ template <> struct Serializer> { size_t max_size = 8 + static_cast(total_bytes); buffer.grow(static_cast(max_size)); uint32_t writer_index = buffer.writer_index(); - writer_index += - buffer.put_var_uint32(writer_index, static_cast(total_bytes)); + writer_index += buffer.put_var_uint32_unchecked( + writer_index, static_cast(total_bytes)); if (total_bytes > 0) { buffer.unsafe_put(writer_index, vec.data(), static_cast(total_bytes)); } - buffer.writer_index(writer_index + static_cast(total_bytes)); + buffer.unsafe_set_writer_index(writer_index + + static_cast(total_bytes)); } static inline void write_data_generic(const std::vector &vec, diff --git a/cpp/fory/util/buffer.h b/cpp/fory/util/buffer.h index 69ecc256f4..8ea0d43e6e 100644 --- a/cpp/fory/util/buffer.h +++ b/cpp/fory/util/buffer.h @@ -39,11 +39,6 @@ namespace fory { class StdInputStream; class PyInputStream; -namespace serialization::detail { -struct PrimitiveVectorWriter; -struct StructFieldWriter; -struct StringFieldWriter; -} // namespace serialization::detail // A buffer class for storing raw bytes with various methods for reading and // writing the bytes. @@ -235,11 +230,17 @@ class Buffer { reader_index_ += diff; } - // Unsafe methods don't check bound + // Unsafe operations deliberately trust their callers to prove the complete + // physical extent or cursor position. Keep that contract explicit instead of + // adding local checks or access-control wrappers that duplicate the proof. FORY_ALWAYS_INLINE void unsafe_increase_reader_index(uint32_t diff) { reader_index_ += diff; } + FORY_ALWAYS_INLINE void unsafe_set_writer_index(uint32_t writer_index) { + writer_index_ = writer_index; + } + template FORY_ALWAYS_INLINE void unsafe_put(uint32_t offset, T value) { store_unaligned(data_ + offset, value); @@ -361,18 +362,116 @@ class Buffer { return Result(); } - /// Put unsigned varint32 at offset using optimized bulk writes. - /// Returns number of bytes written (1-5). - /// Uses bit manipulation to build encoded value, then single memory write. - FORY_ALWAYS_INLINE uint32_t put_var_uint32(uint32_t offset, uint32_t value) { - const uint32_t extent = value < 0x80 ? 1 - : value < 0x4000 ? 2 - : value < 0x10000000 ? 4 - : 8; - if (FORY_PREDICT_FALSE(!range_in_bounds(offset, extent))) { - fail_range(offset, extent); + /// Put unsigned varint32 at an unchecked offset using optimized bulk writes. + /// The caller must reserve the complete physical store extent, which can be + /// larger than the returned logical byte count. + FORY_ALWAYS_INLINE uint32_t put_var_uint32_unchecked(uint32_t offset, + uint32_t value) { + if (value < 0x80) { + data_[offset] = static_cast(value); + return 1; + } + // Bulk stores keep the hot path branch-light. Callers must establish the + // physical extent selected below, which may exceed the logical encoding. + uint64_t encoded = (value & 0x7F) | 0x80; + encoded |= (static_cast(value & 0x3F80) << 1); + if (value < 0x4000) { + store_unaligned(data_ + offset, static_cast(encoded)); + return 2; + } + encoded |= (static_cast(value & 0x1FC000) << 2) | 0x8000; + if (value < 0x200000) { + store_unaligned(data_ + offset, static_cast(encoded)); + return 3; } - return put_var_uint32_unchecked(offset, value); + encoded |= (static_cast(value & 0xFE00000) << 3) | 0x800000; + if (value < 0x10000000) { + store_unaligned(data_ + offset, static_cast(encoded)); + return 4; + } + encoded |= (static_cast(value >> 28) << 32) | 0x80000000; + store_unaligned(data_ + offset, encoded); + return 5; + } + + /// Put unsigned varint64 at an unchecked offset using PVL encoding. + /// The caller must reserve the complete physical store extent. + FORY_ALWAYS_INLINE uint32_t put_var_uint64_unchecked(uint32_t offset, + uint64_t value) { + if (value < 0x80) { + data_[offset] = static_cast(value); + return 1; + } + uint64_t encoded = (value & 0x7F) | 0x80; + encoded |= ((value & 0x3F80) << 1); + if (value < 0x4000) { + store_unaligned(data_ + offset, static_cast(encoded)); + return 2; + } + encoded |= ((value & 0x1FC000) << 2) | 0x8000; + if (value < 0x200000) { + store_unaligned(data_ + offset, static_cast(encoded)); + return 3; + } + encoded |= ((value & 0xFE00000) << 3) | 0x800000; + if (value < 0x10000000) { + store_unaligned(data_ + offset, static_cast(encoded)); + return 4; + } + encoded |= ((value & 0x7F0000000ULL) << 4) | 0x80000000; + if (value < 0x800000000ULL) { + store_unaligned(data_ + offset, encoded); + return 5; + } + encoded |= ((value & 0x3F800000000ULL) << 5) | 0x8000000000ULL; + if (value < 0x40000000000ULL) { + store_unaligned(data_ + offset, encoded); + return 6; + } + encoded |= ((value & 0x1FC0000000000ULL) << 6) | 0x800000000000ULL; + if (value < 0x2000000000000ULL) { + store_unaligned(data_ + offset, encoded); + return 7; + } + encoded |= ((value & 0xFE000000000000ULL) << 7) | 0x80000000000000ULL; + if (value < 0x100000000000000ULL) { + store_unaligned(data_ + offset, encoded); + return 8; + } + encoded |= 0x8000000000000000ULL; + store_unaligned(data_ + offset, encoded); + data_[offset + 8] = static_cast(value >> 56); + return 9; + } + + /// Put a tagged uint64 at an unchecked offset. Returns 4 or 9 bytes. + FORY_ALWAYS_INLINE uint32_t put_tagged_uint64_unchecked(uint32_t offset, + uint64_t value) { + constexpr uint64_t MAX_SMALL_VALUE = 0x7fffffff; + if (value <= MAX_SMALL_VALUE) { + store_unaligned(data_ + offset, static_cast(value) + << 1); + return 4; + } + data_[offset] = 0b1; + store_unaligned(data_ + offset + 1, value); + return 9; + } + + /// Put a tagged int64 at an unchecked offset. Returns 4 or 9 bytes. + FORY_ALWAYS_INLINE uint32_t put_tagged_int64_unchecked(uint32_t offset, + int64_t value) { + constexpr int64_t MIN_SMALL_VALUE = -1073741824; + constexpr int64_t MAX_SMALL_VALUE = 1073741823; + if (value >= MIN_SMALL_VALUE && value <= MAX_SMALL_VALUE) { + const uint32_t encoded = + static_cast(static_cast(value)) << 1; + store_unaligned(data_ + offset, encoded); + return 4; + } + data_[offset] = 0b1; + store_unaligned(data_ + offset + 1, value); + return 9; } /// get unsigned varint32 from offset using optimized bulk read. @@ -467,21 +566,6 @@ class Buffer { return result; } - /// Put unsigned varint64 at offset using optimized bulk writes. - /// Returns number of bytes written (1-9). - /// Uses PVL (Progressive Variable-length Long) encoding per xlang spec. - FORY_ALWAYS_INLINE uint32_t put_var_uint64(uint32_t offset, uint64_t value) { - const uint32_t extent = value < 0x80 ? 1 - : value < 0x4000 ? 2 - : value < 0x10000000 ? 4 - : value < 0x100000000000000ULL ? 8 - : 9; - if (FORY_PREDICT_FALSE(!range_in_bounds(offset, extent))) { - fail_range(offset, extent); - } - return put_var_uint64_unchecked(offset, value); - } - /// get unsigned varint64 from offset using optimized bulk read. /// Fast path: bulk read 8 bytes + bit extraction when enough bytes available. /// Slow path: byte-by-byte for buffer edge cases. @@ -621,36 +705,6 @@ class Buffer { } } - /// write uint64_t using tagged encoding at given offset. Returns bytes - /// written. - /// - If value is in [0, 0x7fffffff]: write 4 bytes (value << 1), return 4 - /// - Otherwise: write 1 byte flag + 8 bytes uint64, return 9 - FORY_ALWAYS_INLINE uint32_t put_tagged_uint64(uint32_t offset, - uint64_t value) { - constexpr uint64_t MAX_SMALL_VALUE = 0x7fffffff; // INT32_MAX as u64 - const uint32_t extent = value <= MAX_SMALL_VALUE ? 4 : 9; - if (FORY_PREDICT_FALSE(!range_in_bounds(offset, extent))) { - fail_range(offset, extent); - } - return put_tagged_uint64_unchecked(offset, value); - } - - /// write int64_t using tagged encoding at given offset. Returns bytes - /// written. - /// - If value is in [-1073741824, 1073741823]: write 4 bytes (value << 1), - /// return 4 - /// - Otherwise: write 1 byte flag + 8 bytes int64, return 9 - FORY_ALWAYS_INLINE uint32_t put_tagged_int64(uint32_t offset, int64_t value) { - constexpr int64_t MIN_SMALL_VALUE = -1073741824; // -2^30 - constexpr int64_t MAX_SMALL_VALUE = 1073741823; // 2^30 - 1 - const uint32_t extent = - value >= MIN_SMALL_VALUE && value <= MAX_SMALL_VALUE ? 4 : 9; - if (FORY_PREDICT_FALSE(!range_in_bounds(offset, extent))) { - fail_range(offset, extent); - } - return put_tagged_int64_unchecked(offset, value); - } - /// write uint8_t value to buffer at current writer index. /// Automatically grows buffer and advances writer index. FORY_ALWAYS_INLINE void write_uint8(uint8_t value) { @@ -1201,9 +1255,6 @@ class Buffer { std::string hex() const; private: - friend struct serialization::detail::PrimitiveVectorWriter; - friend struct serialization::detail::StructFieldWriter; - friend struct serialization::detail::StringFieldWriter; friend class StdInputStream; friend class PyInputStream; friend class OutputStream; @@ -1300,111 +1351,6 @@ class Buffer { return result; } - FORY_ALWAYS_INLINE uint32_t put_var_uint32_unchecked(uint32_t offset, - uint32_t value) { - if (value < 0x80) { - data_[offset] = static_cast(value); - return 1; - } - // Bulk stores keep the hot path branch-light. Callers must establish the - // physical extent selected below, which may exceed the logical encoding. - uint64_t encoded = (value & 0x7F) | 0x80; - encoded |= (static_cast(value & 0x3F80) << 1); - if (value < 0x4000) { - store_unaligned(data_ + offset, static_cast(encoded)); - return 2; - } - encoded |= (static_cast(value & 0x1FC000) << 2) | 0x8000; - if (value < 0x200000) { - store_unaligned(data_ + offset, static_cast(encoded)); - return 3; - } - encoded |= (static_cast(value & 0xFE00000) << 3) | 0x800000; - if (value < 0x10000000) { - store_unaligned(data_ + offset, static_cast(encoded)); - return 4; - } - encoded |= (static_cast(value >> 28) << 32) | 0x80000000; - store_unaligned(data_ + offset, encoded); - return 5; - } - - FORY_ALWAYS_INLINE uint32_t put_var_uint64_unchecked(uint32_t offset, - uint64_t value) { - if (value < 0x80) { - data_[offset] = static_cast(value); - return 1; - } - uint64_t encoded = (value & 0x7F) | 0x80; - encoded |= ((value & 0x3F80) << 1); - if (value < 0x4000) { - store_unaligned(data_ + offset, static_cast(encoded)); - return 2; - } - encoded |= ((value & 0x1FC000) << 2) | 0x8000; - if (value < 0x200000) { - store_unaligned(data_ + offset, static_cast(encoded)); - return 3; - } - encoded |= ((value & 0xFE00000) << 3) | 0x800000; - if (value < 0x10000000) { - store_unaligned(data_ + offset, static_cast(encoded)); - return 4; - } - encoded |= ((value & 0x7F0000000ULL) << 4) | 0x80000000; - if (value < 0x800000000ULL) { - store_unaligned(data_ + offset, encoded); - return 5; - } - encoded |= ((value & 0x3F800000000ULL) << 5) | 0x8000000000ULL; - if (value < 0x40000000000ULL) { - store_unaligned(data_ + offset, encoded); - return 6; - } - encoded |= ((value & 0x1FC0000000000ULL) << 6) | 0x800000000000ULL; - if (value < 0x2000000000000ULL) { - store_unaligned(data_ + offset, encoded); - return 7; - } - encoded |= ((value & 0xFE000000000000ULL) << 7) | 0x80000000000000ULL; - if (value < 0x100000000000000ULL) { - store_unaligned(data_ + offset, encoded); - return 8; - } - encoded |= 0x8000000000000000ULL; - store_unaligned(data_ + offset, encoded); - data_[offset + 8] = static_cast(value >> 56); - return 9; - } - - FORY_ALWAYS_INLINE uint32_t put_tagged_uint64_unchecked(uint32_t offset, - uint64_t value) { - constexpr uint64_t MAX_SMALL_VALUE = 0x7fffffff; - if (value <= MAX_SMALL_VALUE) { - store_unaligned(data_ + offset, static_cast(value) - << 1); - return 4; - } - data_[offset] = 0b1; - store_unaligned(data_ + offset + 1, value); - return 9; - } - - FORY_ALWAYS_INLINE uint32_t put_tagged_int64_unchecked(uint32_t offset, - int64_t value) { - constexpr int64_t MIN_SMALL_VALUE = -1073741824; - constexpr int64_t MAX_SMALL_VALUE = 1073741823; - if (value >= MIN_SMALL_VALUE && value <= MAX_SMALL_VALUE) { - const uint32_t encoded = - static_cast(static_cast(value)) << 1; - store_unaligned(data_ + offset, encoded); - return 4; - } - data_[offset] = 0b1; - store_unaligned(data_ + offset + 1, value); - return 9; - } - FORY_ALWAYS_INLINE bool range_in_bounds(uint32_t offset, uint32_t length) const { return offset <= size_ && length <= size_ - offset; diff --git a/cpp/fory/util/buffer_test.cc b/cpp/fory/util/buffer_test.cc index d8996a1955..3fb033f35a 100644 --- a/cpp/fory/util/buffer_test.cc +++ b/cpp/fory/util/buffer_test.cc @@ -122,7 +122,8 @@ TEST(Buffer, to_string) { void check_var_uint32(int32_t start_offset, std::shared_ptr buffer, int32_t value, uint32_t bytes_written) { - uint32_t actual_bytes_written = buffer->put_var_uint32(start_offset, value); + uint32_t actual_bytes_written = + buffer->put_var_uint32_unchecked(start_offset, value); EXPECT_EQ(actual_bytes_written, bytes_written); uint32_t read_bytes_length; int32_t var_int = buffer->get_var_uint32(start_offset, &read_bytes_length); @@ -149,7 +150,8 @@ TEST(Buffer, TestVarUint) { void check_var_uint64(int32_t start_offset, std::shared_ptr buffer, uint64_t value, uint32_t bytes_written) { - uint32_t actual_bytes_written = buffer->put_var_uint64(start_offset, value); + uint32_t actual_bytes_written = + buffer->put_var_uint64_unchecked(start_offset, value); EXPECT_EQ(actual_bytes_written, bytes_written); uint32_t read_bytes_length; uint64_t var_int = buffer->get_var_uint64(start_offset, &read_bytes_length); @@ -305,11 +307,6 @@ TEST(Buffer, OffsetRangeRejectsOverflow) { ""); buffer->unsafe_put(0, 1); EXPECT_DEATH(buffer->get_tagged_int64(0, &bytes_read), ""); - EXPECT_DEATH( - buffer->put_tagged_uint64(std::numeric_limits::max(), 0), ""); - EXPECT_DEATH(buffer->put_tagged_int64(0, std::numeric_limits::max()), - ""); - const uint8_t byte = 1; EXPECT_DEATH( buffer->copy_from(std::numeric_limits::max(), &byte, 0, 1), ""); @@ -347,44 +344,6 @@ TEST(Buffer, RepresentedCopySupportsOverlap) { EXPECT_EQ(bytes, (std::vector{1, 1, 2, 3, 4})); } -TEST(Buffer, VarUintPutChecksPhysicalExtent) { - std::shared_ptr one_byte; - std::shared_ptr two_bytes; - std::shared_ptr three_bytes; - std::shared_ptr four_bytes; - std::shared_ptr seven_bytes; - std::shared_ptr eight_bytes; - std::shared_ptr nine_bytes; - ASSERT_TRUE(allocate_buffer(1, &one_byte)); - ASSERT_TRUE(allocate_buffer(2, &two_bytes)); - ASSERT_TRUE(allocate_buffer(3, &three_bytes)); - ASSERT_TRUE(allocate_buffer(4, &four_bytes)); - ASSERT_TRUE(allocate_buffer(7, &seven_bytes)); - ASSERT_TRUE(allocate_buffer(8, &eight_bytes)); - ASSERT_TRUE(allocate_buffer(9, &nine_bytes)); - - EXPECT_EQ(one_byte->put_var_uint32(0, 0x7f), 1U); - EXPECT_EQ(two_bytes->put_var_uint64(0, 0x80), 2U); - constexpr uint32_t kThreeByteValue = uint32_t{1} << 14; - EXPECT_DEATH(three_bytes->put_var_uint32(0, kThreeByteValue), ""); - EXPECT_EQ(four_bytes->put_var_uint32(0, kThreeByteValue), 3U); - constexpr uint64_t kFourByteValue = uint64_t{1} << 21; - EXPECT_EQ(four_bytes->put_var_uint64(0, kFourByteValue), 4U); - - constexpr uint32_t kFiveByteValue = uint32_t{1} << 28; - EXPECT_DEATH(seven_bytes->put_var_uint32(0, kFiveByteValue), ""); - EXPECT_EQ(eight_bytes->put_var_uint32(0, kFiveByteValue), 5U); - uint32_t bytes_read = 0; - EXPECT_EQ(eight_bytes->get_var_uint32(0, &bytes_read), kFiveByteValue); - EXPECT_EQ(bytes_read, 5U); - - constexpr uint64_t kNineByteValue = uint64_t{1} << 56; - EXPECT_DEATH(eight_bytes->put_var_uint64(0, kNineByteValue), ""); - EXPECT_EQ(nine_bytes->put_var_uint64(0, kNineByteValue), 9U); - EXPECT_EQ(nine_bytes->get_var_uint64(0, &bytes_read), kNineByteValue); - EXPECT_EQ(bytes_read, 9U); -} - TEST(Buffer, NegativeEqualsReturnsFalse) { std::vector bytes = {1, 2, 3}; Buffer first(bytes); @@ -419,13 +378,13 @@ TEST(Buffer, TaggedSmallNegative) { std::shared_ptr buffer; ASSERT_TRUE(allocate_buffer(8, &buffer)); - EXPECT_EQ(buffer->put_tagged_int64(0, -1), 4U); + EXPECT_EQ(buffer->put_tagged_int64_unchecked(0, -1), 4U); uint32_t bytes_read = 0; EXPECT_EQ(buffer->get_tagged_int64(0, &bytes_read), -1); EXPECT_EQ(bytes_read, 4U); constexpr uint64_t kMaxSmallUnsigned = 0x7fffffff; - EXPECT_EQ(buffer->put_tagged_uint64(0, kMaxSmallUnsigned), 4U); + EXPECT_EQ(buffer->put_tagged_uint64_unchecked(0, kMaxSmallUnsigned), 4U); EXPECT_EQ(buffer->get_tagged_uint64(0, &bytes_read), kMaxSmallUnsigned); EXPECT_EQ(bytes_read, 4U); diff --git a/dart/packages/fory/lib/src/memory/buffer_mixin.dart b/dart/packages/fory/lib/src/memory/buffer_mixin.dart index 5b4692ea29..d41f1f1f58 100644 --- a/dart/packages/fory/lib/src/memory/buffer_mixin.dart +++ b/dart/packages/fory/lib/src/memory/buffer_mixin.dart @@ -343,62 +343,16 @@ mixin _BufferMixin { if (value < 0 || value > 0xfffffffff) { _throwInvalidVarUint36(); } - ensureWritable(6); - if (value > 0x7fffffff) { - _writeLargeVarUint36(value); - return; - } - var remaining = value; - while (remaining >= 0x80) { - _bytes[_writerIndex] = (remaining & 0x7f) | 0x80; - _writerIndex += 1; - remaining >>>= 7; - } - _bytes[_writerIndex] = remaining; - _writerIndex += 1; - } - - @pragma('vm:never-inline') - void _writeLargeVarUint36(int value) { - var remaining = value; - while (remaining >= 0x80) { - _bytes[_writerIndex] = (remaining % 0x80) | 0x80; - _writerIndex += 1; - remaining ~/= 0x80; - } - _bytes[_writerIndex] = remaining; - _writerIndex += 1; + writeVarUint64(Uint64(value)); } /// Reads a small unsigned integer written by [writeVarUint36Small]. - @pragma('vm:prefer-inline') int readVarUint36Small() { - final byte = readUint8(); - if (byte < 0x80) { - return byte; - } - return _readVarUint36SmallTail(byte & 0x7f); - } - - @pragma('vm:never-inline') - int _readVarUint36SmallTail(int result) { - var value = result; - var factor = 0x80; - // The multi-byte form is a standard varuint: the fifth byte still has a - // continuation bit, and 36-bit values can therefore require six bytes. - for (var index = 1; index < 5; index += 1) { - final byte = readUint8(); - value += (byte & 0x7f) * factor; - if (byte < 0x80) { - return value; - } - factor *= 0x80; - } - final sixthByte = readUint8(); - if (sixthByte > 1) { + final value = readVarUint64(); + if (value > 0xfffffffff) { _throwInvalidVarUint36(); } - return value + sixthByte * factor; + return value.toInt(); } } diff --git a/docs/security/deserialization.md b/docs/security/deserialization.md index a477c0f7e9..0e6fcd1f31 100644 --- a/docs/security/deserialization.md +++ b/docs/security/deserialization.md @@ -232,14 +232,6 @@ downstream buffer-underflow, type, reference, depth, or serializer error is a valid rejection. A decoder does not need a new local check merely to replace that controlled failure with a more specific or more uniform error. -Non-strict, non-precise, delayed, masked, differently typed, or differently -layered controlled errors are not security findings. Security remediation MUST -NOT add checks solely to normalize, sharpen, advance, or otherwise make such an -error deterministic. This remains true when malformed input decodes to an -incorrect value, provided the path cannot cause a crash, panic, undefined memory -access, OOM, attacker-controlled memory amplification, or another security -invariant violation defined by this document. - Tests for malformed input should prove that the root operation fails, cleanup remains correct, and any relevant security invariant is preserved. They should not pin an exact error type or message when doing so would require additional diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java b/java/fory-core/src/main/java/org/apache/fory/Fory.java index 422455660c..a004eef527 100644 --- a/java/fory-core/src/main/java/org/apache/fory/Fory.java +++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.IdentityHashMap; import java.util.function.Consumer; import java.util.function.Function; @@ -318,7 +317,7 @@ public byte[] serialize(Object obj) { MemoryBuffer buf = getBuffer(); buf.writerIndex(0); serialize(buf, obj, null); - byte[] bytes = copyWrittenBytes(buf); + byte[] bytes = buf.getBytes(0, buf.writerIndex()); resetBuffer(); return bytes; } @@ -328,7 +327,7 @@ public byte[] serialize(Object obj, BufferCallback callback) { MemoryBuffer buf = getBuffer(); buf.writerIndex(0); serialize(buf, obj, callback); - byte[] bytes = copyWrittenBytes(buf); + byte[] bytes = buf.getBytes(0, buf.writerIndex()); resetBuffer(); return bytes; } @@ -375,15 +374,6 @@ public void serialize(OutputStream outputStream, Object obj, BufferCallback call serializeToStream(outputStream, buf -> serialize(buf, obj, callback)); } - private byte[] copyWrittenBytes(MemoryBuffer buffer) { - int length = buffer.writerIndex(); - if (buffer.isHeapFullyWriteable()) { - // Fory owns this writer index; keep public arbitrary-range validation out of the root path. - return Arrays.copyOf(buffer.getHeapMemory(), length); - } - return buffer.getBytes(0, length); - } - private ForyException processSerializationError(Throwable e) { if (!config.trackingRef()) { String msg = diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java index 55010323ee..f7c45d4b65 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java @@ -31,9 +31,8 @@ public interface MemoryAllocator { /** * Grows an existing buffer to accommodate the new capacity. The implementation must grow the - * buffer in-place by modifying the existing buffer instance. A successful return guarantees that - * {@code buffer.size()} is at least {@code newCapacity}; callers intentionally do not repeat this - * postcondition check on the hot path. + * buffer in-place by modifying the existing buffer instance and leave its size at least {@code + * newCapacity}. * * @param buffer the existing buffer to grow * @param newCapacity the required new capacity diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java index a32cc7a73c..aac96e32f4 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java @@ -1790,22 +1790,12 @@ public void writeBooleansWithSize(boolean[] values) { MemoryOps.writeBooleansWithSize(this, values); } else { writeVarUInt32Small7(values.length); - writeBooleans(values); + writeBooleans(values, 0, values.length); } } - // Full-array overloads own their source range and stay direct so serializers do not inline the - // larger arbitrary-range validation path. Offset/count overloads below retain that validation. public void writeBooleans(boolean[] values) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.writeBooleans(this, values, 0, values.length); - } else { - final int writerIdx = writerIndex; - final int newIdx = writerIdx + values.length; - ensure(newIdx); - copyMemory(values, BOOLEAN_ARRAY_OFFSET, heapMemory, address + writerIdx, values.length); - writerIndex = newIdx; - } + writeBooleans(values, 0, values.length); } public void writeBooleans(boolean[] values, int offset, int numElements) { @@ -1830,21 +1820,12 @@ public void writeCharsWithSize(char[] values) { } else { int numBytes = Math.multiplyExact(values.length, 2); writeVarUInt32Small7(numBytes); - writeChars(values); + writeChars(values, 0, values.length); } } public void writeChars(char[] values) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.writeChars(this, values, 0, values.length); - } else { - int numBytes = Math.multiplyExact(values.length, 2); - final int writerIdx = writerIndex; - final int newIdx = writerIdx + numBytes; - ensure(newIdx); - copyMemory(values, CHAR_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); - writerIndex = newIdx; - } + writeChars(values, 0, values.length); } public void writeChars(char[] values, int offset, int numElements) { @@ -1874,21 +1855,12 @@ public void writeShortsWithSize(short[] values) { } else { int numBytes = Math.multiplyExact(values.length, 2); writeVarUInt32Small7(numBytes); - writeShorts(values); + writeShorts(values, 0, values.length); } } public void writeShorts(short[] values) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.writeShorts(this, values, 0, values.length); - } else { - int numBytes = Math.multiplyExact(values.length, 2); - final int writerIdx = writerIndex; - final int newIdx = writerIdx + numBytes; - ensure(newIdx); - copyMemory(values, SHORT_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); - writerIndex = newIdx; - } + writeShorts(values, 0, values.length); } public void writeShorts(short[] values, int offset, int numElements) { @@ -1918,21 +1890,12 @@ public void writeIntsWithSize(int[] values) { } else { int numBytes = Math.multiplyExact(values.length, 4); writeVarUInt32Small7(numBytes); - writeInts(values); + writeInts(values, 0, values.length); } } public void writeInts(int[] values) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.writeInts(this, values, 0, values.length); - } else { - int numBytes = Math.multiplyExact(values.length, 4); - final int writerIdx = writerIndex; - final int newIdx = writerIdx + numBytes; - ensure(newIdx); - copyMemory(values, INT_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); - writerIndex = newIdx; - } + writeInts(values, 0, values.length); } public void writeInts(int[] values, int offset, int numElements) { @@ -1962,21 +1925,12 @@ public void writeLongsWithSize(long[] values) { } else { int numBytes = Math.multiplyExact(values.length, 8); writeVarUInt32Small7(numBytes); - writeLongs(values); + writeLongs(values, 0, values.length); } } public void writeLongs(long[] values) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.writeLongs(this, values, 0, values.length); - } else { - int numBytes = Math.multiplyExact(values.length, 8); - final int writerIdx = writerIndex; - final int newIdx = writerIdx + numBytes; - ensure(newIdx); - copyMemory(values, LONG_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); - writerIndex = newIdx; - } + writeLongs(values, 0, values.length); } public void writeLongs(long[] values, int offset, int numElements) { @@ -2006,21 +1960,12 @@ public void writeFloatsWithSize(float[] values) { } else { int numBytes = Math.multiplyExact(values.length, 4); writeVarUInt32Small7(numBytes); - writeFloats(values); + writeFloats(values, 0, values.length); } } public void writeFloats(float[] values) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.writeFloats(this, values, 0, values.length); - } else { - int numBytes = Math.multiplyExact(values.length, 4); - final int writerIdx = writerIndex; - final int newIdx = writerIdx + numBytes; - ensure(newIdx); - copyMemory(values, FLOAT_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); - writerIndex = newIdx; - } + writeFloats(values, 0, values.length); } public void writeFloats(float[] values, int offset, int numElements) { @@ -2050,21 +1995,12 @@ public void writeDoublesWithSize(double[] values) { } else { int numBytes = Math.multiplyExact(values.length, 8); writeVarUInt32Small7(numBytes); - writeDoubles(values); + writeDoubles(values, 0, values.length); } } public void writeDoubles(double[] values) { - if (AndroidSupport.IS_ANDROID) { - MemoryOps.writeDoubles(this, values, 0, values.length); - } else { - int numBytes = Math.multiplyExact(values.length, 8); - final int writerIdx = writerIndex; - final int newIdx = writerIdx + numBytes; - ensure(newIdx); - copyMemory(values, DOUBLE_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); - writerIndex = newIdx; - } + writeDoubles(values, 0, values.length); } public void writeDoubles(double[] values, int offset, int numElements) { diff --git a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java index c8ce159d1d..eb0793ecda 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java @@ -57,11 +57,11 @@ *

Warning: The instance of this class should not be held at GraalVM build time; build-time heap * buffers do not represent the runtime heap layout. * - *

The Java 25 multi-release implementation intentionally does not duplicate logical range - * checks around indexed array, absolute {@link ByteBuffer}, or {@link VarHandle} access. Those JVM - * accessors own bounds enforcement and already fail safely. The exact exception type, message, and - * detection point are not contracts, so adding checks only to make an invalid access fail earlier - * or more precisely would burden every hot buffer operation without improving memory safety. + *

In the Java 25 multi-release implementation, absolute random-access get/put methods are + * internal fast paths. Callers must pass legal indices and ranges; read/write entry points perform + * logical {@link MemoryBuffer} range validation before reaching these methods. The implementation + * relies on {@link VarHandle}, array, and {@link ByteBuffer} access only for JVM memory safety and + * does not repeat root Unsafe-style logical bounds checks. * *

Note(chaokunyang): Buffer operations are very common, and jvm inline and branch elimination is * not reliable even in c2 compiler, so we try to inline and avoid checks as we can manually. jvm @@ -157,12 +157,7 @@ private MemoryBuffer(byte[] buffer, int offset, int length, ForyStreamReader str throw new IllegalArgumentException( String.format("%d exceeds buffer size %d", offset + length, buffer.length)); } - this.heapMemory = buffer; - this.heapOffset = offset; - final long startPos = BYTE_ARRAY_OFFSET + offset; - this.address = startPos; - this.size = length; - this.addressLimit = startPos + length; + initHeapBuffer(buffer, offset, length); if (streamReader != null) { this.streamReader = streamReader; } else { @@ -2024,7 +2019,6 @@ public void writeDoubles(double[] values, int offset, int numElements) { public void grow(int neededSize) { int length = writerIndex + neededSize; if (length > size) { - // MemoryAllocator owns the requested-capacity postcondition; do not recheck it here. globalAllocator.grow(this, length); } } @@ -2032,7 +2026,6 @@ public void grow(int neededSize) { /** For off-heap buffer, this will make a heap buffer internally. */ public void ensure(int length) { if (length > size) { - // MemoryAllocator owns the requested-capacity postcondition; do not recheck it here. globalAllocator.grow(this, length); } } diff --git a/rust/fory-core/src/buffer.rs b/rust/fory-core/src/buffer.rs index e6d1da4938..5bfa4707c3 100644 --- a/rust/fory-core/src/buffer.rs +++ b/rust/fory-core/src/buffer.rs @@ -202,34 +202,14 @@ impl<'a> Writer<'a> { #[inline(always)] fn write_u24(&mut self, value: u32) { - let offset = self.bf.len(); - self.bf.reserve(4); - // The four-byte store stays inside reserved capacity; only the three wire bytes become - // initialized vector contents. - unsafe { - self.bf - .as_mut_ptr() - .add(offset) - .cast::() - .write_unaligned(value.to_le()); - self.bf.set_len(offset + 3); - } + let bytes = value.to_le_bytes(); + self.bf.extend_from_slice(&bytes[..3]); } #[inline(always)] fn write_u40(&mut self, value: u64) { - let offset = self.bf.len(); - self.bf.reserve(8); - // The eight-byte store stays inside reserved capacity; only the five wire bytes become - // initialized vector contents. - unsafe { - self.bf - .as_mut_ptr() - .add(offset) - .cast::() - .write_unaligned(value.to_le()); - self.bf.set_len(offset + 5); - } + let bytes = value.to_le_bytes(); + self.bf.extend_from_slice(&bytes[..5]); } // ============ VAR_UINT32 (TypeId = 12) ============ @@ -1100,7 +1080,7 @@ impl<'a> Reader<'a> { let slice = self.slice_after_cursor(); if slice.len() >= 8 { - let bulk = LittleEndian::read_u64(&slice[..8]); + let bulk = self.read_u64()?; let mut result = bulk & 0x7F; let mut read_idx = start; diff --git a/swift/Sources/Fory/ByteBuffer.swift b/swift/Sources/Fory/ByteBuffer.swift index 7fdb95481f..a767b847fb 100644 --- a/swift/Sources/Fory/ByteBuffer.swift +++ b/swift/Sources/Fory/ByteBuffer.swift @@ -192,13 +192,6 @@ public final class ByteBuffer { cursor -= amount } - @usableFromInline - @inline(__always) - internal func moveBackUnchecked(_ amount: Int) { - // Hot read owners use this only to rewind bytes they just consumed successfully. - cursor -= amount - } - @inlinable @inline(__always) internal func appendLittleEndian(_ value: T) { @@ -768,7 +761,7 @@ public final class ByteBuffer { if (first & 1) == 0 { return Int64(first >> 1) } - moveBackUnchecked(3) + cursor -= 3 return try readInt64() } @@ -778,7 +771,7 @@ public final class ByteBuffer { if (first & 1) == 0 { return UInt64(first >> 1) } - moveBackUnchecked(3) + cursor -= 3 return try readUInt64() } From 6f0c489adacaa23adc4ceee8b98ad0a7a2357f9b Mon Sep 17 00:00:00 2001 From: Release Validation Test Date: Thu, 27 Aug 2026 11:28:01 +0800 Subject: [PATCH 2/6] chore(swift): document ASCII UTF-8 conversion --- swift/Sources/Fory/ByteBuffer.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/swift/Sources/Fory/ByteBuffer.swift b/swift/Sources/Fory/ByteBuffer.swift index a767b847fb..0c82279830 100644 --- a/swift/Sources/Fory/ByteBuffer.swift +++ b/swift/Sources/Fory/ByteBuffer.swift @@ -844,6 +844,8 @@ public final class ByteBuffer { } } if isASCII { + // ASCII bytes are always valid UTF-8. + // swiftlint:disable:next optional_data_string_conversion return String(decoding: utf8Bytes, as: UTF8.self) } if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { From 25eeeb65a73ebc3f4630346bca856b1c59d627ff Mon Sep 17 00:00:00 2001 From: Release Validation Test Date: Thu, 27 Aug 2026 11:42:13 +0800 Subject: [PATCH 3/6] refactor: remove remaining codec cleanup layers --- cpp/fory/util/buffer.h | 46 ++++++++----------- .../fory/test/string_serializer_test.dart | 12 ----- .../org/apache/fory/memory/MemoryBuffer.java | 45 +++++------------- python/pyfory/cpp/pyfory.cc | 4 +- python/pyfory/number.pxi | 13 ------ python/pyfory/tests/test_buffer.py | 37 --------------- 6 files changed, 32 insertions(+), 125 deletions(-) diff --git a/cpp/fory/util/buffer.h b/cpp/fory/util/buffer.h index 8ea0d43e6e..76a9cea8b2 100644 --- a/cpp/fory/util/buffer.h +++ b/cpp/fory/util/buffer.h @@ -230,9 +230,8 @@ class Buffer { reader_index_ += diff; } - // Unsafe operations deliberately trust their callers to prove the complete - // physical extent or cursor position. Keep that contract explicit instead of - // adding local checks or access-control wrappers that duplicate the proof. + // Unsafe operations trust callers to prove the complete physical extent or + // cursor position. FORY_ALWAYS_INLINE void unsafe_increase_reader_index(uint32_t diff) { reader_index_ += diff; } @@ -710,7 +709,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_uint8(uint8_t value) { grow(1); unsafe_put_byte(writer_index_, value); - advance_writer_index_unchecked(1); + writer_index_ += 1; } /// write int8_t value to buffer at current writer index. @@ -718,7 +717,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_int8(int8_t value) { grow(1); unsafe_put_byte(writer_index_, static_cast(value)); - advance_writer_index_unchecked(1); + writer_index_ += 1; } /// write uint16_t value as fixed 2 bytes to buffer at current writer index. @@ -726,7 +725,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_uint16(uint16_t value) { grow(2); unsafe_put(writer_index_, value); - advance_writer_index_unchecked(2); + writer_index_ += 2; } /// write int16_t value as fixed 2 bytes to buffer at current writer index. @@ -734,7 +733,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_int16(int16_t value) { grow(2); unsafe_put(writer_index_, value); - advance_writer_index_unchecked(2); + writer_index_ += 2; } /// write int24 value as fixed 3 bytes to buffer at current writer index. @@ -742,7 +741,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_int24(int32_t value) { grow(3); put_int24(writer_index_, value); - advance_writer_index_unchecked(3); + writer_index_ += 3; } /// write int32_t value as fixed 4 bytes to buffer at current writer index. @@ -750,7 +749,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_int32(int32_t value) { grow(4); unsafe_put(writer_index_, value); - advance_writer_index_unchecked(4); + writer_index_ += 4; } /// write uint32_t value as fixed 4 bytes to buffer at current writer index. @@ -758,7 +757,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_uint32(uint32_t value) { grow(4); unsafe_put(writer_index_, value); - advance_writer_index_unchecked(4); + writer_index_ += 4; } /// write int64_t value as fixed 8 bytes to buffer at current writer index. @@ -766,7 +765,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_int64(int64_t value) { grow(8); unsafe_put(writer_index_, value); - advance_writer_index_unchecked(8); + writer_index_ += 8; } /// write float value as fixed 4 bytes to buffer at current writer index. @@ -774,7 +773,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_float(float value) { grow(4); unsafe_put(writer_index_, value); - advance_writer_index_unchecked(4); + writer_index_ += 4; } /// write double value as fixed 8 bytes to buffer at current writer index. @@ -782,7 +781,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_double(double value) { grow(8); unsafe_put(writer_index_, value); - advance_writer_index_unchecked(8); + writer_index_ += 8; } /// Write float16_t as fixed 2 bytes (raw IEEE 754 bits, little-endian). @@ -790,7 +789,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_f16(float16_t value) { grow(2); unsafe_put(writer_index_, value.to_bits()); - advance_writer_index_unchecked(2); + writer_index_ += 2; } /// Write bfloat16_t as fixed 2 bytes (raw IEEE 754 bits, little-endian). @@ -798,7 +797,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_bf16(bfloat16_t value) { grow(2); unsafe_put(writer_index_, value.to_bits()); - advance_writer_index_unchecked(2); + writer_index_ += 2; } /// write uint32_t value as varint to buffer at current writer index. @@ -806,7 +805,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_var_uint32(uint32_t value) { grow(8); // bulk write may write 8 bytes for varint32 uint32_t len = put_var_uint32_unchecked(writer_index_, value); - advance_writer_index_unchecked(len); + writer_index_ += len; } /// write int32_t value as varint (zigzag encoded) to buffer at current @@ -822,7 +821,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_var_uint64(uint64_t value) { grow(9); // Max 9 bytes for varint64 uint32_t len = put_var_uint64_unchecked(writer_index_, value); - advance_writer_index_unchecked(len); + writer_index_ += len; } /// write int64_t value as varint (zigzag encoded) to buffer at current @@ -848,7 +847,7 @@ class Buffer { FORY_ALWAYS_INLINE void write_bytes(const void *data, uint32_t length) { grow(length); unsafe_put(writer_index_, data, length); - advance_writer_index_unchecked(length); + writer_index_ += length; if (FORY_PREDICT_FALSE(output_stream_ != nullptr && writer_index_ > 4096)) { output_stream_->try_flush(); } @@ -1080,7 +1079,7 @@ class Buffer { grow(9); data_[writer_index_] = 0b1; unsafe_put(writer_index_ + 1, value); - advance_writer_index_unchecked(9); + writer_index_ += 9; } } @@ -1116,7 +1115,7 @@ class Buffer { grow(9); data_[writer_index_] = 0b1; unsafe_put(writer_index_ + 1, value); - advance_writer_index_unchecked(9); + writer_index_ += 9; } } @@ -1356,13 +1355,6 @@ class Buffer { return offset <= size_ && length <= size_ - offset; } - // Auto-growing writers call this only after grow() has proved the complete - // physical store extent and the uint32 writer-index range. Rechecking here - // would make every scalar write pay twice for the same safety proof. - FORY_ALWAYS_INLINE void advance_writer_index_unchecked(uint32_t diff) { - writer_index_ += diff; - } - FORY_ALWAYS_INLINE void grow_to_fit(uint32_t required_size) { constexpr uint64_t kMaxBufferSize = std::numeric_limits::max(); if (required_size <= size_) { diff --git a/dart/packages/fory/test/string_serializer_test.dart b/dart/packages/fory/test/string_serializer_test.dart index 1feec02fa7..566274aa8f 100644 --- a/dart/packages/fory/test/string_serializer_test.dart +++ b/dart/packages/fory/test/string_serializer_test.dart @@ -155,18 +155,6 @@ void main() { byteLength: 93, headerLength: 2, ), - ( - value: _repeat('a', 4095), - encoding: stringLatin1Encoding, - byteLength: 4095, - headerLength: 2, - ), - ( - value: _repeat('a', 4096), - encoding: stringLatin1Encoding, - byteLength: 4096, - headerLength: 3, - ), ]; for (final testCase in cases) { diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java index aac96e32f4..9e82a1d357 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java @@ -206,21 +206,7 @@ private MemoryBuffer(byte[] buffer, int offset, int length) { * @param streamReader a reader for reading from a stream. */ private MemoryBuffer(byte[] buffer, int offset, int length, ForyStreamReader streamReader) { - checkArgument(offset >= 0 && length >= 0); - if (offset > buffer.length - length) { - throw new IllegalArgumentException( - String.format("%d exceeds buffer size %d", (long) offset + length, buffer.length)); - } - if (AndroidSupport.IS_ANDROID) { - MemoryOps.initHeapBuffer(this, buffer, offset, length); - } else { - this.heapMemory = buffer; - this.heapOffset = offset; - final long startPos = BYTE_ARRAY_OFFSET + offset; - this.address = startPos; - this.size = length; - this.addressLimit = startPos + length; - } + initHeapBuffer(buffer, offset, length); if (streamReader != null) { this.streamReader = streamReader; } else { @@ -343,17 +329,17 @@ public MemoryBuffer getBuffer() { } public void initHeapBuffer(byte[] buffer, int offset, int length) { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + if (offset < 0 || length < 0 || offset > buffer.length - length) { + throw new IllegalArgumentException( + String.format( + "offset %d and length %d exceed buffer size %d", offset, length, buffer.length)); + } if (AndroidSupport.IS_ANDROID) { MemoryOps.initHeapBuffer(this, buffer, offset, length); } else { - if (buffer == null) { - throw new NullPointerException("buffer"); - } - if (offset < 0 || length < 0 || offset > buffer.length - length) { - throw new IllegalArgumentException( - String.format( - "offset %d and length %d exceed buffer size %d", offset, length, buffer.length)); - } this.heapMemory = buffer; this.heapOffset = offset; final long startPos = BYTE_ARRAY_OFFSET + offset; @@ -2033,10 +2019,9 @@ public void grow(int neededSize) { } private void growSlow(long length) { - if (length > Integer.MAX_VALUE) { + if (length < 0 || length > Integer.MAX_VALUE) { throwOOBException(); } - // MemoryAllocator owns the requested-capacity postcondition; do not recheck it here. globalAllocator.grow(this, (int) length); } @@ -2045,16 +2030,8 @@ public void ensure(int length) { // Negative extents are overflowed writer positions. Compare as unsigned so the common path // keeps one branch while overflow rejection and allocation stay cold before any Unsafe access. if (Integer.compareUnsigned(length, size) > 0) { - ensureSlow(length); - } - } - - private void ensureSlow(int length) { - if (length < 0) { - throwOOBException(); + growSlow(length); } - // MemoryAllocator owns the requested-capacity postcondition; do not recheck it here. - globalAllocator.grow(this, length); } // ------------------------------------------------------------------------- diff --git a/python/pyfory/cpp/pyfory.cc b/python/pyfory/cpp/pyfory.cc index 8c0c37ddca..de38b357ad 100644 --- a/python/pyfory/cpp/pyfory.cc +++ b/python/pyfory/cpp/pyfory.cc @@ -956,7 +956,7 @@ static int write_primitive_sequence(PyObject **items, Py_ssize_t size, } const uint64_t zigzag = (static_cast(v) << 1) ^ static_cast(v >> 63); - offset += buffer->put_var_uint64(offset, zigzag); + offset += buffer->put_var_uint64_unchecked(offset, zigzag); } buffer->increase_writer_index(offset - writer_index); } @@ -980,7 +980,7 @@ static int write_primitive_sequence(PyObject **items, Py_ssize_t size, } const uint32_t zigzag = (static_cast(v) << 1) ^ static_cast(v >> 31); - offset += buffer->put_var_uint32(offset, zigzag); + offset += buffer->put_var_uint32_unchecked(offset, zigzag); } buffer->increase_writer_index(offset - writer_index); } diff --git a/python/pyfory/number.pxi b/python/pyfory/number.pxi index 317cb10ad7..79cc2bc5e4 100644 --- a/python/pyfory/number.pxi +++ b/python/pyfory/number.pxi @@ -61,31 +61,18 @@ cdef class _ForyArray: cpdef Py_ssize_t _size(self): raise NotImplementedError - # These cpdef helpers are public; normalize here so direct calls share sequence bounds semantics. - cpdef object _get(self, Py_ssize_t index): - return self._get_unchecked(self._normalize_index(index)) - cdef object _get_unchecked(self, Py_ssize_t index): raise NotImplementedError - cpdef _set(self, Py_ssize_t index, object value): - self._set_unchecked(self._normalize_index(index), value) - cdef _set_unchecked(self, Py_ssize_t index, object value): raise NotImplementedError cpdef _append(self, object value): raise NotImplementedError - cpdef _insert(self, Py_ssize_t index, object value): - self._insert_unchecked(self._normalize_insert_index(index), value) - cdef _insert_unchecked(self, Py_ssize_t index, object value): raise NotImplementedError - cpdef _delete(self, Py_ssize_t index): - self._delete_unchecked(self._normalize_index(index)) - cdef _delete_unchecked(self, Py_ssize_t index): raise NotImplementedError diff --git a/python/pyfory/tests/test_buffer.py b/python/pyfory/tests/test_buffer.py index 3a29bb6467..f13590a2d4 100644 --- a/python/pyfory/tests/test_buffer.py +++ b/python/pyfory/tests/test_buffer.py @@ -341,43 +341,6 @@ def test_bit_helper_ranges(): set_bit_to(buffer, 2**32 - 1, 2**32 - 1, True) -@pytest.mark.parametrize( - "array_type,values", - [ - (pyfory.BoolArray, [True]), - (pyfory.Int8Array, [1]), - (pyfory.Int16Array, [1]), - (pyfory.Int32Array, [1]), - (pyfory.Int64Array, [1]), - (pyfory.UInt8Array, [1]), - (pyfory.UInt16Array, [1]), - (pyfory.UInt32Array, [1]), - (pyfory.UInt64Array, [1]), - (pyfory.Float16Array, [1.0]), - (pyfory.BFloat16Array, [1.0]), - (pyfory.Float32Array, [1.0]), - (pyfory.Float64Array, [1.0]), - ], -) -def test_native_array_range_owner(array_type, values): - value = array_type(values) - assert value[0] == values[0] - assert value._get(-1) == values[0] - value._set(-1, values[0]) - with pytest.raises(Exception): - _ = value[1] - with pytest.raises(Exception): - value._get(1) - with pytest.raises(Exception): - value._set(1, values[0]) - with pytest.raises(Exception): - value._delete(1) - value._insert(99, values[0]) - assert len(value) == 2 - value._delete(-1) - assert value == values - - def test_hash_buffer_empty_input(): assert pyfory.mmh3.hash_buffer(b"") == (0, 0) From 7b798c1040bfe093a176338e9b892ec4a9bc453f Mon Sep 17 00:00:00 2001 From: Release Validation Test Date: Thu, 27 Aug 2026 11:48:07 +0800 Subject: [PATCH 4/6] docs: refine codec ownership guidance --- .agents/languages/cpp.md | 5 +++++ .agents/languages/go.md | 5 +++++ .agents/languages/java.md | 9 +++++++++ AGENTS.md | 6 ++++-- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.agents/languages/cpp.md b/.agents/languages/cpp.md index aae2f0399b..c5a45fa410 100644 --- a/.agents/languages/cpp.md +++ b/.agents/languages/cpp.md @@ -13,6 +13,11 @@ Load this file when changing `cpp/`, Cython build plumbing, or C++ xlang behavio - When invoking a method that returns `Result`, use `FORY_TRY` unless you are in control-flow logic that cannot use it cleanly. - Wrap error checks with `FORY_PREDICT_FALSE` for branch prediction. - Continue on trivial errors; return early only for critical errors such as buffer overflow. +- `ReadContext` intentionally records codec errors for inspection at existing serializer or root + safepoints. After an error, work may continue only while it remains bounds-safe and cannot cause + resource amplification, publish reference or cache state that survives root cleanup, or return + success past the required safepoint. Do not add per-field checks, cursor rollback, or tests that + pin the first detection point solely to make an error earlier or more precise. - Put private methods last in class definitions, immediately before private fields. - Do not redesign alias-based or low-level public type shapes to add convenience methods unless the user explicitly asks for that API change. - For cross-language feature ports, match protocol behavior but use idiomatic C++ ownership and layering instead of mirroring Java structure literally. diff --git a/.agents/languages/go.md b/.agents/languages/go.md index 829383509a..5192fc3e7b 100644 --- a/.agents/languages/go.md +++ b/.agents/languages/go.md @@ -7,6 +7,11 @@ Load this file when changing `go/fory/` or Go xlang behavior. - Run Go commands from within `go/fory/`. - Changes under `go/` must pass formatting and tests. - The Go implementation focuses on fast serializers. +- Go `ReadContext` intentionally defers codec errors to existing `HasError` or `CheckError` + boundaries. After an error, work may continue only while it remains panic- and bounds-safe and + cannot cause disproportionate work or allocation, publish state that survives root cleanup, or + return success past the required boundary. Do not add per-field or per-element checks, cursor + rollback, or tests that pin the first detection point solely to change error timing or precision. - Root deserialization graph memory budget state belongs to `ReadContext`. `WithMaxGraphMemoryBytes` uses a fixed `128 MiB` default; positive explicit values override it, and explicit non-positive values are invalid at config creation. diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 93d058fea7..707031a6a4 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -159,6 +159,15 @@ Load this file when changing anything under `java/` or when Java drives a cross- - In `MemoryBuffer` and `MemoryOps` hot paths, duplicate small straight-line copy/read/write logic when that keeps control flow direct. Do not add private helper indirection to hot paths just to reduce local code duplication; keep helpers for slow, cold, or error paths. +- Preserve the JDK 25 `MemoryBuffer` ownership documented by its class Javadoc: read/write entry + points own logical range validation, while internal absolute get/put fast paths receive proven + legal indices and rely on indexed array, absolute `ByteBuffer`, or `VarHandle` access for physical + bounds. Do not repeat the entry-point check inside those accessors solely to move or normalize + error detection. +- `MemoryAllocator.grow` owns the postcondition that a successful return leaves the buffer capacity + at least the requested capacity. Callers must reject invalid or overflowed requests before the + call, but must not recheck the allocator postcondition afterward; fix a violating allocator at + the allocator implementation. - In JDK 25 Fory JSON C2-sensitive code, preserve measured, naturally large hot-method boundaries. A method that exceeds HotSpot's 325-byte hot-inline limit through real representation, scalar, array, collection, or generated-schema work is an independent subtree owner. Generated group diff --git a/AGENTS.md b/AGENTS.md index e985c28e4d..d3aae6514c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,8 +47,10 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th - Malformed input must surface as a controlled root-operation error and still run root cleanup, but the exact exception type, error code, message, detection layer, and detection point are not contracts unless a public API or - specification explicitly says otherwise. An existing bounded downstream - buffer, type, reference, depth, or serializer error is sufficient. Do not add + specification explicitly says otherwise. Differences only in error type, + message, layer, offset, or detection point are not security findings. An + existing bounded downstream buffer, type, reference, depth, or serializer + error is sufficient. Do not add hot-path branches, helper APIs, allocations, or generated-code expansion solely to make an error earlier, more specific, or more uniform, and do not write tests that force such error normalization. From 84f17ecfd9f44fae3761c4f27c3e19708bc655d7 Mon Sep 17 00:00:00 2001 From: Release Validation Test Date: Thu, 27 Aug 2026 11:51:00 +0800 Subject: [PATCH 5/6] fix(rust): preserve cursor on invalid varuint36 --- rust/fory-core/src/buffer.rs | 3 ++- rust/tests/tests/test_buffer.rs | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rust/fory-core/src/buffer.rs b/rust/fory-core/src/buffer.rs index 5bfa4707c3..731014a4b0 100644 --- a/rust/fory-core/src/buffer.rs +++ b/rust/fory-core/src/buffer.rs @@ -1080,7 +1080,8 @@ impl<'a> Reader<'a> { let slice = self.slice_after_cursor(); if slice.len() >= 8 { - let bulk = self.read_u64()?; + // Decode speculatively without advancing so malformed input leaves the cursor intact. + let bulk = LittleEndian::read_u64(&slice[..8]); let mut result = bulk & 0x7F; let mut read_idx = start; diff --git a/rust/tests/tests/test_buffer.rs b/rust/tests/tests/test_buffer.rs index f18e4b3702..3ce7c3c38c 100644 --- a/rust/tests/tests/test_buffer.rs +++ b/rust/tests/tests/test_buffer.rs @@ -112,6 +112,11 @@ fn test_var_u36_bad_framing() { let mut reader = Reader::new(&bytes); assert!(reader.read_var_u36_small().is_err()); } + + let padded = [0x80, 0x80, 0x80, 0x80, 0x80, 0x02, 0x2a, 0x2b]; + let mut reader = Reader::new(&padded); + assert!(reader.read_var_u36_small().is_err()); + assert_eq!(reader.get_cursor(), 0); } #[test] From e454a4571e1f223c7f9b48edb050dda1fc912117 Mon Sep 17 00:00:00 2001 From: Release Validation Test Date: Thu, 27 Aug 2026 12:17:56 +0800 Subject: [PATCH 6/6] fix: preserve codec hot paths and byte order --- .agents/languages/java.md | 22 +- AGENTS.md | 4 + cpp/fory/serialization/stream_test.cc | 34 ++ cpp/fory/serialization/string_serializer.h | 10 +- .../fory/lib/src/memory/buffer_mixin.dart | 54 ++- .../fory/test/string_serializer_test.dart | 12 + .../apache/fory/memory/MemoryAllocator.java | 5 +- .../org/apache/fory/memory/MemoryBuffer.java | 419 ++++++++++++++---- .../serializer/PrimitiveArraySerializers.java | 236 +--------- .../org/apache/fory/memory/MemoryBuffer.java | 194 +++++++- .../apache/fory/memory/MemoryBufferTest.java | 114 ++++- rust/fory-core/src/buffer.rs | 28 +- 12 files changed, 758 insertions(+), 374 deletions(-) diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 707031a6a4..a96d04b7f0 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -159,11 +159,23 @@ Load this file when changing anything under `java/` or when Java drives a cross- - In `MemoryBuffer` and `MemoryOps` hot paths, duplicate small straight-line copy/read/write logic when that keeps control flow direct. Do not add private helper indirection to hot paths just to reduce local code duplication; keep helpers for slow, cold, or error paths. -- Preserve the JDK 25 `MemoryBuffer` ownership documented by its class Javadoc: read/write entry - points own logical range validation, while internal absolute get/put fast paths receive proven - legal indices and rely on indexed array, absolute `ByteBuffer`, or `VarHandle` access for physical - bounds. Do not repeat the entry-point check inside those accessors solely to move or normalize - error detection. +- `MemoryBuffer` semantic primitive-array `write*` and `read*` methods own the canonical + little-endian element order. Keep native bulk copy as the little-endian hot path and isolate + big-endian conversion in separate slow helpers; serializers must not duplicate that endian + branch. The explicit `copyTo*Array` and `copyFrom*Array` methods remain raw native-memory copies, + so their format owner must handle byte order when required. +- Add a Java `MemoryBuffer` check only when its absence can cause a JVM or native crash, OOM, or + attacker-controlled memory amplification. Delayed, masked, less precise, or differently typed + failures do not justify a check, and neither does an incorrect decoded result without one of + those crash or memory consequences. Do not duplicate an array, `ByteBuffer`, `VarHandle`, stream, + or other existing bounds owner merely to move or normalize an error. +- The JDK 25 `MemoryBuffer` overlay intentionally does not duplicate logical range checks around + indexed array, absolute `ByteBuffer`, or `VarHandle` access. Those JVM accessors own physical + bounds enforcement and already provide a controlled failure; the exact exception type, message, + and detection point are not contracts. Do not copy JDK 8-24 Unsafe-path checks into the overlay + solely to make invalid access fail earlier or more precisely. Keep an explicit check before + allocation, capacity growth, or another side effect when it is needed to prevent a crash, OOM, or + attacker-controlled memory amplification that the access owner cannot contain. - `MemoryAllocator.grow` owns the postcondition that a successful return leaves the buffer capacity at least the requested capacity. Callers must reject invalid or overflowed requests before the call, but must not recheck the allocator postcondition afterward; fix a violating allocator at diff --git a/AGENTS.md b/AGENTS.md index d3aae6514c..10f82994a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,6 +202,10 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th - Do not make design tradeoffs the user did not request. If a refactor appears to require a behavior, logic, protocol, or performance tradeoff, stop and ask. - Treat existing low-level or optimized code as deliberate by default. During a refactor, preserve the current implementation strategy unless the user explicitly asks to redesign or optimize it. - Do not replace existing C, C++, Cython, unsafe, or other low-level optimized paths with simpler high-level implementations just to make a refactor easier. +- When removing a redundant wrapper or helper, preserve any aggregate capacity proof, fused + operation, reserved wide store, specialized overload, or unchecked primitive path in the natural + owner. Do not route that work through a generic checked path unless matched benchmarks justify the + implementation change. - If a refactor accidentally changes logic or implementation strategy, revert that part and re-implement the refactor around the existing logic. - Use English only in code, comments, and documentation. - Do not use emoji in documentation, including headings, feature lists, status diff --git a/cpp/fory/serialization/stream_test.cc b/cpp/fory/serialization/stream_test.cc index 4d05d50a86..67982cddd9 100644 --- a/cpp/fory/serialization/stream_test.cc +++ b/cpp/fory/serialization/stream_test.cc @@ -155,6 +155,25 @@ class OneByteOStream final : public std::ostream { OneByteOutputStreamBuf buf_; }; +class CountingOutputStream final : public OutputStream { +public: + Result write_to_stream(const uint8_t *src, + uint32_t length) override { + ++write_calls_; + data_.insert(data_.end(), src, src + length); + return Result(); + } + + Result flush_stream() override { return Result(); } + + const std::vector &data() const { return data_; } + uint32_t write_calls() const { return write_calls_; } + +private: + std::vector data_; + uint32_t write_calls_ = 0; +}; + static inline void register_stream_types(Fory &fory) { uint32_t type_id = 1; fory.register_struct(type_id++); @@ -360,6 +379,21 @@ TEST(StreamSerializationTest, SerializeToOutputStreamRoundTrip) { EXPECT_EQ(roundtrip.value(), original); } +TEST(StreamSerializationTest, LargeStringsFlushIncrementally) { + auto fory = + Fory::builder().xlang(true).compatible(false).track_ref(false).build(); + std::vector original{std::string(5000, 'a'), + std::string(5000, 'b')}; + auto expected = fory.serialize(original); + ASSERT_TRUE(expected.ok()) << expected.error().to_string(); + + CountingOutputStream writer; + auto streamed = fory.serialize(writer, original); + ASSERT_TRUE(streamed.ok()) << streamed.error().to_string(); + EXPECT_GE(writer.write_calls(), 2U); + EXPECT_EQ(writer.data(), expected.value()); +} + TEST(StreamSerializationTest, SerializeToOStreamOverloadParity) { auto fory = Fory::builder().xlang(true).compatible(false).track_ref(true).build(); diff --git a/cpp/fory/serialization/string_serializer.h b/cpp/fory/serialization/string_serializer.h index 80b5a9b306..21ea099432 100644 --- a/cpp/fory/serialization/string_serializer.h +++ b/cpp/fory/serialization/string_serializer.h @@ -61,10 +61,16 @@ inline void write_string_data(const char *data, size_t size, const uint64_t length = static_cast(size); const uint64_t size_with_encoding = (length << 2) | static_cast(StringEncoding::UTF8); - ctx.write_var_uint36_small(size_with_encoding); + Buffer &buffer = ctx.buffer(); + buffer.grow(static_cast(size + 9)); + uint32_t writer_index = buffer.writer_index(); + writer_index += + buffer.put_var_uint64_unchecked(writer_index, size_with_encoding); if (size > 0) { - ctx.write_bytes(data, static_cast(size)); + buffer.unsafe_put(writer_index, data, static_cast(size)); } + buffer.unsafe_set_writer_index(writer_index + static_cast(size)); + ctx.try_flush(); } /// write UTF-16 string data, converting to UTF-8 or using native encoding diff --git a/dart/packages/fory/lib/src/memory/buffer_mixin.dart b/dart/packages/fory/lib/src/memory/buffer_mixin.dart index d41f1f1f58..5b4692ea29 100644 --- a/dart/packages/fory/lib/src/memory/buffer_mixin.dart +++ b/dart/packages/fory/lib/src/memory/buffer_mixin.dart @@ -343,16 +343,62 @@ mixin _BufferMixin { if (value < 0 || value > 0xfffffffff) { _throwInvalidVarUint36(); } - writeVarUint64(Uint64(value)); + ensureWritable(6); + if (value > 0x7fffffff) { + _writeLargeVarUint36(value); + return; + } + var remaining = value; + while (remaining >= 0x80) { + _bytes[_writerIndex] = (remaining & 0x7f) | 0x80; + _writerIndex += 1; + remaining >>>= 7; + } + _bytes[_writerIndex] = remaining; + _writerIndex += 1; + } + + @pragma('vm:never-inline') + void _writeLargeVarUint36(int value) { + var remaining = value; + while (remaining >= 0x80) { + _bytes[_writerIndex] = (remaining % 0x80) | 0x80; + _writerIndex += 1; + remaining ~/= 0x80; + } + _bytes[_writerIndex] = remaining; + _writerIndex += 1; } /// Reads a small unsigned integer written by [writeVarUint36Small]. + @pragma('vm:prefer-inline') int readVarUint36Small() { - final value = readVarUint64(); - if (value > 0xfffffffff) { + final byte = readUint8(); + if (byte < 0x80) { + return byte; + } + return _readVarUint36SmallTail(byte & 0x7f); + } + + @pragma('vm:never-inline') + int _readVarUint36SmallTail(int result) { + var value = result; + var factor = 0x80; + // The multi-byte form is a standard varuint: the fifth byte still has a + // continuation bit, and 36-bit values can therefore require six bytes. + for (var index = 1; index < 5; index += 1) { + final byte = readUint8(); + value += (byte & 0x7f) * factor; + if (byte < 0x80) { + return value; + } + factor *= 0x80; + } + final sixthByte = readUint8(); + if (sixthByte > 1) { _throwInvalidVarUint36(); } - return value.toInt(); + return value + sixthByte * factor; } } diff --git a/dart/packages/fory/test/string_serializer_test.dart b/dart/packages/fory/test/string_serializer_test.dart index 566274aa8f..1feec02fa7 100644 --- a/dart/packages/fory/test/string_serializer_test.dart +++ b/dart/packages/fory/test/string_serializer_test.dart @@ -155,6 +155,18 @@ void main() { byteLength: 93, headerLength: 2, ), + ( + value: _repeat('a', 4095), + encoding: stringLatin1Encoding, + byteLength: 4095, + headerLength: 2, + ), + ( + value: _repeat('a', 4096), + encoding: stringLatin1Encoding, + byteLength: 4096, + headerLength: 3, + ), ]; for (final testCase in cases) { diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java index f7c45d4b65..55010323ee 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryAllocator.java @@ -31,8 +31,9 @@ public interface MemoryAllocator { /** * Grows an existing buffer to accommodate the new capacity. The implementation must grow the - * buffer in-place by modifying the existing buffer instance and leave its size at least {@code - * newCapacity}. + * buffer in-place by modifying the existing buffer instance. A successful return guarantees that + * {@code buffer.size()} is at least {@code newCapacity}; callers intentionally do not repeat this + * postcondition check on the hot path. * * @param buffer the existing buffer to grow * @param newCapacity the required new capacity diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java index 9e82a1d357..8738beab9d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java @@ -1776,12 +1776,22 @@ public void writeBooleansWithSize(boolean[] values) { MemoryOps.writeBooleansWithSize(this, values); } else { writeVarUInt32Small7(values.length); - writeBooleans(values, 0, values.length); + writeBooleans(values); } } + // Full-array overloads own their source range and stay direct so serializers do not inline the + // larger arbitrary-range validation path. Offset/count overloads below retain that validation. public void writeBooleans(boolean[] values) { - writeBooleans(values, 0, values.length); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeBooleans(this, values, 0, values.length); + } else { + final int writerIdx = writerIndex; + final int newIdx = writerIdx + values.length; + ensure(newIdx); + copyMemory(values, BOOLEAN_ARRAY_OFFSET, heapMemory, address + writerIdx, values.length); + writerIndex = newIdx; + } } public void writeBooleans(boolean[] values, int offset, int numElements) { @@ -1806,12 +1816,25 @@ public void writeCharsWithSize(char[] values) { } else { int numBytes = Math.multiplyExact(values.length, 2); writeVarUInt32Small7(numBytes); - writeChars(values, 0, values.length); + writeChars(values); } } public void writeChars(char[] values) { - writeChars(values, 0, values.length); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeChars(this, values, 0, values.length); + } else { + int numBytes = Math.multiplyExact(values.length, 2); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + if (LITTLE_ENDIAN) { + copyMemory(values, CHAR_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); + } else { + writeCharsBigEndian(values, 0, values.length, writerIdx); + } + writerIndex = newIdx; + } } public void writeChars(char[] values, int offset, int numElements) { @@ -1825,12 +1848,16 @@ public void writeChars(char[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - copyMemory( - values, - CHAR_ARRAY_OFFSET + ((long) offset << 1), - heapMemory, - address + writerIdx, - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + values, + CHAR_ARRAY_OFFSET + ((long) offset << 1), + heapMemory, + address + writerIdx, + numBytes); + } else { + writeCharsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } } @@ -1841,12 +1868,25 @@ public void writeShortsWithSize(short[] values) { } else { int numBytes = Math.multiplyExact(values.length, 2); writeVarUInt32Small7(numBytes); - writeShorts(values, 0, values.length); + writeShorts(values); } } public void writeShorts(short[] values) { - writeShorts(values, 0, values.length); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeShorts(this, values, 0, values.length); + } else { + int numBytes = Math.multiplyExact(values.length, 2); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + if (LITTLE_ENDIAN) { + copyMemory(values, SHORT_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); + } else { + writeShortsBigEndian(values, 0, values.length, writerIdx); + } + writerIndex = newIdx; + } } public void writeShorts(short[] values, int offset, int numElements) { @@ -1860,12 +1900,16 @@ public void writeShorts(short[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - copyMemory( - values, - SHORT_ARRAY_OFFSET + ((long) offset << 1), - heapMemory, - address + writerIdx, - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + values, + SHORT_ARRAY_OFFSET + ((long) offset << 1), + heapMemory, + address + writerIdx, + numBytes); + } else { + writeShortsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } } @@ -1876,12 +1920,25 @@ public void writeIntsWithSize(int[] values) { } else { int numBytes = Math.multiplyExact(values.length, 4); writeVarUInt32Small7(numBytes); - writeInts(values, 0, values.length); + writeInts(values); } } public void writeInts(int[] values) { - writeInts(values, 0, values.length); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeInts(this, values, 0, values.length); + } else { + int numBytes = Math.multiplyExact(values.length, 4); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + if (LITTLE_ENDIAN) { + copyMemory(values, INT_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); + } else { + writeIntsBigEndian(values, 0, values.length, writerIdx); + } + writerIndex = newIdx; + } } public void writeInts(int[] values, int offset, int numElements) { @@ -1895,12 +1952,16 @@ public void writeInts(int[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - copyMemory( - values, - INT_ARRAY_OFFSET + ((long) offset << 2), - heapMemory, - address + writerIdx, - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + values, + INT_ARRAY_OFFSET + ((long) offset << 2), + heapMemory, + address + writerIdx, + numBytes); + } else { + writeIntsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } } @@ -1911,12 +1972,25 @@ public void writeLongsWithSize(long[] values) { } else { int numBytes = Math.multiplyExact(values.length, 8); writeVarUInt32Small7(numBytes); - writeLongs(values, 0, values.length); + writeLongs(values); } } public void writeLongs(long[] values) { - writeLongs(values, 0, values.length); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeLongs(this, values, 0, values.length); + } else { + int numBytes = Math.multiplyExact(values.length, 8); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + if (LITTLE_ENDIAN) { + copyMemory(values, LONG_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); + } else { + writeLongsBigEndian(values, 0, values.length, writerIdx); + } + writerIndex = newIdx; + } } public void writeLongs(long[] values, int offset, int numElements) { @@ -1930,12 +2004,16 @@ public void writeLongs(long[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - copyMemory( - values, - LONG_ARRAY_OFFSET + ((long) offset << 3), - heapMemory, - address + writerIdx, - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + values, + LONG_ARRAY_OFFSET + ((long) offset << 3), + heapMemory, + address + writerIdx, + numBytes); + } else { + writeLongsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } } @@ -1946,12 +2024,25 @@ public void writeFloatsWithSize(float[] values) { } else { int numBytes = Math.multiplyExact(values.length, 4); writeVarUInt32Small7(numBytes); - writeFloats(values, 0, values.length); + writeFloats(values); } } public void writeFloats(float[] values) { - writeFloats(values, 0, values.length); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeFloats(this, values, 0, values.length); + } else { + int numBytes = Math.multiplyExact(values.length, 4); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + if (LITTLE_ENDIAN) { + copyMemory(values, FLOAT_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); + } else { + writeFloatsBigEndian(values, 0, values.length, writerIdx); + } + writerIndex = newIdx; + } } public void writeFloats(float[] values, int offset, int numElements) { @@ -1965,12 +2056,16 @@ public void writeFloats(float[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - copyMemory( - values, - FLOAT_ARRAY_OFFSET + ((long) offset << 2), - heapMemory, - address + writerIdx, - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + values, + FLOAT_ARRAY_OFFSET + ((long) offset << 2), + heapMemory, + address + writerIdx, + numBytes); + } else { + writeFloatsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } } @@ -1981,12 +2076,25 @@ public void writeDoublesWithSize(double[] values) { } else { int numBytes = Math.multiplyExact(values.length, 8); writeVarUInt32Small7(numBytes); - writeDoubles(values, 0, values.length); + writeDoubles(values); } } public void writeDoubles(double[] values) { - writeDoubles(values, 0, values.length); + if (AndroidSupport.IS_ANDROID) { + MemoryOps.writeDoubles(this, values, 0, values.length); + } else { + int numBytes = Math.multiplyExact(values.length, 8); + final int writerIdx = writerIndex; + final int newIdx = writerIdx + numBytes; + ensure(newIdx); + if (LITTLE_ENDIAN) { + copyMemory(values, DOUBLE_ARRAY_OFFSET, heapMemory, address + writerIdx, numBytes); + } else { + writeDoublesBigEndian(values, 0, values.length, writerIdx); + } + writerIndex = newIdx; + } } public void writeDoubles(double[] values, int offset, int numElements) { @@ -2000,16 +2108,58 @@ public void writeDoubles(double[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - copyMemory( - values, - DOUBLE_ARRAY_OFFSET + ((long) offset << 3), - heapMemory, - address + writerIdx, - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + values, + DOUBLE_ARRAY_OFFSET + ((long) offset << 3), + heapMemory, + address + writerIdx, + numBytes); + } else { + writeDoublesBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } } + // Big-endian JVMs use the scalar little-endian primitives here. Keeping these methods separate + // leaves native bulk copy as the only little-endian hot path. + private void writeCharsBigEndian(char[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt16(writerIdx + i * 2, (short) values[offset + i]); + } + } + + private void writeShortsBigEndian(short[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt16(writerIdx + i * 2, values[offset + i]); + } + } + + private void writeIntsBigEndian(int[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt32(writerIdx + i * 4, values[offset + i]); + } + } + + private void writeLongsBigEndian(long[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt64(writerIdx + i * 8, values[offset + i]); + } + } + + private void writeFloatsBigEndian(float[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt32(writerIdx + i * 4, Float.floatToRawIntBits(values[offset + i])); + } + } + + private void writeDoublesBigEndian(double[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt64(writerIdx + i * 8, Double.doubleToRawLongBits(values[offset + i])); + } + } + /** For off-heap buffer, this will make a heap buffer internally. */ public void grow(int neededSize) { long length = (long) writerIndex + neededSize; @@ -2022,6 +2172,7 @@ private void growSlow(long length) { if (length < 0 || length > Integer.MAX_VALUE) { throwOOBException(); } + // MemoryAllocator owns the requested-capacity postcondition; do not recheck it here. globalAllocator.grow(this, (int) length); } @@ -3448,7 +3599,11 @@ public void readCharArrayBytes(char[] values, int numBytes) { streamReader.readChars(values, 0, numBytes >>> 1); return; } - copyMemory(heapMemory, address + readerIdx, values, CHAR_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + copyMemory(heapMemory, address + readerIdx, values, CHAR_ARRAY_OFFSET, numBytes); + } else { + readCharsBigEndian(values, 0, numBytes >>> 1, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3469,7 +3624,11 @@ public void readInt16ArrayBytes(short[] values, int numBytes) { streamReader.readShorts(values, 0, numBytes >>> 1); return; } - copyMemory(heapMemory, address + readerIdx, values, SHORT_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + copyMemory(heapMemory, address + readerIdx, values, SHORT_ARRAY_OFFSET, numBytes); + } else { + readShortsBigEndian(values, 0, numBytes >>> 1, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3490,7 +3649,11 @@ public void readInt32ArrayBytes(int[] values, int numBytes) { streamReader.readInts(values, 0, numBytes >>> 2); return; } - copyMemory(heapMemory, address + readerIdx, values, INT_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + copyMemory(heapMemory, address + readerIdx, values, INT_ARRAY_OFFSET, numBytes); + } else { + readIntsBigEndian(values, 0, numBytes >>> 2, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3511,7 +3674,11 @@ public void readInt64ArrayBytes(long[] values, int numBytes) { streamReader.readLongs(values, 0, numBytes >>> 3); return; } - copyMemory(heapMemory, address + readerIdx, values, LONG_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + copyMemory(heapMemory, address + readerIdx, values, LONG_ARRAY_OFFSET, numBytes); + } else { + readLongsBigEndian(values, 0, numBytes >>> 3, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3532,7 +3699,11 @@ public void readFloat32ArrayBytes(float[] values, int numBytes) { streamReader.readFloats(values, 0, numBytes >>> 2); return; } - copyMemory(heapMemory, address + readerIdx, values, FLOAT_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + copyMemory(heapMemory, address + readerIdx, values, FLOAT_ARRAY_OFFSET, numBytes); + } else { + readFloatsBigEndian(values, 0, numBytes >>> 2, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3553,7 +3724,11 @@ public void readFloat64ArrayBytes(double[] values, int numBytes) { streamReader.readDoubles(values, 0, numBytes >>> 3); return; } - copyMemory(heapMemory, address + readerIdx, values, DOUBLE_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + copyMemory(heapMemory, address + readerIdx, values, DOUBLE_ARRAY_OFFSET, numBytes); + } else { + readDoublesBigEndian(values, 0, numBytes >>> 3, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3595,12 +3770,16 @@ public void readChars(char[] chars, int offset, int numElements) { return; } int readerIdx = readerIndex; - copyMemory( - heapMemory, - address + readerIdx, - chars, - CHAR_ARRAY_OFFSET + ((long) offset << 1), - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + heapMemory, + address + readerIdx, + chars, + CHAR_ARRAY_OFFSET + ((long) offset << 1), + numBytes); + } else { + readCharsBigEndian(chars, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3633,12 +3812,16 @@ public void readShorts(short[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - copyMemory( - heapMemory, - address + readerIdx, - values, - SHORT_ARRAY_OFFSET + ((long) offset << 1), - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + heapMemory, + address + readerIdx, + values, + SHORT_ARRAY_OFFSET + ((long) offset << 1), + numBytes); + } else { + readShortsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3657,12 +3840,16 @@ public void readInts(int[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - copyMemory( - heapMemory, - address + readerIdx, - values, - INT_ARRAY_OFFSET + ((long) offset << 2), - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + heapMemory, + address + readerIdx, + values, + INT_ARRAY_OFFSET + ((long) offset << 2), + numBytes); + } else { + readIntsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3681,12 +3868,16 @@ public void readLongs(long[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - copyMemory( - heapMemory, - address + readerIdx, - values, - LONG_ARRAY_OFFSET + ((long) offset << 3), - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + heapMemory, + address + readerIdx, + values, + LONG_ARRAY_OFFSET + ((long) offset << 3), + numBytes); + } else { + readLongsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3705,12 +3896,16 @@ public void readFloats(float[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - copyMemory( - heapMemory, - address + readerIdx, - values, - FLOAT_ARRAY_OFFSET + ((long) offset << 2), - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + heapMemory, + address + readerIdx, + values, + FLOAT_ARRAY_OFFSET + ((long) offset << 2), + numBytes); + } else { + readFloatsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } } @@ -3729,16 +3924,56 @@ public void readDoubles(double[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - copyMemory( - heapMemory, - address + readerIdx, - values, - DOUBLE_ARRAY_OFFSET + ((long) offset << 3), - numBytes); + if (LITTLE_ENDIAN) { + copyMemory( + heapMemory, + address + readerIdx, + values, + DOUBLE_ARRAY_OFFSET + ((long) offset << 3), + numBytes); + } else { + readDoublesBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } } + private void readCharsBigEndian(char[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = (char) _unsafeGetInt16(readerIdx + i * 2); + } + } + + private void readShortsBigEndian(short[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = _unsafeGetInt16(readerIdx + i * 2); + } + } + + private void readIntsBigEndian(int[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = _unsafeGetInt32(readerIdx + i * 4); + } + } + + private void readLongsBigEndian(long[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = _unsafeGetInt64(readerIdx + i * 8); + } + } + + private void readFloatsBigEndian(float[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = Float.intBitsToFloat(_unsafeGetInt32(readerIdx + i * 4)); + } + } + + private void readDoublesBigEndian(double[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = Double.longBitsToDouble(_unsafeGetInt64(readerIdx + i * 8)); + } + } + public void checkReadableBytes(int minimumReadableBytes) { // use subtract to avoid overflow int remaining = size - readerIndex; diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/PrimitiveArraySerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/PrimitiveArraySerializers.java index 1dc6060692..30a333ce55 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/PrimitiveArraySerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/PrimitiveArraySerializers.java @@ -27,7 +27,6 @@ import org.apache.fory.context.WriteContext; import org.apache.fory.exception.DeserializationException; import org.apache.fory.memory.MemoryBuffer; -import org.apache.fory.memory.NativeByteOrder; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.type.BFloat16Array; import org.apache.fory.type.Float16Array; @@ -138,11 +137,7 @@ protected final short[] readShortBits(ReadContext readContext) { buf.checkReadableBytes(size); reserveArray(readContext, numElements, 2); short[] values = new short[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buf.readInt16ArrayBytes(values, size); - } else { - readInt16BySwapEndian(buf, values, numElements); - } + buf.readInt16ArrayBytes(values, size); return values; } int size = buffer.readVarUInt32Small7(); @@ -156,11 +151,7 @@ protected final short[] readShortBits(ReadContext readContext) { buffer.checkReadableBytes(size); reserveArray(readContext, numElements, 2); short[] values = new short[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.readInt16ArrayBytes(values, size); - } else { - readInt16BySwapEndian(buffer, values, numElements); - } + buffer.readInt16ArrayBytes(values, size); return values; } } @@ -280,28 +271,13 @@ public void write(WriteContext writeContext, char[] value) { throw new UnsupportedOperationException(); } if (writeContext.getBufferCallback() == null) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.writeCharsWithSize(value); - } else { - writeCharBySwapEndian(buffer, value); - } + buffer.writeCharsWithSize(value); } else { writeContext.writeBufferObject( new PrimitiveArrayBufferObject(value, Types.UINT16_ARRAY, 2, value.length)); } } - private void writeCharBySwapEndian(MemoryBuffer buffer, char[] value) { - int idx = buffer.writerIndex(); - int length = value.length; - buffer.ensure(idx + 5 + length * 2); - idx += buffer._unsafeWriteVarUInt32(length * 2); - for (int i = 0; i < length; i++) { - buffer._unsafePutInt16(idx + i * 2, (short) value[i]); - } - buffer._unsafeWriterIndex(idx + length * 2); - } - @Override public char[] copy(CopyContext copyContext, char[] originArray) { return Arrays.copyOf(originArray, originArray.length); @@ -323,11 +299,7 @@ public char[] read(ReadContext readContext) { buf.checkReadableBytes(size); reserveArray(readContext, numElements, 2); char[] values = new char[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buf.readCharArrayBytes(values, size); - } else { - readCharBySwapEndian(buf, values, numElements); - } + buf.readCharArrayBytes(values, size); return values; } int size = buffer.readVarUInt32Small7(); @@ -341,22 +313,8 @@ public char[] read(ReadContext readContext) { buffer.checkReadableBytes(size); reserveArray(readContext, numElements, 2); char[] values = new char[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.readCharArrayBytes(values, size); - } else { - readCharBySwapEndian(buffer, values, numElements); - } - return values; - } - - private void readCharBySwapEndian(MemoryBuffer buffer, char[] values, int numElements) { - int size = numElements << 1; - // Do not loop through MemoryBuffer._unsafeGet* here; those helpers carry Android dispatch. - // Copy the body bytes once, then byte-swap the destination values locally. buffer.readCharArrayBytes(values, size); - for (int i = 0; i < numElements; i++) { - values[i] = Character.reverseBytes(values[i]); - } + return values; } } @@ -394,28 +352,13 @@ public void write(WriteContext writeContext, int[] value) { writeInt32Compressed(buffer, value); return; } - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.writeIntsWithSize(value); - } else { - writeInt32BySwapEndian(buffer, value); - } + buffer.writeIntsWithSize(value); } else { writeContext.writeBufferObject( new PrimitiveArrayBufferObject(value, Types.INT32_ARRAY, 4, value.length)); } } - private void writeInt32BySwapEndian(MemoryBuffer buffer, int[] value) { - int idx = buffer.writerIndex(); - int length = value.length; - buffer.ensure(idx + 5 + length * 4); - idx += buffer._unsafeWriteVarUInt32(length * 4); - for (int i = 0; i < length; i++) { - buffer._unsafePutInt32(idx + i * 4, value[i]); - } - buffer._unsafeWriterIndex(idx + length * 4); - } - @Override public int[] copy(CopyContext copyContext, int[] originArray) { return Arrays.copyOf(originArray, originArray.length); @@ -435,11 +378,7 @@ public int[] read(ReadContext readContext) { reserveArray(readContext, numElements, 4); int[] values = new int[numElements]; if (size > 0) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buf.readInt32ArrayBytes(values, size); - } else { - readInt32BySwapEndian(buf, values, numElements); - } + buf.readInt32ArrayBytes(values, size); } return values; } @@ -458,25 +397,11 @@ public int[] read(ReadContext readContext) { reserveArray(readContext, numElements, 4); int[] values = new int[numElements]; if (size > 0) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.readInt32ArrayBytes(values, size); - } else { - readInt32BySwapEndian(buffer, values, numElements); - } + buffer.readInt32ArrayBytes(values, size); } return values; } - private void readInt32BySwapEndian(MemoryBuffer buffer, int[] values, int numElements) { - int size = numElements << 2; - // Do not loop through MemoryBuffer._unsafeGet* here; those helpers carry Android dispatch. - // Copy the body bytes once, then byte-swap the destination values locally. - buffer.readInt32ArrayBytes(values, size); - for (int i = 0; i < numElements; i++) { - values[i] = Integer.reverseBytes(values[i]); - } - } - private void writeInt32Compressed(MemoryBuffer buffer, int[] value) { buffer.writeVarUInt32Small7(value.length); for (int i : value) { @@ -519,28 +444,13 @@ public void write(WriteContext writeContext, long[] value) { writeInt64Compressed(buffer, value, config.longEncoding()); return; } - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.writeLongsWithSize(value); - } else { - writeInt64BySwapEndian(buffer, value); - } + buffer.writeLongsWithSize(value); } else { writeContext.writeBufferObject( new PrimitiveArrayBufferObject(value, Types.INT64_ARRAY, 8, value.length)); } } - private void writeInt64BySwapEndian(MemoryBuffer buffer, long[] value) { - int idx = buffer.writerIndex(); - int length = value.length; - buffer.ensure(idx + 5 + length * 8); - idx += buffer._unsafeWriteVarUInt32(length * 8); - for (int i = 0; i < length; i++) { - buffer._unsafePutInt64(idx + i * 8, value[i]); - } - buffer._unsafeWriterIndex(idx + length * 8); - } - @Override public long[] copy(CopyContext copyContext, long[] originArray) { return Arrays.copyOf(originArray, originArray.length); @@ -560,11 +470,7 @@ public long[] read(ReadContext readContext) { reserveArray(readContext, numElements, 8); long[] values = new long[numElements]; if (size > 0) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buf.readInt64ArrayBytes(values, size); - } else { - readInt64BySwapEndian(buf, values, numElements); - } + buf.readInt64ArrayBytes(values, size); } return values; } @@ -583,25 +489,11 @@ public long[] read(ReadContext readContext) { reserveArray(readContext, numElements, 8); long[] values = new long[numElements]; if (size > 0) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.readInt64ArrayBytes(values, size); - } else { - readInt64BySwapEndian(buffer, values, numElements); - } + buffer.readInt64ArrayBytes(values, size); } return values; } - private void readInt64BySwapEndian(MemoryBuffer buffer, long[] values, int numElements) { - int size = numElements << 3; - // Do not loop through MemoryBuffer._unsafeGet* here; those helpers carry Android dispatch. - // Copy the body bytes once, then byte-swap the destination values locally. - buffer.readInt64ArrayBytes(values, size); - for (int i = 0; i < numElements; i++) { - values[i] = Long.reverseBytes(values[i]); - } - } - private void writeInt64Compressed( MemoryBuffer buffer, long[] value, Int64Encoding longEncoding) { int length = value.length; @@ -648,28 +540,13 @@ public FloatArraySerializer(TypeResolver typeResolver) { public void write(WriteContext writeContext, float[] value) { MemoryBuffer buffer = writeContext.getBuffer(); if (writeContext.getBufferCallback() == null) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.writeFloatsWithSize(value); - } else { - writeFloat32BySwapEndian(buffer, value); - } + buffer.writeFloatsWithSize(value); } else { writeContext.writeBufferObject( new PrimitiveArrayBufferObject(value, Types.FLOAT32_ARRAY, 4, value.length)); } } - private void writeFloat32BySwapEndian(MemoryBuffer buffer, float[] value) { - int idx = buffer.writerIndex(); - int length = value.length; - buffer.ensure(idx + 5 + length * 4); - idx += buffer._unsafeWriteVarUInt32(length * 4); - for (int i = 0; i < length; i++) { - buffer._unsafePutInt32(idx + i * 4, Float.floatToRawIntBits(value[i])); - } - buffer._unsafeWriterIndex(idx + length * 4); - } - @Override public float[] copy(CopyContext copyContext, float[] originArray) { return Arrays.copyOf(originArray, originArray.length); @@ -688,11 +565,7 @@ public float[] read(ReadContext readContext) { buf.checkReadableBytes(size); reserveArray(readContext, numElements, 4); float[] values = new float[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buf.readFloat32ArrayBytes(values, size); - } else { - readFloat32BySwapEndian(buf, values, numElements); - } + buf.readFloat32ArrayBytes(values, size); return values; } int size = buffer.readVarUInt32Small7(); @@ -706,22 +579,8 @@ public float[] read(ReadContext readContext) { buffer.checkReadableBytes(size); reserveArray(readContext, numElements, 4); float[] values = new float[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.readFloat32ArrayBytes(values, size); - } else { - readFloat32BySwapEndian(buffer, values, numElements); - } - return values; - } - - private void readFloat32BySwapEndian(MemoryBuffer buffer, float[] values, int numElements) { - int size = numElements << 2; - // Do not loop through MemoryBuffer._unsafeGet* here; those helpers carry Android dispatch. - // Copy the body bytes once, then byte-swap the destination values locally. buffer.readFloat32ArrayBytes(values, size); - for (int i = 0; i < numElements; i++) { - values[i] = Float.intBitsToFloat(Integer.reverseBytes(Float.floatToRawIntBits(values[i]))); - } + return values; } } @@ -734,28 +593,13 @@ public DoubleArraySerializer(TypeResolver typeResolver) { public void write(WriteContext writeContext, double[] value) { MemoryBuffer buffer = writeContext.getBuffer(); if (writeContext.getBufferCallback() == null) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.writeDoublesWithSize(value); - } else { - writeFloat64BySwapEndian(buffer, value); - } + buffer.writeDoublesWithSize(value); } else { writeContext.writeBufferObject( new PrimitiveArrayBufferObject(value, Types.FLOAT64_ARRAY, 8, value.length)); } } - private void writeFloat64BySwapEndian(MemoryBuffer buffer, double[] value) { - int idx = buffer.writerIndex(); - int length = value.length; - buffer.ensure(idx + 5 + length * 8); - idx += buffer._unsafeWriteVarUInt32(length * 8); - for (int i = 0; i < length; i++) { - buffer._unsafePutInt64(idx + i * 8, Double.doubleToRawLongBits(value[i])); - } - buffer._unsafeWriterIndex(idx + length * 8); - } - @Override public double[] copy(CopyContext copyContext, double[] originArray) { return Arrays.copyOf(originArray, originArray.length); @@ -774,11 +618,7 @@ public double[] read(ReadContext readContext) { buf.checkReadableBytes(size); reserveArray(readContext, numElements, 8); double[] values = new double[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buf.readFloat64ArrayBytes(values, size); - } else { - readFloat64BySwapEndian(buf, values, numElements); - } + buf.readFloat64ArrayBytes(values, size); return values; } int size = buffer.readVarUInt32Small7(); @@ -792,23 +632,8 @@ public double[] read(ReadContext readContext) { buffer.checkReadableBytes(size); reserveArray(readContext, numElements, 8); double[] values = new double[numElements]; - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.readFloat64ArrayBytes(values, size); - } else { - readFloat64BySwapEndian(buffer, values, numElements); - } - return values; - } - - private void readFloat64BySwapEndian(MemoryBuffer buffer, double[] values, int numElements) { - int size = numElements << 3; - // Do not loop through MemoryBuffer._unsafeGet* here; those helpers carry Android dispatch. - // Copy the body bytes once, then byte-swap the destination values locally. buffer.readFloat64ArrayBytes(values, size); - for (int i = 0; i < numElements; i++) { - values[i] = - Double.longBitsToDouble(Long.reverseBytes(Double.doubleToRawLongBits(values[i]))); - } + return values; } } @@ -858,38 +683,13 @@ public BFloat16Array read(ReadContext readContext) { private static void writeShortBits(WriteContext writeContext, short[] value) { MemoryBuffer buffer = writeContext.getBuffer(); if (writeContext.getBufferCallback() == null) { - if (NativeByteOrder.IS_LITTLE_ENDIAN) { - buffer.writeShortsWithSize(value); - } else { - writeInt16BySwapEndian(buffer, value); - } + buffer.writeShortsWithSize(value); } else { writeContext.writeBufferObject( new PrimitiveArrayBufferObject(value, Types.INT16_ARRAY, 2, value.length)); } } - private static void writeInt16BySwapEndian(MemoryBuffer buffer, short[] value) { - int idx = buffer.writerIndex(); - int length = value.length; - buffer.ensure(idx + 5 + length * 2); - idx += buffer._unsafeWriteVarUInt32(length * 2); - for (int i = 0; i < length; i++) { - buffer._unsafePutInt16(idx + i * 2, value[i]); - } - buffer._unsafeWriterIndex(idx + length * 2); - } - - private static void readInt16BySwapEndian(MemoryBuffer buffer, short[] values, int numElements) { - int size = numElements << 1; - // Do not loop through MemoryBuffer._unsafeGet* here; those helpers carry Android dispatch. - // Copy the body bytes once, then byte-swap the destination values locally. - buffer.readInt16ArrayBytes(values, size); - for (int i = 0; i < numElements; i++) { - values[i] = Short.reverseBytes(values[i]); - } - } - public static void registerDefaultSerializers(TypeResolver resolver) { resolver.registerInternalSerializer(byte[].class, new ByteArraySerializer(resolver)); resolver.registerInternalSerializer(char[].class, new CharArraySerializer(resolver)); diff --git a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java index eb0793ecda..2e3a834b71 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java @@ -57,11 +57,11 @@ *

Warning: The instance of this class should not be held at GraalVM build time; build-time heap * buffers do not represent the runtime heap layout. * - *

In the Java 25 multi-release implementation, absolute random-access get/put methods are - * internal fast paths. Callers must pass legal indices and ranges; read/write entry points perform - * logical {@link MemoryBuffer} range validation before reaching these methods. The implementation - * relies on {@link VarHandle}, array, and {@link ByteBuffer} access only for JVM memory safety and - * does not repeat root Unsafe-style logical bounds checks. + *

The Java 25 multi-release implementation intentionally does not duplicate logical range + * checks around indexed array, absolute {@link ByteBuffer}, or {@link VarHandle} access. Those JVM + * accessors own bounds enforcement and already fail safely. The exact exception type, message, and + * detection point are not contracts, so adding checks only to make an invalid access fail earlier + * or more precisely would burden every hot buffer operation without improving memory safety. * *

Note(chaokunyang): Buffer operations are very common, and jvm inline and branch elimination is * not reliable even in c2 compiler, so we try to inline and avoid checks as we can manually. jvm @@ -1916,7 +1916,11 @@ public void writeChars(char[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - writeCharsFromArray(address + writerIdx, values, CHAR_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + writeCharsFromArray(address + writerIdx, values, CHAR_ARRAY_OFFSET + offset, numBytes); + } else { + writeCharsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } @@ -1935,7 +1939,11 @@ public void writeShorts(short[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - writeShortsFromArray(address + writerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + writeShortsFromArray(address + writerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); + } else { + writeShortsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } @@ -1954,7 +1962,11 @@ public void writeInts(int[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - writeIntsFromArray(address + writerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + writeIntsFromArray(address + writerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); + } else { + writeIntsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } @@ -1973,7 +1985,11 @@ public void writeLongs(long[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - writeLongsFromArray(address + writerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + writeLongsFromArray(address + writerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); + } else { + writeLongsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } @@ -1992,7 +2008,11 @@ public void writeFloats(float[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - writeFloatsFromArray(address + writerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + writeFloatsFromArray(address + writerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); + } else { + writeFloatsBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } @@ -2011,14 +2031,57 @@ public void writeDoubles(double[] values, int offset, int numElements) { final int writerIdx = writerIndex; final int newIdx = writerIdx + numBytes; ensure(newIdx); - writeDoublesFromArray(address + writerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + writeDoublesFromArray(address + writerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); + } else { + writeDoublesBigEndian(values, offset, numElements, writerIdx); + } writerIndex = newIdx; } + // Big-endian JVMs use the scalar little-endian primitives here. Keeping these methods separate + // leaves native bulk copy as the only little-endian hot path. + private void writeCharsBigEndian(char[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt16(writerIdx + i * 2, (short) values[offset + i]); + } + } + + private void writeShortsBigEndian(short[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt16(writerIdx + i * 2, values[offset + i]); + } + } + + private void writeIntsBigEndian(int[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt32(writerIdx + i * 4, values[offset + i]); + } + } + + private void writeLongsBigEndian(long[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt64(writerIdx + i * 8, values[offset + i]); + } + } + + private void writeFloatsBigEndian(float[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt32(writerIdx + i * 4, Float.floatToRawIntBits(values[offset + i])); + } + } + + private void writeDoublesBigEndian(double[] values, int offset, int length, int writerIdx) { + for (int i = 0; i < length; i++) { + _unsafePutInt64(writerIdx + i * 8, Double.doubleToRawLongBits(values[offset + i])); + } + } + /** For off-heap buffer, this will make a heap buffer internally. */ public void grow(int neededSize) { int length = writerIndex + neededSize; if (length > size) { + // MemoryAllocator owns the requested-capacity postcondition; do not recheck it here. globalAllocator.grow(this, length); } } @@ -2026,6 +2089,7 @@ public void grow(int neededSize) { /** For off-heap buffer, this will make a heap buffer internally. */ public void ensure(int length) { if (length > size) { + // MemoryAllocator owns the requested-capacity postcondition; do not recheck it here. globalAllocator.grow(this, length); } } @@ -3292,7 +3356,11 @@ public void readCharArrayBytes(char[] values, int numBytes) { streamReader.readChars(values, 0, numBytes >>> 1); return; } - readCharsToArray(address + readerIdx, values, CHAR_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + readCharsToArray(address + readerIdx, values, CHAR_ARRAY_OFFSET, numBytes); + } else { + readCharsBigEndian(values, 0, numBytes >>> 1, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3307,7 +3375,11 @@ public void readInt16ArrayBytes(short[] values, int numBytes) { streamReader.readShorts(values, 0, numBytes >>> 1); return; } - readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET, numBytes); + } else { + readShortsBigEndian(values, 0, numBytes >>> 1, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3322,7 +3394,11 @@ public void readInt32ArrayBytes(int[] values, int numBytes) { streamReader.readInts(values, 0, numBytes >>> 2); return; } - readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET, numBytes); + } else { + readIntsBigEndian(values, 0, numBytes >>> 2, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3337,7 +3413,11 @@ public void readInt64ArrayBytes(long[] values, int numBytes) { streamReader.readLongs(values, 0, numBytes >>> 3); return; } - readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET, numBytes); + } else { + readLongsBigEndian(values, 0, numBytes >>> 3, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3352,7 +3432,11 @@ public void readFloat32ArrayBytes(float[] values, int numBytes) { streamReader.readFloats(values, 0, numBytes >>> 2); return; } - readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET, numBytes); + } else { + readFloatsBigEndian(values, 0, numBytes >>> 2, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3367,7 +3451,11 @@ public void readFloat64ArrayBytes(double[] values, int numBytes) { streamReader.readDoubles(values, 0, numBytes >>> 3); return; } - readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET, numBytes); + if (LITTLE_ENDIAN) { + readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET, numBytes); + } else { + readDoublesBigEndian(values, 0, numBytes >>> 3, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3400,7 +3488,11 @@ public void readChars(char[] chars, int offset, int numElements) { return; } int readerIdx = readerIndex; - readCharsToArray(address + readerIdx, chars, CHAR_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + readCharsToArray(address + readerIdx, chars, CHAR_ARRAY_OFFSET + offset, numBytes); + } else { + readCharsBigEndian(chars, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3429,7 +3521,11 @@ public void readShorts(short[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + readShortsToArray(address + readerIdx, values, SHORT_ARRAY_OFFSET + offset, numBytes); + } else { + readShortsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3444,7 +3540,11 @@ public void readInts(int[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + readIntsToArray(address + readerIdx, values, INT_ARRAY_OFFSET + offset, numBytes); + } else { + readIntsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3459,7 +3559,11 @@ public void readLongs(long[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + readLongsToArray(address + readerIdx, values, LONG_ARRAY_OFFSET + offset, numBytes); + } else { + readLongsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3474,7 +3578,11 @@ public void readFloats(float[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + readFloatsToArray(address + readerIdx, values, FLOAT_ARRAY_OFFSET + offset, numBytes); + } else { + readFloatsBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } @@ -3489,10 +3597,50 @@ public void readDoubles(double[] values, int offset, int numElements) { return; } int readerIdx = readerIndex; - readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); + if (LITTLE_ENDIAN) { + readDoublesToArray(address + readerIdx, values, DOUBLE_ARRAY_OFFSET + offset, numBytes); + } else { + readDoublesBigEndian(values, offset, numElements, readerIdx); + } readerIndex = readerIdx + numBytes; } + private void readCharsBigEndian(char[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = (char) _unsafeGetInt16(readerIdx + i * 2); + } + } + + private void readShortsBigEndian(short[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = _unsafeGetInt16(readerIdx + i * 2); + } + } + + private void readIntsBigEndian(int[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = _unsafeGetInt32(readerIdx + i * 4); + } + } + + private void readLongsBigEndian(long[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = _unsafeGetInt64(readerIdx + i * 8); + } + } + + private void readFloatsBigEndian(float[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = Float.intBitsToFloat(_unsafeGetInt32(readerIdx + i * 4)); + } + } + + private void readDoublesBigEndian(double[] values, int offset, int length, int readerIdx) { + for (int i = 0; i < length; i++) { + values[offset + i] = Double.longBitsToDouble(_unsafeGetInt64(readerIdx + i * 8)); + } + } + public void checkReadableBytes(int minimumReadableBytes) { // use subtract to avoid overflow int remaining = size - readerIndex; diff --git a/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java b/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java index 093bd3945f..99fe14686b 100644 --- a/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/memory/MemoryBufferTest.java @@ -649,62 +649,128 @@ public void testTypedArrayCopies() { private void assertTypedArrayCopies(MemoryBuffer buffer) { byte[] bytes = {1, 2, 3, 4}; - int byteOffset = buffer.writerIndex(); - buffer.writeBytes(bytes); + buffer.copyFromByteArray(0, bytes, 0, bytes.length); byte[] byteCopy = new byte[bytes.length]; - buffer.copyToByteArray(byteOffset, byteCopy, 0, bytes.length); + buffer.copyToByteArray(0, byteCopy, 0, bytes.length); assertEquals(byteCopy, bytes); boolean[] booleans = {true, false, true}; - int booleanOffset = buffer.writerIndex(); - buffer.writeBooleans(booleans); + buffer.copyFromBooleanArray(0, booleans, 0, booleans.length); boolean[] booleanCopy = new boolean[booleans.length]; - buffer.copyToBooleanArray(booleanOffset, booleanCopy, 0, booleans.length); + buffer.copyToBooleanArray(0, booleanCopy, 0, booleans.length); assertEquals(booleanCopy, booleans); char[] chars = {'a', 0x1234, Character.MAX_VALUE}; - int charOffset = buffer.writerIndex(); - buffer.writeChars(chars); + buffer.copyFromCharArray(0, chars, 0, chars.length * Character.BYTES); char[] charCopy = new char[chars.length]; - buffer.copyToCharArray(charOffset, charCopy, 0, chars.length * Character.BYTES); + buffer.copyToCharArray(0, charCopy, 0, chars.length * Character.BYTES); assertEquals(charCopy, chars); short[] shorts = {1, -2, Short.MAX_VALUE}; - int shortOffset = buffer.writerIndex(); - buffer.writeShorts(shorts); + buffer.copyFromShortArray(0, shorts, 0, shorts.length * Short.BYTES); short[] shortCopy = new short[shorts.length]; - buffer.copyToShortArray(shortOffset, shortCopy, 0, shorts.length * Short.BYTES); + buffer.copyToShortArray(0, shortCopy, 0, shorts.length * Short.BYTES); assertEquals(shortCopy, shorts); int[] ints = {1, -2, Integer.MIN_VALUE}; - int intOffset = buffer.writerIndex(); - buffer.writeInts(ints); + buffer.copyFromIntArray(0, ints, 0, ints.length * Integer.BYTES); int[] intCopy = new int[ints.length]; - buffer.copyToIntArray(intOffset, intCopy, 0, ints.length * Integer.BYTES); + buffer.copyToIntArray(0, intCopy, 0, ints.length * Integer.BYTES); assertEquals(intCopy, ints); long[] longs = {1L, -2L, Long.MAX_VALUE}; - int longOffset = buffer.writerIndex(); - buffer.writeLongs(longs); + buffer.copyFromLongArray(0, longs, 0, longs.length * Long.BYTES); long[] longCopy = new long[longs.length]; - buffer.copyToLongArray(longOffset, longCopy, 0, longs.length * Long.BYTES); + buffer.copyToLongArray(0, longCopy, 0, longs.length * Long.BYTES); assertEquals(longCopy, longs); float[] floats = {1.5f, -2.5f, Float.MAX_VALUE}; - int floatOffset = buffer.writerIndex(); - buffer.writeFloats(floats); + buffer.copyFromFloatArray(0, floats, 0, floats.length * Float.BYTES); float[] floatCopy = new float[floats.length]; - buffer.copyToFloatArray(floatOffset, floatCopy, 0, floats.length * Float.BYTES); + buffer.copyToFloatArray(0, floatCopy, 0, floats.length * Float.BYTES); assertEquals(floatCopy, floats); double[] doubles = {1.5d, -2.5d, Double.MAX_VALUE}; - int doubleOffset = buffer.writerIndex(); - buffer.writeDoubles(doubles); + buffer.copyFromDoubleArray(0, doubles, 0, doubles.length * Double.BYTES); double[] doubleCopy = new double[doubles.length]; - buffer.copyToDoubleArray(doubleOffset, doubleCopy, 0, doubles.length * Double.BYTES); + buffer.copyToDoubleArray(0, doubleCopy, 0, doubles.length * Double.BYTES); assertEquals(doubleCopy, doubles); } + @Test + public void testPrimitiveArrayLittleEndian() { + assertPrimitiveArrayLittleEndian(MemoryUtils.buffer(64)); + assertPrimitiveArrayLittleEndian(MemoryUtils.wrap(ByteBuffer.allocateDirect(64))); + } + + private void assertPrimitiveArrayLittleEndian(MemoryBuffer buffer) { + char[] chars = {0x1234}; + short[] shorts = {(short) 0x1234}; + int[] ints = {0x12345678}; + long[] longs = {0x0102030405060708L}; + float[] floats = {1.0f}; + double[] doubles = {1.0d}; + buffer.writeChars(chars); + buffer.writeShorts(shorts); + buffer.writeInts(ints); + buffer.writeLongs(longs); + buffer.writeFloats(floats); + buffer.writeDoubles(doubles); + + assertEquals( + buffer.getBytes(0, buffer.writerIndex()), + new byte[] { + 0x34, + 0x12, + 0x34, + 0x12, + 0x78, + 0x56, + 0x34, + 0x12, + 0x08, + 0x07, + 0x06, + 0x05, + 0x04, + 0x03, + 0x02, + 0x01, + 0x00, + 0x00, + (byte) 0x80, + 0x3f, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + (byte) 0xf0, + 0x3f + }); + + buffer.readerIndex(0); + char[] readChars = new char[1]; + short[] readShorts = new short[1]; + int[] readInts = new int[1]; + long[] readLongs = new long[1]; + float[] readFloats = new float[1]; + double[] readDoubles = new double[1]; + buffer.readChars(readChars, 0, 1); + buffer.readShorts(readShorts, 0, 1); + buffer.readInts(readInts, 0, 1); + buffer.readLongs(readLongs, 0, 1); + buffer.readFloats(readFloats, 0, 1); + buffer.readDoubles(readDoubles, 0, 1); + assertEquals(readChars, chars); + assertEquals(readShorts, shorts); + assertEquals(readInts, ints); + assertEquals(readLongs, longs); + assertEquals(readFloats, floats); + assertEquals(readDoubles, doubles); + } + @Test public void testWritePrimitiveArrayWithSizeEmbedded() { MemoryBuffer buf = MemoryUtils.buffer(16); diff --git a/rust/fory-core/src/buffer.rs b/rust/fory-core/src/buffer.rs index 731014a4b0..53a24b4cc1 100644 --- a/rust/fory-core/src/buffer.rs +++ b/rust/fory-core/src/buffer.rs @@ -202,14 +202,34 @@ impl<'a> Writer<'a> { #[inline(always)] fn write_u24(&mut self, value: u32) { - let bytes = value.to_le_bytes(); - self.bf.extend_from_slice(&bytes[..3]); + let offset = self.bf.len(); + self.bf.reserve(4); + // The four-byte store stays inside reserved capacity; only the three wire bytes become + // initialized vector contents. + unsafe { + self.bf + .as_mut_ptr() + .add(offset) + .cast::() + .write_unaligned(value.to_le()); + self.bf.set_len(offset + 3); + } } #[inline(always)] fn write_u40(&mut self, value: u64) { - let bytes = value.to_le_bytes(); - self.bf.extend_from_slice(&bytes[..5]); + let offset = self.bf.len(); + self.bf.reserve(8); + // The eight-byte store stays inside reserved capacity; only the five wire bytes become + // initialized vector contents. + unsafe { + self.bf + .as_mut_ptr() + .add(offset) + .cast::() + .write_unaligned(value.to_le()); + self.bf.set_len(offset + 5); + } } // ============ VAR_UINT32 (TypeId = 12) ============