Skip to content

Commit 8f3720c

Browse files
author
xuan.zhao
committed
feat(puffin): add puffin file reader and writer
- PuffinWriter: in-memory writer that builds complete Puffin files - Add() writes blobs with optional compression - Finish() serializes footer with JSON metadata - Tracks BlobMetadata for all written blobs - PuffinReader: in-memory reader that parses Puffin files - ReadFileMetadata() parses footer and validates magic bytes - ReadBlob() reads and decompresses individual blobs - ReadAll() reads all blobs from metadata - Expose Compress/Decompress as public API in puffin_format.h - Register new sources in CMake and Meson build systems - Add comprehensive tests including Java binary compatibility
1 parent d690aaa commit 8f3720c

12 files changed

Lines changed: 989 additions & 16 deletions

src/iceberg/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ set(ICEBERG_DATA_SOURCES
170170
deletes/roaring_position_bitmap.cc
171171
puffin/file_metadata.cc
172172
puffin/json_serde.cc
173-
puffin/puffin_format.cc)
173+
puffin/puffin_format.cc
174+
puffin/puffin_reader.cc
175+
puffin/puffin_writer.cc)
174176

175177
set(ICEBERG_DATA_STATIC_BUILD_INTERFACE_LIBS)
176178
set(ICEBERG_DATA_SHARED_BUILD_INTERFACE_LIBS)

src/iceberg/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ iceberg_data_sources = files(
152152
'puffin/file_metadata.cc',
153153
'puffin/json_serde.cc',
154154
'puffin/puffin_format.cc',
155+
'puffin/puffin_reader.cc',
156+
'puffin/puffin_writer.cc',
155157
)
156158

157159
# CRoaring does not export symbols, so on Windows it must

src/iceberg/puffin/meson.build

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
# under the License.
1717

1818
install_headers(
19-
['file_metadata.h', 'puffin_format.h', 'type_fwd.h'],
19+
[
20+
'file_metadata.h',
21+
'puffin_format.h',
22+
'puffin_reader.h',
23+
'puffin_writer.h',
24+
'type_fwd.h',
25+
],
2026
subdir: 'iceberg/puffin',
2127
)

src/iceberg/puffin/puffin_format.cc

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ constexpr std::pair<int, int> GetFlagPosition(PuffinFlag flag) {
3636
std::unreachable();
3737
}
3838

39+
} // namespace
40+
41+
bool IsFlagSet(std::span<const uint8_t, 4> flags, PuffinFlag flag) {
42+
auto [byte_num, bit_num] = GetFlagPosition(flag);
43+
return (flags[byte_num] & (1 << bit_num)) != 0;
44+
}
45+
46+
void SetFlag(std::span<uint8_t, 4> flags, PuffinFlag flag) {
47+
auto [byte_num, bit_num] = GetFlagPosition(flag);
48+
flags[byte_num] |= (1 << bit_num);
49+
}
50+
3951
// TODO(zhaoxuan1994): Move compression logic to a unified codec interface.
4052
Result<std::vector<std::byte>> Compress(PuffinCompressionCodec codec,
4153
std::span<const std::byte> input) {
@@ -63,16 +75,4 @@ Result<std::vector<std::byte>> Decompress(PuffinCompressionCodec codec,
6375
std::unreachable();
6476
}
6577

66-
} // namespace
67-
68-
bool IsFlagSet(std::span<const uint8_t, 4> flags, PuffinFlag flag) {
69-
auto [byte_num, bit_num] = GetFlagPosition(flag);
70-
return (flags[byte_num] & (1 << bit_num)) != 0;
71-
}
72-
73-
void SetFlag(std::span<uint8_t, 4> flags, PuffinFlag flag) {
74-
auto [byte_num, bit_num] = GetFlagPosition(flag);
75-
flags[byte_num] |= (1 << bit_num);
76-
}
77-
7878
} // namespace iceberg::puffin

src/iceberg/puffin/puffin_format.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
/// Puffin file format constants and utilities.
2424

2525
#include <array>
26+
#include <cstddef>
2627
#include <cstdint>
2728
#include <span>
29+
#include <vector>
2830

