Skip to content

Commit 49f8a87

Browse files
authored
fix(manifest): validate enum fields when parsing manifests (#1034)
Manifest and manifest list readers cast raw integers from the file straight into ManifestStatus, DataFile::Content and ManifestContent. Out-of-range values then reached switches ending in unreachable(), which is undefined behavior. Validate these fields through the *FromInt helpers at parse time, and return an error instead of unreachable() for an unknown status in ManifestWriter::WriteEntry. ParseDataFile's hand-rolled loop for DataFile::kFileFormatFieldId skipped the field when null instead of checking `required` and returning MakeNullError like every other field in the switch, addressing review feedback on PR #1034.
1 parent 1b1232b commit 49f8a87

5 files changed

Lines changed: 170 additions & 8 deletions

File tree

‎src/iceberg/manifest/manifest_list.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,19 @@ ICEBERG_EXPORT inline constexpr Result<ManifestContent> ManifestContentFromStrin
276276
return InvalidArgument("Invalid manifest content type: {}", str);
277277
}
278278

279+
/// \brief Get the relative manifest content type from int
280+
ICEBERG_EXPORT inline constexpr Result<ManifestContent> ManifestContentFromInt(
281+
int32_t content) noexcept {
282+
switch (content) {
283+
case 0:
284+
return ManifestContent::kData;
285+
case 1:
286+
return ManifestContent::kDeletes;
287+
default:
288+
return InvalidArgument("Invalid manifest content type: {}", content);
289+
}
290+
}
291+
279292
} // namespace iceberg
280293

281294
namespace std {

‎src/iceberg/manifest/manifest_reader.cc‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,31 @@ Status ParseIntegerField(const ArrowArrayView* array_view, Container& container,
133133
array_view, container, accessor, std::forward<Args>(args)...);
134134
}
135135

