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

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


The following commit(s) were added to refs/heads/main by this push:
     new d48bd750ab3 NIFI-16427 Sanitize Protobuf custom options before message 
name resolution (#11800)
d48bd750ab3 is described below

commit d48bd750ab3b017eb2ae8ff6390760a9b16d21ae
Author: Alaksiej Ščarbaty <[email protected]>
AuthorDate: Mon Oct 12 19:50:19 2026 +0200

    NIFI-16427 Sanitize Protobuf custom options before message name resolution 
(#11800)
---
 .../services/protobuf/ProtobufSchemaCompiler.java  |   5 +-
 ...java => ProtobufSchemaDefinitionSanitizer.java} |  52 ++-
 .../services/protobuf/StandardProtobufReader.java  |  10 +-
 .../services/protobuf/StandardProtobufWriter.java  |   7 +-
 .../protobuf/TestProtobufSchemaCompiler.java       | 261 ---------------
 ... => TestProtobufSchemaDefinitionSanitizer.java} | 360 ++++++---------------
 ...stStandardProtobufWriterConfluentRoundTrip.java | 150 ++++++---
 7 files changed, 269 insertions(+), 576 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufSchemaCompiler.java
 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufSchemaCompiler.java
index 63423ce6d34..5db6af45a02 100644
--- 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufSchemaCompiler.java
+++ 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufSchemaCompiler.java
@@ -215,9 +215,8 @@ final class ProtobufSchemaCompiler {
             Files.createDirectories(parentDirectory);
         }
 
-        final String preparedSchemaText = 
ProtobufCustomOptionRemover.remove(schemaText, Location.get(relativePath), 
logger);
-        Files.write(schemaFile, 
preparedSchemaText.getBytes(StandardCharsets.UTF_8), CREATE, WRITE, 
TRUNCATE_EXISTING);
-        logger.debug("Successfully wrote schema to file: {} (string length: 
{})", schemaFile, preparedSchemaText.length());
+        Files.write(schemaFile, schemaText.getBytes(StandardCharsets.UTF_8), 
CREATE, WRITE, TRUNCATE_EXISTING);
+        logger.debug("Successfully wrote schema to file: {} (string length: 
{})", schemaFile, schemaText.length());
     }
 
     /**
diff --git 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufCustomOptionRemover.java
 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufSchemaDefinitionSanitizer.java
similarity index 81%
rename from 
nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufCustomOptionRemover.java
rename to 
nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufSchemaDefinitionSanitizer.java
index e147d604662..51733448595 100644
--- 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufCustomOptionRemover.java
+++ 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/ProtobufSchemaDefinitionSanitizer.java
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.services.protobuf;
 
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
 import com.squareup.wire.schema.Location;
 import com.squareup.wire.schema.internal.parser.EnumConstantElement;
 import com.squareup.wire.schema.internal.parser.EnumElement;
@@ -32,26 +34,58 @@ import com.squareup.wire.schema.internal.parser.RpcElement;
 import com.squareup.wire.schema.internal.parser.ServiceElement;
 import com.squareup.wire.schema.internal.parser.TypeElement;
 import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.schemaregistry.services.SchemaDefinition;
+import org.apache.nifi.schemaregistry.services.StandardSchemaDefinition;
+import org.apache.nifi.serialization.record.SchemaIdentifier;
 
+import java.time.Duration;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
-final class ProtobufCustomOptionRemover {
+final class ProtobufSchemaDefinitionSanitizer {
 
-    private ProtobufCustomOptionRemover() {
+    private static final Duration SANITIZED_SCHEMAS_CACHE_TTL = 
Duration.ofHours(1);
+    private static final int SANITIZED_SCHEMAS_CACHE_SIZE = 200;
+
+    private final Cache<SchemaIdentifier, SchemaDefinition> 
sanitizedSchemaCache;
+    private final ComponentLog logger;
+
+    ProtobufSchemaDefinitionSanitizer(final ComponentLog logger) {
+        this.logger = logger;
+        this.sanitizedSchemaCache = Caffeine.newBuilder()
+            .expireAfterAccess(SANITIZED_SCHEMAS_CACHE_TTL)
+            .maximumSize(SANITIZED_SCHEMAS_CACHE_SIZE)
+            .build();
     }
 
     /**
-     * Removes custom (parenthesized) Protocol Buffer options from schema text.
+     * Removes custom (parenthesized) Protocol Buffer options from a schema 
definition and its references.
      * Built-in options such as {@code packed} and {@code java_package} are 
retained.
-     * When the text cannot be parsed, the original string is returned.
      *
-     * @param schemaText source proto schema to remove custom options from
-     * @param location   source location used for parse diagnostics and 
regenerated comments
-     * @param logger     component logger
-     * @return schema text without custom options, or the original text when 
parsing fails or no custom options are present
+     * @param schemaDefinition original schema definition
+     * @return sanitized schema definition
      */
