This is an automated email from the ASF dual-hosted git repository.

paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-nanoarrow.git


The following commit(s) were added to refs/heads/main by this push:
     new cc67c6be feat: Allow the stream reader's validation level to be 
configured (#964)
cc67c6be is described below

commit cc67c6becadd11d3890f36049fd2b41db3394fa7
Author: Rusty Conover <[email protected]>
AuthorDate: Sat Oct 17 00:33:13 2026 -0400

    feat: Allow the stream reader's validation level to be configured (#964)
    
    `ArrowIpcArrayStreamReader` always decodes with
    `NANOARROW_VALIDATION_LEVEL_FULL`. That's the right default for bytes of
    unknown origin. But full validation walks every offset of every string,
    binary and list column and every dictionary index, and a caller that
    already trusts its producer can't opt out of that pass. The lower-level
    decoder functions already take a validation level; the stream reader
    just doesn't expose one.
    
    This adds a `validation_level` field to
    `ArrowIpcArrayStreamReaderOptions`. It applies to both record batches
    and dictionary batches. Passing `NULL` options keeps the current
    behaviour (`FULL`). An out-of-range value makes
    `ArrowIpcArrayStreamReaderInit()` return `EINVAL`.
    
    Motivation: I'm moving a WebAssembly build of DuckDB from Arrow C++ to
    nanoarrow. There, Arrow IPC streams written by Arrow JS in the same page
    are read into the database. Arrow C++'s reader doesn't do full
    validation by default, and the extra pass was a large part of an insert
    slowdown I measured after switching.
    
    Reading a 23 MB stream from an in-memory buffer (1M rows of int32,
    double and string, in 16 batches), Release build, Apple M3, median of 50
    runs:
    
    | validation level | time |
    |---|---|
    | `FULL` (current behaviour) | 0.66 ms |
    | `DEFAULT` | 0.38 ms |
    
    The float64-only fixtures from `dev/benchmarks` don't change, because
    full validation has nothing extra to check for them.
    
    Compatibility: callers that fill in `ArrowIpcArrayStreamReaderOptions`
    field by field need to set the new field as well. That's the same
    situation as when `use_shared_buffers` was added in #165. The R and
    Python bindings pass `NULL`, so they're unaffected. The `EINVAL` check
    should catch most callers that miss the new field, rather than letting
    them silently get a random level.
    
    Tests: `StreamReaderValidationLevel` points a dictionary index past the
    end of the dictionary in the existing dictionary fixture, then checks
    that `FULL` rejects the batch and `DEFAULT` accepts it.
    `StreamReaderInvalidValidationLevel` covers the `EINVAL` path. The two
    existing tests that build options by hand now set the field. ctest
    passes locally (macOS arm64, Debug, extra warnings). Under ASan/UBSan
    the IPC tests pass, and the only failures are the same six allocation
    tests that also fail on `main` under ASan.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 src/nanoarrow/ipc/reader.c       | 23 +++++++++++---
 src/nanoarrow/ipc/reader_test.cc | 69 ++++++++++++++++++++++++++++++++++++++++
 src/nanoarrow/nanoarrow_ipc.h    |  8 +++++
 3 files changed, 96 insertions(+), 4 deletions(-)

diff --git a/src/nanoarrow/ipc/reader.c b/src/nanoarrow/ipc/reader.c
index c699ca33..3348a9b6 100644
--- a/src/nanoarrow/ipc/reader.c
+++ b/src/nanoarrow/ipc/reader.c
@@ -187,6 +187,7 @@ struct ArrowIpcArrayStreamReaderPrivate {
   struct ArrowIpcInputStream input;
   struct ArrowIpcDecoder decoder;
   int use_shared_buffers;
+  enum ArrowValidationLevel validation_level;
   struct ArrowSchema out_schema;
   int64_t field_index;
   struct ArrowBuffer header;
@@ -459,7 +460,7 @@ static int ArrowIpcArrayStreamReaderProcessRecordBatch(
         ArrowSharedBufferInit(&shared, &private_data->body), 
&private_data->error);
     ArrowErrorCode result = 
ArrowIpcDecoderDecodeArrayFromSharedWithDictionaries(
         &private_data->decoder, &shared, private_data->field_index,
-        &private_data->dictionaries, out, NANOARROW_VALIDATION_LEVEL_FULL,
+        &private_data->dictionaries, out, private_data->validation_level,
         &private_data->error);
     ArrowBufferReset(&shared);
     NANOARROW_RETURN_NOT_OK(result);
@@ -470,7 +471,7 @@ static int ArrowIpcArrayStreamReaderProcessRecordBatch(
 
     NANOARROW_RETURN_NOT_OK(ArrowIpcDecoderDecodeArrayWithDictionaries(
         &private_data->decoder, body_view, private_data->field_index,
-        &private_data->dictionaries, out, NANOARROW_VALIDATION_LEVEL_FULL,
+        &private_data->dictionaries, out, private_data->validation_level,
         &private_data->error));
   }
 
@@ -488,7 +489,7 @@ static int ArrowIpcArrayStreamReaderProcessDictionary(
     NANOARROW_RETURN_NOT_OK_WITH_ERROR(
         ArrowSharedBufferInit(&shared, &private_data->body), 
&private_data->error);
     int result = ArrowIpcDecoderDecodeDictionaryFromShared(
-        &private_data->decoder, &shared, NANOARROW_VALIDATION_LEVEL_FULL,
+        &private_data->decoder, &shared, private_data->validation_level,
         &private_data->dictionaries, &private_data->error);
     ArrowBufferReset(&shared);
     NANOARROW_RETURN_NOT_OK(result);
@@ -497,7 +498,7 @@ static int ArrowIpcArrayStreamReaderProcessDictionary(
     body_view.data.data = private_data->body.data;
     body_view.size_bytes = private_data->body.size_bytes;
     NANOARROW_RETURN_NOT_OK(ArrowIpcDecoderDecodeDictionary(
-        &private_data->decoder, body_view, NANOARROW_VALIDATION_LEVEL_FULL,
+        &private_data->decoder, body_view, private_data->validation_level,
         &private_data->dictionaries, &private_data->error));
   }
 
@@ -573,6 +574,18 @@ static const char* ArrowIpcArrayStreamReaderGetLastError(
 ArrowErrorCode ArrowIpcArrayStreamReaderInit(
     struct ArrowArrayStream* out, struct ArrowIpcInputStream* input_stream,
     struct ArrowIpcArrayStreamReaderOptions* options) {
+  if (options != NULL) {
+    switch (options->validation_level) {
+      case NANOARROW_VALIDATION_LEVEL_NONE:
+      case NANOARROW_VALIDATION_LEVEL_MINIMAL:
+      case NANOARROW_VALIDATION_LEVEL_DEFAULT:
+      case NANOARROW_VALIDATION_LEVEL_FULL:
+        break;
+      default:
+        return EINVAL;
+    }
+  }
+
   struct ArrowIpcArrayStreamReaderPrivate* private_data =
       (struct ArrowIpcArrayStreamReaderPrivate*)ArrowMalloc(
           sizeof(struct ArrowIpcArrayStreamReaderPrivate));
@@ -596,9 +609,11 @@ ArrowErrorCode ArrowIpcArrayStreamReaderInit(
   if (options != NULL) {
     private_data->field_index = options->field_index;
     private_data->use_shared_buffers = options->use_shared_buffers;
+    private_data->validation_level = options->validation_level;
   } else {
     private_data->field_index = -1;
     private_data->use_shared_buffers = ArrowSharedBufferIsThreadSafe();
+    private_data->validation_level = NANOARROW_VALIDATION_LEVEL_FULL;
   }
 
   out->private_data = private_data;
diff --git a/src/nanoarrow/ipc/reader_test.cc b/src/nanoarrow/ipc/reader_test.cc
index d257e111..13545a69 100644
--- a/src/nanoarrow/ipc/reader_test.cc
+++ b/src/nanoarrow/ipc/reader_test.cc
@@ -19,6 +19,8 @@
 
 #include <stdio.h>
 
+#include <string>
+
 #include "nanoarrow/nanoarrow_ipc.h"
 
 static uint8_t kSimpleSchema[] = {
@@ -272,6 +274,7 @@ TEST(NanoarrowIpcReader, StreamReaderBasicNoSharedBuffers) {
   struct ArrowIpcArrayStreamReaderOptions options;
   options.field_index = -1;
   options.use_shared_buffers = 0;
+  options.validation_level = NANOARROW_VALIDATION_LEVEL_FULL;
   ASSERT_EQ(ArrowIpcArrayStreamReaderInit(&stream, &input, &options), 
NANOARROW_OK);
 
   struct ArrowSchema schema;
@@ -495,6 +498,7 @@ TEST(NanoarrowIpcReader, StreamReaderUnsupportedFieldIndex) 
{
   struct ArrowIpcArrayStreamReaderOptions options;
   options.field_index = 0;
   options.use_shared_buffers = 0;
+  options.validation_level = NANOARROW_VALIDATION_LEVEL_FULL;
   ASSERT_EQ(ArrowIpcArrayStreamReaderInit(&stream, &input, &options), 
NANOARROW_OK);
 
   struct ArrowSchema schema;
@@ -586,6 +590,71 @@ TEST(NanoarrowIpcReader, StreamReaderDictionary) {
   ArrowArrayStreamRelease(&stream);
 }
 
+TEST(NanoarrowIpcReader, StreamReaderValidationLevel) {
+  // Point the last dictionary index past the end of the three-value 
dictionary.
+  // Only full validation inspects index values, so a lower level accepts the 
batch.
+  uint8_t record_batch[sizeof(kDictionaryRecordBatch)];
+  memcpy(record_batch, kDictionaryRecordBatch, sizeof(kDictionaryRecordBatch));
+  ASSERT_EQ(record_batch[sizeof(record_batch) - 6], 0x02);
+  record_batch[sizeof(record_batch) - 6] = 0x07;
+
+  for (auto validation_level :
+       {NANOARROW_VALIDATION_LEVEL_FULL, NANOARROW_VALIDATION_LEVEL_DEFAULT}) {
+    SCOPED_TRACE(validation_level);
+    struct ArrowBuffer input_buffer;
+    ArrowBufferInit(&input_buffer);
+    ASSERT_EQ(
+        ArrowBufferAppend(&input_buffer, kDictionarySchema, 
sizeof(kDictionarySchema)),
+        NANOARROW_OK);
+    ASSERT_EQ(
+        ArrowBufferAppend(&input_buffer, kDictionaryBatch, 
sizeof(kDictionaryBatch)),
+        NANOARROW_OK);
+    ASSERT_EQ(ArrowBufferAppend(&input_buffer, record_batch, 
sizeof(record_batch)),
+              NANOARROW_OK);
+
+    struct ArrowIpcInputStream input;
+    ASSERT_EQ(ArrowIpcInputStreamInitBuffer(&input, &input_buffer), 
NANOARROW_OK);
+
+    struct ArrowArrayStream stream;
+    struct ArrowIpcArrayStreamReaderOptions options;
+    options.field_index = -1;
+    options.use_shared_buffers = ArrowSharedBufferIsThreadSafe();
+    options.validation_level = validation_level;
+    ASSERT_EQ(ArrowIpcArrayStreamReaderInit(&stream, &input, &options), 
NANOARROW_OK);
+
+    struct ArrowArray array;
+    if (validation_level == NANOARROW_VALIDATION_LEVEL_FULL) {
+      ASSERT_EQ(ArrowArrayStreamGetNext(&stream, &array, nullptr), EINVAL);
+      EXPECT_NE(std::string(ArrowArrayStreamGetLastError(&stream))
+                    .find("Expected dictionary index >= 0 and < 3 but found 
value 7"),
+                std::string::npos);
+    } else {
+      ASSERT_EQ(ArrowArrayStreamGetNext(&stream, &array, nullptr), 
NANOARROW_OK);
+      EXPECT_EQ(array.length, 3);
+      ArrowArrayRelease(&array);
+    }
+
+    ArrowArrayStreamRelease(&stream);
+  }
+}
+
+TEST(NanoarrowIpcReader, StreamReaderInvalidValidationLevel) {
+  struct ArrowBuffer input_buffer;
+  ArrowBufferInit(&input_buffer);
+  struct ArrowIpcInputStream input;
+  ASSERT_EQ(ArrowIpcInputStreamInitBuffer(&input, &input_buffer), 
NANOARROW_OK);
+
+  struct ArrowArrayStream stream;
+  struct ArrowIpcArrayStreamReaderOptions options;
+  options.field_index = -1;
+  options.use_shared_buffers = 0;
+  // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange): 
deliberately invalid
+  options.validation_level = static_cast<ArrowValidationLevel>(4);
+  ASSERT_EQ(ArrowIpcArrayStreamReaderInit(&stream, &input, &options), EINVAL);
+
+  input.release(&input);
+}
+
 TEST(NanoarrowIpcReader, StreamReaderDictionaryBatchWithoutDictionarySchema) {
   // Send a dictionary batch when the schema has no dictionaries
   struct ArrowBuffer input_buffer;
diff --git a/src/nanoarrow/nanoarrow_ipc.h b/src/nanoarrow/nanoarrow_ipc.h
index 7f555975..38b57f97 100644
--- a/src/nanoarrow/nanoarrow_ipc.h
+++ b/src/nanoarrow/nanoarrow_ipc.h
@@ -936,6 +936,14 @@ struct ArrowIpcArrayStreamReaderOptions {
   /// a single field there is probably no advantage to using shared buffers.
   /// Defaults to the value of ArrowSharedBufferIsThreadSafe().
   int use_shared_buffers;
+
+  /// \brief The level of validation to apply to each decoded array
+  ///
+  /// Defaults to NANOARROW_VALIDATION_LEVEL_FULL, which is appropriate for 
untrusted
+  /// input. Use a lower level (e.g., NANOARROW_VALIDATION_LEVEL_DEFAULT) to 
skip checks
+  /// that are O(n) in the length of the decoded buffers when the input comes 
from a
+  /// trusted producer.
+  enum ArrowValidationLevel validation_level;
 };
 
 /// \brief Initialize an ArrowArrayStream from an input stream of bytes

Reply via email to