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

zhoujinsong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/amoro.git


The following commit(s) were added to refs/heads/master by this push:
     new 9e63e8ebd [AMORO-4232][common] Improve AmsThriftUrl parsing and 
validation (#4372)
9e63e8ebd is described below

commit 9e63e8ebda0f9664b3efa1b4075a2ec695609893
Author: Sivakumar Mahalingam 
<[email protected]>
AuthorDate: Tue Sep 22 10:30:48 2026 +0400

    [AMORO-4232][common] Improve AmsThriftUrl parsing and validation (#4372)
    
    fix: improve AmsThriftUrl parsing and validation
---
 .../java/org/apache/amoro/client/AmsThriftUrl.java |  91 ++++++++++------
 .../org/apache/amoro/client/TestAmsThriftUrl.java  | 115 +++++++++++++++++++++
 2 files changed, 173 insertions(+), 33 deletions(-)

diff --git 
a/amoro-common/src/main/java/org/apache/amoro/client/AmsThriftUrl.java 
b/amoro-common/src/main/java/org/apache/amoro/client/AmsThriftUrl.java
index 860d5c6dc..238a08a24 100644
--- a/amoro-common/src/main/java/org/apache/amoro/client/AmsThriftUrl.java
+++ b/amoro-common/src/main/java/org/apache/amoro/client/AmsThriftUrl.java
@@ -34,7 +34,6 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -44,8 +43,12 @@ public class AmsThriftUrl {
   public static final String ZOOKEEPER_FLAG = "zookeeper";
   public static final String THRIFT_URL_FORMAT = "thrift://%s:%d/%s%s";
   public static final int MAX_RETRIES = 3;
+  private static final String THRIFT_SCHEME = "thrift";
+  private static final String ZOOKEEPER_SCHEME_PREFIX = ZOOKEEPER_FLAG + ":";
+  private static final int MAX_PORT = 65535;
   private static final Logger logger = 
LoggerFactory.getLogger(AmsThriftUrl.class);
-  private static final Pattern PATTERN = 
Pattern.compile("zookeeper://(\\S+)/([\\w-]+)");
+  private static final Pattern PATTERN =
+      Pattern.compile("zookeeper://(\\S+)/([\\w-]+)", 
Pattern.CASE_INSENSITIVE);
   private final String schema;
   private final String host;
   private final int port;
@@ -77,7 +80,7 @@ public class AmsThriftUrl {
     if (url == null) {
       throw new IllegalArgumentException("thrift url is null");
     }
-    if (url.startsWith(ZOOKEEPER_FLAG)) {
+    if (url.regionMatches(true, 0, ZOOKEEPER_SCHEME_PREFIX, 0, 
ZOOKEEPER_SCHEME_PREFIX.length())) {
       return parserZookeeperUrl(url, serviceName);
     } else {
       return parserThriftUrl(url);
@@ -85,31 +88,57 @@ public class AmsThriftUrl {
   }
 
   private static AmsThriftUrl parserThriftUrl(String url) {
-    int socketTimeout = DEFAULT_SOCKET_TIMEOUT;
     try {
-      URI uri = new URI(url.toLowerCase(Locale.ROOT));
+      URI uri = new URI(url);
       String schema = uri.getScheme();
+      if (!THRIFT_SCHEME.equalsIgnoreCase(schema)) {
+        throw new IllegalArgumentException(
+            String.format("Unsupported thrift URL scheme '%s' in URL: %s", 
schema, url));
+      }
+
+      uri = uri.parseServerAuthority();
       String host = uri.getHost();
+      if (host == null || host.isEmpty()) {
+        throw new IllegalArgumentException(String.format("Missing host in 
thrift URL: %s", url));
+      }
+
       int port = uri.getPort();
+      if (port == -1) {
+        throw new IllegalArgumentException(String.format("Missing port in 
thrift URL: %s", url));
+      } else if (port <= 0 || port > MAX_PORT) {
+        throw new IllegalArgumentException(
+            String.format("Invalid port in thrift URL, expected 1-%d: %s", 
MAX_PORT, url));
+      }
+
       String path = uri.getPath();
       if (path != null && path.startsWith("/")) {
         path = path.substring(1);
       }
-      if (uri.getQuery() != null) {
-        for (String paramExpression : uri.getQuery().split("&")) {
-          String[] paramSplit = paramExpression.split("=");
-          if (paramSplit.length == 2) {
-            if (paramSplit[0].equalsIgnoreCase(PARAM_SOCKET_TIMEOUT)) {
-              socketTimeout = Integer.parseInt(paramSplit[1]);
-            }
+      String catalogName = path;
+      return new AmsThriftUrl(
+          THRIFT_SCHEME, host, port, catalogName, 
parseSocketTimeout(uri.getQuery()), url);
+    } catch (URISyntaxException e) {
+      throw new IllegalArgumentException(
+          String.format("Invalid thrift URL '%s': %s", url, e.getMessage()), 
e);
+    }
+  }
+
+  private static int parseSocketTimeout(String query) {
+    int socketTimeout = DEFAULT_SOCKET_TIMEOUT;
+    if (query != null) {
+      for (String paramExpression : query.split("&")) {
+        String[] paramSplit = paramExpression.split("=");
+        if (paramSplit.length == 2 && 
paramSplit[0].equalsIgnoreCase(PARAM_SOCKET_TIMEOUT)) {
+          try {
+            socketTimeout = Integer.parseInt(paramSplit[1]);
+          } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(
+                String.format("Invalid socketTimeout value '%s'", 
paramSplit[1]), e);
           }
         }
       }
-      String catalogName = path;
-      return new AmsThriftUrl(schema, host, port, catalogName, socketTimeout, 
url);
-    } catch (URISyntaxException e) {
-      throw new IllegalArgumentException("parse metastore url failed", e);
     }
+    return socketTimeout;
   }
 
   private static AmsThriftUrl parserZookeeperUrl(String url, String 
serviceName) {
@@ -132,33 +161,25 @@ public class AmsThriftUrl {
         zkServerAddress = m.group(1);
         cluster = m.group(2);
       }
+      int socketTimeout = parseSocketTimeout(query.replace("?", ""));
       int retryCount = 0;
       while (retryCount < MAX_RETRIES) {
         try {
           AmsServerInfo serverInfo = findAmsServerInfo(serviceName, 
zkServerAddress, cluster);
-          url =
+          String resolvedUrl =
               String.format(
                   THRIFT_URL_FORMAT,
                   serverInfo.getHost(),
                   serverInfo.getThriftBindPort(),
                   catalog,
                   query);
-          int socketTimeout = DEFAULT_SOCKET_TIMEOUT;
-          for (String paramExpression : query.replace("?", "").split("&")) {
-            String[] paramSplit = paramExpression.split("=");
-            if (paramSplit.length == 2) {
-              if (paramSplit[0].equalsIgnoreCase(PARAM_SOCKET_TIMEOUT)) {
-                socketTimeout = Integer.parseInt(paramSplit[1]);
-              }
-            }
-          }
           return new AmsThriftUrl(
-              "thrift",
+              THRIFT_SCHEME,
               serverInfo.getHost(),
               serverInfo.getThriftBindPort(),
-              catalog.toLowerCase(),
+              catalog,
               socketTimeout,
-              url);
+              resolvedUrl);
         } catch (KeeperException.AuthFailedException authFailedException) {
           // If kerberos authentication is not enabled on the zk,
           // an error occurs when the thread carrying kerberos authentication 
information accesses
@@ -182,11 +203,14 @@ public class AmsThriftUrl {
           retryCount++;
           logger.error(
               String.format("Caught exception, retrying... (retry count: %s)", 
retryCount), e);
-          throw new RuntimeException(String.format("invalid ams url %s", url));
+          throw new RuntimeException(
+              String.format("Failed to resolve AMS URL from ZooKeeper URL: 
%s", url), e);
         }
       }
     } else {
-      throw new RuntimeException(String.format("invalid ams url %s", url));
+      throw new IllegalArgumentException(
+          String.format(
+              "Invalid ZooKeeper URL, expected 
zookeeper://host:port/cluster[/catalog]: %s", url));
     }
     return null;
   }
@@ -217,7 +241,7 @@ public class AmsThriftUrl {
   }
 
   private static List<AmsServerInfo> 
parserZookeeperUrlListForMasterSlaveMode(String url) {
-    if (!url.startsWith(ZOOKEEPER_FLAG)) {
+    if (!url.regionMatches(true, 0, ZOOKEEPER_SCHEME_PREFIX, 0, 
ZOOKEEPER_SCHEME_PREFIX.length())) {
       throw new IllegalArgumentException(
           "parseMasterSlaveAmsNodes only supports ZooKeeper URL format: 
zookeeper://host:port/cluster");
     }
@@ -227,7 +251,8 @@ public class AmsThriftUrl {
     }
     Matcher m = PATTERN.matcher(thriftUrl);
     if (!m.matches()) {
-      throw new RuntimeException(String.format("invalid ams url %s", url));
+      throw new IllegalArgumentException(
+          String.format("Invalid ZooKeeper URL, expected 
zookeeper://host:port/cluster: %s", url));
     }
     String zkServerAddress;
     String cluster;
diff --git 
a/amoro-common/src/test/java/org/apache/amoro/client/TestAmsThriftUrl.java 
b/amoro-common/src/test/java/org/apache/amoro/client/TestAmsThriftUrl.java
new file mode 100644
index 000000000..55e57a329
--- /dev/null
+++ b/amoro-common/src/test/java/org/apache/amoro/client/TestAmsThriftUrl.java
@@ -0,0 +1,115 @@
+/*
+ * 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.amoro.client;
+
+import org.apache.amoro.Constants;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestAmsThriftUrl {
+  @Test
+  public void testParseMixedCaseThriftUrl() {
+    String url = "ThRiFt://LOCALHOST:1260/MyCatalog?socketTimeout=6000";
+
+    AmsThriftUrl thriftUrl = AmsThriftUrl.parse(url, null);
+
+    Assertions.assertEquals("thrift", thriftUrl.schema());
+    Assertions.assertEquals("LOCALHOST", thriftUrl.host());
+    Assertions.assertEquals(1260, thriftUrl.port());
+    Assertions.assertEquals("MyCatalog", thriftUrl.catalogName());
+    Assertions.assertEquals(6000, thriftUrl.socketTimeout());
+    Assertions.assertEquals(url, thriftUrl.url());
+  }
+
+  @Test
+  public void testParseThriftUrl() {
+    AmsThriftUrl thriftUrl =
+        AmsThriftUrl.parse(
+            "thrift://127.0.0.1:1260/catalog?socketTimeout=7000",
+            Constants.THRIFT_TABLE_SERVICE_NAME);
+
+    Assertions.assertEquals("127.0.0.1", thriftUrl.host());
+    Assertions.assertEquals(1260, thriftUrl.port());
+    Assertions.assertEquals("catalog", thriftUrl.catalogName());
+    Assertions.assertEquals(7000, thriftUrl.socketTimeout());
+  }
+
+  @Test
+  public void testRejectUnsupportedScheme() {
+    assertInvalidUrl("http://127.0.0.1:1260/catalog";, "scheme");
+    assertInvalidUrl("zookeeperx://127.0.0.1:2181/cluster", "scheme");
+  }
+
+  @Test
+  public void testRejectMissingHost() {
+    assertInvalidUrl("thrift:///catalog", "host");
+  }
+
+  @Test
+  public void testRejectMissingPort() {
+    assertInvalidUrl("thrift://127.0.0.1/catalog", "port");
+  }
+
+  @Test
+  public void testRejectInvalidPort() {
+    assertInvalidUrl("thrift://127.0.0.1:65536/catalog", "port");
+  }
+
+  @Test
+  public void testRejectInvalidZookeeperUrl() {
+    assertInvalidUrl("zookeeper:/127.0.0.1:2181/cluster", "ZooKeeper URL");
+  }
+
+  @Test
+  public void testParseZookeeperUrlBeforeServiceResolution() {
+    String url = 
"ZoOkEePeR://127.0.0.1:2181/test-cluster/MyCatalog?socketTimeout=8000";
+
+    RuntimeException exception =
+        Assertions.assertThrows(
+            RuntimeException.class, () -> AmsThriftUrl.parse(url, 
"unsupported-service"));
+
+    Assertions.assertTrue(
+        exception.getMessage().contains("Failed to resolve AMS URL from 
ZooKeeper URL"),
+        exception.getMessage());
+    Assertions.assertNotNull(exception.getCause());
+    Assertions.assertTrue(
+        exception.getCause().getMessage().contains("invalid service name 
unsupported-service"),
+        exception.getCause().getMessage());
+  }
+
+  @Test
+  public void testRejectInvalidZookeeperSocketTimeout() {
+    String url = 
"ZoOkEePeR://127.0.0.1:2181/test-cluster/MyCatalog?socketTimeout=invalid";
+
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () -> AmsThriftUrl.parse(url, 
Constants.THRIFT_TABLE_SERVICE_NAME));
+
+    Assertions.assertTrue(
+        exception.getMessage().contains("Invalid socketTimeout value"), 
exception.getMessage());
+  }
+
+  private void assertInvalidUrl(String url, String messagePart) {
+    IllegalArgumentException exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class, () -> AmsThriftUrl.parse(url, 
null));
+    Assertions.assertTrue(exception.getMessage().contains(messagePart), 
exception.getMessage());
+  }
+}

Reply via email to