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

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


The following commit(s) were added to refs/heads/main by this push:
     new e74bb67919 GH-50216: [C++][Parquet] Add RleBitPackedToBitmapDecoder 
(#50217)
e74bb67919 is described below

commit e74bb67919a72fe224bc1c4f7f9cbb868997dfd4
Author: Antoine Prouvost <[email protected]>
AuthorDate: Thu Jun 25 12:46:16 2026 +0200

    GH-50216: [C++][Parquet] Add RleBitPackedToBitmapDecoder (#50217)
    
    ### Rationale for this change
    Add a `RleBitPackedToBitmapDecoder` capable of decoding a Parquet mixed Rle 
/ BitPacked byte stream directly into a bitmap. This is an optimization to be 
used when target and source bit_width = 1 that will improve decoding nullable 
columns in a  follow-up PR.
    
    ### What changes are included in this PR?
    New classes, that reuse previous Runs and parser.
    Their API is slighly different from the existing one as it fits a 
partilcular case.
      - `RleRunToBitmapDecoder`
      - `BitPackedRunToBitmapDecoder`
      - `RleRunToBitmapDecoder`
    
    ### Are these changes tested?
    Yes.
    
    ### Are there any user-facing changes?
    No.
    
    * GitHub Issue: #50216
    
    Authored-by: AntoinePrv <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/util/CMakeLists.txt          |   1 +
 cpp/src/arrow/util/bit_util.h              |  36 ++
 cpp/src/arrow/util/bit_util_test.cc        |  43 +++
 cpp/src/arrow/util/rle_bitmap_internal.h   | 369 ++++++++++++++++++
 cpp/src/arrow/util/rle_bitmap_test.cc      | 602 +++++++++++++++++++++++++++++
 cpp/src/arrow/util/rle_encoding_internal.h |  79 ++--
 6 files changed, 1098 insertions(+), 32 deletions(-)

diff --git a/cpp/src/arrow/util/CMakeLists.txt 
b/cpp/src/arrow/util/CMakeLists.txt
index 28ea215bd2..628e9a4d1c 100644
--- a/cpp/src/arrow/util/CMakeLists.txt
+++ b/cpp/src/arrow/util/CMakeLists.txt
@@ -99,6 +99,7 @@ add_arrow_test(bit-utility-test
                bit_util_test.cc
                bitmap_test.cc
                bpacking_test.cc
+               rle_bitmap_test.cc
                rle_encoding_test.cc
                test_common.cc)
 
diff --git a/cpp/src/arrow/util/bit_util.h b/cpp/src/arrow/util/bit_util.h
index bffc3c14a1..cd6d15f913 100644
--- a/cpp/src/arrow/util/bit_util.h
+++ b/cpp/src/arrow/util/bit_util.h
@@ -18,9 +18,12 @@
 #pragma once
 
 #include <bit>
+#include <cassert>
 #include <cstdint>
+#include <cstring>
 #include <type_traits>
 
+#include "arrow/util/endian.h"
 #include "arrow/util/macros.h"
 #include "arrow/util/visibility.h"
 
@@ -176,6 +179,39 @@ static constexpr bool GetBitFromByte(uint8_t byte, uint8_t 
i) {
   return byte & kBitmask[i];
 }
 
+template <typename Uint>
+struct CopyBitsParams {
+  Uint src = {};
+  Uint dst = {};
+  int start = {};
+  int end = {};
+};
+
+/// Copy a contiguous span of bits from src into dst.
+///
+/// Copy bits [start, end[ from src into the position [start, end[ in dst
+/// and return the result (inputs are unmodified).
+/// Setting ``kAllowFullCopy`` to false is an optimization when the caller can
+/// guarantee that the range of bits to copy does not cover the whole range.
+template <typename Uint, bool kAllowFullCopy = true>
+[[nodiscard]] constexpr Uint CopyBitsInInteger(const CopyBitsParams<Uint>& 
params) {
+  constexpr auto kUintSizeBits = static_cast<int>(sizeof(Uint) * 8);
+  assert(params.start <= params.end);
+  assert(params.start < kUintSizeBits);
+  assert(params.end <= kUintSizeBits);
+
+  const int length = params.end - params.start;
+  if constexpr (kAllowFullCopy) {
+    if (length == kUintSizeBits) {
+      return params.src;
+    }
+  }
+  assert(length < kUintSizeBits);
+  const Uint mask =
+      static_cast<Uint>(LeastSignificantBitMask<Uint, false>(length) << 
params.start);
+  return (~mask & params.dst) | (mask & params.src);
+}
+
 static inline void ClearBit(uint8_t* bits, int64_t i) {
   bits[i / 8] &= kFlippedBitmask[i % 8];
 }
diff --git a/cpp/src/arrow/util/bit_util_test.cc 
b/cpp/src/arrow/util/bit_util_test.cc
index 1e7714540e..a6af004f7c 100644
--- a/cpp/src/arrow/util/bit_util_test.cc
+++ b/cpp/src/arrow/util/bit_util_test.cc
@@ -1065,4 +1065,47 @@ TEST(SpliceWord, SpliceWord) {
             0xfedc456789abcdef);
 }
 
+TEST(BitUtil, CopyBits) {
+  // Copy bits [start, end[ from src into dst, keeping dst's other bits.
+  using bit_util::CopyBitsInInteger;
+
+  // Empty range: result equals dst.
+  ASSERT_EQ(CopyBitsInInteger<uint8_t>(
+                {.src = 0b11111111, .dst = 0b00010010, .start = 3, .end = 3}),
+            0b00010010);
+  // dst = 0101 0101, src = 1010 1010  ->  0101 1010
+  ASSERT_EQ(CopyBitsInInteger<uint8_t>(
+                {.src = 0b10101010, .dst = 0b01010101, .start = 0, .end = 4}),
+            0b01011010);
+  // Copy a middle span [2, 5[ of all-ones into an all-zeros dst.
+  ASSERT_EQ(CopyBitsInInteger<uint8_t>(
+                {.src = 0b11111111, .dst = 0b00000000, .start = 2, .end = 5}),
+            0b00011100);
+  // Copy a middle span [2, 5[ of all-zeros into an all-ones dst.
+  ASSERT_EQ(CopyBitsInInteger<uint8_t>(
+                {.src = 0b00000000, .dst = 0b11111111, .start = 2, .end = 5}),
+            0b11100011);
+  // Full-word copy returns src unchanged.
+  ASSERT_EQ(CopyBitsInInteger<uint8_t>(
+                {.src = 0b10101011, .dst = 0b00010010, .start = 0, .end = 8}),
+            0b10101011);
+  // uint16_t partial range [4, 12[: dst keeps its bits outside, src fills 
inside.
+  ASSERT_EQ(
+      CopyBitsInInteger<uint16_t>(
+          {.src = 0b1010101010101010, .dst = 0b0101010101010101, .start = 4, 
.end = 12}),
+      0b0101101010100101);
+  // uint64_t
+  ASSERT_EQ(CopyBitsInInteger<uint64_t>({
+                .src = 0x0123456789abcdef,
+                .dst = 0xfedcba9876543210,
+                .start = 0,
+                .end = 64,
+            }),
+            0x0123456789abcdef);
+  // constexpr-evaluable.
+  static_assert(CopyBitsInInteger<uint8_t>(
+                    {.src = 0b10101010, .dst = 0b01010101, .start = 0, .end = 
4}) ==
+                0b01011010);
+}
+
 }  // namespace arrow