2931
#include "iceberg/iceberg_data_export.h"
3032
#include "iceberg/puffin/file_metadata.h"
@@ -66,4 +68,12 @@ ICEBERG_DATA_EXPORT bool IsFlagSet(std::span<const uint8_t, 4> flags, PuffinFlag
6668
/// \brief Set a flag in the flags bytes.
6769
ICEBERG_DATA_EXPORT void SetFlag(std::span<uint8_t, 4> flags, PuffinFlag flag);
6870

71+
/// \brief Compress data using the specified codec.
72+
ICEBERG_DATA_EXPORT Result<std::vector<std::byte>> Compress(
73+
PuffinCompressionCodec codec, std::span<const std::byte> input);
74+
75+
/// \brief Decompress data using the specified codec.
76+
ICEBERG_DATA_EXPORT Result<std::vector<std::byte>> Decompress(
77+
PuffinCompressionCodec codec, std::span<const std::byte> input);
78+
6979
} // namespace iceberg::puffin
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/puffin/puffin_reader.h"
21+
22+
#include <algorithm>
23+
#include <array>
24+
#include <cstdint>
25+
#include <cstring>
26+
#include <string_view>
27+
28+
#include "iceberg/file_io.h"
29+
#include "iceberg/puffin/json_serde_internal.h"
30+
#include "iceberg/puffin/puffin_format.h"
31+
#include "iceberg/util/endian.h"
32+
#include "iceberg/util/macros.h"
33+
34+
namespace iceberg::puffin {
35+
36+
namespace {
37+
38+
// Validate magic bytes in a buffer at offset 0.
39+
Status CheckMagic(std::span<const std::byte> data) {
40+
if (static_cast<int64_t>(data.size()) < PuffinFormat::kMagicLength) {
41+
return Invalid("Invalid file: buffer too small for magic");
42+
}
43+
auto* begin = reinterpret_cast<const uint8_t*>(data.data());
44+
if (!std::equal(PuffinFormat::kMagicV1.begin(), PuffinFormat::kMagicV1.end(), begin)) {
45+
return Invalid(
46+
"Invalid file: expected magic, got [{:#04x}, {:#04x}, {:#04x}, {:#04x}]",
47+
begin[0], begin[1], begin[2], begin[3]);
48+
}
49+
return {};
50+
}
51+
52+
// Validate that no unknown flag bits are set.
53+
Status CheckUnknownFlags(std::span<const uint8_t, 4> flags) {
54+
constexpr uint8_t kKnownBitsMask = 0x01;
55+
if ((flags[0] & ~kKnownBitsMask) != 0 || flags[1] != 0 || flags[2] != 0 ||
56+
flags[3] != 0) {
57+
return Invalid(
58+
"Invalid file: unknown footer flags set [{:#04x}, {:#04x}, {:#04x}, {:#04x}]",
59+
flags[0], flags[1], flags[2], flags[3]);
60+
}
61+
return {};
62+
}
63+
64+
} // namespace
65+
66+
PuffinReader::PuffinReader(std::unique_ptr<SeekableInputStream> stream, int64_t file_size,
67+
std::optional<int64_t> known_footer_size)
68+
: stream_(std::move(stream)),
69+
file_size_(file_size),
70+
known_footer_size_(known_footer_size) {}
71+
72+
PuffinReader::~PuffinReader() = default;
73+
74+
Result<std::unique_ptr<PuffinReader>> PuffinReader::Make(
75+
std::unique_ptr<InputFile> input_file, std::optional<int64_t> footer_size) {
76+
if (!input_file) {
77+
return InvalidArgument("input_file must not be null");
78+
}
79+
ICEBERG_ASSIGN_OR_RAISE(auto file_size, input_file->Size());
80+
ICEBERG_ASSIGN_OR_RAISE(auto stream, input_file->Open());
81+
return std::unique_ptr<PuffinReader>(
82+
new PuffinReader(std::move(stream), file_size, footer_size));
83+
}
84+
85+
Result<std::vector<std::byte>> PuffinReader::ReadBytes(int64_t offset, int64_t length) {
86+
if (offset < 0 || length < 0 || offset > file_size_ || length > file_size_ - offset) {
87+
return Invalid("Read out of bounds: offset {} + length {} exceeds file size {}",
88+
offset, length, file_size_);
89+
}
90+
std::vector<std::byte> buf(length);
91+
ICEBERG_RETURN_UNEXPECTED(stream_->ReadFully(offset, buf));
92+
return buf;
93+
}
94+
95+
Result<FileMetadata> PuffinReader::ReadFileMetadata() {
96+
if (file_size_ < PuffinFormat::kFooterStructLength) {
97+
return Invalid("Invalid file: file length {} is less than minimal footer size {}",
98+
file_size_, PuffinFormat::kFooterStructLength);
99+
}
100+
101+
// Validate header magic
102+
ICEBERG_ASSIGN_OR_RAISE(auto header_bytes, ReadBytes(0, PuffinFormat::kMagicLength));
103+
ICEBERG_RETURN_UNEXPECTED(CheckMagic(header_bytes));
104+
105+
// Read footer struct from end of file
106+
auto footer_struct_offset = file_size_ - PuffinFormat::kFooterStructLength;
107+
ICEBERG_ASSIGN_OR_RAISE(
108+
auto footer_struct,
109+
ReadBytes(footer_struct_offset, PuffinFormat::kFooterStructLength));
110+
111+
// Validate footer end magic
112+
std::span<const std::byte> footer_end_magic(
113+
footer_struct.data() + PuffinFormat::kFooterStructMagicOffset,
114+
PuffinFormat::kMagicLength);
115+
ICEBERG_RETURN_UNEXPECTED(CheckMagic(footer_end_magic));
116+
117+
// Read payload size
118+
auto payload_size = ReadLittleEndian<int32_t>(
119+
footer_struct.data() + PuffinFormat::kFooterStructPayloadSizeOffset);
120+
121+
if (payload_size < 0) {
122+
return Invalid("Invalid file: negative payload size {}", payload_size);
123+
}
124+
125+
// Calculate total footer size and validate
126+
int64_t footer_size = PuffinFormat::kFooterStartMagicLength +
127+
static_cast<int64_t>(payload_size) +
128+
PuffinFormat::kFooterStructLength;
129+
auto footer_offset = file_size_ - footer_size;
130+
if (footer_offset < 0) {
131+
return Invalid("Invalid file: footer size {} exceeds file size {}", footer_size,
132+
file_size_);
133+
}
134+
135+
// Validate footer start magic
136+
ICEBERG_ASSIGN_OR_RAISE(auto footer_start_magic,
137+
ReadBytes(footer_offset, PuffinFormat::kMagicLength));
138+
ICEBERG_RETURN_UNEXPECTED(CheckMagic(footer_start_magic));
139+
140+
// Check flags
141+
std::array<uint8_t, 4> flags{};
142+
std::memcpy(flags.data(), footer_struct.data() + PuffinFormat::kFooterStructFlagsOffset,
143+
4);
144+
ICEBERG_RETURN_UNEXPECTED(CheckUnknownFlags(flags));
145+
146+
PuffinCompressionCodec footer_compression = PuffinCompressionCodec::kNone;
147+
if (IsFlagSet(flags, PuffinFlag::kFooterPayloadCompressed)) {
148+
footer_compression = PuffinFormat::kDefaultFooterCompressionCodec;
149+
}
150+
151+
// Read and decompress footer payload
152+
auto payload_offset = footer_offset + PuffinFormat::kFooterStartMagicLength;
153+
ICEBERG_ASSIGN_OR_RAISE(auto payload_bytes, ReadBytes(payload_offset, payload_size));
154+
ICEBERG_ASSIGN_OR_RAISE(auto decompressed,
155+
Decompress(footer_compression, payload_bytes));
156+
157+
// Parse JSON
158+
std::string_view json_str(reinterpret_cast<const char*>(decompressed.data()),
159+
decompressed.size());
160+
return FileMetadataFromJsonString(json_str);
161+
}
162+
163+
Result<std::pair<BlobMetadata, std::vector<std::byte>>> PuffinReader::ReadBlob(
164+
const BlobMetadata& blob_metadata) {
165+
if (blob_metadata.offset < 0 || blob_metadata.length < 0 ||
166+
blob_metadata.offset > file_size_ ||
167+
blob_metadata.length > file_size_ - blob_metadata.offset) {
168+
return Invalid("Invalid blob: offset {} + length {} exceeds file size {}",
169+
blob_metadata.offset, blob_metadata.length, file_size_);
170+
}
171+
172+
ICEBERG_ASSIGN_OR_RAISE(auto raw_data,
173+
ReadBytes(blob_metadata.offset, blob_metadata.length));
174+
175+
ICEBERG_ASSIGN_OR_RAISE(
176+
auto codec, PuffinCompressionCodecFromName(blob_metadata.compression_codec));
177+
ICEBERG_ASSIGN_OR_RAISE(auto decompressed, Decompress(codec, raw_data));
178+
179+
return std::pair{blob_metadata, std::move(decompressed)};
180+
}
181+
182+
Result<std::vector<std::pair<BlobMetadata, std::vector<std::byte>>>>
183+
PuffinReader::ReadAll(const std::vector<BlobMetadata>& blobs) {
184+
// Sort by offset for sequential I/O access pattern
185+
std::vector<const BlobMetadata*> sorted;
186+
sorted.reserve(blobs.size());
187+
for (const auto& blob : blobs) {
188+
sorted.push_back(&blob);
189+
}
190+
std::ranges::sort(sorted,
191+
[](const auto* a, const auto* b) { return a->offset < b->offset; });
192+
193+
std::vector<std::pair<BlobMetadata, std::vector<std::byte>>> results;
194+
results.reserve(blobs.size());
195+
for (const auto* blob : sorted) {
196+
ICEBERG_ASSIGN_OR_RAISE(auto blob_pair, ReadBlob(*blob));
197+
results.push_back(std::move(blob_pair));
198+
}
199+
return results;
200+
}
201+
202+
} // namespace iceberg::puffin

