From 0d2ce42eeaf4c1fe9cb7e526f057bb4b7420d94e Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 30 Jul 2026 21:41:56 +0200 Subject: [PATCH 1/2] fix(manifest): reject unsupported format versions Signed-off-by: Minh Vu --- manifest.go | 18 ++++++++++++++++-- manifest_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/manifest.go b/manifest.go index 532660b7b..43f84f854 100644 --- a/manifest.go +++ b/manifest.go @@ -546,10 +546,10 @@ type hasFieldToIDMap interface { setFieldIDToDecimalScaleMap(map[int]int) } -// ManifestFile is the interface which covers both V1 and V2 manifest files. +// ManifestFile is the interface which covers manifest files for supported table versions. type ManifestFile interface { // Version returns the version number of this manifest file. - // It should be 1 or 2. + // It should be 1, 2, or 3. Version() int // FilePath is the location URI of this manifest file. FilePath() string @@ -744,6 +744,9 @@ func NewManifestReader(file ManifestFile, in io.Reader) (*ManifestReader, error) return nil, fmt.Errorf("manifest file's 'format-version' metadata is invalid: %w", err) } } + if err := validateManifestFormatVersion(formatVersion); err != nil { + return nil, err + } // The manifest's own metadata is authoritative for its version. A v2/v3 // manifest list may reference older manifests so a table can be upgraded // in place without rewriting history; only a manifest newer than the list @@ -1015,6 +1018,9 @@ func ReadManifestList(in io.Reader) ([]ManifestFile, error) { version = v } + if err := validateManifestFormatVersion(version); err != nil { + return nil, err + } if version == 1 { return manifestFileV1Reader, nil @@ -1033,6 +1039,14 @@ func ReadManifestList(in io.Reader) ([]ManifestFile, error) { return decodeManifests[*manifestFile](rd, version) } +func validateManifestFormatVersion(version int) error { + if version < 1 || version > 3 { + return fmt.Errorf("unsupported manifest format version: %d", version) + } + + return nil +} + type writerImpl interface { prepareEntry(*manifestEntry, int64) (ManifestEntry, error) } diff --git a/manifest_test.go b/manifest_test.go index ffb163699..002b5fd37 100644 --- a/manifest_test.go +++ b/manifest_test.go @@ -1026,6 +1026,23 @@ func (m *ManifestTestSuite) TestReadManifestListMissingFormatVersion() { m.Empty(files) // the file has no entries, just headers } +func (m *ManifestTestSuite) TestReadManifestListRejectsUnsupportedFormatVersion() { + for _, version := range []int{-1, 0, 4} { + m.Run(strconv.Itoa(version), func() { + fileSchema, err := internal.NewManifestFileSchema(2) + m.Require().NoError(err) + var buf bytes.Buffer + writer, err := ocf.NewWriter(&buf, fileSchema, + ocf.WithMetadata(map[string][]byte{"format-version": []byte(strconv.Itoa(version))})) + m.Require().NoError(err) + m.Require().NoError(writer.Close()) + + _, err = ReadManifestList(&buf) + m.ErrorContains(err, "unsupported manifest format version") + }) + } +} + // writeManifestNoFormatVersion writes a valid v1 manifest entry Avro file that // omits the "format-version" metadata key, simulating files produced by the Java // Iceberg library (format-version is optional for v1 per the Iceberg spec). @@ -1079,6 +1096,34 @@ func (m *ManifestTestSuite) TestNewManifestReaderMissingFormatVersion() { m.NoError(reader.Close()) } +func (m *ManifestTestSuite) TestNewManifestReaderRejectsUnsupportedFormatVersion() { + for _, version := range []int{-1, 0, 4} { + m.Run(strconv.Itoa(version), func() { + spec := NewPartitionSpec() + partitionSchema, err := partitionTypeToAvroSchema(spec.PartitionType(testSchema)) + m.Require().NoError(err) + entrySchema, err := internal.NewManifestEntrySchema(partitionSchema, 1) + m.Require().NoError(err) + schemaJSON, err := json.Marshal(testSchema) + m.Require().NoError(err) + var manifest bytes.Buffer + writer, err := ocf.NewWriter(&manifest, entrySchema, ocf.WithMetadata(map[string][]byte{ + "format-version": []byte(strconv.Itoa(version)), + "schema": schemaJSON, + "schema-id": []byte(strconv.Itoa(testSchema.ID)), + "partition-spec": []byte("[]"), + "partition-spec-id": []byte("0"), + "content": []byte("data"), + })) + m.Require().NoError(err) + m.Require().NoError(writer.Close()) + + _, err = NewManifestReader(&manifestFile{version: version}, &manifest) + m.ErrorContains(err, "unsupported manifest format version") + }) + } +} + func (m *ManifestTestSuite) TestV3DataManifestFirstRowIDInheritance() { // Build a v3 data manifest with two entries that have null first_row_id. partitionSpec := NewPartitionSpecID(1, From 199ba1f7fa9e3b5b88a67623596020c110419311 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 30 Jul 2026 22:16:46 +0200 Subject: [PATCH 2/2] docs(manifest): clarify supported manifest versions --- manifest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.go b/manifest.go index 43f84f854..d41c008b2 100644 --- a/manifest.go +++ b/manifest.go @@ -546,10 +546,10 @@ type hasFieldToIDMap interface { setFieldIDToDecimalScaleMap(map[int]int) } -// ManifestFile is the interface which covers manifest files for supported table versions. +// ManifestFile is the interface for version 1, 2, and 3 manifest files. type ManifestFile interface { // Version returns the version number of this manifest file. - // It should be 1, 2, or 3. + // It must be 1, 2, or 3. Version() int // FilePath is the location URI of this manifest file. FilePath() string