136+
// Validate integer-encoded enums so out-of-range values from the file never
137+
// become enum values that no switch handles.
138+
template <typename FromInt, typename Container, typename Accessor>
139+
requires std::ranges::forward_range<Container>
140+
Status ParseEnumField(FromInt from_int, const ArrowArrayView* array_view,
141+
Container& container, Accessor accessor,
142+
std::string_view field_name, bool required) {
143+
auto iter = std::ranges::begin(container);
144+
for (int64_t row_idx = 0; row_idx < array_view->length; row_idx++, iter++) {
145+
if (!ArrowArrayViewIsNull(array_view, row_idx)) {
146+
const auto raw =
147+
static_cast<int32_t>(ArrowArrayViewGetIntUnsafe(array_view, row_idx));
148+
auto value = std::invoke(from_int, raw);
149+
if (!value.has_value()) [[unlikely]] {
150+
return InvalidArgument("Field '{}' at row {}: {}", field_name, row_idx,
151+
value.error().message);
152+
}
153+
std::invoke(accessor, *iter) = std::move(value.value());
154+
} else if (required) {
155+
return MakeNullError(field_name, row_idx);
156+
}
157+
}
158+
return {};
159+
}
160+
136161
template <typename... Args>
137162
Status ParseStringField(Args&&... args) {
138163
return ParseField(
@@ -322,8 +347,9 @@ Result<std::vector<ManifestFile>> ParseManifestList(ArrowSchema* arrow_schema,
322347
field_name, required));
323348
break;
324349
case ManifestFile::kContentFieldId:
325-
ICEBERG_RETURN_UNEXPECTED(ParseIntegerField(
326-
field_view, manifest_files, &ManifestFile::content, field_name, required));
350+
ICEBERG_RETURN_UNEXPECTED(ParseEnumField(ManifestContentFromInt, field_view,
351+
manifest_files, &ManifestFile::content,
352+
field_name, required));
327353
break;
328354
case ManifestFile::kSequenceNumberFieldId:
329355
ICEBERG_RETURN_UNEXPECTED(ParseIntegerField(field_view, manifest_files,
@@ -465,9 +491,9 @@ Status ParseDataFile(const std::shared_ptr<StructType>& data_file_schema,
465491

466492
switch (field_id) {
467493
case DataFile::kContentFieldId:
468-
ICEBERG_RETURN_UNEXPECTED(ParseIntegerField(field_view, manifest_entries,
469-
proj_data_file(&DataFile::content),
470-
field_name, required));
494+
ICEBERG_RETURN_UNEXPECTED(
495+
ParseEnumField(DataFileContentFromInt, field_view, manifest_entries,
496+
proj_data_file(&DataFile::content), field_name, required));
471497
break;
472498
case DataFile::kFilePathFieldId:
473499
ICEBERG_RETURN_UNEXPECTED(ParseStringField(field_view, manifest_entries,
@@ -481,6 +507,8 @@ Status ParseDataFile(const std::shared_ptr<StructType>& data_file_schema,
481507
std::string_view path_str(value.data, value.size_bytes);
482508
ICEBERG_ASSIGN_OR_RAISE(manifest_entries[row_idx].data_file->file_format,
483509
FileFormatTypeFromString(path_str));
510+
} else if (required) {
511+
return MakeNullError(field_name, row_idx);
484512
}
485513
}
486514
break;
@@ -638,8 +666,9 @@ Result<std::vector<ManifestEntry>> ParseManifestEntry(
638666

639667
switch (field_id) {
640668
case ManifestEntry::kStatusFieldId:
641-
ICEBERG_RETURN_UNEXPECTED(ParseIntegerField(
642-
field_view, manifest_entries, &ManifestEntry::status, field_name, required));
669+
ICEBERG_RETURN_UNEXPECTED(ParseEnumField(ManifestStatusFromInt, field_view,
670+
manifest_entries, &ManifestEntry::status,
671+
field_name, required));
643672
break;
644673
case ManifestEntry::kSnapshotIdFieldId:
645674
ICEBERG_RETURN_UNEXPECTED(ParseIntegerField(field_view, manifest_entries,

‎src/iceberg/manifest/manifest_writer.cc‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ Status ManifestWriter::WriteEntry(const ManifestEntry& entry) {
163163
delete_rows_count_ += entry.data_file->record_count;
164164
break;
165165
default:
166-
std::unreachable();
166+
return InvalidArgument("Invalid manifest entry status: {}",
167+
static_cast<int32_t>(entry.status));
167168
}
168169

169170
if (entry.IsAlive() && entry.sequence_number.has_value()) {

‎src/iceberg/test/manifest_list_versions_test.cc‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,39 @@ TEST_F(TestManifestListVersions, TestManifestsWithoutRowStats) {
425425
EXPECT_FALSE(manifest.first_row_id.has_value());
426426
}
427427

428+
TEST_F(TestManifestListVersions, RejectsInvalidManifestContent) {
429+
auto schema = std::make_shared<Schema>(std::vector<SchemaField>{
430+
ManifestFile::kManifestPath,
431+
ManifestFile::kManifestLength,
432+
ManifestFile::kPartitionSpecId,
433+
ManifestFile::kContent,
434+
ManifestFile::kAddedSnapshotId,
435+
});
436+
437+
ArrowSchema arrow_c_schema;
438+
EXPECT_THAT(ToArrowSchema(*schema, &arrow_c_schema), IsOk());
439+
auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie();
440+
auto array = ::arrow::json::ArrayFromJSONString(
441+
arrow_schema, R"([["path/to/manifest.avro", 1024, 1, 5, 100]])")
442+
.ValueOrDie();
443+
ArrowArray arrow_array;
444+
EXPECT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
445+
446+
std::string manifest_list_path = CreateManifestListPath();
447+
ICEBERG_UNWRAP_OR_FAIL(
448+
auto writer, WriterFactoryRegistry::Open(
449+
FileFormatType::kAvro,
450+
{.path = manifest_list_path, .schema = schema, .io = file_io_}));
451+
EXPECT_THAT(writer->Write(&arrow_array), IsOk());
452+
EXPECT_THAT(writer->Close(), IsOk());
453+
454+
ICEBERG_UNWRAP_OR_FAIL(auto reader,
455+
ManifestListReader::Make(manifest_list_path, file_io_));
456+
auto files = reader->Files();
457+
EXPECT_THAT(files, IsError(ErrorKind::kInvalidArgument));
458+
EXPECT_THAT(files, HasErrorMessage("Invalid manifest content type: 5"));
459+
}
460+
428461
TEST_F(TestManifestListVersions, TestManifestsPartitionSummary) {
429462
auto first_summary_lower_bound = Literal::Int(10).Serialize().value();
430463
auto first_summary_upper_bound = Literal::Int(100).Serialize().value();

‎src/iceberg/test/manifest_reader_test.cc‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,21 @@
2626
#include <string>
2727
#include <vector>
2828

29+
#include <arrow/array.h>
30+
#include <arrow/c/bridge.h>
31+
#include <arrow/json/from_string.h>
2932
#include <gtest/gtest.h>
3033

3134
#include "iceberg/arrow/arrow_io_util.h"
3235
#include "iceberg/avro/avro_register.h"
3336
#include "iceberg/expression/expressions.h"
37+
#include "iceberg/file_writer.h"
3438
#include "iceberg/manifest/manifest_entry.h"
3539
#include "iceberg/manifest/manifest_list.h"
3640
#include "iceberg/manifest/manifest_writer.h"
3741
#include "iceberg/partition_spec.h"
3842
#include "iceberg/schema.h"
43+
#include "iceberg/schema_internal.h"
3944
#include "iceberg/test/matchers.h"
4045
#include "iceberg/transform.h"
4146
#include "iceberg/type.h"
@@ -156,6 +161,54 @@ class TestManifestReader : public testing::TestWithParam<int8_t> {
156161
return std::move(manifest_result.value());
157162
}
158163

164+
// Writes manifest entries given as JSON rows of
165+
// [status, snapshot_id, [content, file_path, file_format, [partition], record_count,
166+
// file_size_in_bytes]], bypassing ManifestWriter validation.
167+
ManifestFile WriteRawManifest(const std::string& json_rows) {
168+
auto partition_type = spec_->PartitionType(*schema_);
169+
EXPECT_THAT(partition_type, IsOk());
170+
auto data_file_type = std::make_shared<StructType>(std::vector<SchemaField>{
171+
DataFile::kContent, DataFile::kFilePath,
172+
// Written as optional so tests can bypass the writer's own non-null
173+
// enforcement and exercise the reader's required-field validation instead.
174+
SchemaField::MakeOptional(DataFile::kFileFormatFieldId, "file_format", string()),
175+
SchemaField::MakeRequired(DataFile::kPartitionFieldId, DataFile::kPartitionField,
176+
std::move(partition_type.value())),
177+
DataFile::kRecordCount, DataFile::kFileSize});
178+
auto entry_schema = std::make_shared<Schema>(std::vector<SchemaField>{
179+
ManifestEntry::kStatus, ManifestEntry::kSnapshotId,
180+
SchemaField::MakeRequired(ManifestEntry::kDataFileFieldId,
181+
ManifestEntry::kDataFileField, data_file_type)});
182+
183+
ArrowSchema arrow_c_schema;
184+
EXPECT_THAT(ToArrowSchema(*entry_schema, &arrow_c_schema), IsOk());
185+
auto arrow_schema = ::arrow::ImportType(&arrow_c_schema).ValueOrDie();
186+
auto array = ::arrow::json::ArrayFromJSONString(arrow_schema, json_rows).ValueOrDie();
187+
ArrowArray arrow_array;
188+
EXPECT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
189+
190+
const std::string manifest_path = MakeManifestPath();
191+
auto writer_result = WriterFactoryRegistry::Open(
192+
FileFormatType::kAvro,
193+
{.path = manifest_path, .schema = entry_schema, .io = file_io_});
194+
EXPECT_THAT(writer_result, IsOk());
195+
auto writer = std::move(writer_result.value());
196+
EXPECT_THAT(writer->Write(&arrow_array), IsOk());
197+
EXPECT_THAT(writer->Close(), IsOk());
198+
auto length = writer->length();
199+
EXPECT_THAT(length, IsOk());
200+
201+
return ManifestFile{
202+
.manifest_path = manifest_path,
203+
.manifest_length = length.value(),
204+
.partition_spec_id = spec_->spec_id(),
205+
.content = ManifestContent::kData,
206+
.sequence_number = 0,
207+
.min_sequence_number = 0,
208+
.added_snapshot_id = 1000L,
209+
};
210+
}
211+
159212
std::shared_ptr<FileIO> file_io_;
160213
std::shared_ptr<Schema> schema_;
161214
std::shared_ptr<PartitionSpec> spec_;
@@ -629,6 +682,39 @@ TEST_P(TestManifestReader, SelectDistinguishesEmptyAndWildcard) {
629682
EXPECT_EQ(all_entries.front().data_file->partition.values().front(), Literal::Int(3));
630683
}
631684

685+
TEST_P(TestManifestReader, RejectsInvalidDataFileContent) {
686+
auto manifest = WriteRawManifest(
687+
R"([[1, 1000, [3, "/path/to/data.parquet", "parquet", [0], 1, 10]]])");
688+
689+
ICEBERG_UNWRAP_OR_FAIL(auto reader,
690+
ManifestReader::Make(manifest, file_io_, schema_, spec_));
691+
auto entries = reader->Entries();
692+
EXPECT_THAT(entries, IsError(ErrorKind::kInvalidArgument));
693+
EXPECT_THAT(entries, HasErrorMessage("Invalid data file content: 3"));
694+
}
695+
696+
TEST_P(TestManifestReader, RejectsInvalidEntryStatus) {
697+
auto manifest = WriteRawManifest(
698+
R"([[7, 1000, [0, "/path/to/data.parquet", "parquet", [0], 1, 10]]])");
699+
700+
ICEBERG_UNWRAP_OR_FAIL(auto reader,
701+
ManifestReader::Make(manifest, file_io_, schema_, spec_));
702+
auto entries = reader->Entries();
703+
EXPECT_THAT(entries, IsError(ErrorKind::kInvalidArgument));
704+
EXPECT_THAT(entries, HasErrorMessage("Invalid manifest status: 7"));
705+
}
706+
707+
TEST_P(TestManifestReader, RejectsNullRequiredFileFormat) {
708+
auto manifest =
709+
WriteRawManifest(R"([[0, 1000, [0, "/path/to/data.parquet", null, [0], 1, 10]]])");
710+
711+
ICEBERG_UNWRAP_OR_FAIL(auto reader,
712+
ManifestReader::Make(manifest, file_io_, schema_, spec_));
713+
auto entries = reader->Entries();
714+
EXPECT_THAT(entries, IsError(ErrorKind::kInvalidArgument));
715+
EXPECT_THAT(entries, HasErrorMessage("Required field 'file_format' is null at row 0"));
716+
}
717+
632718
TEST(ManifestReaderStaticTest, TestShouldDropStats) {
633719
EXPECT_TRUE(ManifestReader::ShouldDropStats({}));
634720
EXPECT_FALSE(ManifestReader::ShouldDropStats({std::string(Schema::kAllColumns)}));

0 commit comments

Comments
 (0)