diff --git a/cpp/src/arrow/util/rle_bitmap_internal.h 
b/cpp/src/arrow/util/rle_bitmap_internal.h
new file mode 100644
index 0000000000..0667a9f0a8
--- /dev/null
+++ b/cpp/src/arrow/util/rle_bitmap_internal.h
@@ -0,0 +1,369 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <algorithm>
+#include <cstring>
+
+#include "arrow/util/bit_util.h"
+#include "arrow/util/bitmap_ops.h"
+#include "arrow/util/logging.h"
+#include "arrow/util/macros.h"
+#include "arrow/util/rle_encoding_internal.h"
+
+namespace arrow::util {
+
+/// A lightweight view over a bitmap.
+template <typename B = uint8_t>
+class BitmapSpan {
+ public:
+  using size_type = rle_size_t;
+  using byte_type = B;
+
+  explicit constexpr BitmapSpan(byte_type* data, size_type bit_start = 0) 
noexcept
+      : data_{data}, bit_start_{bit_start} {
+    Normalize();
+  }
+
+  /// Pointer to the byte where the first value is stored.
+  constexpr byte_type* data() const noexcept { return data_; }
+
+  /// Bit offset of the first value in the first byte.
+  constexpr size_type bit_start() const noexcept { return bit_start_; }
+
+  /// Return a new span starting at the given position.
+  constexpr BitmapSpan NewStartingAt(size_type bit_start) const noexcept {
+    auto out = *this;
+    out.bit_start_ += bit_start;
+    out.Normalize();
+    return out;
+  }
+
+ private:
+  byte_type* data_;
+  size_type bit_start_ = 0;
+
+  /// Ensure `bit_start` always ends up in [0, 8[, even when it starts 
negative.
+  constexpr void Normalize() {
+    // On two's-complement targets an arithmetic right shift floors, and the 
AND mask
+    // yields the non-negative remainder (e.g. -1 -> byte -1, offset 7), 
unlike `/` and
+    // `%` which truncate toward zero.
+    data_ += bit_start_ >> 3;
+    bit_start_ &= 0b0111;
+  }
+};
+
+using BitmapSpanMut = BitmapSpan<uint8_t>;
+using BitmapSpanConst = BitmapSpan<const uint8_t>;
+
+class RleRunToBitmapDecoder {
+ public:
+  /// The type of run that can be decoded.
+  using RunType = RleRun;
+
+  constexpr RleRunToBitmapDecoder() noexcept = default;
+
+  explicit RleRunToBitmapDecoder(const RunType& run) noexcept { Reset(run); }
+
+  void Reset(const RunType& run) noexcept {
+    values_left_ = run.values_count();
+    if (run.value_little_endian() == 0) {
+      value_pattern_ = uint8_t{0};
+    } else {
+      value_pattern_ = uint8_t{0xFF};
+    }
+  }
+
+  /// Return the number of values that can be advanced.
+  rle_size_t remaining() const { return values_left_; }
+
+  /// Return the repeated value of this decoder.
+  constexpr bool value() const { return value_pattern_ != 0; }
+
+  /// Return how much the decoder would advance if asked to.
+  ///
+  /// Does not modify input.
+  rle_size_t GetAdvanceCapacity(rle_size_t batch_size) const noexcept {
+    const auto n_vals = std::min(batch_size, remaining());
+    return n_vals;
+  }
+
+  /// Advance by as many values as provided or until exhaustion of the decoder.
+  /// Return the number of values skipped.
+  [[nodiscard]] rle_size_t Advance(rle_size_t batch_size) {
+    const auto n_vals = GetAdvanceCapacity(batch_size);
+    values_left_ -= n_vals;
+    ARROW_DCHECK_GE(remaining(), 0);
+    return n_vals;
+  }
+
+  /// Read the next value into `out` and return false if there are no more.
+  [[nodiscard]] bool Get(BitmapSpanMut out) { return GetBatch(out, 1) == 1; }
+
+  /// Get a batch of values into `out` and return the number of decoded 
elements.
+  ///
+  /// May write fewer elements to the output than requested if there are not
+  /// enough values left.
+  [[nodiscard]] rle_size_t GetBatch(BitmapSpanMut out, rle_size_t batch_size) {
+    const auto out_bit_offset = out.bit_start();
+    ARROW_DCHECK_GE(out_bit_offset, 0);
+    ARROW_DCHECK_LT(out_bit_offset, 8);
+
+    if (ARROW_PREDICT_FALSE(remaining() == 0 || batch_size == 0)) {
+      return 0;
+    }
+
+    rle_size_t n_vals = 0;
+
+    // HEADER: Writing inside the first byte if caller gives a non-aligned 
input
+    if (out_bit_offset != 0) {
+      n_vals = GetBatchInByte(out, batch_size);
+    }
+
+    // Writing full bytes
+    n_vals += GetBatchFullBytes(out.NewStartingAt(n_vals), batch_size - 
n_vals);
+
+    // TRAILER: Writing inside the last byte if caller asked for non multiple 
of 8 values
+    const auto n_last_vals = std::min(batch_size - n_vals, remaining());
+    if (n_last_vals > 0) {
+      ARROW_DCHECK_LT(n_last_vals, 8);
+      n_vals += GetBatchInByte(out.NewStartingAt(n_vals), n_last_vals);
+    }
+
+    return n_vals;
+  }
+
+ private:
+  /// The byte pattern for 8 values (full ones or full zeros).
+  uint8_t value_pattern_ = {};
+  /// Number of values left to decode.
+  rle_size_t values_left_ = 0;
+
+  void AdvanceUnsafe(rle_size_t batch_size) { values_left_ -= batch_size; }
+
+  /// Get batch values to fill the first incomplete byte of the output.
+  [[nodiscard]] rle_size_t GetBatchInByte(BitmapSpanMut out, rle_size_t 
batch_size) {
+    const auto out_bit_offset = out.bit_start();
+    ARROW_DCHECK_GE(out_bit_offset, 0);
+    ARROW_DCHECK_LT(out_bit_offset, 8);
+    ARROW_DCHECK_GT(remaining(), 0);
+    ARROW_DCHECK_GE(batch_size, 0);
+
+    // Empty bits in first byte that we can fill
+    const auto empty_bits = rle_size_t{8} - out_bit_offset;
+    // Number of bits in first byte that we want to fill
+    const auto desired_bits = std::min(empty_bits, batch_size);
+    // Try to advance, and get number of bits we had remaining
+    const auto n_bits = Advance(desired_bits);
+    // Copy relevant bits from the value pattern to the output.
+    *out.data() = bit_util::CopyBitsInInteger<uint8_t, /* kAllowFullCopy= */ 
false>({
+        .src = value_pattern_,
+        .dst = *out.data(),
+        .start = static_cast<uint8_t>(out_bit_offset),
+        .end = static_cast<uint8_t>(out_bit_offset + n_bits),
+    });
+
+    return n_bits;
+  }
+
+  /// Get batch in full bytes using memset.
+  [[nodiscard]] rle_size_t GetBatchFullBytes(BitmapSpanMut out, rle_size_t 
batch_size) {
+    const auto n_bytes = std::min(batch_size, remaining()) / 8;
+    ARROW_DCHECK(out.bit_start() == 0 || n_bytes == 0);
+    std::memset(out.data(), value_pattern_, n_bytes);
+    const auto n_vals = 8 * n_bytes;
+    AdvanceUnsafe(n_vals);
+    return n_vals;
+  }
+};
+
+class BitPackedRunToBitmapDecoder {
+ public:
+  /// The type of run that can be decoded.
+  using RunType = BitPackedRun;
+
+  constexpr BitPackedRunToBitmapDecoder() noexcept = default;
+
+  explicit BitPackedRunToBitmapDecoder(const RunType& run) noexcept { 
Reset(run); }
+
+  void Reset(const RunType& run) noexcept {
+    data_ = run.raw_data_ptr();
+    values_count_ = run.values_count();
+    values_read_ = 0;
+    ARROW_DCHECK(run.raw_data_max_size() < 0 ||
+                 bit_util::BytesForBits(values_count_) <= 
run.raw_data_max_size());
+  }
+
+  /// Return the number of values that can be advanced.
+  constexpr rle_size_t remaining() const { return values_count_ - 
values_read_; }
+
+  /// Return how much the decoder would advance if asked to.
+  ///
+  /// Does not modify input.
+  rle_size_t GetAdvanceCapacity(rle_size_t batch_size) const noexcept {
+    const auto n_vals = std::min(batch_size, remaining());
+    return n_vals;
+  }
+
+  /// Advance by as many values as provided or until exhaustion of the decoder.
+  /// Return the number of values skipped.
+  [[nodiscard]] rle_size_t Advance(rle_size_t batch_size) {
+    const auto n_vals = GetAdvanceCapacity(batch_size);
+    values_read_ += n_vals;
+    ARROW_DCHECK_GE(remaining(), 0);
+    return n_vals;
+  }
+
+  /// Get the next value and return false if there are no more.
+  [[nodiscard]] bool Get(BitmapSpanMut out) { return GetBatch(out, 1) == 1; }
+
+  /// Get a batch of values return the number of decoded elements.
+  /// May write fewer elements to the output than requested if there are not 
enough values
+  /// left.
+  [[nodiscard]] rle_size_t GetBatch(BitmapSpanMut out, rle_size_t batch_size) {
+    auto n_vals = GetAdvanceCapacity(batch_size);
+    arrow::internal::CopyBitmap(unread_values_ptr(), 
unread_values_bit_offset(), n_vals,
+                                out.data(), out.bit_start());
+    return Advance(n_vals);
+  }
+
+ private:
+  /// The pointer to the beginning of the run
+  const uint8_t* data_ = nullptr;
+  /// The total number of values in the run
+  rle_size_t values_count_ = 0;
+  /// The number of values read by the decoder
+  rle_size_t values_read_ = 0;
+
+  /// Start pointer of the unread values (may contain values already read).
+  const uint8_t* unread_values_ptr() const noexcept { return data_ + 
(values_read_ / 8); }
+
+  /// Bit in @ref unread_values_ptr where the unread values start.
+  rle_size_t unread_values_bit_offset() const noexcept { return values_read_ % 
8; }
+};
+
+/// A specialized decoder class to extract RLE+bitpacked booleans.
+///
+/// In some cases, such as when reading definition levels for nullable values 
(with
+/// no repetition and no nesting), we know values to be decoded will end up in 
an
+/// Arrow validity bitmap. In such cases, decoding values to a ``int16`` before
+/// encoding them again in overly wasteful.
+class RleBitPackedToBitmapDecoder {
+ public:
+  RleBitPackedToBitmapDecoder() noexcept = default;
+
+  /// Create a decoder object.
+  ///
+  /// data and data_size are the raw bytes to decode.
+  RleBitPackedToBitmapDecoder(const uint8_t* data, rle_size_t data_size) 
noexcept {
+    Reset(data, data_size);
+  }
+
+  void Reset(const uint8_t* data, rle_size_t data_size) noexcept {
+    parser_.Reset(data, data_size, /* value_bit_width= */ 1);
+    decoder_ = {};
+  }
+
+  /// Whether there is still runs to iterate over.
+  bool exhausted() const { return (run_remaining() == 0) && 
parser_.exhausted(); }
+
+  /// Get a batch of values return the number of decoded elements.
+  /// May write fewer elements to the output than requested if there are not 
enough
+  /// values left or if an error occurred.
+  [[nodiscard]] rle_size_t GetBatch(BitmapSpanMut out, rle_size_t batch_size);
+
+ private:
+  RleBitPackedParser parser_ = {};
+  std::variant<RleRunToBitmapDecoder, BitPackedRunToBitmapDecoder> decoder_ = 
{};
+
+  /// Return the number of values that are remaining in the current run.
+  rle_size_t run_remaining() const {
+    return std::visit([](const auto& dec) { return dec.remaining(); }, 
decoder_);
+  }
+
+  /// Get a batch of values from the current run and return the number 
elements read.
+  [[nodiscard]] rle_size_t RunGetBatch(BitmapSpanMut out, rle_size_t 
batch_size) {
+    return std::visit([&](auto& dec) { return dec.GetBatch(out, batch_size); 
}, decoder_);
+  }
+};
+
+/************************************************
+ *  RleBitPackedToBitmapDecoder implementation  *
+ ************************************************/
+
+/// Utility to map a run type to the associate decoder.
+template <typename Run>
+struct RleBitPackedToBitmapDecoderGetDecoder;
+
+template <>
+struct RleBitPackedToBitmapDecoderGetDecoder<RleRun> {
+  using type = RleRunToBitmapDecoder;
+};
+
+template <>
+struct RleBitPackedToBitmapDecoderGetDecoder<BitPackedRun> {
+  using type = BitPackedRunToBitmapDecoder;
+};
+
+inline auto RleBitPackedToBitmapDecoder::GetBatch(BitmapSpanMut out,
+                                                  rle_size_t batch_size) -> 
rle_size_t {
+  using ControlFlow = RleBitPackedParser::ControlFlow;
+
+  if (ARROW_PREDICT_FALSE(batch_size == 0 || exhausted())) {
+    return 0;
+  }
+
+  rle_size_t values_read = 0;
+
+  // Remaining from a previous call that would have left some unread data from 
a run.
+  if (ARROW_PREDICT_FALSE(run_remaining() > 0)) {
+    const auto read = RunGetBatch(out, batch_size);
+    values_read += read;
+
+    // Either we fulfilled all the batch to be read or we finished remaining 
run.
+    if (ARROW_PREDICT_FALSE(values_read == batch_size)) {
+      return values_read;
+    }
+    ARROW_DCHECK(run_remaining() == 0);
+  }
+
+  parser_.ParseWithCallable([&](auto run) {
+    using RunDecoder = 
RleBitPackedToBitmapDecoderGetDecoder<decltype(run)>::type;
+
+    ARROW_DCHECK_LT(values_read, batch_size);
+    RunDecoder decoder(run);
+    // The output span carries its own bit offset, so advancing it past the 
values
+    // already written keeps successive runs correctly aligned in the bitmap.
+    const auto read =
+        decoder.GetBatch(out.NewStartingAt(values_read), batch_size - 
values_read);
+    ARROW_DCHECK_LE(read, batch_size - values_read);
+    values_read += read;
+
+    // Stop reading and store remaining decoder
+    if (ARROW_PREDICT_FALSE(values_read == batch_size || read == 0)) {
+      decoder_ = std::move(decoder);
+      return ControlFlow::Break;
+    }
+
+    return ControlFlow::Continue;
+  });
+
+  return values_read;
+}
+
+}  // namespace arrow::util
diff --git a/cpp/src/arrow/util/rle_bitmap_test.cc 
b/cpp/src/arrow/util/rle_bitmap_test.cc
new file mode 100644
index 0000000000..7dc572eb5a
--- /dev/null
+++ b/cpp/src/arrow/util/rle_bitmap_test.cc
@@ -0,0 +1,602 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <array>
+#include <cstdint>
+#include <random>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "arrow/testing/gtest_util.h"
+#include "arrow/util/bit_util.h"
+#include "arrow/util/rle_bitmap_internal.h"
+#include "arrow/util/rle_encoding_internal.h"
+
+namespace arrow::util {
+
+namespace {
+
+/// Make a vector of `size` pseudo-random bytes, deterministic for a given 
`seed`.
+std::vector<uint8_t> MakeRandomBytes(size_t size, uint32_t seed = 56) {
+  std::vector<uint8_t> bytes(size);
+  std::minstd_rand gen(seed);
+  std::uniform_int_distribution<int> dist(0, 255);  // no standard support for 
uint8_t
+  for (auto& byte : bytes) {
+    byte = static_cast<uint8_t>(dist(gen));
+  }
+  return bytes;
+}
+
+/// Read the first `count` bits of `bytes` (LSB first) into a vector of 
booleans.
+std::vector<bool> BitsFromBytes(const std::vector<uint8_t>& bytes, rle_size_t 
count) {
+  std::vector<bool> bits(count);
+  for (rle_size_t i = 0; i < count; ++i) {
+    bits[i] = bit_util::GetBit(bytes.data(), i);
+  }
+  return bits;
+}
+
+struct CheckDecodedBitsParams {
+  const std::vector<uint8_t>& actual;
+  const std::vector<bool>& expected;
+  rle_size_t count;
+  rle_size_t actual_start_bit = 0;
+  rle_size_t expected_start_idx = 0;
+};
+
+/// Check the decoded output in `out` against `expected`.
+void CheckDecodedBits(const CheckDecodedBitsParams& params) {
+  ARROW_SCOPED_TRACE("out_start_bit = ", params.actual_start_bit,
+                     ", expected_start_idx = ", params.expected_start_idx);
+  for (rle_size_t i = 0; i < params.count; ++i) {
+    ASSERT_EQ(bit_util::GetBit(params.actual.data(), params.actual_start_bit + 
i),
+              params.expected[params.expected_start_idx + i])
+        << "first difference at bit " << i;
+  }
+}
+
+struct CheckBitsEqualParams {
+  const std::vector<uint8_t>& actual;
+  const std::vector<uint8_t>& expected;
+  rle_size_t count;
+  rle_size_t actual_start_bit = 0;
+  rle_size_t expected_start_bit = 0;
+};
+
+/// Check that two bit ranges, stored in `actual` and `expected`, are equal.
+void CheckBitsEqual(const CheckBitsEqualParams& params) {
+  ARROW_SCOPED_TRACE("actual_start_bit = ", params.actual_start_bit,
+                     ", expected_start_bit = ", params.expected_start_bit);
+  for (rle_size_t i = 0; i < params.count; ++i) {
+    ASSERT_EQ(bit_util::GetBit(params.actual.data(), params.actual_start_bit + 
i),
+              bit_util::GetBit(params.expected.data(), 
params.expected_start_bit + i))
+        << "first difference at bit " << i;
+  }
+}
+
+/// Skip the first `expected_skip` values with Advance(), then decode the rest 
of the run
+/// into one output bitmap, `chunk_size` values at a time. Compare against 
`expected`.
+///
+/// `chunk_size` controls output bit alignment. When `chunk_size` is not a 
multiple of 8,
+/// later calls start at a non-zero output bit offset.
+///
+/// `expected_skip` shifts the decoder's read offset relative to the output 
offset.
+/// A non-zero `expected_skip` makes the two differ, which exercises the 
bit-unaligned
+/// read path of BitPackedRunToBitmapDecoder. With `expected_skip == 0` they 
stay in sync
+/// and only the aligned path runs.
+template <typename Decoder>
+void CheckDecoderValuesChunked(const typename Decoder::RunType& run,
+                               const std::vector<bool>& expected,
+                               rle_size_t chunk_size = 1, rle_size_t 
expected_skip = 0) {
+  ARROW_SCOPED_TRACE("chunk_size = ", chunk_size, ", expected_skip = ", 
expected_skip);
+
+  const auto n_vals = static_cast<rle_size_t>(expected.size());
+  ASSERT_LE(expected_skip, n_vals);
+
+  Decoder decoder(run);
+  const auto advanced = decoder.Advance(expected_skip);
+  ASSERT_EQ(advanced, expected_skip);
+  const auto n_vals_to_decode = n_vals - expected_skip;
+
+  // Output buffer
+  const auto n_bytes = 
static_cast<size_t>(bit_util::BytesForBits(n_vals_to_decode));
+  std::vector<uint8_t> out(n_bytes, 0);
+
+  rle_size_t n_val_read = 0;
+  while (n_val_read < n_vals_to_decode) {
+    const auto want = std::min(chunk_size, n_vals_to_decode - n_val_read);
+    const auto got =
+        decoder.GetBatch(BitmapSpanMut(out.data(), /*bit_start=*/n_val_read), 
want);
+    EXPECT_EQ(got, want) << "at pos " << n_val_read;
+    ASSERT_GT(got, 0) << "at pos " << n_val_read;  // break on failure
+    n_val_read += got;
+    EXPECT_EQ(decoder.remaining(), n_vals_to_decode - n_val_read);
+  }
+
+  EXPECT_EQ(decoder.remaining(), 0);
+  CheckDecodedBits({
+      .actual = out,
+      .expected = expected,
+      .count = n_vals_to_decode,
+      .actual_start_bit = 0,
+      .expected_start_idx = expected_skip,
+  });
+}
+
+/// Decode a chunk of data into a known output to check for out of bounds 
write.
+///
+/// @see CheckDecoderValuesChunked
+template <typename Decoder>
+void CheckDecoderClobber(const typename Decoder::RunType& run,
+                         const std::vector<bool>& expected, rle_size_t 
chunk_size = 1,
+                         rle_size_t expected_skip = 0) {
+  ARROW_SCOPED_TRACE("chunk_size = ", chunk_size, ", expected_skip = ", 
expected_skip);
+
+  const auto n_vals = static_cast<rle_size_t>(expected.size());
+  ASSERT_LE(expected_skip, n_vals);
+
+  Decoder decoder(run);
+  const auto advanced = decoder.Advance(expected_skip);
+  ASSERT_EQ(advanced, expected_skip);
+  const auto n_vals_to_decode = n_vals - expected_skip;
+
+  // Output buffer with enough capacity to store a full chunk plus extra bytes 
as
+  // clobbers/guard to check for out of bounds write.
+  const auto n_bytes = static_cast<size_t>(bit_util::BytesForBits(chunk_size) +
+                                           bit_util::CeilDiv(n_vals, 
chunk_size) + 2);
+  // This seed is arbitrary and of little importance. We are simply trying to 
avoid an
+  // unlikely case where guards have the same pattern in all invocations.
+  const auto out_pattern =
+      MakeRandomBytes(n_bytes, /* seed= */ (chunk_size << 16) ^ expected_skip);
+  auto out = out_pattern;
+
+  rle_size_t n_val_read = 0;
+  rle_size_t out_bit_start = 0;
+  while (n_val_read < n_vals_to_decode) {
+    // Clean output buffer
+    out = out_pattern;
+    const auto want = std::min(chunk_size, n_vals_to_decode - n_val_read);
+    const auto got = decoder.GetBatch(BitmapSpanMut(out.data(), 
out_bit_start), want);
+    ASSERT_GT(got, 0) << "at pos " << n_val_read;  // break on failure
+    EXPECT_EQ(got, want) << "at pos " << n_val_read;
+    // Check that the leading bits have not been modified
+    CheckBitsEqual({.actual = out, .expected = out_pattern, .count = 
out_bit_start});
+    // Check that the trailing bits have not been modified
+    CheckBitsEqual({
+        .actual = out,
+        .expected = out_pattern,
+        .count = static_cast<rle_size_t>(8 * n_bytes) - (out_bit_start + want),
+        .actual_start_bit = out_bit_start + want,
+        .expected_start_bit = out_bit_start + want,
+    });
+    // Check decoded bits are also correct
+    CheckDecodedBits({
+        .actual = out,
+        .expected = expected,
+        .count = want,
+        .actual_start_bit = out_bit_start,
+        .expected_start_idx = expected_skip + n_val_read,
+    });
+
+    n_val_read += got;
+    ++out_bit_start;
+    EXPECT_EQ(decoder.remaining(), n_vals_to_decode - n_val_read);
+  }
+}
+
+/// All the checks shared by both decoder types.
+///
+/// `expected` is the full sequence of booleans the run should decode to.
+template <typename Decoder>
+void CheckBitmapDecoder(const typename Decoder::RunType& run,
+                        const std::vector<bool>& expected) {
+  const auto n_vals = static_cast<rle_size_t>(expected.size());
+
+  // remaining() reflects the run size before any value is read.
+  {
+    Decoder decoder(run);
+    EXPECT_EQ(decoder.remaining(), n_vals);
+  }
+
+  // Empty requests are a no-op.
+  {
+    Decoder decoder(run);
+    uint8_t out = 0;
+    const auto got = decoder.GetBatch(BitmapSpanMut(&out), /*batch_size=*/0);
+    EXPECT_EQ(got, 0);
+    EXPECT_EQ(decoder.remaining(), n_vals);
+  }
+
+  // Decode the whole run in several chunks.
+  for (const rle_size_t chunk_size : {rle_size_t{1}, rle_size_t{3}, 
rle_size_t{7},
+                                      rle_size_t{8}, rle_size_t{9}, n_vals, 
n_vals + 1}) {
+    CheckDecoderValuesChunked<Decoder>(run, expected, chunk_size);
+  }
+
+  // Decode the whole run in several chunks, after an initial Advance that 
shifts
+  // the run and output bit alignment.
+  for (const rle_size_t chunk_size : {rle_size_t{1}, rle_size_t{3}, 
rle_size_t{7},
+                                      rle_size_t{8}, rle_size_t{9}, n_vals, 
n_vals + 1}) {
+    for (rle_size_t expected_skip = 1; expected_skip < 8 && expected_skip < 
n_vals;
+         ++expected_skip) {
+      // Check the decoding happens as expected
+      CheckDecoderValuesChunked<Decoder>(run, expected, chunk_size, 
expected_skip);
+      // Check the decoding does not write out of bounds
+      CheckDecoderClobber<Decoder>(run, expected, chunk_size, expected_skip);
+    }
+  }
+
+  // Get() one value at a time, then read past the end.
+  {
+    Decoder decoder(run);
+    std::vector<uint8_t> 
out(static_cast<size_t>(bit_util::BytesForBits(n_vals)) + 1, 0);
+    for (rle_size_t i = 0; i < n_vals; ++i) {
+      const bool ok = decoder.Get(BitmapSpanMut(out.data(), /*bit_start=*/i));
+      EXPECT_TRUE(ok);
+      EXPECT_EQ(decoder.remaining(), n_vals - i - 1);
+    }
+    // Exhausted: nothing more can be read or advanced.
+    const bool ok = decoder.Get(BitmapSpanMut(out.data()));
+    EXPECT_FALSE(ok);
+    const auto advanced = decoder.Advance(1);
+    EXPECT_EQ(advanced, 0);
+    EXPECT_EQ(decoder.remaining(), 0);
+    CheckDecodedBits({.actual = out, .expected = expected, .count = n_vals});
+  }
+
+  // Advancing more than available stops at the run boundary.
+  {
+    Decoder decoder(run);
+    const auto advanced = decoder.Advance(n_vals + 100);
+    EXPECT_EQ(advanced, n_vals);
+    EXPECT_EQ(decoder.remaining(), 0);
+  }
+
+  // Reset() rewinds the decoder so the run can be decoded again.
+  {
+    Decoder decoder(run);
+    std::vector<uint8_t> 
out_1(static_cast<size_t>(bit_util::BytesForBits(n_vals)), 0);
+    const auto scratch_got = decoder.GetBatch(BitmapSpanMut(out_1.data()), 
n_vals);
+    EXPECT_EQ(scratch_got, n_vals);
+    EXPECT_EQ(decoder.remaining(), 0);
+
+    decoder.Reset(run);
+    EXPECT_EQ(decoder.remaining(), n_vals);
+    std::vector<uint8_t> 
out_2(static_cast<size_t>(bit_util::BytesForBits(n_vals)), 0);
+    const auto got = decoder.GetBatch(BitmapSpanMut(out_2.data()), n_vals);
+    EXPECT_EQ(got, n_vals);
+    CheckDecodedBits({.actual = out_2, .expected = expected, .count = n_vals});
+  }
+}
+
+}  // namespace
+
+/***************************
+ *  RleRunToBitmapDecoder  *
+ ***************************/
+
+class RleRunToBitmapDecoderTest : public ::testing::TestWithParam<rle_size_t> 
{};
+
+TEST_P(RleRunToBitmapDecoderTest, Decode) {
+  const auto& count = GetParam();
+
+  // Only two possible repeated value
+  for (bool value : {true, false}) {
+    ARROW_SCOPED_TRACE("value = ", value);
+
+    // A boolean RLE run stores its value in a single (1-bit-wide) byte.
+    const uint8_t data = value ? 1 : 0;
+    const auto run = RleRun(&data, count, /*value_bit_width=*/1);
+
+    // value() reports the repeated boolean.
+    RleRunToBitmapDecoder decoder(run);
+    EXPECT_EQ(decoder.value(), value);
+
+    const std::vector<bool> expected(count, value);
+    CheckBitmapDecoder<RleRunToBitmapDecoder>(run, expected);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(  //
+    RleBitmap, RleRunToBitmapDecoderTest,
+    ::testing::Values(0, 1, 3, 8, 9, 13, 64, 100, 177));
+
+/*********************************
+ *  BitPackedRunToBitmapDecoder  *
+ *********************************/
+
+struct BitPackedBitmapCase {
+  std::string name;
+  // The raw bit-packed bytes (LSB first). Must hold at least `count` bits.
+  std::vector<uint8_t> bytes;
+  // The number of values in the run.
+  rle_size_t count;
+};
+
+// Gtest representation without which, gtest falls back to dumping
+// the raw object bytes, which reads uninitialised padding and trips valgrind.
+void PrintTo(const BitPackedBitmapCase& param, std::ostream* os) { *os << 
param.name; }
+
+class BitPackedRunToBitmapDecoderTest
+    : public ::testing::TestWithParam<BitPackedBitmapCase> {};
+
+TEST_P(BitPackedRunToBitmapDecoderTest, Decode) {
+  const auto& param = GetParam();
+  ASSERT_GE(param.bytes.size(), 
static_cast<size_t>(bit_util::BytesForBits(param.count)));
+
+  const auto run = BitPackedRun(param.bytes.data(), param.count, 
/*value_bit_width=*/1,
+                                /*max_read_bytes=*/-1);
+
+  const std::vector<bool> expected = BitsFromBytes(param.bytes, param.count);
+  CheckBitmapDecoder<BitPackedRunToBitmapDecoder>(run, expected);
+}
+
+INSTANTIATE_TEST_SUITE_P(  //
+    RleBitmap, BitPackedRunToBitmapDecoderTest,
+    ::testing::Values(  //
+        BitPackedBitmapCase{.name = "empty", .bytes = {0b10110010}, .count = 
0},
+        BitPackedBitmapCase{.name = "single", .bytes = {0b00000001}, .count = 
1},
+        BitPackedBitmapCase{.name = "three", .bytes = {0b00000101}, .count = 
3},
+        BitPackedBitmapCase{.name = "eight", .bytes = {0b11010010}, .count = 
8},
+        BitPackedBitmapCase{
+            .name = "alternating", .bytes = {0b10101010, 0b10101010}, .count = 
13},
+        BitPackedBitmapCase{.name = "all_zeros", .bytes = {0x00, 0x00}, .count 
= 16},
+        BitPackedBitmapCase{.name = "all_ones", .bytes = {0xFF, 0xFF}, .count 
= 16},
+        BitPackedBitmapCase{
+            .name = "mixed", .bytes = {0b11001010, 0b00001111, 0b10110001}, 
.count = 24},
+        BitPackedBitmapCase{
+            .name = "unaligned_count", .bytes = {0b00110101, 0b11100100}, 
.count = 11},
+        BitPackedBitmapCase{
+            .name = "large",
+            .bytes = std::vector<uint8_t>(16, 0b01101001),
+            .count = 128,
+        }),
+    [](const ::testing::TestParamInfo<BitPackedBitmapCase>& info) {
+      return info.param.name;
+    });
+
+/*********************************
+ *  RleBitPackedToBitmapDecoder  *
+ *********************************/
+
+namespace {
+
+/// Append the LEB128 (unsigned, little-endian base-128) encoding of `value`.
+void AppendLeb128(std::vector<uint8_t>& out, uint32_t value) {
+  std::array<uint8_t, bit_util::kMaxLEB128ByteLenFor<uint32_t>> buf = {};
+  const auto n_bytes =
+      bit_util::WriteLEB128(value, buf.data(), 
static_cast<int32_t>(buf.size()));
+  ASSERT_GT(n_bytes, 0);
+  out.insert(out.end(), buf.data(), buf.data() + n_bytes);
+}
+
+void AppendRleRun(std::vector<uint8_t>& bytes, std::vector<bool>& expected, 
bool value,
+                  rle_size_t count) {
+  AppendLeb128(bytes, static_cast<uint32_t>(count) << 1);  // low bit 0 => RLE
+  bytes.push_back(value ? 1 : 0);
+  expected.insert(expected.end(), count, value);
+}
+
+void AppendBitPackedRun(std::vector<uint8_t>& bytes, std::vector<bool>& 
expected,
+                        const std::vector<uint8_t>& packed) {
+  const auto groups = static_cast<rle_size_t>(packed.size());
+  AppendLeb128(bytes, (static_cast<uint32_t>(groups) << 1) | 1);  // low bit 1 
=> packed
+  bytes.insert(bytes.end(), packed.begin(), packed.end());
+  for (rle_size_t i = 0; i < groups * 8; ++i) {
+    expected.push_back(bit_util::GetBit(packed.data(), i));
+  }
+}
+
+/// Decode the whole `bytes` into a bitmap and check it against `expected`.
+///
+/// Decode `chunk_size` values per GetBatch call to check the decoder state 
between
+/// calls. The output starts at bit offset `out_offset`. A non-zero offset 
makes
+/// the output and the encoded `bytes` use different bit alignment.
+void CheckRleBitPackedDecode(const std::vector<uint8_t>& bytes,
+                             const std::vector<bool>& expected, rle_size_t 
chunk_size,
+                             rle_size_t out_offset = 0) {
+  ARROW_SCOPED_TRACE("chunk_size = ", chunk_size, ", out_offset = ", 
out_offset);
+  const auto n_vals = static_cast<rle_size_t>(expected.size());
+
+  RleBitPackedToBitmapDecoder decoder(bytes.data(),
+                                      static_cast<rle_size_t>(bytes.size()));
+  EXPECT_EQ(decoder.exhausted(), n_vals == 0);
+
+  // Output buffer with one guard byte to catch out-of-bounds writes.
+  std::vector<uint8_t> out(
+      static_cast<size_t>(bit_util::BytesForBits(out_offset + n_vals)) + 1, 0);
+  const uint8_t guard = 0xA5;
+  out.back() = guard;
+
+  rle_size_t read = 0;
+  while (read < n_vals) {
+    const auto want = std::min(chunk_size, n_vals - read);
+    const auto got = decoder.GetBatch(
+        BitmapSpanMut(out.data(), /*bit_start=*/out_offset + read), want);
+    EXPECT_EQ(got, want) << "at pos " << read;
+    ASSERT_GT(got, 0) << "at pos " << read;  // break on failure
+    read += got;
+  }
+
+  EXPECT_EQ(read, n_vals);
+  EXPECT_TRUE(decoder.exhausted());
+  // Reading past the end yields nothing and leaves the decoder exhausted.
+  uint8_t scratch = 0;
+  const auto past_end = decoder.GetBatch(BitmapSpanMut(&scratch), 8);
+  EXPECT_EQ(past_end, 0);
+  EXPECT_TRUE(decoder.exhausted());
+
+  EXPECT_EQ(out.back(), guard) << "decoder wrote past the end of the output";
+  CheckDecodedBits({
+      .actual = out,
+      .expected = expected,
+      .count = n_vals,
+      .actual_start_bit = out_offset,
+  });
+}
+
+/// Run the decode check over a battery of chunk sizes and output offsets.
+void CheckRleBitPackedToBitmap(const std::vector<uint8_t>& bytes,
+                               const std::vector<bool>& expected) {
+  const auto n_vals = static_cast<rle_size_t>(expected.size());
+  ASSERT_GT(n_vals, 0);
+  for (const rle_size_t chunk_size :
+       {rle_size_t{1}, rle_size_t{3}, rle_size_t{7}, rle_size_t{8}, 
rle_size_t{9},
+        rle_size_t{33}, n_vals, n_vals + 1}) {
+    CheckRleBitPackedDecode(bytes, expected, chunk_size);
+    // A non-zero output offset forces the first run to start at a non-byte
+    // aligned output position.
+    for (rle_size_t out_offset = 1; out_offset < 8; ++out_offset) {
+      CheckRleBitPackedDecode(bytes, expected, chunk_size, out_offset);
+    }
+  }
+}
+
+}  // namespace
+
+TEST(RleBitPackedToBitmapDecoder, Empty) {
+  // A default-constructed decoder is already exhausted.
+  RleBitPackedToBitmapDecoder decoder;
+  EXPECT_TRUE(decoder.exhausted());
+  uint8_t out = 0;
+  auto got = decoder.GetBatch(BitmapSpanMut(&out), 8);
+  EXPECT_EQ(got, 0);
+
+  // So is one reset on an empty buffer.
+  decoder.Reset(nullptr, 0);
+  EXPECT_TRUE(decoder.exhausted());
+  got = decoder.GetBatch(BitmapSpanMut(&out), 8);
+  EXPECT_EQ(got, 0);
+}
+
+TEST(RleBitPackedToBitmapDecoder, SingleRleZeros) {
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendRleRun(bytes, expected, /*value=*/false, /*count=*/100);
+  CheckRleBitPackedToBitmap(bytes, expected);
+}
+
+TEST(RleBitPackedToBitmapDecoder, SingleRleOnes) {
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendRleRun(bytes, expected, /*value=*/true, /*count=*/100);
+  CheckRleBitPackedToBitmap(bytes, expected);
+}
+
+TEST(RleBitPackedToBitmapDecoder, SingleBitPacked) {
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendBitPackedRun(bytes, expected, {0b10101010, 0b11001100, 0b11110000});
+  CheckRleBitPackedToBitmap(bytes, expected);
+}
+
+TEST(RleBitPackedToBitmapDecoder, MixedRunsAligned) {
+  // All runs end on a byte boundary, so each run starts byte-aligned in the
+  // output.
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendRleRun(bytes, expected, /*value=*/false, /*count=*/16);
+  AppendBitPackedRun(bytes, expected, {0b10101010, 0b01010101});
+  AppendRleRun(bytes, expected, /*value=*/true, /*count=*/64);
+  AppendBitPackedRun(bytes, expected, {0b00001111});
+  CheckRleBitPackedToBitmap(bytes, expected);
+}
+
+TEST(RleBitPackedToBitmapDecoder, MixedRunsUnaligned) {
+  // RLE runs with counts that are not multiples of 8 make each following run
+  // start at a non-byte-aligned output position.
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendRleRun(bytes, expected, /*value=*/true, /*count=*/13);
+  AppendBitPackedRun(bytes, expected, {0b01101001, 0b10010110});
+  AppendRleRun(bytes, expected, /*value=*/false, /*count=*/5);
+  AppendRleRun(bytes, expected, /*value=*/true, /*count=*/200);
+  AppendBitPackedRun(bytes, expected, {0b11110000});
+  AppendRleRun(bytes, expected, /*value=*/false, /*count=*/3);
+  AppendBitPackedRun(bytes, expected, {0b10110001, 0b00011101});
+  CheckRleBitPackedToBitmap(bytes, expected);
+}
+
+TEST(RleBitPackedToBitmapDecoder, ReadPastEnd) {
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendRleRun(bytes, expected, /*value=*/true, /*count=*/10);
+  AppendBitPackedRun(bytes, expected, {0b10110010});
+  const auto n_vals = static_cast<rle_size_t>(expected.size());
+
+  RleBitPackedToBitmapDecoder decoder(bytes.data(),
+                                      static_cast<rle_size_t>(bytes.size()));
+  std::vector<uint8_t> out(static_cast<size_t>(bit_util::BytesForBits(n_vals)) 
+ 1, 0);
+  // Requesting more values than available produces only the available ones.
+  auto got = decoder.GetBatch(BitmapSpanMut(out.data()), n_vals + 100);
+  EXPECT_EQ(got, n_vals);
+  EXPECT_TRUE(decoder.exhausted());
+  got = decoder.GetBatch(BitmapSpanMut(out.data()), 10);
+  EXPECT_EQ(got, 0);
+  CheckDecodedBits({.actual = out, .expected = expected, .count = n_vals});
+}
+
+TEST(RleBitPackedToBitmapDecoder, Reset) {
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendRleRun(bytes, expected, /*value=*/true, /*count=*/13);
+  AppendBitPackedRun(bytes, expected, {0b01101001, 0b10010110});
+  AppendRleRun(bytes, expected, /*value=*/false, /*count=*/20);
+  const auto n_vals = static_cast<rle_size_t>(expected.size());
+  const auto data_size = static_cast<rle_size_t>(bytes.size());
+
+  RleBitPackedToBitmapDecoder decoder(bytes.data(), data_size);
+  std::vector<uint8_t> 
out_1(static_cast<size_t>(bit_util::BytesForBits(n_vals)), 0);
+  const auto got_1 = decoder.GetBatch(BitmapSpanMut(out_1.data()), n_vals);
+  EXPECT_EQ(got_1, n_vals);
+  EXPECT_TRUE(decoder.exhausted());
+
+  // Reset rewinds the decoder so the same buffer decodes again.
+  decoder.Reset(bytes.data(), data_size);
+  EXPECT_FALSE(decoder.exhausted());
+  std::vector<uint8_t> 
out_2(static_cast<size_t>(bit_util::BytesForBits(n_vals)), 0);
+  const auto got_2 = decoder.GetBatch(BitmapSpanMut(out_2.data()), n_vals);
+  EXPECT_EQ(got_2, n_vals);
+  EXPECT_TRUE(decoder.exhausted());
+  CheckDecodedBits({.actual = out_2, .expected = expected, .count = n_vals});
+}
+
+TEST(RleBitPackedToBitmapDecoder, Truncated) {
+  // Malformed input: a bit-packed run declares more values than the buffer
+  // holds. The decoder should return the values it can read and report that it
+  // is not exhausted, rather than crash or read out of bounds.
+  std::vector<uint8_t> bytes;
+  std::vector<bool> expected;
+  AppendRleRun(bytes, expected, /*value=*/true, /*count=*/10);
+  // The header declares 4 bytes (4 * 8 = 32 values) of bit-packed data, but 
only
+  // 1 byte follows.
+  AppendLeb128(bytes, (4u << 1) | 1);
+  bytes.push_back(0b10101010);
+
+  RleBitPackedToBitmapDecoder decoder(bytes.data(),
+                                      static_cast<rle_size_t>(bytes.size()));
+  std::vector<uint8_t> out(16, 0);
+  // The RLE run decodes fully; the truncated bit-packed run cannot be parsed.
+  const auto got = decoder.GetBatch(BitmapSpanMut(out.data()), 1000);
+  EXPECT_EQ(got, 10);
+  EXPECT_FALSE(decoder.exhausted());
+  CheckDecodedBits({.actual = out, .expected = expected, .count = 10});
+}
+
+}  // namespace arrow::util
diff --git a/cpp/src/arrow/util/rle_encoding_internal.h 
b/cpp/src/arrow/util/rle_encoding_internal.h
index 7b4f211480..825cd253df 100644
--- a/cpp/src/arrow/util/rle_encoding_internal.h
+++ b/cpp/src/arrow/util/rle_encoding_internal.h
@@ -22,6 +22,7 @@
 
 #include <algorithm>
 #include <array>
+#include <cstring>
 #include <limits>
 #include <type_traits>
 #include <variant>
@@ -32,6 +33,7 @@
 #include "arrow/util/bpacking_internal.h"
 #include "arrow/util/logging.h"
 #include "arrow/util/macros.h"
+#include "arrow/util/ubsan.h"
 
 namespace arrow::util {
 
@@ -102,10 +104,6 @@ class RleRunDecoder;
 /// 10 % on some benchmarks.
 class RleRun {
  public:
-  /// The decoder class used to decode a single run in the given type.
-  template <typename T>
-  using DecoderType = RleRunDecoder<T>;
-
   constexpr RleRun() noexcept = default;
 
   explicit RleRun(const uint8_t* data, rle_size_t values_count,
@@ -116,6 +114,12 @@ class RleRun {
     std::copy(data, data + raw_data_size(value_bit_width), data_.begin());
   }
 
+  /// The repeated value in the run in little endian form (as stored in the 
buffer).
+  uint64_t value_little_endian() const noexcept {
+    // Underlying memcpy is required to avoid undefined behavior.
+    return SafeLoadAs<uint64_t>(data_.data());
+  }
+
   /// The number of repeated values in this run.
   constexpr rle_size_t values_count() const noexcept { return values_count_; }
 
@@ -132,7 +136,7 @@ class RleRun {
  private:
   /// The repeated value raw bytes stored inside the class with enough space 
to store
   /// up to a 64 bit value.
-  std::array<uint8_t, 8> data_ = {};
+  alignas(8) std::array<uint8_t, 8> data_ = {};
   /// The number of time the value is repeated.
   rle_size_t values_count_ = 0;
 };
@@ -150,10 +154,6 @@ class BitPackedRunDecoder;
 /// 10 % on some benchmarks.
 class BitPackedRun {
  public:
-  /// The decoder class used to decode a single run in the given type.
-  template <typename T>
-  using DecoderType = BitPackedRunDecoder<T>;
-
   constexpr BitPackedRun() noexcept = default;
 
   constexpr BitPackedRun(const uint8_t* data, rle_size_t values_count,
@@ -240,6 +240,10 @@ class RleBitPackedParser {
   template <typename Handler>
   void Parse(Handler&& handler);
 
+  /// Call the parser with a single callable for all event types.
+  template <typename Callable>
+  void ParseWithCallable(Callable&& func);
+
  private:
   /// The pointer to the beginning of the run
   const uint8_t* data_ = nullptr;
@@ -277,10 +281,8 @@ class RleRunDecoder {
       // if the bool value isn't 0 or 1.
       value_ = *run.raw_data_ptr() & 1;
     } else {
-      // Memcopy is required to avoid undefined behavior.
-      value_ = {};
-      std::memcpy(&value_, run.raw_data_ptr(), 
run.raw_data_size(value_bit_width));
-      value_ = ::arrow::bit_util::FromLittleEndian(value_);
+      value_ = static_cast<value_type>(
+          ::arrow::bit_util::FromLittleEndian(run.value_little_endian()));
     }
   }
 
@@ -505,10 +507,6 @@ class RleBitPackedDecoder {
         decoder_);
   }
 
-  /// Call the parser with a single callable for all event types.
-  template <typename Callable>
-  void ParseWithCallable(Callable&& func);
-
   /// Utility methods for retrieving spaced values.
   template <typename Converter>
   [[nodiscard]] rle_size_t GetSpaced(Converter converter,
@@ -670,6 +668,17 @@ void RleBitPackedParser::Parse(Handler&& handler) {
   }
 }
 
+template <typename Callable>
+void RleBitPackedParser::ParseWithCallable(Callable&& func) {
+  struct {
+    Callable func;
+    auto OnBitPackedRun(BitPackedRun run) { return func(std::move(run)); }
+    auto OnRleRun(RleRun run) { return func(std::move(run)); }
+  } handler{std::move(func)};
+
+  return Parse(std::move(handler));
+}
+
 namespace internal {
 /// The maximal unsigned size that a variable can fit.
 template <typename T>
@@ -758,17 +767,19 @@ auto RleBitPackedParser::PeekImpl(Handler&& handler) const
  *  RleBitPackedDecoder  *
  *************************/
 
+/// Utility to map a run type to the associate decoder.
+template <typename T, typename Run>
+struct RleBitPackedDecoderGetRunDecoder;
+
 template <typename T>
-template <typename Callable>
-void RleBitPackedDecoder<T>::ParseWithCallable(Callable&& func) {
-  struct {
-    Callable func;
-    auto OnBitPackedRun(BitPackedRun run) { return func(std::move(run)); }
-    auto OnRleRun(RleRun run) { return func(std::move(run)); }
-  } handler{std::move(func)};
+struct RleBitPackedDecoderGetRunDecoder<T, RleRun> {
+  using type = RleRunDecoder<T>;
+};
 
-  parser_.Parse(std::move(handler));
-}
+template <typename T>
+struct RleBitPackedDecoderGetRunDecoder<T, BitPackedRun> {
+  using type = BitPackedRunDecoder<T>;
+};
 
 template <typename T>
 bool RleBitPackedDecoder<T>::Get(value_type* val) {
@@ -780,6 +791,10 @@ auto RleBitPackedDecoder<T>::GetBatch(value_type* out,
                                       rle_size_t batch_size) -> rle_size_t {
   using ControlFlow = RleBitPackedParser::ControlFlow;
 
+  if (ARROW_PREDICT_FALSE(batch_size == 0 || exhausted())) {
+    return 0;
+  }
+
   rle_size_t values_read = 0;
 
   // Remaining from a previous call that would have left some unread data from 
a run.
@@ -795,8 +810,8 @@ auto RleBitPackedDecoder<T>::GetBatch(value_type* out,
     ARROW_DCHECK(run_remaining() == 0);
   }
 
-  ParseWithCallable([&](auto run) {
-    using RunDecoder = typename decltype(run)::template 
DecoderType<value_type>;
+  parser_.ParseWithCallable([&](auto run) {
+    using RunDecoder = RleBitPackedDecoderGetRunDecoder<value_type, 
decltype(run)>::type;
 
     ARROW_DCHECK_LT(values_read, batch_size);
     RunDecoder decoder(run, value_bit_width_);
@@ -1108,8 +1123,8 @@ auto RleBitPackedDecoder<T>::GetSpaced(Converter 
converter,
     ARROW_DCHECK(run_remaining() == 0);
   }
 
-  ParseWithCallable([&](auto run) {
-    using RunDecoder = typename decltype(run)::template 
DecoderType<value_type>;
+  parser_.ParseWithCallable([&](auto run) {
+    using RunDecoder = RleBitPackedDecoderGetRunDecoder<value_type, 
decltype(run)>::type;
 
     RunDecoder decoder(run, value_bit_width_);
 
@@ -1290,8 +1305,8 @@ auto RleBitPackedDecoder<T>::GetBatchWithDict(const V* 
dictionary,
     ARROW_DCHECK(run_remaining() == 0);
   }
 
-  ParseWithCallable([&](auto run) {
-    using RunDecoder = typename decltype(run)::template 
DecoderType<value_type>;
+  parser_.ParseWithCallable([&](auto run) {
+    using RunDecoder = RleBitPackedDecoderGetRunDecoder<value_type, 
decltype(run)>::type;
 
     RunDecoder decoder(run, value_bit_width_);
 

Reply via email to