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

francischuang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git


The following commit(s) were added to refs/heads/main by this push:
     new 8f2b77551 [CALCITE-7436] Add high-coverage Jazzer fuzzing for Avatica 
core modules
8f2b77551 is described below

commit 8f2b775513ae82cec7cd2bf24b5ed88b57b20dfe
Author: Vishal S <[email protected]>
AuthorDate: Sat Mar 7 21:06:27 2026 +0530

    [CALCITE-7436] Add high-coverage Jazzer fuzzing for Avatica core modules
---
 .github/workflows/cifuzz.yml                       |  70 +++++++
 core/build.gradle.kts                              |   6 +
 .../calcite/avatica/fuzz/AvaticaSiteFuzzer.java    | 217 +++++++++++++++++++++
 .../apache/calcite/avatica/fuzz/Base64Fuzzer.java  |  48 +++++
 .../avatica/fuzz/ConnectStringParserFuzzer.java    |  44 +++++
 .../calcite/avatica/fuzz/JsonHandlerFuzzer.java    | 133 +++++++++++++
 .../avatica/fuzz/ProtobufHandlerFuzzer.java        | 129 ++++++++++++
 .../calcite/avatica/fuzz/TypedValueFuzzer.java     | 117 +++++++++++
 8 files changed, 764 insertions(+)

diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
new file mode 100644
index 000000000..0efdcb04c
--- /dev/null
+++ b/.github/workflows/cifuzz.yml
@@ -0,0 +1,70 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+name: CIFuzz
+on:
+  pull_request:
+    branches:
+      - main
+      - master
+
+permissions:
+  contents: read
+  security-events: write
+
+jobs:
+  Fuzzing:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Build Fuzzers
+        id: build
+        uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
+        with:
+          oss-fuzz-project-name: "calcite-avatica"
+          language: jvm
+
+      - name: Run Fuzzers
+        uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
+        with:
+          oss-fuzz-project-name: "calcite-avatica"
+          language: jvm
+          fuzz-seconds: 600
+          output-sarif: true
+          fail: false
+
+      - name: Upload SARIF results
+        uses: github/codeql-action/upload-sarif@v3
+        if: always() && steps.build.outcome == 'success'
+        with:
+          sarif_file: ./out/results.sarif.json
+          category: fuzzing
+
+      - name: Upload Crash Artifacts
+        uses: actions/upload-artifact@v4
+        if: steps.run.outcome == 'failure' && steps.build.outcome == 'success'
+        with:
+          name: fuzzing-crash-artifacts
+          path: ./out/artifacts
+          retention-days: 30
+
+      - name: Report Fuzzing Status
+        if: steps.run.outcome == 'failure'
+        run: |
+          echo "::notice::Fuzzing found potential bugs! Check the artifacts 
for details."
+          echo "To reproduce locally: Download artifacts and run with Jazzer"
+          echo ""
+          echo "IMPORTANT: This is an informational check. The PR can still be 
merged."
+          echo "For bug tracking, see: 
https://issues.apache.org/jira/browse/CALCITE";
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index ffa8ecc6d..6f0adda7e 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
     implementation("org.apache.httpcomponents.client5:httpclient5")
     implementation("org.apache.httpcomponents.core5:httpcore5")
     implementation("org.slf4j:slf4j-api")
+    testImplementation("com.code-intelligence:jazzer-api:0.22.1")
     testImplementation("junit:junit")
     testImplementation("org.mockito:mockito-core")
     testImplementation("org.mockito:mockito-inline")
@@ -117,3 +118,8 @@ tasks.autostyleJavaCheck {
     dependsOn(filterJava)
     dependsOn(tasks.getByName("generateProto"))
 }