-    static String remove(final String schemaText, final Location location, 
final ComponentLog logger) {
+    SchemaDefinition sanitizeOrGetFromCache(final SchemaDefinition 
schemaDefinition) {
+        return sanitizedSchemaCache.get(schemaDefinition.getIdentifier(),
+            identifier -> sanitize(schemaDefinition, identifier.toString()));
+    }
+
+    private SchemaDefinition sanitize(final SchemaDefinition schemaDefinition, 
final String schemaLocation) {
+        final String sanitizedText = 
removeCustomOptions(schemaDefinition.getText(), Location.get(schemaLocation));
+
+        final Map<String, SchemaDefinition> references = 
schemaDefinition.getReferences();
+        final Map<String, SchemaDefinition> sanitizedReferences = 
HashMap.newHashMap(references.size());
+        for (final Map.Entry<String, SchemaDefinition> reference : 
references.entrySet()) {
+            final String referencePath = reference.getKey();
+            sanitizedReferences.put(referencePath, 
sanitize(reference.getValue(), referencePath));
+        }
+
+        return new StandardSchemaDefinition(schemaDefinition.getIdentifier(), 
sanitizedText, schemaDefinition.getSchemaType(), sanitizedReferences);
+    }
+
+    private String removeCustomOptions(final String schemaText, final Location 
location) {
         if (schemaText == null || schemaText.isBlank()) {
             return schemaText;
         }
diff --git 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufReader.java
 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufReader.java
index 225180f5de8..accad838042 100644
--- 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufReader.java
+++ 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufReader.java
@@ -119,6 +119,7 @@ public class StandardProtobufReader extends 
SchemaRegistryService implements Rec
     private static final String PROTO_EXTENSION = ".proto";
 
     private volatile ProtobufSchemaCompiler schemaCompiler;
+    private volatile ProtobufSchemaDefinitionSanitizer schemaSanitizer;
     private volatile MessageNameResolver messageNameResolver;
     private volatile SchemaReferenceReader schemaReferenceReader;
     private volatile SchemaRegistry schemaRegistry;
@@ -163,6 +164,7 @@ public class StandardProtobufReader extends 
SchemaRegistryService implements Rec
     protected void init(final ControllerServiceInitializationContext config) 
throws InitializationException {
         super.init(config);
         schemaCompiler = new ProtobufSchemaCompiler(getIdentifier(), 
getLogger());
+        schemaSanitizer = new ProtobufSchemaDefinitionSanitizer(getLogger());
     }
 
     @Override
@@ -250,13 +252,15 @@ public class StandardProtobufReader extends 
SchemaRegistryService implements Rec
 
     private Schema validateSchemaCompiles(final SchemaIdentifier 
schemaIdentifier, final String schemaTextValue) {
         final SchemaDefinition schemaDefinition = new 
StandardSchemaDefinition(schemaIdentifier, schemaTextValue, 
SchemaDefinition.SchemaType.PROTOBUF);
-        return schemaCompiler.compileOrGetFromCache(schemaDefinition);
+        final SchemaDefinition sanitizedSchemaDefinition = 
schemaSanitizer.sanitizeOrGetFromCache(schemaDefinition);
+        return schemaCompiler.compileOrGetFromCache(sanitizedSchemaDefinition);
     }
 
     private RecordReader createProtobufRecordReader(final Map<String, String> 
variables, final InputStream in, final SchemaDefinition schemaDefinition) 
throws IOException {
-        final Schema schema = 
schemaCompiler.compileOrGetFromCache(schemaDefinition);
+        final SchemaDefinition sanitizedSchemaDefinition = 
schemaSanitizer.sanitizeOrGetFromCache(schemaDefinition);
+        final Schema schema = 
schemaCompiler.compileOrGetFromCache(sanitizedSchemaDefinition);
         final ProtoSchemaParser schemaParser = new ProtoSchemaParser(schema);
-        final MessageName messageName = 
messageNameResolver.getMessageName(variables, schemaDefinition, in);
+        final MessageName messageName = 
messageNameResolver.getMessageName(variables, sanitizedSchemaDefinition, in);
         final RecordSchema recordSchema = 
schemaParser.createSchema(messageName.getFullyQualifiedName());
         return new ProtobufRecordReader(schema, 
messageName.getFullyQualifiedName(), in, recordSchema);
     }
diff --git 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufWriter.java
 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufWriter.java
index dc39351c6e5..7fe588d1e1c 100644
--- 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufWriter.java
+++ 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/main/java/org/apache/nifi/services/protobuf/StandardProtobufWriter.java
@@ -154,6 +154,7 @@ public class StandardProtobufWriter extends 
SchemaRegistryService implements Rec
     private static final String PROTO_EXTENSION = ".proto";
 
     private volatile ProtobufSchemaCompiler schemaCompiler;
+    private volatile ProtobufSchemaDefinitionSanitizer schemaSanitizer;
     private volatile MessageNameResolver messageNameResolver;
     private volatile SchemaReferenceWriter schemaReferenceWriter;
     private volatile MessageIndexWriter messageIndexWriter;
@@ -182,6 +183,7 @@ public class StandardProtobufWriter extends 
SchemaRegistryService implements Rec
     protected void init(final ControllerServiceInitializationContext config) 
throws InitializationException {
         super.init(config);
         schemaCompiler = new ProtobufSchemaCompiler(getIdentifier(), 
getLogger());
+        schemaSanitizer = new ProtobufSchemaDefinitionSanitizer(getLogger());
     }
 
     @Override
@@ -235,7 +237,7 @@ public class StandardProtobufWriter extends 
SchemaRegistryService implements Rec
     }
 
     private ProtobufWriteContext createWriteContext(final Map<String, String> 
variables, final RecordSchema suppliedSchema) throws SchemaNotFoundException, 
IOException {
-        final SchemaDefinition schemaDefinition = 
createSchemaDefinition(variables, suppliedSchema);
+        final SchemaDefinition schemaDefinition = 
schemaSanitizer.sanitizeOrGetFromCache(createSchemaDefinition(variables, 
suppliedSchema));
         final Schema schema = 
schemaCompiler.compileOrGetFromCache(schemaDefinition);
         final MessageName messageName = 
messageNameResolver.getMessageName(variables, schemaDefinition, 
InputStream.nullInputStream());
 
@@ -278,7 +280,8 @@ public class StandardProtobufWriter extends 
SchemaRegistryService implements Rec
                 .name(sha256Hex(schemaTextValue) + PROTO_EXTENSION)
                 .build();
             final SchemaDefinition schemaDefinition = new 
StandardSchemaDefinition(schemaIdentifier, schemaTextValue, 
SchemaDefinition.SchemaType.PROTOBUF);
-            compiledSchema = 
schemaCompiler.compileOrGetFromCache(schemaDefinition);
+            final SchemaDefinition sanitizedSchemaDefinition = 
schemaSanitizer.sanitizeOrGetFromCache(schemaDefinition);
+            compiledSchema = 
schemaCompiler.compileOrGetFromCache(sanitizedSchemaDefinition);
         } catch (final RuntimeException e) {
             // The compiler reports Wire schema errors as 
SchemaCompilationException and other failures as other
             // runtime exceptions, all of which indicate that the configured 
schema text cannot be used
diff --git 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaCompiler.java
 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaCompiler.java
index e73b3aca656..5b26a44d16b 100644
--- 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaCompiler.java
+++ 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaCompiler.java
@@ -16,40 +16,23 @@
  */
 package org.apache.nifi.services.protobuf;
 
-import com.squareup.wire.schema.EnumConstant;
-import com.squareup.wire.schema.EnumType;
-import com.squareup.wire.schema.Field;
 import com.squareup.wire.schema.MessageType;
-import com.squareup.wire.schema.OneOf;
-import com.squareup.wire.schema.Options;
 import com.squareup.wire.schema.ProtoFile;
-import com.squareup.wire.schema.Rpc;
 import com.squareup.wire.schema.Schema;
 import com.squareup.wire.schema.SchemaException;
-import com.squareup.wire.schema.Service;
-import com.squareup.wire.schema.internal.parser.OptionElement;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.schemaregistry.services.SchemaDefinition;
 import org.apache.nifi.schemaregistry.services.StandardSchemaDefinition;
 import org.apache.nifi.serialization.record.SchemaIdentifier;
 import org.apache.nifi.util.MockComponentLog;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.function.Executable;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 
-import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class TestProtobufSchemaCompiler {
 
@@ -57,15 +40,11 @@ class TestProtobufSchemaCompiler {
 
     private static final String PACKAGE = "example";
     private static final String SAMPLE_TYPE = PACKAGE + ".Sample";
-    private static final String WRAPPER_TYPE = PACKAGE + ".Wrapper";
-    private static final String WRAPPER_PAYLOAD_TYPE = WRAPPER_TYPE + 
".Payload";
 
     private static final String SAMPLE_PROTO = "sample.proto";
-    private static final String COMMON_PROTO = "common.proto";
 
     private static final String FIELD_VALUE = "value";
     private static final String FIELD_EXTRA = "extra";
-    private static final String ENUM_RED = "RED";
 
     // Confluent Schema Registry keys each reference by the import path used 
inside the .proto file, while the
     // identifier of the referenced schema carries the registry subject, which 
is not required to look like a file path
@@ -91,102 +70,6 @@ class TestProtobufSchemaCompiler {
 
     private final ProtobufSchemaCompiler compiler = new 
ProtobufSchemaCompiler("test", COMPONENT_LOG);
 
-    @Test
-    void testCompileSchemaWithCustomOptionWithoutImport() {
-        final String schemaText = """
-            syntax = "proto3";
-            package example;
-
-            message Sample {
-              optional int32 enabled = 1 [(confluent.field_meta) = {
-                params: [
-                  {
-                    key: "connect.type",
-                    value: "int16"
-                  }
-                ]
-              }];
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-
-        final Field enabled = schema.getField(SAMPLE_TYPE, "enabled");
-
-        assertNotNull(enabled);
-        assertNoParenthesizedOptions(enabled.getOptions());
-    }
-
-    @Test
-    void testCompileNestedSchemaWithRepeatedFieldMetaOptions() throws 
IOException {
-        final String schemaText = 
readTestResource("/org/apache/nifi/protobuf/test/nested_wrapper_with_field_meta.proto");
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-
-        final Field flagA = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_a");
-        final Field flagB = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_b");
-        final Field flagC = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_c");
-
-        assertNotNull(schema.getType(WRAPPER_TYPE));
-        assertNotNull(schema.getType(WRAPPER_TYPE + ".Origin"));
-        assertNotNull(schema.getType(WRAPPER_TYPE + ".chunk"));
-        assertNotNull(flagA);
-        assertNotNull(flagB);
-        assertNotNull(flagC);
-        assertNoParenthesizedOptions(flagA.getOptions());
-        assertNoParenthesizedOptions(flagB.getOptions());
-        assertNoParenthesizedOptions(flagC.getOptions());
-    }
-
-    @Test
-    void testCompileKeepsPackedWhenCustomFieldOptionPresent() {
-        final String schemaText = """
-            syntax = "proto2";
-            package example;
-
-            message Sample {
-              repeated int32 values = 1 [packed = true, (confluent.field_meta) 
= {
-                tags: "x"
-              }];
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-        final Field values = schema.getField(SAMPLE_TYPE, "values");
-
-        assertNotNull(values);
-        assertTrue(values.isRepeated());
-        assertTrue(values.isPacked());
-        assertNoParenthesizedOptions(values.getOptions());
-    }
-
-    @Test
-    void testCompileKeepsJavaPackageWhenCustomFileOptionPresent() {
-        final String schemaText = """
-            syntax = "proto3";
-            package example;
-
-            option java_package = "com.example";
-            option (custom.file_meta) = "docs";
-
-            message Sample {
-              int32 value = 1;
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-        final ProtoFile protoFile = schema.protoFile(SAMPLE_PROTO);
-
-        assertNotNull(schema.getType(SAMPLE_TYPE));
-        assertNotNull(protoFile);
-        assertEquals("com.example", protoFile.javaPackage());
-        assertNoParenthesizedOptions(protoFile.getOptions());
-    }
-
     @Test
     void testCompileSchemaWithoutCustomOptions() {
         final String schemaText = """
@@ -204,40 +87,6 @@ class TestProtobufSchemaCompiler {
         assertNotNull(schema.getField(SAMPLE_TYPE, FIELD_VALUE));
     }
 
-    @Test
-    void testCompileReferencedSchemaWithCustomOptions() {
-        final String referencedSchemaText = """
-            syntax = "proto3";
-            package example;
-
-            message Common {
-              int32 id = 1 [(custom.field_meta) = "c"];
-            }
-            """;
-        final String rootSchemaText = """
-            syntax = "proto3";
-            package example;
-
-            import "common.proto";
-
-            message Root {
-              Common item = 1 [(custom.field_meta) = "r"];
-            }
-            """;
-        final SchemaDefinition referencedSchema = 
schemaDefinition(COMMON_PROTO, referencedSchemaText);
-        final SchemaDefinition rootSchema = schemaDefinition(SAMPLE_PROTO, 
rootSchemaText, Map.of(COMMON_PROTO, referencedSchema));
-
-        final Schema schema = compiler.compileOrGetFromCache(rootSchema);
-
-        final Field id = schema.getField(PACKAGE + ".Common", "id");
-        final Field item = schema.getField(PACKAGE + ".Root", "item");
-
-        assertNotNull(id);
-        assertNoParenthesizedOptions(id.getOptions());
-        assertNotNull(item);
-        assertNoParenthesizedOptions(item.getOptions());
-    }
-
     @Test
     void testCompileFailsWhenMessageTypeIsUnresolved() {
         final String schemaText = """
@@ -274,43 +123,6 @@ class TestProtobufSchemaCompiler {
         assertInstanceOf(SchemaException.class, exception.getCause());
     }
 
-    @Test
-    void testCompileNestedMessageAndEnumWithCustomOptions() {
-        final String schemaText = """
-            syntax = "proto3";
-            package example;
-
-            message Outer {
-              option (custom.message_meta) = true;
-
-              message Inner {
-                int32 value = 1 [(custom.field_meta) = "y"];
-              }
-
-              Inner inner = 1;
-            }
-
-            enum Color {
-              option (custom.enum_meta) = true;
-              RED = 0 [(custom.enum_value_meta) = "r"];
-              GREEN = 1;
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-        final MessageType outer = assertInstanceOf(MessageType.class, 
schema.getType(PACKAGE + ".Outer"));
-        final EnumType color = assertInstanceOf(EnumType.class, 
schema.getType(PACKAGE + ".Color"));
-        final EnumConstant red = color.constant(ENUM_RED);
-
-        assertNotNull(outer.field("inner"));
-        assertNotNull(schema.getField(PACKAGE + ".Outer.Inner", FIELD_VALUE));
-        assertNotNull(red);
-        assertNotNull(color.constant("GREEN"));
-        assertNoParenthesizedOptions(color.getOptions());
-        assertNoParenthesizedOptions(red.getOptions());
-    }
-
     @Test
     void testPayloadExtendDeclarationsArePreserved() {
         final String schemaText = """
@@ -338,62 +150,6 @@ class TestProtobufSchemaCompiler {
         assertEquals(FIELD_EXTRA, 
protoFile.getExtendList().get(0).getFields().get(0).getName());
     }
 
-    @Test
-    void testCompileRemovesCustomOptionsFromAllSyntaxElements() throws 
IOException {
-        final String schemaText = 
readTestResource("/org/apache/nifi/protobuf/test/custom_options_on_all_elements.proto");
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-
-        final ProtoFile protoFile = schema.protoFile(SAMPLE_PROTO);
-        assertNotNull(protoFile);
-        assertEquals("com.example.comprehensive", protoFile.javaPackage());
-        assertNoParenthesizedOptions(protoFile.getOptions());
-
-        final MessageType wrapper = assertInstanceOf(MessageType.class, 
schema.getType(WRAPPER_TYPE));
-        assertNoParenthesizedOptions(wrapper.getOptions());
-
-        final Field flag = schema.getField(WRAPPER_TYPE, "flag");
-        assertNotNull(flag);
-        assertNoParenthesizedOptions(flag.getOptions());
-
-        final OneOf kind = wrapper.oneOf("kind");
-        assertNotNull(kind);
-        assertNoParenthesizedOptions(kind.getOptions());
-
-        final Field text = schema.getField(WRAPPER_TYPE, "text");
-        assertNotNull(text);
-        assertNoParenthesizedOptions(text.getOptions());
-
-        assertEquals(1, protoFile.getExtendList().size());
-        final Field extra = 
protoFile.getExtendList().get(0).getFields().get(0);
-        assertEquals(FIELD_EXTRA, extra.getName());
-        assertNoParenthesizedOptions(extra.getOptions());
-
-        assertNotNull(schema.getType(WRAPPER_TYPE + ".Nested"));
-        assertEquals(1, wrapper.getNestedExtendList().size());
-        final Field nestedExtra = 
wrapper.getNestedExtendList().get(0).getFields().get(0);
-        assertEquals("nested_extra", nestedExtra.getName());
-        assertNoParenthesizedOptions(nestedExtra.getOptions());
-
-        final EnumType color = assertInstanceOf(EnumType.class, 
schema.getType(WRAPPER_TYPE + ".Color"));
-        assertNoParenthesizedOptions(color.getOptions());
-
-        final EnumConstant red = color.constant(ENUM_RED);
-        assertNotNull(red);
-        assertNoParenthesizedOptions(red.getOptions());
-
-        final Service catalog = schema.getService(PACKAGE + ".Catalog");
-        assertNotNull(catalog);
-        assertNoParenthesizedOptions(catalog.options());
-
-        final Rpc lookup = catalog.rpc("Lookup");
-        assertNotNull(lookup);
-        assertEquals(PACKAGE + ".Request", lookup.getRequestType().toString());
-        assertEquals(PACKAGE + ".Response", 
lookup.getResponseType().toString());
-        assertNoParenthesizedOptions(lookup.getOptions());
-    }
-
     @Test
     void testCompileSchemaWithCrossFileImportFromRegistry() {
         final SchemaDefinition referencedSchema = new StandardSchemaDefinition(
@@ -451,23 +207,6 @@ class TestProtobufSchemaCompiler {
         assertNotNull(compiled.getType("airlines.ph.cdm.common.Audit"));
     }
 
-    private void assertNoParenthesizedOptions(final Options options) {
-        final List<OptionElement> elements = options.getElements();
-        final List<Executable> assertions = new ArrayList<>(elements.size());
-        for (final OptionElement option : elements) {
-            assertions.add(() -> assertFalse(option.isParenthesized()));
-        }
-
-        assertAll(assertions);
-    }
-
-    private String readTestResource(final String resourcePath) throws 
IOException {
-        try (InputStream inputStream = 
getClass().getResourceAsStream(resourcePath)) {
-            assertNotNull(inputStream, "Test resource not found: " + 
resourcePath);
-            return new String(inputStream.readAllBytes(), 
StandardCharsets.UTF_8);
-        }
-    }
-
     private SchemaDefinition schemaDefinition(final String name, final String 
schemaText) {
         return schemaDefinition(name, schemaText, Map.of());
     }
diff --git 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaCompiler.java
 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaDefinitionSanitizer.java
similarity index 51%
copy from 
nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaCompiler.java
copy to 
nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaDefinitionSanitizer.java
index e73b3aca656..7cbdb87f8e5 100644
--- 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaCompiler.java
+++ 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestProtobufSchemaDefinitionSanitizer.java
@@ -25,8 +25,8 @@ import com.squareup.wire.schema.Options;
 import com.squareup.wire.schema.ProtoFile;
 import com.squareup.wire.schema.Rpc;
 import com.squareup.wire.schema.Schema;
-import com.squareup.wire.schema.SchemaException;
 import com.squareup.wire.schema.Service;
+import com.squareup.wire.schema.Type;
 import com.squareup.wire.schema.internal.parser.OptionElement;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.schemaregistry.services.SchemaDefinition;
@@ -48,12 +48,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-class TestProtobufSchemaCompiler {
+class TestProtobufSchemaDefinitionSanitizer {
 
-    private static final ComponentLog COMPONENT_LOG = new 
MockComponentLog("test", TestProtobufSchemaCompiler.class);
+    private static final ComponentLog COMPONENT_LOG = new 
MockComponentLog("test", TestProtobufSchemaDefinitionSanitizer.class);
 
     private static final String PACKAGE = "example";
     private static final String SAMPLE_TYPE = PACKAGE + ".Sample";
@@ -63,149 +63,13 @@ class TestProtobufSchemaCompiler {
     private static final String SAMPLE_PROTO = "sample.proto";
     private static final String COMMON_PROTO = "common.proto";
 
-    private static final String FIELD_VALUE = "value";
-    private static final String FIELD_EXTRA = "extra";
     private static final String ENUM_RED = "RED";
 
-    // Confluent Schema Registry keys each reference by the import path used 
inside the .proto file, while the
-    // identifier of the referenced schema carries the registry subject, which 
is not required to look like a file path
-    private static final String ROOT_SUBJECT = 
"airlines.ph.cdm.reservation.AirlineReservation";
-    private static final String IMPORT_PATH = "airlines/ph/cdm/shared.proto";
-    private static final String REFERENCE_SUBJECT = "airlines.ph.cdm.shared";
-
-    private static final String ROOT_SCHEMA = """
-        syntax = "proto3";
-        package airlines.ph.cdm.reservation;
-        import "airlines/ph/cdm/shared.proto";
-        message AirlineReservation {
-          string reservation_id = 1;
-          airlines.ph.cdm.Status status = 2;
-        }""";
-
-    private static final String REFERENCED_SCHEMA = """
-        syntax = "proto3";
-        package airlines.ph.cdm;
-        message Status {
-          string code = 1;
-        }""";
-
+    private final ProtobufSchemaDefinitionSanitizer sanitizer = new 
ProtobufSchemaDefinitionSanitizer(COMPONENT_LOG);
     private final ProtobufSchemaCompiler compiler = new 
ProtobufSchemaCompiler("test", COMPONENT_LOG);
 
     @Test
-    void testCompileSchemaWithCustomOptionWithoutImport() {
-        final String schemaText = """
-            syntax = "proto3";
-            package example;
-
-            message Sample {
-              optional int32 enabled = 1 [(confluent.field_meta) = {
-                params: [
-                  {
-                    key: "connect.type",
-                    value: "int16"
-                  }
-                ]
-              }];
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-
-        final Field enabled = schema.getField(SAMPLE_TYPE, "enabled");
-
-        assertNotNull(enabled);
-        assertNoParenthesizedOptions(enabled.getOptions());
-    }
-
-    @Test
-    void testCompileNestedSchemaWithRepeatedFieldMetaOptions() throws 
IOException {
-        final String schemaText = 
readTestResource("/org/apache/nifi/protobuf/test/nested_wrapper_with_field_meta.proto");
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-
-        final Field flagA = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_a");
-        final Field flagB = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_b");
-        final Field flagC = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_c");
-
-        assertNotNull(schema.getType(WRAPPER_TYPE));
-        assertNotNull(schema.getType(WRAPPER_TYPE + ".Origin"));
-        assertNotNull(schema.getType(WRAPPER_TYPE + ".chunk"));
-        assertNotNull(flagA);
-        assertNotNull(flagB);
-        assertNotNull(flagC);
-        assertNoParenthesizedOptions(flagA.getOptions());
-        assertNoParenthesizedOptions(flagB.getOptions());
-        assertNoParenthesizedOptions(flagC.getOptions());
-    }
-
-    @Test
-    void testCompileKeepsPackedWhenCustomFieldOptionPresent() {
-        final String schemaText = """
-            syntax = "proto2";
-            package example;
-
-            message Sample {
-              repeated int32 values = 1 [packed = true, (confluent.field_meta) 
= {
-                tags: "x"
-              }];
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-        final Field values = schema.getField(SAMPLE_TYPE, "values");
-
-        assertNotNull(values);
-        assertTrue(values.isRepeated());
-        assertTrue(values.isPacked());
-        assertNoParenthesizedOptions(values.getOptions());
-    }
-
-    @Test
-    void testCompileKeepsJavaPackageWhenCustomFileOptionPresent() {
-        final String schemaText = """
-            syntax = "proto3";
-            package example;
-
-            option java_package = "com.example";
-            option (custom.file_meta) = "docs";
-
-            message Sample {
-              int32 value = 1;
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-        final ProtoFile protoFile = schema.protoFile(SAMPLE_PROTO);
-
-        assertNotNull(schema.getType(SAMPLE_TYPE));
-        assertNotNull(protoFile);
-        assertEquals("com.example", protoFile.javaPackage());
-        assertNoParenthesizedOptions(protoFile.getOptions());
-    }
-
-    @Test
-    void testCompileSchemaWithoutCustomOptions() {
-        final String schemaText = """
-            syntax = "proto3";
-            package example;
-
-            message Sample {
-              int32 value = 1;
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
-
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-
-        assertNotNull(schema.getField(SAMPLE_TYPE, FIELD_VALUE));
-    }
-
-    @Test
-    void testCompileReferencedSchemaWithCustomOptions() {
+    void testSanitizeRemovesCustomOptionsFromSchemaAndReferences() {
         final String referencedSchemaText = """
             syntax = "proto3";
             package example;
@@ -221,14 +85,22 @@ class TestProtobufSchemaCompiler {
             import "common.proto";
 
             message Root {
-              Common item = 1 [(custom.field_meta) = "r"];
+              Common item = 1 [(confluent.field_meta) = { params: [ { key: 
"k", value: "v" } ] }];
             }
             """;
         final SchemaDefinition referencedSchema = 
schemaDefinition(COMMON_PROTO, referencedSchemaText);
         final SchemaDefinition rootSchema = schemaDefinition(SAMPLE_PROTO, 
rootSchemaText, Map.of(COMMON_PROTO, referencedSchema));
 
-        final Schema schema = compiler.compileOrGetFromCache(rootSchema);
+        final SchemaDefinition sanitized = 
sanitizer.sanitizeOrGetFromCache(rootSchema);
+
+        assertEquals(rootSchema.getIdentifier(), sanitized.getIdentifier());
+        assertEquals(rootSchema.getSchemaType(), sanitized.getSchemaType());
+        final SchemaDefinition sanitizedReference = 
sanitized.getReferences().get(COMMON_PROTO);
+        assertNotNull(sanitizedReference);
+        assertEquals(referencedSchema.getIdentifier(), 
sanitizedReference.getIdentifier());
+        assertEquals(referencedSchema.getSchemaType(), 
sanitizedReference.getSchemaType());
 
+        final Schema schema = compiler.compileOrGetFromCache(sanitized);
         final Field id = schema.getField(PACKAGE + ".Common", "id");
         final Field item = schema.getField(PACKAGE + ".Root", "item");
 
@@ -239,111 +111,110 @@ class TestProtobufSchemaCompiler {
     }
 
     @Test
-    void testCompileFailsWhenMessageTypeIsUnresolved() {
+    void testSanitizeKeepsSchemaTextWithoutCustomOptions() {
         final String schemaText = """
             syntax = "proto3";
             package example;
 
-            message Root {
-              Common item = 1;
+            message Sample {
+              int32 value = 1;
             }
             """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
 
-        final RuntimeException exception = 
assertThrows(RuntimeException.class, () -> 
compiler.compileOrGetFromCache(schemaDefinition));
+        final SchemaDefinition sanitized = 
sanitizer.sanitizeOrGetFromCache(schemaDefinition(SAMPLE_PROTO, schemaText));
 
-        assertInstanceOf(SchemaException.class, exception.getCause());
+        assertEquals(schemaText, sanitized.getText());
     }
 
     @Test
-    void testCompileFailsWhenTypeImportIsMissing() {
+    void testSanitizeReturnsCachedDefinitionForSameIdentifier() {
         final String schemaText = """
             syntax = "proto3";
             package example;
 
-            import "common.proto";
-
-            message Root {
-              Common item = 1;
+            message Sample {
+              int32 value = 1 [(custom.field_meta) = "v"];
             }
             """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
 
-        final RuntimeException exception = 
assertThrows(RuntimeException.class, () -> 
compiler.compileOrGetFromCache(schemaDefinition));
+        final SchemaDefinition first = 
sanitizer.sanitizeOrGetFromCache(schemaDefinition(SAMPLE_PROTO, schemaText));
+        final SchemaDefinition second = 
sanitizer.sanitizeOrGetFromCache(schemaDefinition(SAMPLE_PROTO, schemaText));
 
-        assertInstanceOf(SchemaException.class, exception.getCause());
+        assertSame(first, second);
     }
 
     @Test
-    void testCompileNestedMessageAndEnumWithCustomOptions() {
+    void testSanitizeSchemaWithCustomOptionWithoutImport() {
         final String schemaText = """
             syntax = "proto3";
             package example;
 
-            message Outer {
-              option (custom.message_meta) = true;
+            message Sample {
+              optional int32 enabled = 1 [(confluent.field_meta) = {
+                params: [
+                  {
+                    key: "connect.type",
+                    value: "int16"
+                  }
+                ]
+              }];
+            }
+            """;
 
-              message Inner {
-                int32 value = 1 [(custom.field_meta) = "y"];
-              }
+        final Schema schema = sanitizeAndCompile(schemaText);
+        final Field enabled = schema.getField(SAMPLE_TYPE, "enabled");
 
-              Inner inner = 1;
-            }
+        assertNotNull(enabled);
+        assertEquals(1, enabled.getTag());
+        assertNoParenthesizedOptions(enabled.getOptions());
+    }
 
-            enum Color {
-              option (custom.enum_meta) = true;
-              RED = 0 [(custom.enum_value_meta) = "r"];
-              GREEN = 1;
-            }
-            """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
+    @Test
+    void testSanitizeNestedSchemaWithRepeatedFieldMetaOptions() throws 
IOException {
+        final String schemaText = 
readTestResource("/org/apache/nifi/protobuf/test/nested_wrapper_with_field_meta.proto");
 
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-        final MessageType outer = assertInstanceOf(MessageType.class, 
schema.getType(PACKAGE + ".Outer"));
-        final EnumType color = assertInstanceOf(EnumType.class, 
schema.getType(PACKAGE + ".Color"));
-        final EnumConstant red = color.constant(ENUM_RED);
+        final Schema schema = sanitizeAndCompile(schemaText);
+        final Field flagA = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_a");
+        final Field flagB = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_b");
+        final Field flagC = schema.getField(WRAPPER_PAYLOAD_TYPE, "flag_c");
 
-        assertNotNull(outer.field("inner"));
-        assertNotNull(schema.getField(PACKAGE + ".Outer.Inner", FIELD_VALUE));
-        assertNotNull(red);
-        assertNotNull(color.constant("GREEN"));
-        assertNoParenthesizedOptions(color.getOptions());
-        assertNoParenthesizedOptions(red.getOptions());
+        assertNotNull(schema.getType(WRAPPER_TYPE + ".Origin"));
+        assertNotNull(schema.getType(WRAPPER_TYPE + ".chunk"));
+        assertNotNull(flagA);
+        assertNotNull(flagB);
+        assertNotNull(flagC);
+        assertNoParenthesizedOptions(flagA.getOptions());
+        assertNoParenthesizedOptions(flagB.getOptions());
+        assertNoParenthesizedOptions(flagC.getOptions());
     }
 
     @Test
-    void testPayloadExtendDeclarationsArePreserved() {
+    void testSanitizeKeepsPackedWhenCustomFieldOptionPresent() {
         final String schemaText = """
             syntax = "proto2";
             package example;
 
-            message Base {
-              extensions 100 to 199;
-              optional string name = 1;
-            }
-
-            extend Base {
-              optional int32 extra = 100;
+            message Sample {
+              repeated int32 values = 1 [packed = true, (confluent.field_meta) 
= {
+                tags: "x"
+              }];
             }
             """;
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
 
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
-        final MessageType base = assertInstanceOf(MessageType.class, 
schema.getType(PACKAGE + ".Base"));
-        final ProtoFile protoFile = schema.protoFile(SAMPLE_PROTO);
+        final Schema schema = sanitizeAndCompile(schemaText);
+        final Field values = schema.getField(SAMPLE_TYPE, "values");
 
-        assertNotNull(base.field("name"));
-        assertNotNull(protoFile);
-        assertEquals(1, protoFile.getExtendList().size());
-        assertEquals(FIELD_EXTRA, 
protoFile.getExtendList().get(0).getFields().get(0).getName());
+        assertNotNull(values);
+        assertTrue(values.isRepeated());
+        assertTrue(values.isPacked());
+        assertNoParenthesizedOptions(values.getOptions());
     }
 
     @Test
-    void testCompileRemovesCustomOptionsFromAllSyntaxElements() throws 
IOException {
+    void testSanitizeRemovesCustomOptionsFromAllSyntaxElements() throws 
IOException {
         final String schemaText = 
readTestResource("/org/apache/nifi/protobuf/test/custom_options_on_all_elements.proto");
-        final SchemaDefinition schemaDefinition = 
schemaDefinition(SAMPLE_PROTO, schemaText);
 
-        final Schema schema = compiler.compileOrGetFromCache(schemaDefinition);
+        final Schema schema = sanitizeAndCompile(schemaText);
 
         final ProtoFile protoFile = schema.protoFile(SAMPLE_PROTO);
         assertNotNull(protoFile);
@@ -367,7 +238,7 @@ class TestProtobufSchemaCompiler {
 
         assertEquals(1, protoFile.getExtendList().size());
         final Field extra = 
protoFile.getExtendList().get(0).getFields().get(0);
-        assertEquals(FIELD_EXTRA, extra.getName());
+        assertEquals("extra", extra.getName());
         assertNoParenthesizedOptions(extra.getOptions());
 
         assertNotNull(schema.getType(WRAPPER_TYPE + ".Nested"));
@@ -395,60 +266,33 @@ class TestProtobufSchemaCompiler {
     }
 
     @Test
-    void testCompileSchemaWithCrossFileImportFromRegistry() {
-        final SchemaDefinition referencedSchema = new StandardSchemaDefinition(
-            
SchemaIdentifier.builder().name(REFERENCE_SUBJECT).id(4L).version(1).build(),
-            REFERENCED_SCHEMA,
-            SchemaDefinition.SchemaType.PROTOBUF);
-
-        final SchemaDefinition rootSchema = new StandardSchemaDefinition(
-            
SchemaIdentifier.builder().name(ROOT_SUBJECT).id(3L).version(2).build(),
-            ROOT_SCHEMA,
-            SchemaDefinition.SchemaType.PROTOBUF,
-            Map.of(IMPORT_PATH, referencedSchema));
-
-        final Schema compiled = compiler.compileOrGetFromCache(rootSchema);
-
-        
assertNotNull(compiled.getType("airlines.ph.cdm.reservation.AirlineReservation"));
-        assertNotNull(compiled.getType("airlines.ph.cdm.Status"));
+    void testSanitizePreservesTopLevelMessageOrder() {
+        final String schemaText = """
+            syntax = "proto3";
+            package example;
+
+            message A {
+              int32 value = 1 [(custom.field_meta) = "a"];
+            }
+
+            message B {
+              int32 value = 1 [(custom.field_meta) = "b"];
+            }
+
+            message C {
+              int32 value = 1 [(custom.field_meta) = "c"];
+            }
+            """;
+
+        final Schema schema = sanitizeAndCompile(schemaText);
+        final ProtoFile protoFile = schema.protoFile(SAMPLE_PROTO);
+
+        assertNotNull(protoFile);
+        assertEquals(List.of("A", "B", "C"), 
protoFile.getTypes().stream().map(Type::getName).toList());
     }
 
-    @Test
-    void testCompileSchemaWithNestedCrossFileImports() {
-        final SchemaDefinition leafSchema = new StandardSchemaDefinition(
-            
SchemaIdentifier.builder().name("airlines.ph.cdm.common").id(5L).version(1).build(),
-            """
-                syntax = "proto3";
-                package airlines.ph.cdm.common;
-                message Audit {
-                  string created_by = 1;
-                }""",
-            SchemaDefinition.SchemaType.PROTOBUF);
-
-        final SchemaDefinition referencedSchema = new StandardSchemaDefinition(
-            
SchemaIdentifier.builder().name(REFERENCE_SUBJECT).id(4L).version(1).build(),
-            """
-                syntax = "proto3";
-                package airlines.ph.cdm;
-                import "airlines/ph/cdm/common/audit.proto";
-                message Status {
-                  string code = 1;
-                  airlines.ph.cdm.common.Audit audit = 2;
-                }""",
-            SchemaDefinition.SchemaType.PROTOBUF,
-            Map.of("airlines/ph/cdm/common/audit.proto", leafSchema));
-
-        final SchemaDefinition rootSchema = new StandardSchemaDefinition(
-            
SchemaIdentifier.builder().name(ROOT_SUBJECT).id(3L).version(2).build(),
-            ROOT_SCHEMA,
-            SchemaDefinition.SchemaType.PROTOBUF,
-            Map.of(IMPORT_PATH, referencedSchema));
-
-        final Schema compiled = compiler.compileOrGetFromCache(rootSchema);
-
-        
assertNotNull(compiled.getType("airlines.ph.cdm.reservation.AirlineReservation"));
-        assertNotNull(compiled.getType("airlines.ph.cdm.Status"));
-        assertNotNull(compiled.getType("airlines.ph.cdm.common.Audit"));
+    private Schema sanitizeAndCompile(final String schemaText) {
+        return 
compiler.compileOrGetFromCache(sanitizer.sanitizeOrGetFromCache(schemaDefinition(SAMPLE_PROTO,
 schemaText)));
     }
 
     private void assertNoParenthesizedOptions(final Options options) {
@@ -473,10 +317,6 @@ class TestProtobufSchemaCompiler {
     }
 
     private SchemaDefinition schemaDefinition(final String name, final String 
schemaText, final Map<String, SchemaDefinition> references) {
-        return new StandardSchemaDefinition(
-            SchemaIdentifier.builder().name(name).build(),
-            schemaText,
-            SchemaDefinition.SchemaType.PROTOBUF,
-            references);
+        return new 
StandardSchemaDefinition(SchemaIdentifier.builder().name(name).build(), 
schemaText, SchemaDefinition.SchemaType.PROTOBUF, references);
     }
 }
diff --git 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestStandardProtobufWriterConfluentRoundTrip.java
 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestStandardProtobufWriterConfluentRoundTrip.java
index dd2d5d6b13e..92a2747690f 100644
--- 
a/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestStandardProtobufWriterConfluentRoundTrip.java
+++ 
b/nifi-extension-bundles/nifi-protobuf-bundle/nifi-protobuf-services/src/test/java/org/apache/nifi/services/protobuf/TestStandardProtobufWriterConfluentRoundTrip.java
@@ -73,9 +73,16 @@ import static org.junit.jupiter.api.Assertions.assertNull;
  */
 class TestStandardProtobufWriterConfluentRoundTrip {
 
-    private static final int SCHEMA_ID = 42;
+    private static final long SCHEMA_ID = 42;
     // Confluent header: magic byte 0x00 followed by the big-endian schema id 
(42).
     private static final byte[] EXPECTED_HEADER = {0x00, 0x00, 0x00, 0x00, 
0x2A};
+    private static final int HEADER_LENGTH = EXPECTED_HEADER.length;
+
+    private static final String REGISTRY_ID = "registry";
+    private static final String REFERENCE_WRITER_ID = "referenceWriter";
+    private static final String INDEX_WRITER_ID = "indexWriter";
+    private static final String REFERENCE_READER_ID = "referenceReader";
+    private static final String MESSAGE_NAME_RESOLVER_ID = 
"messageNameResolver";
 
     private static final String SCHEMA_TEXT_VALUE = """
         syntax = "proto3";
@@ -90,6 +97,44 @@ class TestStandardProtobufWriterConfluentRoundTrip {
           string name = 1;
         }""";
 
+    private static final String CUSTOM_OPTIONS_SCHEMA_TEXT_VALUE = """
+        syntax = "proto3";
+        message User {
+          int32 id = 1 [(confluent.field_meta) = {
+            params: [
+              {
+                key: "connect.type",
+                value: "int16"
+              }
+            ]
+          }];
+          string name = 2 [(confluent.field_meta) = { doc: "name" }];
+          message Profile {
+            string bio = 1 [(confluent.field_meta) = { params: [ { key: "k", 
value: "v" } ] }];
+          }
+        }
+        message Company {
+          string name = 1 [(confluent.field_meta) = { doc: "company" }];
+        }""";
+
+    private static final String USER_MESSAGE = "User";
+    private static final String PROFILE_MESSAGE = "User.Profile";
+    private static final String COMPANY_MESSAGE = "Company";
+
+    // Nested message User.Profile -> declaration path [0, 0] -> length 2 then 
indexes 0, 0 (zigzag varints).
+    private static final byte[] NESTED_PROFILE_MESSAGE_INDEX = {0x04, 0x00, 
0x00};
+    // Root message Company -> declaration path [1] -> length 1 then index 1 
(zigzag varints).
+    private static final byte[] COMPANY_MESSAGE_INDEX = {0x02, 0x02};
+
+    private static final String ID_FIELD = "id";
+    private static final String NAME_FIELD = "name";
+    private static final String BIO_FIELD = "bio";
+
+    private static final int ID_VALUE = 7;
+    private static final String USER_NAME_VALUE = "Alice";
+    private static final String COMPANY_NAME_VALUE = "Acme";
+    private static final String BIO_VALUE = "hello";
+
     private TestRunner runner;
     private MockSchemaRegistry schemaRegistry;
     private ConfluentEncodedSchemaReferenceWriter referenceWriter;
@@ -107,45 +152,72 @@ class TestStandardProtobufWriterConfluentRoundTrip {
         referenceReader = new ConfluentEncodedSchemaReferenceReader();
         messageNameResolver = new ConfluentProtobufMessageNameResolver();
 
-        enableService("registry", schemaRegistry);
-        enableService("referenceWriter", referenceWriter);
-        enableService("indexWriter", indexWriter);
-        enableService("referenceReader", referenceReader);
-        enableService("messageNameResolver", messageNameResolver);
+        enableService(REGISTRY_ID, schemaRegistry);
+        enableService(REFERENCE_WRITER_ID, referenceWriter);
+        enableService(INDEX_WRITER_ID, indexWriter);
+        enableService(REFERENCE_READER_ID, referenceReader);
+        enableService(MESSAGE_NAME_RESOLVER_ID, messageNameResolver);
     }
 
     @Test
     void testFirstRootMessageSingleByteIndexAndRoundTrip() throws Exception {
         final Map<String, Object> values = new HashMap<>();
-        values.put("id", 7);
-        values.put("name", "Alice");
-        final MapRecord userRecord = new MapRecord(recordSchema("id", "name"), 
values);
+        values.put(ID_FIELD, ID_VALUE);
+        values.put(NAME_FIELD, USER_NAME_VALUE);
+        final MapRecord userRecord = new MapRecord(recordSchema(ID_FIELD, 
NAME_FIELD), values);
 
-        final byte[] output = writeConfluent("User", userRecord);
+        final byte[] output = writeConfluent(USER_MESSAGE, userRecord);
 
         // [header][message index][payload]; the first root message collapses 
to the single index byte 0x00.
-        assertArrayEquals(EXPECTED_HEADER, slice(output, 0, 5));
-        assertEquals(0x00, output[5]);
+        assertArrayEquals(EXPECTED_HEADER, slice(output, 0, HEADER_LENGTH));
+        assertEquals(0x00, output[HEADER_LENGTH]);
 
         final Record readBack = readConfluent(output);
-        assertEquals(7, readBack.getValue("id"));
-        assertEquals("Alice", readBack.getValue("name"));
+        assertEquals(ID_VALUE, readBack.getValue(ID_FIELD));
+        assertEquals(USER_NAME_VALUE, readBack.getValue(NAME_FIELD));
     }
 
     @Test
     void testNestedMessageIndexAndRoundTrip() throws Exception {
         final Map<String, Object> values = new HashMap<>();
-        values.put("bio", "hello");
-        final MapRecord profileRecord = new MapRecord(recordSchema("bio"), 
values);
+        values.put(BIO_FIELD, BIO_VALUE);
+        final MapRecord profileRecord = new MapRecord(recordSchema(BIO_FIELD), 
values);
 
-        final byte[] output = writeConfluent("User.Profile", profileRecord);
+        final byte[] output = writeConfluent(PROFILE_MESSAGE, profileRecord);
 
-        // Nested message User.Profile -> declaration path [0, 0] -> length 2 
then indexes 0, 0 (zigzag varints).
-        assertArrayEquals(EXPECTED_HEADER, slice(output, 0, 5));
-        assertArrayEquals(new byte[] {0x04, 0x00, 0x00}, slice(output, 5, 8));
+        assertArrayEquals(EXPECTED_HEADER, slice(output, 0, HEADER_LENGTH));
+        assertArrayEquals(NESTED_PROFILE_MESSAGE_INDEX, slice(output, 
HEADER_LENGTH, NESTED_PROFILE_MESSAGE_INDEX.length));
 
         final Record readBack = readConfluent(output);
-        assertEquals("hello", readBack.getValue("bio"));
+        assertEquals(BIO_VALUE, readBack.getValue(BIO_FIELD));
+    }
+
+    @Test
+    void testCustomOptionsNestedMessageRoundTrip() throws Exception {
+        schemaRegistry.returnSchemaText(CUSTOM_OPTIONS_SCHEMA_TEXT_VALUE);
+        final MapRecord profileRecord = new MapRecord(recordSchema(BIO_FIELD), 
Map.of(BIO_FIELD, BIO_VALUE));
+
+        final byte[] output = writeConfluent(PROFILE_MESSAGE, profileRecord);
+
+        assertArrayEquals(EXPECTED_HEADER, slice(output, 0, HEADER_LENGTH));
+        assertArrayEquals(NESTED_PROFILE_MESSAGE_INDEX, slice(output, 
HEADER_LENGTH, NESTED_PROFILE_MESSAGE_INDEX.length));
+
+        final Record readBack = readConfluent(output);
+        assertEquals(BIO_VALUE, readBack.getValue(BIO_FIELD));
+    }
+
+    @Test
+    void testCustomOptionsSecondRootMessageRoundTrip() throws Exception {
+        schemaRegistry.returnSchemaText(CUSTOM_OPTIONS_SCHEMA_TEXT_VALUE);
+        final MapRecord companyRecord = new 
MapRecord(recordSchema(NAME_FIELD), Map.of(NAME_FIELD, COMPANY_NAME_VALUE));
+
+        final byte[] output = writeConfluent(COMPANY_MESSAGE, companyRecord);
+
+        assertArrayEquals(EXPECTED_HEADER, slice(output, 0, HEADER_LENGTH));
+        assertArrayEquals(COMPANY_MESSAGE_INDEX, slice(output, HEADER_LENGTH, 
COMPANY_MESSAGE_INDEX.length));
+
+        final Record readBack = readConfluent(output);
+        assertEquals(COMPANY_NAME_VALUE, readBack.getValue(NAME_FIELD));
     }
 
     @Test
@@ -154,11 +226,11 @@ class TestStandardProtobufWriterConfluentRoundTrip {
         runner.addControllerService("schemaTextWriter", writer);
         runner.setProperty(writer, SCHEMA_ACCESS_STRATEGY, 
SCHEMA_TEXT_PROPERTY.getValue());
         runner.setProperty(writer, SCHEMA_TEXT, SCHEMA_TEXT_VALUE);
-        runner.setProperty(writer, StandardProtobufWriter.MESSAGE_NAME, 
"User");
+        runner.setProperty(writer, StandardProtobufWriter.MESSAGE_NAME, 
USER_MESSAGE);
         runner.assertValid(writer);
 
         // Inline schema text has no registry identifier or version for the 
Confluent header
-        runner.setProperty(writer, 
StandardProtobufWriter.SCHEMA_REFERENCE_WRITER, "referenceWriter");
+        runner.setProperty(writer, 
StandardProtobufWriter.SCHEMA_REFERENCE_WRITER, REFERENCE_WRITER_ID);
         runner.assertNotValid(writer);
     }
 
@@ -166,13 +238,13 @@ class TestStandardProtobufWriterConfluentRoundTrip {
         final StandardProtobufWriter writer = new StandardProtobufWriter();
         runner.addControllerService("writer-" + messageName, writer);
         runner.setProperty(writer, SCHEMA_ACCESS_STRATEGY, 
SCHEMA_NAME_PROPERTY.getValue());
-        runner.setProperty(writer, SCHEMA_REGISTRY, "registry");
+        runner.setProperty(writer, SCHEMA_REGISTRY, REGISTRY_ID);
         runner.setProperty(writer, SCHEMA_NAME, "user");
         runner.setProperty(writer, 
StandardProtobufWriter.MESSAGE_NAME_RESOLUTION_STRATEGY,
             
StandardProtobufWriter.MessageNameResolverStrategy.MESSAGE_NAME_PROPERTY.getValue());
         runner.setProperty(writer, StandardProtobufWriter.MESSAGE_NAME, 
messageName);
-        runner.setProperty(writer, 
StandardProtobufWriter.SCHEMA_REFERENCE_WRITER, "referenceWriter");
-        runner.setProperty(writer, 
StandardProtobufWriter.MESSAGE_INDEX_WRITER, "indexWriter");
+        runner.setProperty(writer, 
StandardProtobufWriter.SCHEMA_REFERENCE_WRITER, REFERENCE_WRITER_ID);
+        runner.setProperty(writer, 
StandardProtobufWriter.MESSAGE_INDEX_WRITER, INDEX_WRITER_ID);
         runner.enableControllerService(writer);
 
         final RecordSchema writeSchema = writer.getSchema(emptyMap(), null);
@@ -190,11 +262,11 @@ class TestStandardProtobufWriterConfluentRoundTrip {
         final StandardProtobufReader reader = new StandardProtobufReader();
         runner.addControllerService("reader", reader);
         runner.setProperty(reader, SCHEMA_ACCESS_STRATEGY, 
SCHEMA_REFERENCE_READER_PROPERTY.getValue());
-        runner.setProperty(reader, SCHEMA_REFERENCE_READER, "referenceReader");
-        runner.setProperty(reader, SCHEMA_REGISTRY, "registry");
+        runner.setProperty(reader, SCHEMA_REFERENCE_READER, 
REFERENCE_READER_ID);
+        runner.setProperty(reader, SCHEMA_REGISTRY, REGISTRY_ID);
         runner.setProperty(reader, 
StandardProtobufReader.MESSAGE_NAME_RESOLUTION_STRATEGY,
             
StandardProtobufReader.MessageNameResolverStrategy.MESSAGE_NAME_RESOLVER.getValue());
-        runner.setProperty(reader, 
StandardProtobufReader.MESSAGE_NAME_RESOLVER, "messageNameResolver");
+        runner.setProperty(reader, 
StandardProtobufReader.MESSAGE_NAME_RESOLVER, MESSAGE_NAME_RESOLVER_ID);
         runner.enableControllerService(reader);
 
         final RecordReader recordReader = 
reader.createRecordReader(emptyMap(), new ByteArrayInputStream(data), 
data.length, runner.getLogger());
@@ -206,7 +278,7 @@ class TestStandardProtobufWriterConfluentRoundTrip {
     private RecordSchema recordSchema(final String... fieldNames) {
         final RecordField[] fields = new RecordField[fieldNames.length];
         for (int i = 0; i < fieldNames.length; i++) {
-            final RecordFieldType type = "id".equals(fieldNames[i]) ? 
RecordFieldType.INT : RecordFieldType.STRING;
+            final RecordFieldType type = ID_FIELD.equals(fieldNames[i]) ? 
RecordFieldType.INT : RecordFieldType.STRING;
             fields[i] = new RecordField(fieldNames[i], type.getDataType());
         }
         return new SimpleRecordSchema(List.of(fields));
@@ -217,9 +289,9 @@ class TestStandardProtobufWriterConfluentRoundTrip {
         runner.enableControllerService(service);
     }
 
-    private byte[] slice(final byte[] data, final int from, final int to) {
-        final byte[] out = new byte[to - from];
-        System.arraycopy(data, from, out, 0, to - from);
+    private byte[] slice(final byte[] data, final int from, final int length) {
+        final byte[] out = new byte[length];
+        System.arraycopy(data, from, out, 0, length);
         return out;
     }
 
@@ -228,10 +300,11 @@ class TestStandardProtobufWriterConfluentRoundTrip {
      * both the writer's name-based lookup and the reader's 
Confluent-header-based lookup resolve the same schema.
      */
     static class MockSchemaRegistry extends AbstractControllerService 
implements SchemaRegistry {
-        private final SchemaDefinition schemaDefinition = new 
StandardSchemaDefinition(
-            SchemaIdentifier.builder().name("user.proto").id((long) 
SCHEMA_ID).version(1).build(),
-            SCHEMA_TEXT_VALUE,
-            SchemaDefinition.SchemaType.PROTOBUF);
+        private volatile String schemaText = SCHEMA_TEXT_VALUE;
+
+        void returnSchemaText(final String schemaText) {
+            this.schemaText = schemaText;
+        }
 
         @Override
         public RecordSchema retrieveSchema(final SchemaIdentifier 
schemaIdentifier) {
@@ -240,7 +313,8 @@ class TestStandardProtobufWriterConfluentRoundTrip {
 
         @Override
         public SchemaDefinition retrieveSchemaDefinition(final 
SchemaIdentifier schemaIdentifier) throws SchemaNotFoundException {
-            return schemaDefinition;
+            final SchemaIdentifier identifier = 
SchemaIdentifier.builder().name("user.proto").id(SCHEMA_ID).version(1).build();
+            return new StandardSchemaDefinition(identifier, schemaText, 
SchemaDefinition.SchemaType.PROTOBUF);
         }
 
         @Override

Reply via email to