src/iceberg/puffin/puffin_reader.h

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
/// \file iceberg/puffin/puffin_reader.h
23+
/// Puffin file reader.
24+
25+
#include <cstddef>
26+
#include <cstdint>
27+
#include <memory>
28+
#include <optional>
29+
#include <utility>
30+
#include <vector>
31+
32+
#include "iceberg/iceberg_data_export.h"
33+
#include "iceberg/puffin/file_metadata.h"
34+
#include "iceberg/result.h"
35+
36+
namespace iceberg {
37+
class InputFile;
38+
class SeekableInputStream;
39+
} // namespace iceberg
40+
41+
namespace iceberg::puffin {
42+
43+
/// \brief Reader for Puffin files.
44+
///
45+
/// Reads from an InputFile with seek support for efficient blob access.
46+
class ICEBERG_DATA_EXPORT PuffinReader {
47+
public:
48+
/// \brief Create a PuffinReader for the given input file.
49+
/// \param input_file The input file to read from.
50+
/// \param footer_size Optional known footer size hint to avoid an extra seek.
51+
static Result<std::unique_ptr<PuffinReader>> Make(
52+
std::unique_ptr<InputFile> input_file,
53+
std::optional<int64_t> footer_size = std::nullopt);
54+
55+
~PuffinReader();
56+
57+
/// \brief Read and return the file metadata from the footer.
58+
Result<FileMetadata> ReadFileMetadata();
59+
60+
/// \brief Read a specific blob's data by its metadata.
61+
/// \param blob_metadata The metadata describing the blob to read.
62+
/// \return A pair of (BlobMetadata, decompressed data), or an error.
63+
Result<std::pair<BlobMetadata, std::vector<std::byte>>> ReadBlob(
64+
const BlobMetadata& blob_metadata);
65+
66+
/// \brief Read all blobs described in the file metadata.
67+
/// \return A vector of (BlobMetadata, decompressed data) pairs, or an error.
68+
Result<std::vector<std::pair<BlobMetadata, std::vector<std::byte>>>> ReadAll(
69+
const std::vector<BlobMetadata>& blobs);
70+
71+
private:
72+
PuffinReader(std::unique_ptr<SeekableInputStream> stream, int64_t file_size,
73+
std::optional<int64_t> known_footer_size);
74+
75+
/// Opened input stream.
76+
std::unique_ptr<SeekableInputStream> stream_;
77+
/// Total file size.
78+
int64_t file_size_;
79+
/// Known footer size hint (avoids one seek if provided).
80+
std::optional<int64_t> known_footer_size_;
81+
82+
Result<std::vector<std::byte>> ReadBytes(int64_t offset, int64_t length);
83+
};
84+
85+
} // namespace iceberg::puffin

0 commit comments

Comments
 (0)