+
+tasks.register<Copy>("copyFuzzDependencies") {
+    from(configurations.testRuntimeClasspath)
+    into(layout.buildDirectory.dir("fuzz-dependencies"))
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/fuzz/AvaticaSiteFuzzer.java 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/AvaticaSiteFuzzer.java
new file mode 100644
index 000000000..81b001692
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/AvaticaSiteFuzzer.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.AvaticaParameter;
+import org.apache.calcite.avatica.AvaticaSite;
+import org.apache.calcite.avatica.remote.TypedValue;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.lang.reflect.Proxy;
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Fuzzer for AvaticaSite.
+ */
+public class AvaticaSiteFuzzer {
+
+  private AvaticaSiteFuzzer() {
+  }
+
+  /**
+   * Fuzzes AvaticaSite methods.
+   *
+   * @param data fuzzed data
+   */
+  public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+    try {
+      // Construct dependencies for an AvaticaSite
+      AvaticaParameter param = new AvaticaParameter(
+          data.consumeBoolean(),
+          data.consumeInt(),
+          data.consumeInt(), // scale
+          data.consumeInt(),
+          data.consumeString(10), // typeName
+          data.consumeString(10), // className
+          data.consumeString(10)  // name
+      );
+
+      Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+      TypedValue[] slots = new TypedValue[1];
+
+      // Target object
+      AvaticaSite site = new AvaticaSite(param, calendar, 0, slots);
+
+      // Determine what to fuzz
+      int choice = data.consumeInt(1, 16);
+
+      switch (choice) {
+      case 1:
+        site.setByte(data.consumeByte());
+        break;
+      case 2:
+        site.setChar(data.consumeChar());
+        break;
+      case 3:
+        site.setShort(data.consumeShort());
+        break;
+      case 4:
+        site.setInt(data.consumeInt());
+        break;
+      case 5:
+        site.setLong(data.consumeLong());
+        break;
+      case 6:
+        site.setBoolean(data.consumeBoolean());
+        break;
+      case 7:
+        site.setNString(data.consumeString(50));
+        break;
+      case 8:
+        site.setFloat(data.consumeFloat());
+        break;
+      case 9:
+        site.setDouble(data.consumeDouble());
+        break;
+      case 10:
+        site.setBigDecimal(new BigDecimal(data.consumeDouble()));
+        break;
+      case 11:
+        site.setString(data.consumeString(50));
+        break;
+      case 12:
+        site.setBytes(data.consumeBytes(50));
+        break;
+      case 13:
+        site.setTimestamp(new Timestamp(data.consumeLong()), calendar);
+        break;
+      case 14:
+        site.setTime(new Time(data.consumeLong()), calendar);
+        break;
+      case 15:
+        // Raw object mapping
+        Object obj = null;
+        int objType = data.consumeInt(1, 4);
+        if (objType == 1) {
+          obj = data.consumeBoolean();
+        } else if (objType == 2) {
+          obj = data.consumeString(50);
+        } else if (objType == 3) {
+          obj = data.consumeLong();
+        } else if (objType == 4) {
+          obj = data.consumeBytes(50);
+        }
+
+        site.setObject(obj, data.consumeInt(-10, 100)); // Types constants 
fall in this range
+        break;
+      case 16:
+        // Test the JDBC ResultSet getter mapping using a dynamic proxy
+        org.apache.calcite.avatica.util.Cursor.Accessor accessor =
+            (org.apache.calcite.avatica.util.Cursor.Accessor) 
Proxy.newProxyInstance(
+                
org.apache.calcite.avatica.util.Cursor.Accessor.class.getClassLoader(),
+                new Class<?>[] 
{org.apache.calcite.avatica.util.Cursor.Accessor.class},
+                (proxy, method, args) -> {
+                String name = method.getName();
+                if (name.equals("wasNull")) {
+                  return data.consumeBoolean();
+                }
+                if (name.equals("getString") || name.equals("getNString")) {
+                  return data.consumeString(50);
+                }
+                if (name.equals("getBoolean")) {
+                  return data.consumeBoolean();
+                }
+                if (name.equals("getByte")) {
+                  return data.consumeByte();
+                }
+                if (name.equals("getShort")) {
+                  return data.consumeShort();
+                }
+                if (name.equals("getInt")) {
+                  return data.consumeInt();
+                }
+                if (name.equals("getLong")) {
+                  return data.consumeLong();
+                }
+                if (name.equals("getFloat")) {
+                  return data.consumeFloat();
+                }
+                if (name.equals("getDouble")) {
+                  return data.consumeDouble();
+                }
+                if (name.equals("getBigDecimal")) {
+                  return new BigDecimal(data.consumeDouble());
+                }
+                if (name.equals("getBytes")) {
+                  return data.consumeBytes(50);
+                }
+                if (name.equals("getDate")) {
+                  return new Date(data.consumeLong());
+                }
+                if (name.equals("getTime")) {
+                  return new Time(data.consumeLong());
+                }
+                if (name.equals("getTimestamp")) {
+                  return new Timestamp(data.consumeLong());
+                }
+
+                if (name.equals("getUByte")) {
+                  return org.joou.UByte.valueOf(data.consumeInt(0, 255));
+                }
+                if (name.equals("getUShort")) {
+                  return org.joou.UShort.valueOf(data.consumeInt(0, 65535));
+                }
+                if (name.equals("getUInt")) {
+                  return org.joou.UInteger.valueOf(data.consumeLong(0, 
4294967295L));
+                }
+                if (name.equals("getULong")) {
+                  return org.joou.ULong.valueOf(data.consumeLong(0, 
Long.MAX_VALUE));
+                }
+
+                return null;
+              }
+            );
+
+        try {
+          AvaticaSite.get(accessor, data.consumeInt(-10, 100), 
data.consumeBoolean(), calendar);
+        } catch (SQLException e) {
+          // Expected to throw SQLException for unsupported conversions
+        }
+        break;
+      default:
+        break;
+      }
+
+    } catch (IllegalArgumentException | UnsupportedOperationException e) {
+      // UnsupportedOperationException is explicitly thrown by 
AvaticaSite.notImplemented()
+      // and unsupportedCast() when types don't align.
+    } catch (RuntimeException e) {
+      // TypedValue bindings often throw RuntimeException directly for "not 
implemented"
+      if (!"not implemented".equals(e.getMessage())) {
+        throw e;
+      }
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/fuzz/Base64Fuzzer.java 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/Base64Fuzzer.java
new file mode 100644
index 000000000..56092b7d8
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/Base64Fuzzer.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.util.Base64;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.io.IOException;
+
+/**
+ * Fuzzer for Base64 utility class.
+ */
+public final class Base64Fuzzer {
+  private Base64Fuzzer() {
+  }
+
+  /**
+   * Fuzzes Base64 encode and decode methods.
+   *
+   * @param data fuzzed data provider
+   */
+  public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+    try {
+      if (data.consumeBoolean()) {
+        Base64.encodeBytes(data.consumeRemainingAsBytes());
+      } else {
+        Base64.decode(data.consumeRemainingAsBytes());
+      }
+    } catch (IOException | IllegalArgumentException e) {
+      // Known exception
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/fuzz/ConnectStringParserFuzzer.java
 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ConnectStringParserFuzzer.java
new file mode 100644
index 000000000..f6422a675
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ConnectStringParserFuzzer.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.ConnectStringParser;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.sql.SQLException;
+
+/**
+ * Fuzzer for ConnectStringParser.
+ */
+public final class ConnectStringParserFuzzer {
+  private ConnectStringParserFuzzer() {
+  }
+
+  /**
+   * Fuzzes the ConnectStringParser parse method.
+   *
+   * @param data fuzzed data provider
+   */
+  public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+    try {
+      ConnectStringParser.parse(data.consumeRemainingAsString());
+    } catch (SQLException e) {
+      // Known exception
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/fuzz/JsonHandlerFuzzer.java 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/JsonHandlerFuzzer.java
new file mode 100644
index 000000000..59bf7526c
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/JsonHandlerFuzzer.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.remote.JsonService;
+import org.apache.calcite.avatica.remote.Service;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonMappingException;
+
+import java.io.IOException;
+
+/**
+ * Fuzzer for JsonHandler (JsonService).
+ */
+public class JsonHandlerFuzzer {
+
+  private JsonHandlerFuzzer() {
+  }
+
+  static {
+    // Prevent failure on completely unknown properties that the fuzzer might 
invent
+    
JsonService.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+  }
+
+  /**
+   * Fuzzes JSON serialization/deserialization for Avatica Request/Response 
objects.
+   *
+   * @param data fuzzed data
+   */
+  public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+    try {
+      // The goal here is to hit the deeply nested deserialization logic of
+      // Avatica's Request and Response models.
+      // Avatica uses Jackson to parse strings into classes like
+      // Service.ExecuteRequest, Service.CatalogsRequest, etc.
+
+      boolean isRequest = data.consumeBoolean();
+
+      if (isRequest) {
+        String subType = data.pickValue(new String[]{
+            "getCatalogs", "getSchemas", "getTables", "getTableTypes", 
"getTypeInfo", "getColumns",
+            "execute", "prepare", "prepareAndExecute", "fetch", 
"createStatement", "closeStatement",
+            "openConnection", "closeConnection", "connectionSync", 
"databaseProperties", "syncResults",
+            "commit", "rollback", "prepareAndExecuteBatch", "executeBatch"
+        });
+
+        java.util.Map<String, Object> map = new java.util.HashMap<>();
+        map.put("request", subType);
+
+        // Add random key/value pairs
+        int numFields = data.consumeInt(0, 10);
+        for (int i = 0; i < numFields; i++) {
+          switch (data.consumeInt(1, 4)) {
+          case 1:
+            map.put(data.consumeString(10), data.consumeString(20));
+            break;
+          case 2:
+            map.put(data.consumeString(10), data.consumeInt());
+            break;
+          case 3:
+            map.put(data.consumeString(10), data.consumeBoolean());
+            break;
+          case 4:
+            map.put(data.consumeString(10), null);
+            break;
+          default:
+            break;
+          }
+        }
+
+        String jsonPayload = JsonService.MAPPER.writeValueAsString(map);
+        JsonService.MAPPER.readValue(jsonPayload, Service.Request.class);
+      } else {
+        String subType = data.pickValue(new String[]{
+            "openConnection", "resultSet", "prepare", "fetch", 
"createStatement", "closeStatement",
+            "closeConnection", "connectionSync", "databaseProperties", 
"executeResults", "error",
+            "syncResults", "rpcMetadata", "commit", "rollback", "executeBatch"
+        });
+
+        java.util.Map<String, Object> map = new java.util.HashMap<>();
+        map.put("response", subType);
+
+        // Add random key/value pairs
+        int numFields = data.consumeInt(0, 10);
+        for (int i = 0; i < numFields; i++) {
+          switch (data.consumeInt(1, 4)) {
+          case 1:
+            map.put(data.consumeString(10), data.consumeString(20));
+            break;
+          case 2:
+            map.put(data.consumeString(10), data.consumeInt());
+            break;
+          case 3:
+            map.put(data.consumeString(10), data.consumeBoolean());
+            break;
+          case 4:
+            map.put(data.consumeString(10), null);
+            break;
+          default:
+            break;
+          }
+        }
+
+        String jsonPayload = JsonService.MAPPER.writeValueAsString(map);
+        JsonService.MAPPER.readValue(jsonPayload, Service.Response.class);
+      }
+
+    } catch (JsonParseException | JsonMappingException e) {
+      // Known Jackson exceptions for invalid JSON structure or unmappable 
types
+    } catch (IOException e) {
+      // General IO issues reading the string
+    } catch (IllegalArgumentException | IllegalStateException e) {
+      // Known issues when Jackson encounters valid JSON but violates 
Avatica's preconditions
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/fuzz/ProtobufHandlerFuzzer.java 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ProtobufHandlerFuzzer.java
new file mode 100644
index 000000000..1475eb18a
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ProtobufHandlerFuzzer.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.remote.ProtobufTranslationImpl;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.google.protobuf.ByteString;
+
+import java.io.IOException;
+
+/**
+ * Fuzzer for ProtobufHandler (ProtobufTranslation).
+ */
+public class ProtobufHandlerFuzzer {
+
+  private ProtobufHandlerFuzzer() {
+  }
+
+  private static final ProtobufTranslationImpl TRANSLATOR = new 
ProtobufTranslationImpl();
+
+  /**
+   * Fuzzes Protobuf serialization/deserialization for Avatica 
Request/Response objects.
+   *
+   * @param data fuzzed data
+   */
+  public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+    try {
+      // The goal here is to hit the protobuf deserialization logic.
+      // Avatica maps Protobuf messages (WireMessage) into its POJO 
Service.Request models.
+      // WireMessage requires a "name" string matching a Request subclass.
+
+      boolean isRequest = data.consumeBoolean();
+
+      if (isRequest) {
+        String subType = data.pickValue(new String[]{
+            "org.apache.calcite.avatica.proto.Requests$CatalogsRequest",
+            "org.apache.calcite.avatica.proto.Requests$SchemasRequest",
+            "org.apache.calcite.avatica.proto.Requests$TablesRequest",
+            "org.apache.calcite.avatica.proto.Requests$TableTypesRequest",
+            "org.apache.calcite.avatica.proto.Requests$TypeInfoRequest",
+            "org.apache.calcite.avatica.proto.Requests$ColumnsRequest",
+            "org.apache.calcite.avatica.proto.Requests$ExecuteRequest",
+            "org.apache.calcite.avatica.proto.Requests$PrepareRequest",
+            
"org.apache.calcite.avatica.proto.Requests$PrepareAndExecuteRequest",
+            "org.apache.calcite.avatica.proto.Requests$FetchRequest",
+            "org.apache.calcite.avatica.proto.Requests$CreateStatementRequest",
+            "org.apache.calcite.avatica.proto.Requests$CloseStatementRequest",
+            "org.apache.calcite.avatica.proto.Requests$OpenConnectionRequest",
+            "org.apache.calcite.avatica.proto.Requests$CloseConnectionRequest",
+            "org.apache.calcite.avatica.proto.Requests$ConnectionSyncRequest",
+            
"org.apache.calcite.avatica.proto.Requests$DatabasePropertyRequest",
+            "org.apache.calcite.avatica.proto.Requests$SyncResultsRequest",
+            "org.apache.calcite.avatica.proto.Requests$CommitRequest",
+            "org.apache.calcite.avatica.proto.Requests$RollbackRequest",
+            
"org.apache.calcite.avatica.proto.Requests$PrepareAndExecuteBatchRequest",
+            "org.apache.calcite.avatica.proto.Requests$ExecuteBatchRequest"
+        });
+
+        org.apache.calcite.avatica.proto.Common.WireMessage wireMsg =
+            org.apache.calcite.avatica.proto.Common.WireMessage.newBuilder()
+            .setName(subType)
+            
.setWrappedMessage(ByteString.copyFrom(data.consumeRemainingAsBytes()))
+            .build();
+
+        byte[] protobufPayload = wireMsg.toByteArray();
+        TRANSLATOR.parseRequest(protobufPayload);
+      } else {
+        String subType = data.pickValue(new String[]{
+            
"org.apache.calcite.avatica.proto.Responses$OpenConnectionResponse",
+            
"org.apache.calcite.avatica.proto.Responses$CloseConnectionResponse",
+            
"org.apache.calcite.avatica.proto.Responses$CloseStatementResponse",
+            
"org.apache.calcite.avatica.proto.Responses$ConnectionSyncResponse",
+            
"org.apache.calcite.avatica.proto.Responses$CreateStatementResponse",
+            
"org.apache.calcite.avatica.proto.Responses$DatabasePropertyResponse",
+            "org.apache.calcite.avatica.proto.Responses$ExecuteResponse",
+            "org.apache.calcite.avatica.proto.Responses$FetchResponse",
+            "org.apache.calcite.avatica.proto.Responses$PrepareResponse",
+            "org.apache.calcite.avatica.proto.Responses$ResultSetResponse",
+            "org.apache.calcite.avatica.proto.Responses$ErrorResponse",
+            "org.apache.calcite.avatica.proto.Responses$SyncResultsResponse",
+            "org.apache.calcite.avatica.proto.Responses$RpcMetadata",
+            "org.apache.calcite.avatica.proto.Responses$CommitResponse",
+            "org.apache.calcite.avatica.proto.Responses$RollbackResponse",
+            "org.apache.calcite.avatica.proto.Responses$ExecuteBatchResponse"
+        });
+
+        org.apache.calcite.avatica.proto.Common.WireMessage wireMsg =
+            org.apache.calcite.avatica.proto.Common.WireMessage.newBuilder()
+            .setName(subType)
+            
.setWrappedMessage(ByteString.copyFrom(data.consumeRemainingAsBytes()))
+            .build();
+
+        byte[] protobufPayload = wireMsg.toByteArray();
+        TRANSLATOR.parseResponse(protobufPayload);
+      }
+
+    } catch (IOException e) {
+      // Known exception from protobuf parsing (e.g. 
InvalidProtocolBufferException)
+    } catch (IllegalArgumentException | IllegalStateException | 
NullPointerException e) {
+      // Known issues when Protobuf unmarshalls into Avatica types that fail 
preconditions
+    } catch (RuntimeException e) {
+      // Specifically catching Avatica's custom DeserializationException or
+      // unhandled protobuf issues to ensure the fuzzer survives
+      if (e.getClass().getName().contains("DeserializationException")
+          || e.getClass().getName().contains("InvalidProtocolBufferException")
+          || e.getMessage() != null && e.getMessage().contains("Unknown type:")
+          || e.getMessage() != null && e.getMessage().contains("Unhandled 
type:")) {
+        return;
+      }
+      // If it's a real bug (NullPointerException, etc), let it crash!
+      throw e;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/fuzz/TypedValueFuzzer.java 
b/core/src/test/java/org/apache/calcite/avatica/fuzz/TypedValueFuzzer.java
new file mode 100644
index 000000000..6243382dc
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/TypedValueFuzzer.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.proto.Common;
+import org.apache.calcite.avatica.remote.TypedValue;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Fuzzer for TypedValue.
+ */
+public class TypedValueFuzzer {
+
+  private TypedValueFuzzer() {
+  }
+
+  /**
+   * Fuzzes TypedValue conversion methods.
+   *
+   * @param data fuzzed data
+   */
+  public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+    try {
+      // Use the Fuzzer to generate random Protobuf arrays
+      boolean isFromProto = data.consumeBoolean();
+
+      if (isFromProto) {
+        // Parse it into Common.TypedValue
+        Common.TypedValue protoValue = 
Common.TypedValue.parseFrom(data.consumeRemainingAsBytes());
+
+        // Attempt to convert it into a local Avatica TypedValue and then to 
JDBC representations
+        TypedValue typedValue = TypedValue.fromProto(protoValue);
+
+        // Convert to local and jdbc formats
+        typedValue.toLocal();
+        typedValue.toJdbc(Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT));
+
+        // Attempt Protobuf serialization back
+        typedValue.toProto();
+      } else {
+        // Fuzz the direct POJO creator
+        String typeName = data.pickValue(new String[]{
+            "STRING", "BOOLEAN", "BYTE", "SHORT", "INTEGER", "LONG", "FLOAT", 
"DOUBLE", "DATE", "TIME", "TIMESTAMP"
+        });
+
+        Object fakeValue = null;
+        switch (typeName) {
+        case "STRING":
+          fakeValue = data.consumeString(50);
+          break;
+        case "BOOLEAN":
+          fakeValue = data.consumeBoolean();
+          break;
+        case "BYTE":
+          fakeValue = data.consumeByte();
+          break;
+        case "SHORT":
+          fakeValue = data.consumeShort();
+          break;
+        case "INTEGER":
+          fakeValue = data.consumeInt();
+          break;
+        case "LONG":
+          fakeValue = data.consumeLong();
+          break;
+        case "FLOAT":
+          fakeValue = data.consumeFloat();
+          break;
+        case "DOUBLE":
+          fakeValue = data.consumeDouble();
+          break;
+        case "DATE":
+        case "TIME":
+        case "TIMESTAMP":
+          fakeValue = data.consumeLong();
+          break;
+        default:
+          break;
+        }
+
+        // Fuzz create factory mapping the object value with random type 
identifier
+        TypedValue created = TypedValue.create(typeName, fakeValue);
+
+        // Call accessors
+        created.toLocal();
+        created.toJdbc(Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT));
+        created.toProto();
+      }
+
+    } catch (java.io.IOException e) {
+      // Known exception for invalid protobuf
+    } catch (RuntimeException e) {
+      // TypedValue parser is known to throw unchecked exceptions
+      // when types don't align with values in the protobuf
+      // E.g., asking for a Boolean from a protobuf field that was stored as a 
String.
+    }
+  }
+}

Reply via email to