getBlobItems() {
+ return blobItems;
+ }
+
+ /**
+ * @return the continuation token for the next page, or null if this is the last page
+ */
+ public String getNextMarker() {
+ return nextMarker;
+ }
+
+ /**
+ * @return the total number of records reported by the service, or null if not present
+ */
+ public Integer getNumberOfRecords() {
+ return numberOfRecords;
+ }
+ }
+
+ //endregion
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java
new file mode 100644
index 000000000000..8db6c0b24556
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java
@@ -0,0 +1,570 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.models.BlobListArrowParseException;
+import com.azure.storage.blob.implementation.util.apachearrow.Buffer;
+import com.azure.storage.blob.implementation.util.apachearrow.Endianness;
+import com.azure.storage.blob.implementation.util.apachearrow.Field;
+import com.azure.storage.blob.implementation.util.apachearrow.FieldNode;
+import com.azure.storage.blob.implementation.util.apachearrow.Int;
+import com.azure.storage.blob.implementation.util.apachearrow.KeyValue;
+import com.azure.storage.blob.implementation.util.apachearrow.Message;
+import com.azure.storage.blob.implementation.util.apachearrow.MessageHeader;
+import com.azure.storage.blob.implementation.util.apachearrow.RecordBatch;
+import com.azure.storage.blob.implementation.util.apachearrow.Schema;
+import com.azure.storage.blob.implementation.util.apachearrow.TimeUnit;
+import com.azure.storage.blob.implementation.util.apachearrow.Timestamp;
+import com.azure.storage.blob.implementation.util.apachearrow.Type;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Minimal Apache Arrow IPC stream reader scoped to the needs of the ListBlobs Arrow response.
+ *
+ * This reader intentionally supports only the subset of the Arrow IPC format emitted by the Storage ListBlobs
+ * endpoint: a single schema message followed by zero or more uncompressed, little-endian record batches whose
+ * columns are UTF-8 strings, booleans, integers, second-precision timestamps, and map<string,string>. Anything
+ * outside that subset (dictionaries, compression, big-endian, unsupported types) fails fast with
+ * {@link BlobListArrowParseException}.
+ *
+ * It depends only on the {@code arrow-format} flatbuffer definitions for metadata decoding and reads record batch
+ * bodies directly, so it does not require the {@code arrow-vector} runtime.
+ */
+final class BlobListArrowStreamReader {
+ // This is hex representation of the maximum value of an unsigned 32-bit integer.
+ private static final int CONTINUATION_MARKER = 0xFFFFFFFF;
+
+ private BlobListArrowStreamReader() {
+ }
+
+ /**
+ * The decoded contents of an Arrow IPC stream.
+ */
+ static final class DecodedArrowStream {
+ private final Map schemaMetadata;
+ private final List batches;
+
+ DecodedArrowStream(Map schemaMetadata, List batches) {
+ this.schemaMetadata = schemaMetadata;
+ this.batches = batches;
+ }
+
+ Map getSchemaMetadata() {
+ return schemaMetadata;
+ }
+
+ List getBatches() {
+ return batches;
+ }
+ }
+
+ /**
+ * A single decoded record batch: a row count and columns addressable by field name.
+ */
+ static final class Batch {
+ private final int rowCount;
+ private final Map columns;
+
+ Batch(int rowCount, Map columns) {
+ this.rowCount = rowCount;
+ this.columns = columns;
+ }
+
+ int getRowCount() {
+ return rowCount;
+ }
+
+ Column getColumn(String name) {
+ return columns.get(name);
+ }
+
+ Set getColumnNames() {
+ return Collections.unmodifiableSet(columns.keySet());
+ }
+ }
+
+ /**
+ * Reads and decodes an Arrow IPC stream.
+ *
+ * @param stream the Arrow IPC stream.
+ * @return the decoded schema metadata and record batches.
+ * @throws BlobListArrowParseException if the stream is malformed or uses an unsupported feature.
+ */
+ static DecodedArrowStream read(InputStream stream) {
+ byte[] bytes;
+ try {
+ bytes = readAll(stream);
+ } catch (IOException e) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: unable to read IPC stream.", e);
+ }
+
+ ByteBuffer steamBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+
+ Map schemaMetadata = null;
+ List fields = null;
+ List batches = new ArrayList<>();
+
+ int pos = 0;
+ int length = bytes.length;
+ while (atLeastFourBytesRemaining(pos, length)) {
+ int marker = steamBuffer.getInt(pos);
+ pos += 4;
+
+ int metadataLength;
+ if (isModernStream(marker)) {
+ if (pos + 4 > length) {
+ break;
+ }
+ metadataLength = steamBuffer.getInt(pos);
+ pos += 4;
+ } else {
+ // Pre-0.15 streams used a bare length prefix without the continuation marker.
+ metadataLength = marker;
+ }
+
+ if (metadataLength == 0) {
+ // End-of-stream marker.
+ break;
+ }
+ if (isMessageOutOfBounds(metadataLength, pos, length)) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: message metadata length is out of bounds.");
+ }
+
+ ByteBuffer messageBuffer
+ = ByteBuffer.wrap(bytes, pos, metadataLength).slice().order(ByteOrder.LITTLE_ENDIAN);
+ Message message = Message.getRootAsMessage(messageBuffer);
+ pos += metadataLength;
+
+ long bodyLength = message.bodyLength();
+ if (isMessageOutOfBounds(bodyLength, pos, length)) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: message body length is out of bounds.");
+ }
+ int bodyStart = pos;
+ pos += (int) bodyLength;
+
+ byte headerType = message.headerType();
+ if (headerType == MessageHeader.SCHEMA) {
+ Schema schema = requireHeader((Schema) message.header(new Schema()), "schema");
+ if (schema.endianness() != Endianness.LITTLE) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: only little-endian streams are supported.");
+ }
+ schemaMetadata = readKeyValueMetadata(schema);
+ fields = readFields(schema);
+ } else if (headerType == MessageHeader.RECORD_BATCH) {
+ if (fields == null) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: record batch encountered before schema.");
+ }
+ RecordBatch recordBatch
+ = requireHeader((RecordBatch) message.header(new RecordBatch()), "record batch");
+ if (recordBatch.compression() != null) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: compressed record batches are not supported.");
+ }
+ batches.add(buildBatch(fields, recordBatch, steamBuffer, bodyStart));
+ } else if (headerType == MessageHeader.DICTIONARY_BATCH) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: dictionary-encoded streams are not supported.");
+ }
+ // Other header types (NONE, and the commented-out Tensor/SparseTensor members of the Arrow MessageHeader
+ // union) are not expected in a ListBlobs response and are ignored. See MessageHeader for why those two
+ // constants are kept (commented out) and how to revive them if the service ever starts emitting them.
+ }
+
+ if (fields == null) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: stream contained no schema.");
+ }
+
+ Map finalSchemaMetadata = schemaMetadata == null ? new HashMap<>() : schemaMetadata;
+ return new DecodedArrowStream(finalSchemaMetadata, batches);
+ }
+
+ private static boolean isMessageOutOfBounds(long bodyLength, int pos, int length) {
+ return bodyLength < 0 || pos + bodyLength > length;
+ }
+
+ private static boolean isModernStream(int marker) {
+ return marker == CONTINUATION_MARKER;
+ }
+
+ private static boolean atLeastFourBytesRemaining(int pos, int length) {
+ return pos + 4 <= length;
+ }
+
+ private static T requireHeader(T header, String description) {
+ if (header == null) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: " + description + " message header is missing.");
+ }
+ return header;
+ }
+
+ private static Batch buildBatch(List fields, RecordBatch recordBatch, ByteBuffer body, int bodyStart) {
+ BatchCursor cursor = new BatchCursor(recordBatch, body, bodyStart);
+ Map columns = new LinkedHashMap<>();
+ for (ArrowField field : fields) {
+ columns.put(field.name, buildColumn(field, cursor));
+ }
+ return new Batch((int) recordBatch.length(), columns);
+ }
+
+ private static Column buildColumn(ArrowField field, BatchCursor cursor) {
+ FieldNode node = cursor.nextNode();
+ int valueCount = (int) node.length();
+
+ switch (field.typeType) {
+ case Type.UTF8:
+ case Type.BINARY:
+ return new StringColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), cursor.nextBuffer(),
+ cursor.body, cursor.bodyStart);
+
+ case Type.BOOL:
+ return new BoolColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), cursor.body,
+ cursor.bodyStart);
+
+ case Type.INT:
+ return new IntColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), field.bitWidth, field.signed,
+ cursor.body, cursor.bodyStart);
+
+ case Type.TIMESTAMP:
+ return new TimestampColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), cursor.body,
+ cursor.bodyStart);
+
+ case Type.MAP:
+ BufferRegion mapValidity = cursor.nextBuffer();
+ BufferRegion mapOffsets = cursor.nextBuffer();
+ // Map has a single Struct child ("entries") with key and value children.
+ ArrowField mapEntries = field.children.get(0);
+ StructColumn mapStruct = (StructColumn) buildColumn(mapEntries, cursor);
+ Column mapKeyColumn = mapStruct.children.get(0);
+ Column mapValueColumn = mapStruct.children.get(1);
+ if (!(mapKeyColumn instanceof StringColumn) || !(mapValueColumn instanceof StringColumn)) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + field.name
+ + "' map entries must be string keys and values.");
+ }
+ return new MapColumn(valueCount, mapValidity, mapOffsets, (StringColumn) mapKeyColumn,
+ (StringColumn) mapValueColumn, cursor.body, cursor.bodyStart);
+
+ case Type.STRUCT:
+ cursor.nextBuffer(); // struct validity buffer
+ List structChildren = new ArrayList<>(field.children.size());
+ for (ArrowField child : field.children) {
+ structChildren.add(buildColumn(child, cursor));
+ }
+ return new StructColumn(structChildren);
+
+ default:
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + field.name
+ + "' has unsupported Arrow type '" + Type.name(field.typeType) + "'.");
+ }
+ }
+
+ private static List readFields(Schema schema) {
+ int count = schema.fieldsLength();
+ List fields = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ fields.add(readField(schema.fields(i)));
+ }
+ return fields;
+ }
+
+ private static ArrowField readField(Field field) {
+ String name = field.name();
+ byte typeType = field.typeType();
+ int bitWidth = 0;
+ boolean signed = false;
+
+ if (typeType == Type.INT) {
+ Int intType = (Int) field.type(new Int());
+ if (intType != null) {
+ bitWidth = intType.bitWidth();
+ signed = intType.isSigned();
+ }
+ } else if (typeType == Type.TIMESTAMP) {
+ Timestamp timestamp = (Timestamp) field.type(new Timestamp());
+ if (timestamp != null && timestamp.unit() != TimeUnit.SECOND) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + name
+ + "' uses an unsupported timestamp unit '" + TimeUnit.name(timestamp.unit()) + "'.");
+ }
+ }
+
+ int childCount = field.childrenLength();
+ List children = new ArrayList<>(childCount);
+ for (int i = 0; i < childCount; i++) {
+ children.add(readField(field.children(i)));
+ }
+ return new ArrowField(name, typeType, bitWidth, signed, children);
+ }
+
+ private static Map readKeyValueMetadata(Schema schema) {
+ int count = schema.customMetadataLength();
+ if (count == 0) {
+ return new HashMap<>();
+ }
+ Map metadata = new HashMap<>();
+ for (int i = 0; i < count; i++) {
+ KeyValue keyValue = schema.customMetadata(i);
+ metadata.put(keyValue.key(), keyValue.value());
+ }
+ return metadata;
+ }
+
+ private static byte[] readAll(InputStream stream) throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] chunk = new byte[8192];
+ int read;
+ while ((read = stream.read(chunk)) != -1) {
+ buffer.write(chunk, 0, read);
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * A parsed schema field with the minimal type information required for decoding.
+ */
+ private static final class ArrowField {
+ private final String name;
+ private final byte typeType;
+ private final int bitWidth;
+ private final boolean signed;
+ private final List children;
+
+ ArrowField(String name, byte typeType, int bitWidth, boolean signed, List children) {
+ this.name = name;
+ this.typeType = typeType;
+ this.bitWidth = bitWidth;
+ this.signed = signed;
+ this.children = children;
+ }
+ }
+
+ /**
+ * Sequentially hands out the field nodes and buffers of a record batch in pre-order.
+ */
+ private static final class BatchCursor {
+ private final RecordBatch recordBatch;
+ private final ByteBuffer body;
+ private final int bodyStart;
+ private int nodeIndex;
+ private int bufferIndex;
+
+ BatchCursor(RecordBatch recordBatch, ByteBuffer body, int bodyStart) {
+ this.recordBatch = recordBatch;
+ this.body = body;
+ this.bodyStart = bodyStart;
+ }
+
+ FieldNode nextNode() {
+ if (nodeIndex >= recordBatch.nodesLength()) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: record batch is missing expected field nodes.");
+ }
+ return recordBatch.nodes(nodeIndex++);
+ }
+
+ BufferRegion nextBuffer() {
+ if (bufferIndex >= recordBatch.buffersLength()) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: record batch is missing expected buffers.");
+ }
+ Buffer buffer = recordBatch.buffers(bufferIndex++);
+ return new BufferRegion(buffer.offset(), buffer.length());
+ }
+ }
+
+ /**
+ * Offset and length of a single buffer within the record batch body.
+ */
+ private static final class BufferRegion {
+ private final long offset;
+ private final long length;
+
+ BufferRegion(long offset, long length) {
+ this.offset = offset;
+ this.length = length;
+ }
+ }
+
+ /**
+ * Base class for decoded columns.
+ */
+ abstract static class Column {
+ final int valueCount;
+ final BufferRegion validity;
+ final ByteBuffer body;
+ final int bodyStart;
+
+ Column(int valueCount, BufferRegion validity, ByteBuffer body, int bodyStart) {
+ this.valueCount = valueCount;
+ this.validity = validity;
+ this.body = body;
+ this.bodyStart = bodyStart;
+ }
+
+ boolean isNull(int index) {
+ if (validity == null || validity.length == 0) {
+ return false;
+ }
+ int bytePosition = bodyStart + (int) validity.offset + (index >> 3);
+ int bit = (body.get(bytePosition) >> (index & 7)) & 1;
+ return bit == 0;
+ }
+ }
+
+ /**
+ * UTF-8 string column (Arrow Utf8/Binary).
+ */
+ static final class StringColumn extends Column {
+ private final BufferRegion offsets;
+ private final BufferRegion data;
+
+ StringColumn(int valueCount, BufferRegion validity, BufferRegion offsets, BufferRegion data, ByteBuffer body,
+ int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.offsets = offsets;
+ this.data = data;
+ }
+
+ String get(int index) {
+ int start = body.getInt(bodyStart + (int) offsets.offset + index * 4);
+ int end = body.getInt(bodyStart + (int) offsets.offset + (index + 1) * 4);
+ int dataStart = bodyStart + (int) data.offset + start;
+ byte[] valueBytes = new byte[end - start];
+ for (int i = 0; i < valueBytes.length; i++) {
+ valueBytes[i] = body.get(dataStart + i);
+ }
+ return new String(valueBytes, StandardCharsets.UTF_8);
+ }
+ }
+
+ /**
+ * Boolean column stored as a bitmap (Arrow Bool).
+ */
+ static final class BoolColumn extends Column {
+ private final BufferRegion data;
+
+ BoolColumn(int valueCount, BufferRegion validity, BufferRegion data, ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.data = data;
+ }
+
+ boolean get(int index) {
+ int bytePosition = bodyStart + (int) data.offset + (index >> 3);
+ int bit = (body.get(bytePosition) >> (index & 7)) & 1;
+ return bit == 1;
+ }
+ }
+
+ /**
+ * Integer column (Arrow Int) of width 8/16/32/64, signed or unsigned, returned as a long.
+ */
+ static final class IntColumn extends Column {
+ private final BufferRegion data;
+ private final int bitWidth;
+ private final boolean signed;
+
+ IntColumn(int valueCount, BufferRegion validity, BufferRegion data, int bitWidth, boolean signed,
+ ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.data = data;
+ this.bitWidth = bitWidth;
+ this.signed = signed;
+ }
+
+ long get(int index) {
+ int base = bodyStart + (int) data.offset;
+ switch (bitWidth) {
+ case 64:
+ return body.getLong(base + index * 8);
+
+ case 32:
+ int value32 = body.getInt(base + index * 4);
+ return signed ? value32 : (value32 & 0xFFFFFFFFL);
+
+ case 16:
+ short value16 = body.getShort(base + index * 2);
+ return signed ? value16 : (value16 & 0xFFFF);
+
+ case 8:
+ byte value8 = body.get(base + index);
+ return signed ? value8 : (value8 & 0xFF);
+
+ default:
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: unsupported integer bit width '" + bitWidth + "'.");
+ }
+ }
+ }
+
+ /**
+ * Second-precision timestamp column (Arrow Timestamp, SECOND unit).
+ */
+ static final class TimestampColumn extends Column {
+ private final BufferRegion data;
+
+ TimestampColumn(int valueCount, BufferRegion validity, BufferRegion data, ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.data = data;
+ }
+
+ long getEpochSeconds(int index) {
+ return body.getLong(bodyStart + (int) data.offset + index * 8);
+ }
+ }
+
+ /**
+ * Struct column holding ordered child columns (used internally for map entries).
+ */
+ static final class StructColumn extends Column {
+ private final List children;
+
+ StructColumn(List children) {
+ super(0, null, null, 0);
+ this.children = children;
+ }
+ }
+
+ /**
+ * Map<string,string> column (Arrow Map of Struct<key:utf8,value:utf8>).
+ */
+ static final class MapColumn extends Column {
+ private final BufferRegion offsets;
+ private final StringColumn keys;
+ private final StringColumn values;
+
+ MapColumn(int valueCount, BufferRegion validity, BufferRegion offsets, StringColumn keys, StringColumn values,
+ ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.offsets = offsets;
+ this.keys = keys;
+ this.values = values;
+ }
+
+ Map get(int index) {
+ int start = body.getInt(bodyStart + (int) offsets.offset + index * 4);
+ int end = body.getInt(bodyStart + (int) offsets.offset + (index + 1) * 4);
+ Map map = new HashMap<>();
+ for (int entry = start; entry < end; entry++) {
+ map.put(keys.get(entry), values.get(entry));
+ }
+ return map.isEmpty() ? null : map;
+ }
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java
index cb859dfa595d..6bd5deb8cc55 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java
@@ -47,6 +47,7 @@
import com.azure.storage.blob.models.PageBlobCopyIncrementalRequestConditions;
import com.azure.storage.blob.models.PageRange;
import com.azure.storage.blob.models.ParallelTransferOptions;
+import com.azure.storage.blob.models.StorageResponseSerializationFormat;
import com.azure.storage.blob.models.TaggedBlobItem;
import com.azure.storage.common.Utility;
import com.azure.storage.common.implementation.Constants;
@@ -85,6 +86,14 @@ public final class ModelHelper {
*/
public static final int PAGE_BYTES = 512;
+ /**
+ * The format that {@link StorageResponseSerializationFormat#AUTO} currently resolves to on the
+ * wire. Changing this constant is a behavioral change (and should be noted in the CHANGELOG)
+ * but it is not a public API change.
+ */
+ private static final StorageResponseSerializationFormat DEFAULT_SERIALIZATION_FORMAT
+ = StorageResponseSerializationFormat.XML;
+
/**
* Determines whether the passed authority is IP style, that is, it is of the format {@code :}.
*
@@ -663,6 +672,22 @@ public static BlobStorageException mapToBlobStorageException(BlobStorageExceptio
internal.getResponse(), code, headerName), internal.getResponse(), internal.getValue());
}
+ /**
+ * Resolves a user-supplied {@link StorageResponseSerializationFormat} to the concrete value
+ * to send on the wire. Treats {@code null} and {@link StorageResponseSerializationFormat#AUTO}
+ * identically — both yield {@link #DEFAULT_SERIALIZATION_FORMAT}.
+ *
+ * @param format the format requested by the caller, or {@code null} if unset.
+ * @return the concrete {@link StorageResponseSerializationFormat} to send on the wire.
+ */
+ public static StorageResponseSerializationFormat
+ resolveSerializationFormat(StorageResponseSerializationFormat format) {
+ if (format == null || format == StorageResponseSerializationFormat.AUTO) {
+ return DEFAULT_SERIALIZATION_FORMAT;
+ }
+ return format;
+ }
+
private ModelHelper() {
}
}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/BodyCompression.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/BodyCompression.java
new file mode 100644
index 000000000000..44b985c4ddf6
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/BodyCompression.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code BodyCompression} table.
+ *
+ * The ListBlobs reader only needs to detect the presence of this table to reject compressed record batches, so no
+ * fields are exposed.
+ */
+public final class BodyCompression extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public BodyCompression __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Buffer.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Buffer.java
new file mode 100644
index 000000000000..9016b0595dc8
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Buffer.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Struct;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Buffer} struct (offset/length of a buffer within a record batch body).
+ */
+public final class Buffer extends Struct {
+ /**
+ * Positions this accessor at the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Buffer __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the byte offset of the buffer relative to the start of the record batch body.
+ *
+ * @return the buffer offset.
+ */
+ public long offset() {
+ return bb.getLong(bb_pos);
+ }
+
+ /**
+ * Gets the length, in bytes, of the buffer.
+ *
+ * @return the buffer length.
+ */
+ public long length() {
+ return bb.getLong(bb_pos + 8);
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Endianness.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Endianness.java
new file mode 100644
index 000000000000..af39bc7b6967
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Endianness.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+/**
+ * Values for the Arrow IPC {@code Endianness} enum.
+ */
+public final class Endianness {
+ private Endianness() {
+ }
+
+ /** Little-endian byte order. */
+ public static final short LITTLE = 0;
+ /** Big-endian byte order. */
+ public static final short BIG = 1;
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Field.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Field.java
new file mode 100644
index 000000000000..5a27e0e52f24
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Field.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Field} table describing a single column.
+ */
+public final class Field extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Field __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the field name.
+ *
+ * @return the field name, or {@code null} when absent.
+ */
+ public String name() {
+ int o = __offset(4);
+ return o != 0 ? __string(o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the discriminator identifying the field's {@code type} union (see {@link Type}).
+ *
+ * @return the type union discriminator, or {@code 0} when absent.
+ */
+ public byte typeType() {
+ int o = __offset(8);
+ return o != 0 ? bb.get(o + bb_pos) : 0;
+ }
+
+ /**
+ * Resolves the {@code type} union value into the supplied accessor.
+ *
+ * @param obj the accessor to assign to the union value.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Table type(Table obj) {
+ int o = __offset(10);
+ return o != 0 ? __union(obj, o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the child field at the given index.
+ *
+ * @param j the child index.
+ * @return the child field accessor.
+ */
+ public Field children(int j) {
+ return children(new Field(), j);
+ }
+
+ /**
+ * Gets the child field at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the child index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Field children(Field obj, int j) {
+ int o = __offset(14);
+ return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+ }
+
+ /**
+ * Gets the number of child fields.
+ *
+ * @return the child field count.
+ */
+ public int childrenLength() {
+ int o = __offset(14);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/FieldNode.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/FieldNode.java
new file mode 100644
index 000000000000..2cf4f51fc732
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/FieldNode.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Struct;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code FieldNode} struct (per-column metadata within a record batch).
+ */
+public final class FieldNode extends Struct {
+ /**
+ * Positions this accessor at the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public FieldNode __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the number of value slots in the column.
+ *
+ * @return the value count.
+ */
+ public long length() {
+ return bb.getLong(bb_pos);
+ }
+
+ /**
+ * Gets the number of null value slots in the column.
+ *
+ * @return the null count.
+ */
+ public long nullCount() {
+ return bb.getLong(bb_pos + 8);
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Int.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Int.java
new file mode 100644
index 000000000000..cef3b699d894
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Int.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Int} type table.
+ */
+public final class Int extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Int __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the bit width of the integer (8, 16, 32, or 64).
+ *
+ * @return the bit width, or {@code 0} when absent.
+ */
+ public int bitWidth() {
+ int o = __offset(4);
+ return o != 0 ? bb.getInt(o + bb_pos) : 0;
+ }
+
+ /**
+ * Gets whether the integer is signed.
+ *
+ * @return {@code true} if signed, otherwise {@code false}.
+ */
+ public boolean isSigned() {
+ int o = __offset(6);
+ return o != 0 && 0 != bb.get(o + bb_pos);
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/KeyValue.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/KeyValue.java
new file mode 100644
index 000000000000..7b26bb99a59b
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/KeyValue.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code KeyValue} table (a single custom metadata entry).
+ */
+public final class KeyValue extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public KeyValue __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the metadata key.
+ *
+ * @return the key, or {@code null} when absent.
+ */
+ public String key() {
+ int o = __offset(4);
+ return o != 0 ? __string(o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the metadata value.
+ *
+ * @return the value, or {@code null} when absent.
+ */
+ public String value() {
+ int o = __offset(6);
+ return o != 0 ? __string(o + bb_pos) : null;
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Message.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Message.java
new file mode 100644
index 000000000000..3fef56e061de
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Message.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Accessor for the Arrow IPC {@code Message} table (root of every encapsulated IPC message).
+ */
+public final class Message extends Table {
+ /**
+ * Reads the {@code Message} located at the root offset of the supplied buffer.
+ *
+ * @param bb the little-endian buffer positioned at the start of the message.
+ * @return the {@code Message} accessor.
+ */
+ public static Message getRootAsMessage(ByteBuffer bb) {
+ return getRootAsMessage(bb, new Message());
+ }
+
+ /**
+ * Reads the {@code Message} located at the root offset of the supplied buffer into {@code obj}.
+ *
+ * @param bb the buffer positioned at the start of the message.
+ * @param obj the accessor instance to assign.
+ * @return the assigned {@code Message} accessor.
+ */
+ public static Message getRootAsMessage(ByteBuffer bb, Message obj) {
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ // A FlatBuffer begins with a 4-byte offset (relative to here) pointing to the root table.
+ int rootTableOffset = bb.getInt(bb.position()) + bb.position();
+ return obj.__assign(rootTableOffset, bb);
+ }
+
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Message __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the discriminator identifying the type of the {@code header} union (see {@link MessageHeader}).
+ *
+ * @return the header union type, or {@code 0} when absent.
+ */
+ public byte headerType() {
+ int o = __offset(6);
+ return o != 0 ? bb.get(o + bb_pos) : 0;
+ }
+
+ /**
+ * Resolves the {@code header} union value into the supplied accessor.
+ *
+ * @param obj the accessor to assign to the union value.
+ * @return the assigned accessor, or {@code null} when the header is absent.
+ */
+ public Table header(Table obj) {
+ int o = __offset(8);
+ return o != 0 ? __union(obj, o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the length, in bytes, of the message body that follows the metadata.
+ *
+ * @return the body length, or {@code 0} when absent.
+ */
+ public long bodyLength() {
+ int o = __offset(10);
+ return o != 0 ? bb.getLong(o + bb_pos) : 0L;
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/MessageHeader.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/MessageHeader.java
new file mode 100644
index 000000000000..8eca35389946
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/MessageHeader.java
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+/**
+ * Discriminator values for the Arrow IPC {@code MessageHeader} union.
+ *
+ * Only the subset that a ListBlobs response can contain is defined as active constants: {@link #SCHEMA} and
+ * {@link #RECORD_BATCH} (a ListBlobs payload is a tabular result, i.e. a schema followed by record batches),
+ * plus {@link #DICTIONARY_BATCH}, which the reader recognizes solely to reject it. The union's remaining members,
+ * {@code TENSOR} (4) and {@code SPARSE_TENSOR} (5), are valid in the Arrow format but are never emitted for a
+ * ListBlobs response, so they are intentionally kept commented out below rather than deleted. If a future service or
+ * format change ever starts sending them, uncomment those constants and add the corresponding handling in
+ * {@code BlobListArrowStreamReader}.
+ */
+public final class MessageHeader {
+ private MessageHeader() {
+ }
+
+ /** No header. */
+ public static final byte NONE = 0;
+ /** A {@link Schema} header. */
+ public static final byte SCHEMA = 1;
+ /** A dictionary batch header. */
+ public static final byte DICTIONARY_BATCH = 2;
+ /** A {@link RecordBatch} header. */
+ public static final byte RECORD_BATCH = 3;
+ // TENSOR (4) and SPARSE_TENSOR (5) are omitted on purpose; see the class javadoc for why and how to revive them.
+ // public static final byte TENSOR = 4;
+ // public static final byte SPARSE_TENSOR = 5;
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/RecordBatch.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/RecordBatch.java
new file mode 100644
index 000000000000..63697054c91a
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/RecordBatch.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code RecordBatch} table.
+ */
+public final class RecordBatch extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public RecordBatch __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the number of rows in the record batch.
+ *
+ * @return the row count, or {@code 0} when absent.
+ */
+ public long length() {
+ int o = __offset(4);
+ return o != 0 ? bb.getLong(o + bb_pos) : 0L;
+ }
+
+ /**
+ * Gets the field node at the given index.
+ *
+ * @param j the node index.
+ * @return the field node accessor.
+ */
+ public FieldNode nodes(int j) {
+ return nodes(new FieldNode(), j);
+ }
+
+ /**
+ * Gets the field node at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the node index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public FieldNode nodes(FieldNode obj, int j) {
+ int o = __offset(6);
+ return o != 0 ? obj.__assign(__vector(o) + j * 16, bb) : null;
+ }
+
+ /**
+ * Gets the number of field nodes.
+ *
+ * @return the field node count.
+ */
+ public int nodesLength() {
+ int o = __offset(6);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+
+ /**
+ * Gets the buffer region at the given index.
+ *
+ * @param j the buffer index.
+ * @return the buffer accessor.
+ */
+ public Buffer buffers(int j) {
+ return buffers(new Buffer(), j);
+ }
+
+ /**
+ * Gets the buffer region at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the buffer index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Buffer buffers(Buffer obj, int j) {
+ int o = __offset(8);
+ return o != 0 ? obj.__assign(__vector(o) + j * 16, bb) : null;
+ }
+
+ /**
+ * Gets the number of buffers.
+ *
+ * @return the buffer count.
+ */
+ public int buffersLength() {
+ int o = __offset(8);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+
+ /**
+ * Gets the optional body compression descriptor.
+ *
+ * @return the body compression accessor, or {@code null} when the batch is uncompressed.
+ */
+ public BodyCompression compression() {
+ return compression(new BodyCompression());
+ }
+
+ /**
+ * Gets the optional body compression descriptor into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @return the assigned accessor, or {@code null} when the batch is uncompressed.
+ */
+ public BodyCompression compression(BodyCompression obj) {
+ int o = __offset(10);
+ return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null;
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Schema.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Schema.java
new file mode 100644
index 000000000000..1cf0f8fb0669
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Schema.java
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Schema} table.
+ */
+public final class Schema extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Schema __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the byte order of the schema's buffers (see {@link Endianness}).
+ *
+ * @return the endianness, or {@code 0} ({@link Endianness#LITTLE}) when absent.
+ */
+ public short endianness() {
+ int o = __offset(4);
+ return o != 0 ? bb.getShort(o + bb_pos) : 0;
+ }
+
+ /**
+ * Gets the field at the given index.
+ *
+ * @param j the field index.
+ * @return the field accessor.
+ */
+ public Field fields(int j) {
+ return fields(new Field(), j);
+ }
+
+ /**
+ * Gets the field at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the field index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Field fields(Field obj, int j) {
+ int o = __offset(6);
+ return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+ }
+
+ /**
+ * Gets the number of top-level fields in the schema.
+ *
+ * @return the field count.
+ */
+ public int fieldsLength() {
+ int o = __offset(6);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+
+ /**
+ * Gets the custom metadata entry at the given index.
+ *
+ * @param j the entry index.
+ * @return the key/value accessor.
+ */
+ public KeyValue customMetadata(int j) {
+ return customMetadata(new KeyValue(), j);
+ }
+
+ /**
+ * Gets the custom metadata entry at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the entry index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public KeyValue customMetadata(KeyValue obj, int j) {
+ int o = __offset(8);
+ return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+ }
+
+ /**
+ * Gets the number of custom metadata entries.
+ *
+ * @return the entry count.
+ */
+ public int customMetadataLength() {
+ int o = __offset(8);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/TimeUnit.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/TimeUnit.java
new file mode 100644
index 000000000000..2ba6d3072d40
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/TimeUnit.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+/**
+ * Values for the Arrow IPC {@code TimeUnit} enum.
+ */
+public final class TimeUnit {
+ private TimeUnit() {
+ }
+
+ /** Second resolution. */
+ public static final short SECOND = 0;
+ /** Millisecond resolution. */
+ public static final short MILLISECOND = 1;
+ /** Microsecond resolution. */
+ public static final short MICROSECOND = 2;
+ /** Nanosecond resolution. */
+ public static final short NANOSECOND = 3;
+
+ private static final String[] NAMES = { "SECOND", "MILLISECOND", "MICROSECOND", "NANOSECOND" };
+
+ /**
+ * Gets the canonical Arrow name for a {@code TimeUnit} value, for diagnostic messages.
+ *
+ * @param e the time unit value.
+ * @return the canonical Arrow time unit name.
+ */
+ public static String name(int e) {
+ return NAMES[e];
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Timestamp.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Timestamp.java
new file mode 100644
index 000000000000..e22e9b6147d6
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Timestamp.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Timestamp} type table.
+ */
+public final class Timestamp extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Timestamp __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the timestamp resolution (see {@link TimeUnit}).
+ *
+ * @return the time unit, or {@code 0} ({@link TimeUnit#SECOND}) when absent.
+ */
+ public short unit() {
+ int o = __offset(4);
+ return o != 0 ? bb.getShort(o + bb_pos) : 0;
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Type.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Type.java
new file mode 100644
index 000000000000..690eebf19562
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Type.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+/*
+ * Portions Copyright (c) Microsoft Corporation
+ */
+
+package com.azure.storage.blob.implementation.util.apachearrow;
+
+/**
+ * Discriminator values for the Arrow IPC {@code Type} union, identifying a field's logical type.
+ */
+public final class Type {
+ private Type() {
+ }
+
+ /** No type. */
+ public static final byte NONE = 0;
+ /** Null type. */
+ public static final byte NULL = 1;
+ /** Integer type (see {@link Int}). */
+ public static final byte INT = 2;
+ /** Floating-point type. */
+ public static final byte FLOATING_POINT = 3;
+ /** Variable-length binary type. */
+ public static final byte BINARY = 4;
+ /** Variable-length UTF-8 string type. */
+ public static final byte UTF8 = 5;
+ /** Boolean type. */
+ public static final byte BOOL = 6;
+ /** Decimal type. */
+ public static final byte DECIMAL = 7;
+ /** Date type. */
+ public static final byte DATE = 8;
+ /** Time-of-day type. */
+ public static final byte TIME = 9;
+ /** Timestamp type (see {@link Timestamp}). */
+ public static final byte TIMESTAMP = 10;
+ /** Interval type. */
+ public static final byte INTERVAL = 11;
+ /** List type. */
+ public static final byte LIST = 12;
+ /** Struct type. */
+ public static final byte STRUCT = 13;
+ /** Union type. */
+ public static final byte UNION = 14;
+ /** Fixed-size binary type. */
+ public static final byte FIXED_SIZE_BINARY = 15;
+ /** Fixed-size list type. */
+ public static final byte FIXED_SIZE_LIST = 16;
+ /** Map type. */
+ public static final byte MAP = 17;
+ /** Duration type. */
+ public static final byte DURATION = 18;
+ /** Large variable-length binary type. */
+ public static final byte LARGE_BINARY = 19;
+ /** Large variable-length UTF-8 string type. */
+ public static final byte LARGE_UTF8 = 20;
+ /** Large list type. */
+ public static final byte LARGE_LIST = 21;
+ /** Run-end encoded type. */
+ public static final byte RUN_END_ENCODED = 22;
+ /** Binary view type. */
+ public static final byte BINARY_VIEW = 23;
+ /** UTF-8 string view type. */
+ public static final byte UTF8_VIEW = 24;
+ /** List view type. */
+ public static final byte LIST_VIEW = 25;
+ /** Large list view type. */
+ public static final byte LARGE_LIST_VIEW = 26;
+
+ private static final String[] NAMES = {
+ "NONE",
+ "Null",
+ "Int",
+ "FloatingPoint",
+ "Binary",
+ "Utf8",
+ "Bool",
+ "Decimal",
+ "Date",
+ "Time",
+ "Timestamp",
+ "Interval",
+ "List",
+ "Struct_",
+ "Union",
+ "FixedSizeBinary",
+ "FixedSizeList",
+ "Map",
+ "Duration",
+ "LargeBinary",
+ "LargeUtf8",
+ "LargeList",
+ "RunEndEncoded",
+ "BinaryView",
+ "Utf8View",
+ "ListView",
+ "LargeListView" };
+
+ /**
+ * Gets the canonical Arrow name for a {@code Type} union discriminator, for diagnostic messages.
+ *
+ * @param e the discriminator value.
+ * @return the canonical Arrow type name.
+ */
+ public static String name(int e) {
+ return NAMES[e];
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/package-info.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/package-info.java
new file mode 100644
index 000000000000..dff44c7d8543
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/package-info.java
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * Minimal Apache Arrow IPC FlatBuffer metadata accessors used internally by the Blob Storage ListBlobs Arrow reader.
+ *
+ * The classes in this package are thin {@link com.google.flatbuffers.Table}/{@link com.google.flatbuffers.Struct}
+ * accessors over the FlatBuffer-encoded metadata of the Apache Arrow IPC format. They expose only the small subset of
+ * the {@code org.apache.arrow.flatbuf} schema that {@code BlobListArrowStreamReader} requires, so that the main
+ * (Java 8 baseline) compile classpath does not depend on the {@code arrow-format} artifact, which ships Java 11
+ * bytecode.
+ *
+ * The field orderings (FlatBuffer vtable slots) and enum values are defined by the public Apache Arrow columnar format
+ * specification (see {@code Schema.fbs} and {@code Message.fbs} in the Apache Arrow project, licensed under the Apache
+ * License, Version 2.0) and must match the on-the-wire layout produced by the Storage service.
+ */
+package com.azure.storage.blob.implementation.util.apachearrow;
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
index 6a372f5b2c74..dfd93a535fc6 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
@@ -825,97 +825,6 @@ public BlobDownloadHeaders setEncryptionScope(String encryptionScope) {
return this;
}
- /**
- * Gets the access tier of the blob.
- *
- * @return the access tier of the blob. This is only set for Page blobs on a premium storage account or for Block
- * blobs on blob storage or general purpose V2 account.
- */
- public AccessTier getAccessTier() {
- String accessTier = internalHeaders.getXMsAccessTier();
- return accessTier == null ? null : AccessTier.fromString(accessTier);
- }
-
- /**
- * Sets the access tier of the blob.
- *
- * @param accessTier the access tier of the blob.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setAccessTier(AccessTier accessTier) {
- internalHeaders.setXMsAccessTier(accessTier == null ? null : accessTier.toString());
- return this;
- }
-
- /**
- * Gets the status of the tier being inferred for the blob.
- *
- * @return the status of the tier being inferred for the blob. This is only set for Page blobs on a premium storage
- * account or for Block blobs on blob storage or general purpose V2 account.
- */
- public Boolean isAccessTierInferred() {
- return Boolean.TRUE.equals(internalHeaders.isXMsAccessTierInferred());
- }
-
- /**
- * Sets the status of the tier being inferred for the blob.
- *
- * @param accessTierInferred the status of the tier being inferred for the blob.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setAccessTierInferred(Boolean accessTierInferred) {
- internalHeaders.setXMsAccessTierInferred(accessTierInferred);
- return this;
- }
-
- /**
- * Gets the time when the access tier for the blob was last changed.
- *
- * @return the time when the access tier for the blob was last changed.
- */
- public OffsetDateTime getAccessTierChangeTime() {
- return internalHeaders.getXMsAccessTierChangeTime();
- }
-
- /**
- * Sets the time when the access tier for the blob was last changed.
- *
- * @param accessTierChangeTime the time when the access tier for the blob was last changed.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setAccessTierChangeTime(OffsetDateTime accessTierChangeTime) {
- internalHeaders.setXMsAccessTierChangeTime(accessTierChangeTime);
- return this;
- }
-
- /**
- * Gets the underlying access tier of the blob when its access tier is {@link AccessTier#SMART}.
- *
- * This value is only populated when {@link #getAccessTier()} returns {@link AccessTier#SMART}. In that case, it
- * represents the concrete access tier (for example {@link AccessTier#HOT} or {@link AccessTier#COOL}) that the
- * service has selected for the blob. For all other access tiers, this property is {@code null} and should be
- * ignored.
- *
- * @return the underlying access tier chosen by the service when the blob's access tier is {@link AccessTier#SMART},
- * or {@code null} if the blob is not using the smart access tier.
- */
- public AccessTier getSmartAccessTier() {
- String smartAccessTier = internalHeaders.getXMsSmartAccessTier();
- return smartAccessTier == null ? null : AccessTier.fromString(smartAccessTier);
- }
-
- /**
- * Sets the underlying access tier of the blob when its access tier is {@link AccessTier#SMART}.
- *
- * @param smartAccessTier the underlying access tier chosen by the service when the blob's access tier is
- * {@link AccessTier#SMART}.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setSmartAccessTier(AccessTier smartAccessTier) {
- internalHeaders.setXMsSmartAccessTier(smartAccessTier == null ? null : smartAccessTier.toString());
- return this;
- }
-
/**
* Get the blobContentMD5 property: If the blob has a MD5 hash, and if request contains range header (Range or
* x-ms-range), this response header is returned with the value of the whole blob's MD5 value. This value may or may
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java
index 47f29c2ab2b3..e434681ac6e9 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java
@@ -19,6 +19,8 @@ public final class ListBlobsOptions {
private String prefix;
private String startFrom;
private Integer maxResultsPerPage;
+ private StorageResponseSerializationFormat storageResponseSerializationFormat;
+ private String endBefore;
/**
* Constructs an unpopulated {@link ListBlobsOptions}.
@@ -74,7 +76,7 @@ public ListBlobsOptions setPrefix(String prefix) {
* This parameter is similar to the prefix filter: it allows listing blobs starting from the specified path, rather than from the beginning of the container.
* For non-recursive lists, only one entity level is supported.
*
- * @return the marker indicating where to start listing blobs
+ * @return the marker indicating where to start listing blobs (inclusive)
*/
public String getStartFrom() {
return startFrom;
@@ -84,7 +86,7 @@ public String getStartFrom() {
* Sets an optional parameter that specifies an absolute path within the container. This parameter is similar to the prefix filter: it allows listing blobs starting from the specified path, rather than from the beginning of the container.
* For non-recursive lists, only one entity level is supported.
*
- * @param startFrom The marker indicating where to start listing blobs
+ * @param startFrom The marker indicating where to start listing blobs (inclusive)
* @return the updated ListBlobsOptions object
*/
public ListBlobsOptions setStartFrom(String startFrom) {
@@ -92,6 +94,49 @@ public ListBlobsOptions setStartFrom(String startFrom) {
return this;
}
+ /**
+ * Gets the endBefore value. Only supported with Arrow listings. The listing will end before this path (exclusive).
+ *
+ * @return the endBefore value.
+ */
+ public String getEndBefore() {
+ return endBefore;
+ }
+
+ /**
+ * Sets the endBefore value. Only supported with Arrow listings. The listing will end before this path (exclusive).
+ *
+ * @param endBefore the endBefore value to set.
+ * @return the updated ListBlobsOptions object.
+ */
+ public ListBlobsOptions setEndBefore(String endBefore) {
+ this.endBefore = endBefore;
+ return this;
+ }
+
+ /**
+ * Gets the response serialization format the service should use when listing blobs.
+ *
+ * @return the {@link StorageResponseSerializationFormat}, or {@code null} if unset
+ * (equivalent to {@link StorageResponseSerializationFormat#AUTO}).
+ */
+ public StorageResponseSerializationFormat getStorageResponseSerializationFormat() {
+ return storageResponseSerializationFormat;
+ }
+
+ /**
+ * Sets the response serialization format the service should use when listing blobs.
+ *
+ * @param storageResponseSerializationFormat the format to request. {@code null} and
+ * {@link StorageResponseSerializationFormat#AUTO} both let the SDK pick.
+ * @return the updated {@link ListBlobsOptions} object.
+ */
+ public ListBlobsOptions
+ setStorageResponseSerializationFormat(StorageResponseSerializationFormat storageResponseSerializationFormat) {
+ this.storageResponseSerializationFormat = storageResponseSerializationFormat;
+ return this;
+ }
+
/**
* Specifies the maximum number of blobs to return, including all BlobPrefix elements. If the request does not
* specify maxResultsPerPage or specifies a value greater than 5,000, the server will return up to 5,000 items.
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java
new file mode 100644
index 000000000000..e5017accedbc
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.models;
+
+/**
+ * Defines the serialization format the service uses for list-blobs responses.
+ */
+public enum StorageResponseSerializationFormat {
+ /**
+ * Let the SDK choose the serialization format that is most appropriate for the request.
+ *
+ * The exact format selected by {@code AUTO} is an implementation detail and may change
+ * between SDK releases. Choose {@link #XML} or {@link #ARROW} explicitly if you require
+ * a specific format.
+ */
+ AUTO,
+
+ /**
+ * XML response format.
+ */
+ XML,
+
+ /**
+ * Apache Arrow response format.
+ */
+ ARROW
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/module-info.java b/sdk/storage/azure-storage-blob/src/main/java/module-info.java
index 597a417add7f..e6947c9af0c4 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/module-info.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/module-info.java
@@ -5,6 +5,7 @@
requires transitive com.azure.storage.common;
requires com.azure.storage.internal.avro;
+ requires flatbuffers.java;
exports com.azure.storage.blob;
exports com.azure.storage.blob.models;
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java
index 71c474ba295c..50a9eb63ef21 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java
@@ -543,36 +543,6 @@ public void downloadAllNullBinaryData() {
// headers.getLastAccessedTime() /* TODO (gapra): re-enable when last access time enabled. */
}
- @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06")
- @Test
- public void downloadSmartAccessTierHeaders() {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- bc.setAccessTier(AccessTier.SMART);
-
- BlobDownloadResponse response = bc.downloadStreamWithResponse(stream, null, null, null, false, null, null);
- ByteBuffer body = ByteBuffer.wrap(stream.toByteArray());
-
- assertEquals(DATA.getDefaultData(), body);
- assertSmartAccessTierHeaders(response.getDeserializedHeaders());
- }
-
- @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06")
- @Test
- public void downloadContentSmartAccessTierHeaders() {
- bc.setAccessTier(AccessTier.SMART);
- BlobDownloadContentResponse response = bc.downloadContentWithResponse(null, null, null, null);
-
- TestUtils.assertArraysEqual(DATA.getDefaultBytes(), response.getValue().toBytes());
- assertSmartAccessTierHeaders(response.getDeserializedHeaders());
- }
-
- private static void assertSmartAccessTierHeaders(BlobDownloadHeaders headers) {
- assertEquals(AccessTier.SMART, headers.getAccessTier());
- assertNotNull(headers.getSmartAccessTier());
- assertFalse(headers.isAccessTierInferred());
- assertNotEquals(OffsetDateTime.now(), headers.getAccessTierChangeTime());
- }
-
@Test
public void downloadEmptyFile() {
AppendBlobClient bc = cc.getBlobClient("emptyAppendBlob").getAppendBlobClient();
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java
index ea01df338d18..049e4254e92a 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java
@@ -382,33 +382,6 @@ public void downloadAllNullBinaryData() {
.verifyComplete();
}
- @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06")
- @Test
- public void downloadSmartAccessTierHeaders() {
- Mono response = bc.setAccessTier(AccessTier.SMART)
- .then(bc.downloadStreamWithResponse(null, null, null, false))
- .flatMap(r -> {
- assertSmartAccessTierHeaders(r.getDeserializedHeaders());
- return FluxUtil.collectBytesInByteBufferStream(r.getValue());
- })
- .flatMap(r -> {
- TestUtils.assertArraysEqual(DATA.getDefaultBytes(), r);
- return bc.downloadContentWithResponse(null, null);
- });
-
- StepVerifier.create(response).assertNext(r -> {
- assertSmartAccessTierHeaders(r.getDeserializedHeaders());
- TestUtils.assertArraysEqual(DATA.getDefaultBytes(), r.getValue().toBytes());
- }).verifyComplete();
- }
-
- private static void assertSmartAccessTierHeaders(BlobDownloadHeaders headers) {
- assertEquals(AccessTier.SMART, headers.getAccessTier());
- assertNotNull(headers.getSmartAccessTier());
- assertFalse(headers.isAccessTierInferred());
- assertNotEquals(OffsetDateTime.now(), headers.getAccessTierChangeTime());
- }
-
@Test
public void downloadEmptyFile() {
AppendBlobAsyncClient bc = ccAsync.getBlobAsyncClient("emptyAppendBlob").getAppendBlobAsyncClient();
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java
index f46116acdbb5..4eea5979e577 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java
@@ -34,6 +34,7 @@
import com.azure.storage.blob.models.PublicAccessType;
import com.azure.storage.blob.models.RehydratePriority;
import com.azure.storage.blob.models.StorageAccountInfo;
+import com.azure.storage.blob.models.StorageResponseSerializationFormat;
import com.azure.storage.blob.models.TaggedBlobItem;
import com.azure.storage.blob.options.BlobContainerCreateOptions;
import com.azure.storage.blob.options.BlobParallelUploadOptions;
@@ -45,6 +46,7 @@
import com.azure.storage.blob.specialized.PageBlobClient;
import com.azure.storage.common.Utility;
import com.azure.storage.common.implementation.Constants;
+import com.azure.storage.common.implementation.StorageImplUtils;
import com.azure.storage.common.test.shared.TestHttpClientType;
import com.azure.storage.common.test.shared.extensions.LiveOnly;
import com.azure.storage.common.test.shared.extensions.PlaybackOnly;
@@ -58,7 +60,17 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
+import com.azure.storage.blob.implementation.AzureBlobStorageImpl;
+import com.azure.storage.blob.implementation.AzureBlobStorageImplBuilder;
+import com.azure.storage.blob.implementation.models.ContainersListBlobFlatSegmentApacheArrowHeaders;
+import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer;
+import com.azure.storage.blob.implementation.util.ModelHelper;
+import com.azure.storage.blob.models.ListBlobsIncludeItem;
+import com.azure.core.http.rest.ResponseBase;
+
import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
import java.net.URL;
import java.time.OffsetDateTime;
import java.util.Arrays;
@@ -2128,4 +2140,315 @@ public void getBlobContainerUrlEncodesContainerName() {
// then:
// assertThrows(BlobStorageException.class, () ->
// }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowBasic() {
+ // Upload a test blob
+ String blobName = generateBlobName();
+ cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ assertNotNull(blobs.get(0).getProperties());
+ assertEquals(DATA.getDefaultDataSize(), blobs.get(0).getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, blobs.get(0).getProperties().getBlobType());
+ assertNotNull(blobs.get(0).getProperties().getLastModified());
+ assertNotNull(blobs.get(0).getProperties().getETag());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithTags() {
+ // Upload a blob and set tags
+ String blobName = generateBlobName();
+ cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ Map tags = new HashMap<>();
+ tags.put("tagkey", "tagvalue");
+ cc.getBlobClient(blobName).setTags(tags);
+
+ // List with Arrow + retrieveTags
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveTags(true));
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ assertNotNull(blobs.get(0).getTags());
+ assertEquals("tagvalue", blobs.get(0).getTags().get("tagkey"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("listBlobsFlatRehydratePrioritySupplier")
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowRehydratePriority(RehydratePriority rehydratePriority) {
+ String name = generateBlobName();
+ BlockBlobClient bc = cc.getBlobClient(name).getBlockBlobClient();
+
+ bc.upload(DATA.getDefaultInputStream(), 7);
+
+ if (rehydratePriority != null) {
+ bc.setAccessTier(AccessTier.ARCHIVE);
+ bc.setAccessTierWithResponse(new BlobSetAccessTierOptions(AccessTier.HOT).setPriority(rehydratePriority),
+ null, null);
+ }
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ BlobItem item = cc.listBlobs(options, null).iterator().next();
+
+ assertEquals(rehydratePriority, item.getProperties().getRehydratePriority());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ cc.getBlobClient(blobName)
+ .getBlockBlobClient()
+ .uploadWithResponse(DATA.getDefaultInputStream(), DATA.getDefaultDataSize(), null, metadata, null, null,
+ null, null, null);
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertNotNull(blobs.get(0).getMetadata());
+ assertEquals("testvalue", blobs.get(0).getMetadata().get("testkey"));
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowPagination() {
+ // Upload 3 blobs
+ for (int i = 0; i < 4; i++) {
+ cc.getBlobClient("blob" + i)
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ }
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+ List allBlobs = new ArrayList<>();
+ for (PagedResponse page : cc.listBlobs(options, null).iterableByPage()) {
+ assertTrue(page.getValue().size() <= 1);
+ allBlobs.addAll(page.getValue());
+ }
+
+ cc.listBlobs().iterableByPage(2).forEach(page -> {
+ assertEquals(2, page.getValue().size());
+ });
+
+ assertEquals(4, allBlobs.size());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowNullUseArrowUsesXml() {
+ // Default apacheArrowEnabled is null — should use XML path without error
+ String blobName = generateBlobName();
+ cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options = new ListBlobsOptions();
+ assertNull(options.getStorageResponseSerializationFormat());
+
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ }
+
+ @LiveOnly
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowEncryptedBlob() {
+ // Upload a blob with CPK (customer-provided key)
+ String blobName = generateBlobName();
+ CustomerProvidedKey cpk = new CustomerProvidedKey(Base64.getEncoder().encodeToString(getRandomKey()));
+ BlobClient cpkClient = cc.getBlobClient(blobName).getCustomerProvidedKeyClient(cpk);
+ cpkClient.getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ // CPK blob should have server-encrypted = true
+ assertTrue(blobs.get(0).getProperties().isServerEncrypted());
+ // Metadata should be null (no metadata was set)
+ assertNull(blobs.get(0).getMetadata());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowDeserializer() throws Exception {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ cc.getBlobClient(blobName)
+ .getBlockBlobClient()
+ .uploadWithResponse(DATA.getDefaultInputStream(), 7, null, metadata, null, null, null, null, null);
+
+ AzureBlobStorageImpl impl = new AzureBlobStorageImplBuilder().pipeline(cc.getHttpPipeline())
+ .url(cc.getAccountUrl())
+ .version(BlobServiceVersion.getLatest().getVersion())
+ .buildClient();
+
+ // Call the Arrow endpoint
+ ArrayList include = new ArrayList<>();
+ include.add(ListBlobsIncludeItem.METADATA);
+
+ ResponseBase response = impl.getContainers()
+ .listBlobFlatSegmentApacheArrowWithResponse(containerName, null, null, null, include, null, null, null,
+ null, com.azure.core.util.Context.NONE);
+
+ // Verify Content-Type is Arrow
+ String contentType = response.getDeserializedHeaders().getContentType();
+ assertTrue(
+ StorageImplUtils.hasMatchingHeaderValue(contentType,
+ Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM),
+ "Expected Arrow content type but got: " + contentType);
+
+ // Deserialize using ArrowBlobListDeserializer
+ ArrowBlobListDeserializer.ArrowListBlobsResult result
+ = ArrowBlobListDeserializer.deserialize(response.getValue());
+
+ // Verify pagination — single blob, no next page
+ assertNull(result.getNextMarker());
+
+ // Verify we got exactly one blob
+ assertEquals(1, result.getBlobItems().size());
+
+ com.azure.storage.blob.implementation.models.BlobItemInternal item = result.getBlobItems().get(0);
+
+ // Name
+ assertNotNull(item.getName());
+ assertEquals(blobName, item.getName().getContent());
+
+ // Properties
+ assertNotNull(item.getProperties());
+ assertEquals(7L, (long) item.getProperties().getContentLength());
+ assertEquals("application/octet-stream", item.getProperties().getContentType());
+ assertNotNull(item.getProperties().getETag());
+ assertNotNull(item.getProperties().getLastModified());
+ assertNotNull(item.getProperties().getCreationTime());
+ assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType());
+ assertEquals(AccessTier.HOT, item.getProperties().getAccessTier());
+ assertTrue(item.getProperties().isAccessTierInferred());
+ assertTrue(item.getProperties().isServerEncrypted());
+ assertEquals(LeaseStateType.AVAILABLE, item.getProperties().getLeaseState());
+ assertEquals(LeaseStatusType.UNLOCKED, item.getProperties().getLeaseStatus());
+ assertNotNull(item.getProperties().getContentMd5());
+
+ // Metadata
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+
+ // Verify ModelHelper can convert to public BlobItem
+ BlobItem publicItem = ModelHelper.populateBlobItem(item);
+ assertEquals(blobName, publicItem.getName());
+ assertEquals(7L, (long) publicItem.getProperties().getContentLength());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowBasic() {
+ // Upload blobs in a directory structure
+ cc.getBlobClient("dir/blob1")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ cc.getBlobClient("dir/blob2")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ cc.getBlobClient("topblob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ List items = cc.listBlobsByHierarchy("/", options, null).stream().collect(Collectors.toList());
+
+ // Root level: one prefix "dir/" and one blob "topblob"
+ assertEquals(2, items.size());
+
+ BlobItem prefixItem = items.stream().filter(BlobItem::isPrefix).findFirst().orElse(null);
+ BlobItem blobItem = items.stream().filter(i -> !i.isPrefix()).findFirst().orElse(null);
+
+ assertNotNull(prefixItem);
+ assertEquals("dir/", prefixItem.getName());
+ assertTrue(prefixItem.isPrefix());
+
+ assertNotNull(blobItem);
+ assertEquals("topblob", blobItem.getName());
+ assertFalse(blobItem.isPrefix());
+ assertNotNull(blobItem.getProperties());
+ assertEquals(DATA.getDefaultDataSize(), blobItem.getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, blobItem.getProperties().getBlobType());
+ assertNotNull(blobItem.getProperties().getLastModified());
+ assertNotNull(blobItem.getProperties().getETag());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ cc.getBlobClient("dir/" + blobName)
+ .getBlockBlobClient()
+ .uploadWithResponse(DATA.getDefaultInputStream(), DATA.getDefaultDataSize(), null, metadata, null, null,
+ null, null, null);
+ cc.getBlobClient("topblob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setPrefix("dir/")
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+ List blobs = cc.listBlobsByHierarchy("/", options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertFalse(blobs.get(0).isPrefix());
+ assertNotNull(blobs.get(0).getMetadata());
+ assertEquals("testvalue", blobs.get(0).getMetadata().get("testkey"));
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowPagination() {
+ // Upload blobs across multiple directories
+ for (int i = 0; i < 3; i++) {
+ cc.getBlobClient("dir" + i + "/blob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ }
+ cc.getBlobClient("topblob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+ List allItems = new ArrayList<>();
+ for (PagedResponse page : cc.listBlobsByHierarchy("/", options, null).iterableByPage()) {
+ assertTrue(page.getValue().size() <= 1);
+ allItems.addAll(page.getValue());
+ }
+
+ // 3 prefixes + 1 blob = 4 items
+ assertEquals(4, allItems.size());
+ }
+
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java
index 04ebc06dc2b6..f0b87b33ccd2 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java
@@ -10,8 +10,14 @@
import com.azure.core.test.TestMode;
import com.azure.core.test.utils.MockTokenCredential;
import com.azure.core.util.Context;
+import com.azure.core.util.FluxUtil;
import com.azure.core.util.polling.PollerFlux;
import com.azure.identity.DefaultAzureCredentialBuilder;
+import com.azure.storage.blob.implementation.AzureBlobStorageImpl;
+import com.azure.storage.blob.implementation.AzureBlobStorageImplBuilder;
+import com.azure.storage.blob.implementation.models.BlobItemInternal;
+import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer;
+import com.azure.storage.blob.implementation.util.ModelHelper;
import com.azure.storage.blob.models.*;
import com.azure.storage.blob.options.BlobContainerCreateOptions;
import com.azure.storage.blob.options.BlobParallelUploadOptions;
@@ -22,6 +28,8 @@
import com.azure.storage.blob.specialized.BlobAsyncClientBase;
import com.azure.storage.blob.specialized.BlockBlobAsyncClient;
import com.azure.storage.blob.specialized.PageBlobAsyncClient;
+import com.azure.storage.common.implementation.Constants;
+import com.azure.storage.common.implementation.StorageImplUtils;
import com.azure.storage.common.test.shared.TestHttpClientType;
import com.azure.storage.common.test.shared.extensions.LiveOnly;
import com.azure.storage.common.test.shared.extensions.PlaybackOnly;
@@ -41,6 +49,7 @@
import reactor.util.function.Tuple2;
import java.net.URL;
+import java.io.ByteArrayInputStream;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.*;
@@ -2142,4 +2151,336 @@ public void getBlobContainerUrlEncodesContainerName() {
assertTrue(containerClient.getBlobContainerUrl().contains("my%20container"));
}
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowBasic() {
+ // Upload a test blob
+ String blobName = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ StepVerifier
+ .create(
+ bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).thenMany(ccAsync.listBlobs(options, null)))
+ .assertNext(item -> {
+ assertEquals(blobName, item.getName());
+ assertNotNull(item.getProperties());
+ assertEquals(DATA.getDefaultDataSize(), item.getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType());
+ assertNotNull(item.getProperties().getLastModified());
+ assertNotNull(item.getProperties().getETag());
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+
+ StepVerifier.create(
+ bc.uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, metadata, null, null, null)
+ .thenMany(ccAsync.listBlobs(options, null)))
+ .assertNext(item -> {
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowPagination() {
+ // Upload 4 blobs
+ Flux uploads = Flux.range(0, 4)
+ .flatMap(i -> ccAsync.getBlobAsyncClient("blob" + i)
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+
+ Mono> result = uploads.then(ccAsync.listBlobs(options, null).byPage().doOnNext(page -> {
+ assertTrue(page.getValue().size() <= 1);
+ }).flatMap(page -> Flux.fromIterable(page.getValue())).collectList());
+
+ StepVerifier.create(result).assertNext(allBlobs -> assertEquals(4, allBlobs.size())).verifyComplete();
+
+ // Mirror the sync test's secondary assertion: requesting page size 2 yields exactly 2 blobs per page.
+ StepVerifier.create(ccAsync.listBlobs().byPage(2)).thenConsumeWhile(page -> {
+ assertEquals(2, page.getValue().size());
+ return true;
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowNullUseArrowUsesXml() {
+ // Default apacheArrowEnabled is null — should use XML path without error
+ String blobName = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options = new ListBlobsOptions();
+ assertNull(options.getStorageResponseSerializationFormat());
+
+ StepVerifier
+ .create(
+ bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).thenMany(ccAsync.listBlobs(options, null)))
+ .assertNext(item -> assertEquals(blobName, item.getName()))
+ .verifyComplete();
+ }
+
+ @LiveOnly
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowEncryptedBlob() {
+ // Upload a blob with CPK (customer-provided key)
+ String blobName = generateBlobName();
+ CustomerProvidedKey cpk = new CustomerProvidedKey(Base64.getEncoder().encodeToString(getRandomKey()));
+ BlockBlobAsyncClient cpkClient
+ = ccAsync.getBlobAsyncClient(blobName).getCustomerProvidedKeyAsyncClient(cpk).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ StepVerifier.create(cpkClient.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())
+ .thenMany(ccAsync.listBlobs(options, null))).assertNext(item -> {
+ assertEquals(blobName, item.getName());
+ // CPK blob should have server-encrypted = true
+ assertTrue(item.getProperties().isServerEncrypted());
+ // Metadata should be null (no metadata was set)
+ assertNull(item.getMetadata());
+ }).verifyComplete();
+ }
+
+ @ParameterizedTest
+ @MethodSource("listBlobsFlatRehydratePrioritySupplier")
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowRehydratePriority(RehydratePriority rehydratePriority) {
+ String name = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(name).getBlockBlobAsyncClient();
+
+ Mono> rehydrate = Mono.empty();
+
+ if (rehydratePriority != null) {
+ rehydrate = bc.setAccessTier(AccessTier.ARCHIVE)
+ .then(bc.setAccessTierWithResponse(
+ new BlobSetAccessTierOptions(AccessTier.HOT).setPriority(rehydratePriority)));
+ }
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ Flux response = bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())
+ .then(rehydrate)
+ .thenMany(ccAsync.listBlobs(options, null));
+
+ StepVerifier.create(response)
+ .assertNext(r -> assertEquals(rehydratePriority, r.getProperties().getRehydratePriority()))
+ .verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowDeserializer() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ AzureBlobStorageImpl impl = new AzureBlobStorageImplBuilder().pipeline(ccAsync.getHttpPipeline())
+ .url(ccAsync.getAccountUrl())
+ .version(BlobServiceVersion.getLatest().getVersion())
+ .buildClient();
+
+ List include = new ArrayList<>();
+ include.add(ListBlobsIncludeItem.METADATA);
+
+ Mono testMono
+ = bc.uploadWithResponse(DATA.getDefaultFlux(), 7, null, metadata, null, null, null)
+ .then(impl.getContainers()
+ .listBlobFlatSegmentApacheArrowWithResponseAsync(containerName, null, null, null, include, null,
+ null, null, null))
+ .flatMap(response -> {
+ // Verify Content-Type is Arrow
+ String contentType = response.getDeserializedHeaders().getContentType();
+ assertTrue(
+ StorageImplUtils.hasMatchingHeaderValue(contentType,
+ Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM),
+ "Expected Arrow content type but got: " + contentType);
+
+ // Collect the Flux body into a byte[] and feed it to the deserializer.
+ return FluxUtil.collectBytesInByteBufferStream(response.getValue())
+ .map(bytes -> ArrowBlobListDeserializer.deserialize(new ByteArrayInputStream(bytes)));
+ });
+
+ StepVerifier.create(testMono).assertNext(result -> {
+ // Verify pagination — single blob, no next page
+ assertNull(result.getNextMarker());
+
+ // Verify we got exactly one blob
+ assertEquals(1, result.getBlobItems().size());
+
+ BlobItemInternal item = result.getBlobItems().get(0);
+
+ // Name
+ assertNotNull(item.getName());
+ assertEquals(blobName, item.getName().getContent());
+
+ // Properties
+ assertNotNull(item.getProperties());
+ assertEquals(7L, (long) item.getProperties().getContentLength());
+ assertEquals("application/octet-stream", item.getProperties().getContentType());
+ assertNotNull(item.getProperties().getETag());
+ assertNotNull(item.getProperties().getLastModified());
+ assertNotNull(item.getProperties().getCreationTime());
+ assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType());
+ assertEquals(AccessTier.HOT, item.getProperties().getAccessTier());
+ assertTrue(item.getProperties().isAccessTierInferred());
+ assertTrue(item.getProperties().isServerEncrypted());
+ assertEquals(LeaseStateType.AVAILABLE, item.getProperties().getLeaseState());
+ assertEquals(LeaseStatusType.UNLOCKED, item.getProperties().getLeaseStatus());
+ assertNotNull(item.getProperties().getContentMd5());
+
+ // Metadata
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+
+ // Verify ModelHelper can convert to public BlobItem
+ BlobItem publicItem = ModelHelper.populateBlobItem(item);
+ assertEquals(blobName, publicItem.getName());
+ assertEquals(7L, (long) publicItem.getProperties().getContentLength());
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowBasic() {
+ // Upload blobs in a directory structure
+ Flux uploads = Flux.concat(
+ ccAsync.getBlobAsyncClient("dir/blob1")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()),
+ ccAsync.getBlobAsyncClient("dir/blob2")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()),
+ ccAsync.getBlobAsyncClient("topblob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ Mono> items
+ = uploads.then(ccAsync.listBlobsByHierarchy("/", options).collect(Collectors.toList()));
+
+ StepVerifier.create(items).assertNext(list -> {
+ // Root level: one prefix "dir/" and one blob "topblob"
+ assertEquals(2, list.size());
+
+ BlobItem prefixItem = list.stream().filter(BlobItem::isPrefix).findFirst().orElse(null);
+ BlobItem blobItem = list.stream().filter(i -> !i.isPrefix()).findFirst().orElse(null);
+
+ assertNotNull(prefixItem);
+ assertEquals("dir/", prefixItem.getName());
+ assertTrue(prefixItem.isPrefix());
+
+ assertNotNull(blobItem);
+ assertEquals("topblob", blobItem.getName());
+ assertFalse(blobItem.isPrefix());
+ assertNotNull(blobItem.getProperties());
+ assertEquals(DATA.getDefaultDataSize(), blobItem.getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, blobItem.getProperties().getBlobType());
+ assertNotNull(blobItem.getProperties().getLastModified());
+ assertNotNull(blobItem.getProperties().getETag());
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+
+ Mono> uploads = ccAsync.getBlobAsyncClient("dir/" + blobName)
+ .getBlockBlobAsyncClient()
+ .uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, metadata, null, null, null)
+ .then(ccAsync.getBlobAsyncClient("topblob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setPrefix("dir/")
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+
+ StepVerifier.create(uploads.thenMany(ccAsync.listBlobsByHierarchy("/", options))).assertNext(item -> {
+ assertFalse(item.isPrefix());
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowPagination() {
+ // Upload blobs across multiple directories
+ Flux uploads = Flux.concat(
+ Flux.range(0, 3)
+ .flatMap(i -> ccAsync.getBlobAsyncClient("dir" + i + "/blob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())),
+ ccAsync.getBlobAsyncClient("topblob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+
+ Mono> result
+ = uploads.then(ccAsync.listBlobsByHierarchy("/", options).byPage().doOnNext(page -> {
+ assertTrue(page.getValue().size() <= 1);
+ }).flatMap(page -> Flux.fromIterable(page.getValue())).collectList());
+
+ // 3 prefixes + 1 blob = 4 items
+ StepVerifier.create(result).assertNext(allItems -> assertEquals(4, allItems.size())).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithTags() {
+ // Upload a blob and set tags
+ String blobName = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ Map tags = new HashMap<>();
+ tags.put("tagkey", "tagvalue");
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveTags(true));
+
+ Mono> upload = bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())
+ .then(ccAsync.getBlobAsyncClient(blobName).setTags(tags));
+
+ StepVerifier.create(upload.thenMany(ccAsync.listBlobs(options, null))).assertNext(item -> {
+ assertEquals(blobName, item.getName());
+ assertNotNull(item.getTags());
+ assertEquals("tagvalue", item.getTags().get("tagkey"));
+ }).verifyComplete();
+ }
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowGoldenDecodeTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowGoldenDecodeTests.java
new file mode 100644
index 000000000000..ae5cedda007d
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowGoldenDecodeTests.java
@@ -0,0 +1,148 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.models.BlobItemInternal;
+import com.azure.storage.blob.implementation.models.BlobListArrowParseException;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Golden-payload decode tests for the vendored {@link BlobListArrowStreamReader} / {@link ArrowBlobListDeserializer}.
+ *
+ * The fixture ({@code arrow/representative.arrow.base64}) is a real Arrow IPC stream that was produced once by the
+ * official {@code arrow-vector} writer (see {@code ArrowFixtureGenerator}) and committed as a frozen resource. This test
+ * decodes those exact bytes with the vendored reader and pins every field the ListBlobs path relies on, so any
+ * regression in the vendored FlatBuffer accessors surfaces here without requiring the Arrow library on the test
+ * classpath. It replaces the former differential parity test that re-decoded a freshly written payload with Apache
+ * Arrow's own accessors; the frozen fixture is the same shape but removes the runtime dependency.
+ */
+public class BlobListArrowGoldenDecodeTests {
+
+ @Test
+ public void decodesRepresentativePayload() throws IOException {
+ ArrowBlobListDeserializer.ArrowListBlobsResult result
+ = ArrowBlobListDeserializer.deserialize(openFixture("representative.arrow.base64"));
+
+ // Schema metadata.
+ assertEquals("nextPage", result.getNextMarker());
+ assertEquals(Integer.valueOf(2), result.getNumberOfRecords());
+
+ // Two rows: one blob, one prefix.
+ List items = result.getBlobItems();
+ assertEquals(2, items.size());
+
+ BlobItemInternal blob = items.get(0);
+ assertNotNull(blob.getName());
+ assertEquals("blob1", blob.getName().getContent());
+ assertNull(blob.isPrefix());
+ assertEquals(Boolean.FALSE, blob.isDeleted());
+ assertNotNull(blob.getProperties());
+ assertEquals(7L, (long) blob.getProperties().getContentLength());
+ assertEquals("application/octet-stream", blob.getProperties().getContentType());
+ assertNotNull(blob.getProperties().getCreationTime());
+ assertEquals(1000L, blob.getProperties().getCreationTime().toEpochSecond());
+
+ Map metadata = blob.getMetadata();
+ assertNotNull(metadata);
+ assertEquals("v1", metadata.get("k1"));
+ assertEquals("v2", metadata.get("k2"));
+
+ BlobItemInternal prefix = items.get(1);
+ assertNotNull(prefix.getName());
+ assertEquals("dir/", prefix.getName().getContent());
+ assertTrue(prefix.isPrefix());
+ // The prefix row had no metadata entries; the reader must not surface an (empty) map for it.
+ assertNull(prefix.getMetadata());
+ assertFalse(items.isEmpty());
+ }
+
+ @Test
+ public void allColumnsFixtureDecodesWithoutRejection() throws IOException {
+ // Every column the SDK claims to handle is present in this full-schema fixture and must decode without the
+ // unknown-column guard firing (no false positives) and without a type-mismatch error.
+ ArrowBlobListDeserializer.ArrowListBlobsResult result
+ = ArrowBlobListDeserializer.deserialize(openFixture("allcolumns.arrow.base64"));
+
+ List items = result.getBlobItems();
+ assertEquals(1, items.size());
+ BlobItemInternal blob = items.get(0);
+
+ // Spot-check one field from each Arrow type category to prove full-schema decode.
+ assertEquals("blob1", blob.getName().getContent());
+ assertEquals(Boolean.FALSE, blob.isDeleted());
+ assertEquals(7L, (long) blob.getProperties().getContentLength());
+ assertEquals(1000L, blob.getProperties().getCreationTime().toEpochSecond());
+ assertEquals("mv", blob.getMetadata().get("mk"));
+ }
+
+ @Test
+ public void allColumnsFixtureSchemaMatchesKnownColumnSet() throws IOException {
+ // Drift guard: the frozen full-schema fixture and the deserializer's known-column set must stay identical. If a
+ // column is added to KNOWN_COLUMNS without regenerating the fixture (or vice versa), this fails loudly.
+ Set fixtureColumns;
+ try (InputStream stream = openFixture("allcolumns.arrow.base64")) {
+ BlobListArrowStreamReader.DecodedArrowStream decodedArrowStream = BlobListArrowStreamReader.read(stream);
+ fixtureColumns = decodedArrowStream.getBatches().get(0).getColumnNames();
+ }
+ assertEquals(ArrowBlobListDeserializer.knownColumns(), fixtureColumns);
+ }
+
+ @Test
+ public void rejectsUnknownColumn() throws IOException {
+ // The service added a column a future SDK understands; this SDK must reject loudly and name the offender.
+ try (InputStream stream = openFixture("reject-unknown-column.arrow.base64")) {
+ BlobListArrowParseException ex
+ = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(stream));
+ assertTrue(ex.getMessage().contains("FutureField"), "Unexpected message: " + ex.getMessage());
+ }
+ }
+
+ private static InputStream openFixture(String name) throws IOException {
+ byte[] base64;
+ try (InputStream resource = classpathResource("/arrow/" + name)) {
+ assertNotNull(resource, "missing test fixture: arrow/" + name);
+ base64 = readAll(resource);
+ }
+ return new ByteArrayInputStream(Base64.getDecoder().decode(base64));
+ }
+
+ /**
+ * Loads a resource from the test classpath. Calling {@code getResourceAsStream} on this test's class literal is
+ * equivalent to going through its class loader directly (like
+ * {@code Thread.currentThread().getContextClassLoader().getResourceAsStream(...)}); we use the class literal because
+ * this is a static context with no {@code this} on which to call {@code getClass()}. The leading {@code /} makes the
+ * path absolute from the classpath root.
+ *
+ * @param absolutePath the classpath-absolute resource path, beginning with {@code /}
+ * @return the resource stream, or null if the resource is not found
+ */
+ private static InputStream classpathResource(String absolutePath) {
+ return BlobListArrowGoldenDecodeTests.class.getResourceAsStream(absolutePath);
+ }
+
+ private static byte[] readAll(InputStream in) throws IOException {
+ java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ return out.toByteArray();
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java
new file mode 100644
index 000000000000..349db1cefa48
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.models.BlobListArrowParseException;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Parity tests for Arrow IPC content the ListBlobs reader intentionally rejects or treats as an error. The payloads
+ * are committed golden fixtures (see {@code src/test/resources/arrow}) originally produced with the official
+ * {@code arrow-vector} writer, so these tests validate the vendored reader's rejection paths against genuine Arrow
+ * output with no Arrow dependency. The remaining cases cover malformed/edge inputs that the writer cannot produce
+ * (null and empty streams).
+ */
+public class BlobListArrowStreamReaderRejectionTests {
+
+ @Test
+ public void rejectsNullStream() {
+ BlobListArrowParseException ex
+ = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(null));
+ assertTrue(ex.getMessage().contains("input stream is null"), "Unexpected message: " + ex.getMessage());
+ }
+
+ @Test
+ public void rejectsStreamWithNoSchema() {
+ InputStream stream = new ByteArrayInputStream(new byte[0]);
+ BlobListArrowParseException ex
+ = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(stream));
+ assertTrue(ex.getMessage().contains("stream contained no schema"), "Unexpected message: " + ex.getMessage());
+ }
+
+ @Test
+ public void rejectsDictionaryEncodedStreams() throws IOException {
+ assertRejected("reject-dictionary.arrow.base64", "dictionary-encoded streams are not supported");
+ }
+
+ @Test
+ public void rejectsUnsupportedColumnType() throws IOException {
+ assertRejected("reject-float.arrow.base64", "unsupported Arrow type 'FloatingPoint'");
+ }
+
+ @Test
+ public void rejectsUnsupportedTimestampUnit() throws IOException {
+ assertRejected("reject-timestamp-milli.arrow.base64", "unsupported timestamp unit 'MILLISECOND'");
+ }
+
+ @Test
+ public void rejectsMapWithNonStringValues() throws IOException {
+ assertRejected("reject-map-nonstring.arrow.base64", "map entries must be string keys and values");
+ }
+
+ // region helpers
+
+ private static void assertRejected(String fixture, String expectedMessageFragment) throws IOException {
+ try (InputStream stream = openFixture(fixture)) {
+ BlobListArrowParseException ex
+ = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(stream));
+ assertTrue(ex.getMessage().contains(expectedMessageFragment), "Unexpected message: " + ex.getMessage());
+ }
+ }
+
+ private static InputStream openFixture(String name) throws IOException {
+ byte[] base64;
+ try (InputStream resource
+ = BlobListArrowStreamReaderRejectionTests.class.getResourceAsStream("/arrow/" + name)) {
+ assertNotNull(resource, "missing test fixture: arrow/" + name);
+ base64 = readAll(resource);
+ }
+ return new ByteArrayInputStream(Base64.getDecoder().decode(base64));
+ }
+
+ private static byte[] readAll(InputStream in) throws IOException {
+ java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ return out.toByteArray();
+ }
+
+ //endregion
+}
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/README.md b/sdk/storage/azure-storage-blob/src/test/resources/arrow/README.md
new file mode 100644
index 000000000000..747f82ae3a7f
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/README.md
@@ -0,0 +1,13 @@
+# ListBlobs Arrow golden fixtures
+
+These `*.arrow.base64` files are committed golden fixtures for the vendored ListBlobs Arrow
+deserializer tests (`BlobListArrowGoldenDecodeTests`, `BlobListArrowStreamReaderRejectionTests`).
+Each one is an Apache Arrow IPC stream that has been **Base64-encoded** before being written to disk.
+
+The tests decode the Base64 back to the original Arrow bytes before parsing, so the on-disk
+encoding is transparent to the deserializer under test.
+
+## Naming
+
+`.arrow.base64` — `.arrow` denotes the Apache Arrow IPC stream; `.base64` denotes the
+Base64 text wrapper.
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/allcolumns.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/allcolumns.arrow.base64
new file mode 100644
index 000000000000..44eeb10f79cc
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/allcolumns.arrow.base64
@@ -0,0 +1 @@
+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AAAAAAJgBAAAAAAAAAQAAAAAAAACgAQAAAAAAAAgAAAAAAAAAqAEAAAAAAAABAAAAAAAAALABAAAAAAAACAAAAAAAAAC4AQAAAAAAAAEAAAAAAAAAwAEAAAAAAAAIAAAAAAAAAMgBAAAAAAAAEQAAAAAAAADgAQAAAAAAAAEAAAAAAAAA6AEAAAAAAAAIAAAAAAAAAPABAAAAAAAAAQAAAAAAAAD4AQAAAAAAAAgAAAAAAAAAAAIAAAAAAAAYAAAAAAAAABgCAAAAAAAAAQAAAAAAAAAgAgAAAAAAAAgAAAAAAAAAKAIAAAAAAAAEAAAAAAAAADACAAAAAAAAAQAAAAAAAAA4AgAAAAAAAAgAAAAAAAAAQAIAAAAAAAAFAAAAAAAAAEgCAAAAAAAAAQAAAAAAAABQAgAAAAAAAAgAAAAAAAAAWAIAAAAAAAAGAAAAAAAAAGACAAAAAAAAAQAAAAAAAABoAgAAAAAAAAgAAAAAAAAAcAIAAAAAAAAIAAAAAAAAAHgCAAAAAAAAAQAAAAAAAACAAgAAAAAAAAgAAAAAAAAAiAIAAAAAAAAIAAAAAAAAAJACAAAAAAAAAQAAAAAAAACYAgAAAAAAAAgAAAAAAAAAoAIAAAAAAAAJAAAAAAAAALACAAAAAAAAAQAAAAAAAAC4AgAAAAAAAAgAAAAAAAAAwAIAAAAAAAADAAAAAAAAAMgCAAAAAAAAAQAAAAAAAADQAgAAAAAAAAEAAAAAAAAA2AIAAAAAAAABAAAAAAAAAOACAAAAAAAACAAAAAAAAADoAgAAAAAAAAEAAAAAAAAA8AIAAAAAAAAIAAAAAAAAAPgCAAAAAAAABAAAAAAAAAAAAwAAAAAAAAEAAAAAAAAACAMAAAAAAAAIAAAAAAAAABADAAAAAAAACAAAAAAAAAAYAwAAAAAAAAEAAAAAAAAAIAMAAAAAAAAIAAAAAAAAACgDAAAAAAAACQAAAAAAAAA4AwAAAAAAAAEAAAAAAAAAQAMAAAAAAAAIAAAAAAAAAEgDAAAAAAAACAAAAAAAAABQAwAAAAAAAAEAAAAAAAAAWAMAAAAAAAABAAAAAAAAAGADAAAAAAAAAQAAAAAAAABoAwAAAAAAAAgAAAAAAAAAcAMAAAAAAAArAAAAAAAAAKADAAAAAAAAAQAAAAAAAACoAwAAAAAAAAgAAAAAAAAAsAMAAAAAAAAGAAAAAAAAALgDAAAAAAAAAQAAAAAAAADAAwAAAAAAAAEAAAAAAAAAyAMAAAAAAAABAAAAAAAAANADAAAAAAAACAAAAAAAAADYAwAAAAAAACQAAAAAAAAAAAQAAAAAAAABAAAAAAAAAAgEAAAAAAAACAAAAAAAAAAQBAAAAAAAAAcAAAAAAAAAGAQAAAAAAAABAAAAAAAAACAEAAAAAAAACAAAAAAAAAAoBAAAAAAAADYAAAAAAAAAYAQAAAAAAAABAAAAAAAAAGgEAAAAAAAACAAAAAAAAABwBAAAAAAAAAMAAAAAAAAAeAQAAAAAAAABAAAAAAAAAIAEAAAAAAAACAAAAAAAAACIBAAAAAAAAAEAAAAAAAAAkAQAAAAAAAAIAAAAAAAAAJgEAAAAAAAADQAAAAAAAACoBAAAAAAAAAEAAAAAAAAAsAQAAAAAAAAIAAAAAAAAALgEAAAAAAAAHAAAAAAAAADYBAAAAAAAAAEAAAAAAAAA4AQAAAAAAAAIAAAAAAAAAOgEAAAAAAAAAQAAAAAAAADwBAAAAAAAAAEAAAAAAAAA+AQAAAAAAAABAAAAAAAAAAAFAAAAAAAAAQAAAAAAAAAIBQAAAAAAAAEAAAAAAAAAEAUAAAAAAAAIAAAAAAAAABgFAAAAAAAAAQAAAAAAAAAgBQAAAAAAAAgAAAAAAAAAKAUAAAAAAAABAAAAAAAAADAFAAAAAAAACAAAAAAAAAA4BQAAAAAAAAEAAAAAAAAAQAUAAAAAAAAIAAAAAAAAAEgFAAAAAAAACAAAAAAAAABQBQAAAAAAAAEAAAAAAAAAWAUAAAAAAAAIAAAAAAAAAGAFAAAAAAAAGAAAAAAAAAB4BQAAAAAAAAEAAAAAAAAAgAUAAAAAAAAIAAAAAAAAAIgFAAAAAAAABAAAAAAAAACQBQAAAAAAAAEAAAAAAAAAmAUAAAAAAAAIAAAAAAAAAKAFAAAAAAAAAQAAAAAAAACoBQAAAAAAAAgAAAAAAAAAAAAAADsAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAUAAABibG9iMQAAAAEAAAAAAAAAAAAAAAQAAABibG9iAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAcAAAAMjAyMC0wMS0wMVQwMDowMDowMC4wMDAwMDAwWgAAAAABAAAAAAAAAAAAAAAcAAAAMjAyMC0wMS0wMVQwMDowMDowMC4wMDAwMDAxWgAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAIAAABtawAAAAAAAAEAAAAAAAAAAAAAAAIAAABtdgAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAIAAABvawAAAAAAAAEAAAAAAAAAAAAAAAIAAABvdgAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAIAAAB0awAAAAAAAAEAAAAAAAAAAAAAAAIAAAB0dgAAAAAAAAEAAAAAAAAA6AMAAAAAAAABAAAAAAAAANAHAAAAAAAAAQAAAAAAAAAAAAAAEQAAADB4OEQ4RDhEOEQ4RDhEOEQ4AAAAAAAAAAEAAAAAAAAABwAAAAAAAAABAAAAAAAAAAAAAAAYAAAAYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtAQAAAAAAAAAAAAAABAAAAGd6aXAAAAAAAQAAAAAAAAAAAAAABQAAAGVuLVVTAAAAAQAAAAAAAAAAAAAABgAAAGlubGluZQAAAQAAAAAAAAAAAAAACAAAAG5vLWNhY2hlAQAAAAAAAAAAAAAACAAAAEFRSURCQT09AQAAAAAAAAAAAAAACQAAAEJsb2NrQmxvYgAAAAAAAAABAAAAAAAAAAAAAAADAAAASG90AAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAC4CwAAAAAAAAEAAAAAAAAAAAAAAAQAAABDb29sAAAAAAEAAAAAAAAAAAAAAAgAAAB1bmxvY2tlZAEAAAAAAAAAAAAAAAkAAABhdmFpbGFibGUAAAAAAAAAAQAAAAAAAAAAAAAACAAAAGluZmluaXRlAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAACsAAABQbXQwUjBrazJ2N3E1cUIxUW5ublE0eFEwUTBRMFEwUTBRMFEwUTBRMFE9AAAAAAABAAAAAAAAAAAAAAAGAAAAc2NvcGUxAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAJAAAADAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMQAAAAABAAAAAAAAAAAAAAAHAAAAc3VjY2VzcwABAAAAAAAAAAAAAAA2AAAAaHR0cHM6Ly9hY2NvdW50LmJsb2IuY29yZS53aW5kb3dzLm5ldC9jb250YWluZXIvc291cmNlAAABAAAAAAAAAAAAAAADAAAANy83AAAAAAABAAAAAAAAAKAPAAAAAAAAAQAAAAAAAAAAAAAADQAAAGNvcHkgY29tcGxldGUAAAABAAAAAAAAAAAAAAAcAAAAMjAyMC0wMS0wMlQwMDowMDowMC4wMDAwMDAwWgAAAAABAAAAAAAAACoAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAIgTAAAAAAAAAQAAAAAAAABwFwAAAAAAAAEAAAAAAAAAWBsAAAAAAAABAAAAAAAAAAAAAAAIAAAAdW5sb2NrZWQBAAAAAAAAAAAAAAAYAAAAcmVoeWRyYXRlLXBlbmRpbmctdG8taG90AQAAAAAAAAAAAAAABAAAAEhpZ2gAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAABQAAAAAAAAD/////AAAAAA==
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-dictionary.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-dictionary.arrow.base64
new file mode 100644
index 000000000000..a877f2b8d963
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-dictionary.arrow.base64
@@ -0,0 +1 @@
+/////7gAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAYAAAAAAASABwAGAAXABYAEAAEAAwACAASAAAAKAAAABQAAAAUAAAARAAAAAAABQFAAAAAAAAAAAAAAAAIABAACAAEAAgAAAAUAAAAAQAAAAAAAAAIAAwACAAHAAgAAAAAAAABIAAAAAQABAAEAAAABAAAAE5hbWUAAAAA/////7gAAAAUAAAAAAAAAAwAHAASABsAFAAEAAwAAAAoAAAAAAAAAAAAAAAAAAQAEAAAAAAAAAIIABIACAAEAAgAAAAYAAAAAQAAAAAAAAAAAAoAGAAMAAgABAAKAAAAFAAAAEgAAAACAAAAAAAAAAAAAAADAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAADAAAAAAAAAAYAAAAAAAAAAoAAAAAAAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAABQAAAAoAAAAAAAAAYmxvYjFibG9iMgAAAAAAAP////+IAAAAFAAAAAAAAAAMABYADgAVABAABAAMAAAAEAAAAAAAAAAAAAQAEAAAAAADCgAYAAwACAAEAAoAAAAUAAAAOAAAAAIAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAABAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAEAAAD/////AAAAAA==
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-float.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-float.arrow.base64
new file mode 100644
index 000000000000..8065f28604cf
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-float.arrow.base64
@@ -0,0 +1 @@
+/////5AAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAABQAAAAcAAAAAAADARwAAAAAAAAAAAAAAAAABgAIAAYABgAAAAAAAgAFAAAAU2NvcmUAAAD/////iAAAABQAAAAAAAAADAAWAA4AFQAQAAQADAAAABAAAAAAAAAAAAAEABAAAAAAAwoAGAAMAAgABAAKAAAAFAAAADgAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAPg//////wAAAAA=
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-map-nonstring.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-map-nonstring.arrow.base64
new file mode 100644
index 000000000000..9a26eb1d998a
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-map-nonstring.arrow.base64
@@ -0,0 +1 @@
+/////0gBAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAEAAAAsv///xQAAAAUAAAA5AAAAAAAEQHgAAAAAAAAAAEAAAAEAAAAhv///xQAAAAUAAAArAAAAAAAAA2oAAAAAAAAAAIAAABsAAAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAAAUAAAAHAAAAAAAAgEgAAAAAAAAAAAAAAAIAAwACAAHAAgAAAAAAAABQAAAAAUAAAB2YWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAABQAAAAUAAAAAAAABRAAAAAAAAAAAAAAAOT///8DAAAAa2V5APD///8HAAAAZW50cmllcwAEAAQABAAAAAgAAABNZXRhZGF0YQAAAAAAAAAA/////xgBAAAUAAAAAAAAAAwAFgAOABUAEAAEAAwAAABAAAAAAAAAAAAABAAQAAAAAAMKABgADAAIAAQACgAAABQAAACYAAAAAQAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAABAAAAAAAAABgAAAAAAAAAAQAAAAAAAAAgAAAAAAAAAAgAAAAAAAAAKAAAAAAAAAACAAAAAAAAADAAAAAAAAAAAQAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAgAAAGsxAAAAAAAAAQAAAAAAAAAqAAAAAAAAAP////8AAAAA
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-timestamp-milli.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-timestamp-milli.arrow.base64
new file mode 100644
index 000000000000..58c6263aa995
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-timestamp-milli.arrow.base64
@@ -0,0 +1 @@
+/////5gAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAABQAAAAcAAAAAAAKARwAAAAAAAAAAAAAAAAABgAIAAYABgAAAAAAAQANAAAAQ3JlYXRpb24tVGltZQAAAP////+IAAAAFAAAAAAAAAAMABYADgAVABAABAAMAAAAEAAAAAAAAAAAAAQAEAAAAAADCgAYAAwACAAEAAoAAAAUAAAAOAAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA6AMAAAAAAAD/////AAAAAA==
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-unknown-column.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-unknown-column.arrow.base64
new file mode 100644
index 000000000000..281e0d3be3da
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-unknown-column.arrow.base64
@@ -0,0 +1 @@
+/////8AAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAIAAABQAAAABAAAAMr///8UAAAAFAAAABQAAAAAAAUBEAAAAAAAAAAAAAAAuP///wsAAABGdXR1cmVGaWVsZAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAFAAAABgAAAAAAAUBFAAAAAAAAAAAAAAABAAEAAQAAAAEAAAATmFtZQAAAAD/////2AAAABQAAAAAAAAADAAWAA4AFQAQAAQADAAAADAAAAAAAAAAAAAEABAAAAAAAwoAGAAMAAgABAAKAAAAFAAAAHgAAAABAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAUAAAAAAAAAGAAAAAAAAAABAAAAAAAAACAAAAAAAAAACAAAAAAAAAAoAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAUAAABibG9iMQAAAAEAAAAAAAAAAAAAAAgAAABzdXJwcmlzZf////8AAAAA
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/representative.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/representative.arrow.base64
new file mode 100644
index 000000000000..7e43432b368e
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/representative.arrow.base64
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-blob/swagger/README.md b/sdk/storage/azure-storage-blob/swagger/README.md
index 9f799c000f84..0019d026a0d0 100644
--- a/sdk/storage/azure-storage-blob/swagger/README.md
+++ b/sdk/storage/azure-storage-blob/swagger/README.md
@@ -16,7 +16,7 @@ autorest
### Code generation settings
``` yaml
use: '@autorest/java@4.1.63'
-input-file: https://raw.githubusercontent.com/seanmcc-msft/azure-rest-api-specs/eb29a830edf5db50758e7d044160c7f18077f7f7/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-10-06/blob.json
+input-file: https://raw.githubusercontent.com/nickliu-msft/azure-rest-api-specs/f85584d452061985a5fc21a67b8fc0b46b75188a/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-10-06/blob.json
java: true
output-folder: ../
namespace: com.azure.storage.blob
@@ -591,6 +591,24 @@ directive:
delete $["x-ms-pageable"];
```
+### Delete Container_ListBlobFlatSegment_ApacheArrow x-ms-pageable as response is raw Arrow stream
+``` yaml
+directive:
+- from: swagger-document
+ where: $["x-ms-paths"]["/{containerName}?restype=container&comp=list&flat&arrow"].get
+ transform: >
+ delete $["x-ms-pageable"];
+```
+
+### Delete Container_ListBlobHierarchySegment_ApacheArrow x-ms-pageable as response is raw Arrow stream
+``` yaml
+directive:
+- from: swagger-document
+ where: $["x-ms-paths"]["/{containerName}?restype=container&comp=list&hierarchy&arrow"].get
+ transform: >
+ delete $["x-ms-pageable"];
+```
+
### BlobDeleteType expandable string enum
``` yaml
directive:
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
index 687e80c71961..fd72f67021d5 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
@@ -291,6 +291,14 @@ private HeaderConstants() {
}
}
+ public static final class ContentTypeConstants {
+ public static final String APPLICATION_VND_APACHE_ARROW_STREAM = "application/vnd.apache.arrow.stream";
+
+ private ContentTypeConstants() {
+ // Private to prevent construction.
+ }
+ }
+
/**
* Defines constants for use with URLs.
*
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java
index 90aad19fffbb..081ea1f2df72 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java
@@ -397,6 +397,19 @@ public static String convertStorageExceptionMessage(String message, HttpResponse
return message;
}
+ /**
+ * Checks whether the provided header value is non-null, non-empty, and starts with the expected value.
+ *
+ * @param headerValue The header value to inspect.
+ * @param expectedHeaderValue The expected header value prefix.
+ * @return {@code true} if the header value is present and matches the expected value; otherwise {@code false}.
+ */
+ public static boolean hasMatchingHeaderValue(String headerValue, String expectedHeaderValue) {
+ return !CoreUtils.isNullOrEmpty(headerValue)
+ && !CoreUtils.isNullOrEmpty(expectedHeaderValue)
+ && headerValue.startsWith(expectedHeaderValue);
+ }
+
/**
* Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it with up to
* millisecond precision.
diff --git a/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryAsyncClient.java b/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryAsyncClient.java
index 56646909b01e..da521421629a 100644
--- a/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryAsyncClient.java
+++ b/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryAsyncClient.java
@@ -51,7 +51,6 @@
import reactor.core.publisher.Mono;
import java.time.Duration;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
diff --git a/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryClient.java b/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryClient.java
index 6065f4684853..cfa7b7383b81 100644
--- a/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryClient.java
+++ b/sdk/storage/azure-storage-file-share/src/main/java/com/azure/storage/file/share/ShareDirectoryClient.java
@@ -58,7 +58,6 @@
import com.azure.storage.file.share.sas.ShareServiceSasSignatureValues;
import java.time.Duration;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;