From 523eed3b645d5a39482dd16aa90e186870fd45a7 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 17 Feb 2026 20:29:48 +0000 Subject: [PATCH 01/11] feat(jdbc): enforce strict JDBC URL parsing and validation --- .../cloud/bigquery/jdbc/BigQueryDriver.java | 6 ++ .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 101 ++++++++++++++++-- 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java index 11199c372..e3a6e0b94 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java @@ -17,6 +17,7 @@ package com.google.cloud.bigquery.jdbc; import com.google.cloud.bigquery.exception.BigQueryJdbcException; +import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; import io.grpc.LoadBalancerRegistry; import io.grpc.internal.PickFirstLoadBalancerProvider; import java.io.IOException; @@ -123,6 +124,11 @@ public Connection connect(String url, Properties info) throws SQLException { // strip 'jdbc:' from the URL, add any extra properties String connectionUri = BigQueryJdbcUrlUtility.appendPropertiesToURL(url.substring(5), this.toString(), info); + try { + BigQueryJdbcUrlUtility.parseUrl(connectionUri); + } catch (BigQueryJdbcRuntimeException e) { + throw new BigQueryJdbcException(e.getMessage()); + } // LogLevel String logLevelStr = diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index 50d0d33bb..b2e92d611 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -122,6 +122,11 @@ final class BigQueryJdbcUrlUtility { static final String BYOID_SUBJECT_TOKEN_TYPE_PROPERTY_NAME = "BYOID_SubjectTokenType"; static final String BYOID_TOKEN_URI_PROPERTY_NAME = "BYOID_TokenUri"; static final String PARTNER_TOKEN_PROPERTY_NAME = "PartnerToken"; + private static final Pattern PARTNER_TOKEN_PATTERN = + Pattern.compile( + "(?i)" + + PARTNER_TOKEN_PROPERTY_NAME + + "=\\s*\\(\\s*(GPN:[^;]*?)\\s*(?:;\\s*([^)]*?))?\\s*\\)"); static final String METADATA_FETCH_THREAD_COUNT_PROPERTY_NAME = "MetaDataFetchThreadCount"; static final int DEFAULT_METADATA_FETCH_THREAD_COUNT_VALUE = 32; static final String RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME = "Timeout"; @@ -591,6 +596,29 @@ final class BigQueryJdbcUrlUtility { + " header.") .build()))); + private static final Map PROPERTY_NAME_MAP; + + static { + Map map = new HashMap<>(); + for (BigQueryConnectionProperty p : VALID_PROPERTIES) { + map.put(p.getName().toUpperCase(), p.getName()); + } + for (BigQueryConnectionProperty p : AUTH_PROPERTIES) { + map.put(p.getName().toUpperCase(), p.getName()); + } + for (BigQueryConnectionProperty p : PROXY_PROPERTIES) { + map.put(p.getName().toUpperCase(), p.getName()); + } + for (String p : OVERRIDE_PROPERTIES) { + map.put(p.toUpperCase(), p); + } + for (String p : BYOID_PROPERTIES) { + map.put(p.toUpperCase(), p); + } + map.put(PARTNER_TOKEN_PROPERTY_NAME.toUpperCase(), PARTNER_TOKEN_PROPERTY_NAME); + PROPERTY_NAME_MAP = Collections.unmodifiableMap(map); + } + private BigQueryJdbcUrlUtility() {} /** @@ -601,12 +629,64 @@ private BigQueryJdbcUrlUtility() {} * @return The String value of the property, or the default value if the property is not found. */ static String parseUriProperty(String uri, String property) { - Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property)); - Matcher matcher = pattern.matcher(uri); - if (matcher.find() && matcher.groupCount() == 1) { - return CharEscapers.decodeUriPath(matcher.group(1)); + Map map = parseUrl(uri); + if (PROPERTY_NAME_MAP.containsKey(property.toUpperCase())) { + return map.get(PROPERTY_NAME_MAP.get(property.toUpperCase())); } - return null; + return map.get(property); + } + + /** + * Parses the URL into a map of key-value pairs, validating that all keys are known properties. + * + * @param url The URL to parse. + * @return A map of property names to values. + * @throws BigQueryJdbcRuntimeException if an unknown property is found or the URL is malformed. + */ + static Map parseUrl(String url) { + Map map = new HashMap<>(); + if (url == null) { + return map; + } + + int start = url.indexOf(';'); + if (start == -1) { + return map; + } + + String urlToParse = url.substring(start + 1); + + // Parse PartnerToken separately as it contains ';' + StringBuilder urlBuilder = new StringBuilder(urlToParse); + String partnerToken = parseAndRemovePartnerTokenProperty(urlBuilder, "parseUrl"); + if (partnerToken != null) { + map.put(PARTNER_TOKEN_PROPERTY_NAME, partnerToken); + urlToParse = urlBuilder.toString(); + } + + String[] parts = urlToParse.split(";"); + for (String part : parts) { + if (part.trim().isEmpty()) { + continue; + } + String[] kv = part.split("=", 2); + String key = kv[0].trim(); + if (kv.length == 1) { + if (!key.isEmpty()) { + throw new BigQueryJdbcRuntimeException("Property '" + key + "' has no value."); + } + } else { + String value = kv[1]; // Value might be empty string if "Key=" + if (!key.isEmpty()) { + String upperCaseKey = key.toUpperCase(); + if (!PROPERTY_NAME_MAP.containsKey(upperCaseKey)) { + throw new BigQueryJdbcRuntimeException("Unknown property: " + key); + } + map.put(PROPERTY_NAME_MAP.get(upperCaseKey), CharEscapers.decodeUriPath(value)); + } + } + } + return map; } /** @@ -697,10 +777,12 @@ public static String parsePartnerTokenProperty(String url, String callerClassNam LOG.finest("++enter++\t" + callerClassName); // This property is expected to be set by partners only. For more details on exact format // supported, refer b/396086960 - String regex = - PARTNER_TOKEN_PROPERTY_NAME + "=\\s*\\(\\s*(GPN:[^;]*?)\\s*(?:;\\s*([^)]*?))?\\s*\\)"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(url); + return parseAndRemovePartnerTokenProperty(new StringBuilder(url), callerClassName); + } + + private static String parseAndRemovePartnerTokenProperty( + StringBuilder urlBuilder, String callerClassName) { + Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(urlBuilder); if (matcher.find()) { String gpnPart = matcher.group(1); @@ -712,6 +794,7 @@ public static String parsePartnerTokenProperty(String url, String callerClassNam partnerToken.append(environmentPart); } partnerToken.append(")"); + urlBuilder.delete(matcher.start(), matcher.end()); return partnerToken.toString(); } return null; From 5d6304f3ca85bbd598ef047010d9cd019ea17f7b Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 17 Feb 2026 20:30:05 +0000 Subject: [PATCH 02/11] feat(jdbc): sync DataSource properties with BigQueryConnection --- .../cloud/bigquery/jdbc/DataSource.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java index b1501890b..d5c2ab1a4 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java @@ -78,6 +78,17 @@ public class DataSource implements javax.sql.DataSource { private Integer metadataFetchThreadCount; private String sslTrustStorePath; private String sslTrustStorePassword; + private String labels; + private String requestReason; + private Integer timeout; + private Integer jobTimeout; + private Integer retryInitialDelay; + private Integer retryMaxDelay; + private Integer httpConnectTimeout; + private Integer httpReadTimeout; + private Long maximumBytesBilled; + private Integer swaActivationRowCount; + private Integer swaAppendRowCount; // Make sure the JDBC driver class is loaded. static { @@ -283,6 +294,55 @@ private Properties createProperties() { BigQueryJdbcUrlUtility.SSL_TRUST_STORE_PWD_PROPERTY_NAME, String.valueOf(this.sslTrustStorePassword)); } + if (this.labels != null) { + connectionProperties.setProperty(BigQueryJdbcUrlUtility.LABELS_PROPERTY_NAME, this.labels); + } + if (this.requestReason != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.REQUEST_REASON_PROPERTY_NAME, this.requestReason); + } + if (this.timeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME, String.valueOf(this.timeout)); + } + if (this.jobTimeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.JOB_TIMEOUT_PROPERTY_NAME, String.valueOf(this.jobTimeout)); + } + if (this.retryInitialDelay != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.RETRY_INITIAL_DELAY_PROPERTY_NAME, + String.valueOf(this.retryInitialDelay)); + } + if (this.retryMaxDelay != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.RETRY_MAX_DELAY_PROPERTY_NAME, String.valueOf(this.retryMaxDelay)); + } + if (this.httpConnectTimeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.HTTP_CONNECT_TIMEOUT_PROPERTY_NAME, + String.valueOf(this.httpConnectTimeout)); + } + if (this.httpReadTimeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.HTTP_READ_TIMEOUT_PROPERTY_NAME, + String.valueOf(this.httpReadTimeout)); + } + if (this.maximumBytesBilled != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.MAX_BYTES_BILLED_PROPERTY_NAME, + String.valueOf(this.maximumBytesBilled)); + } + if (this.swaActivationRowCount != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.SWA_ACTIVATION_ROW_COUNT_PROPERTY_NAME, + String.valueOf(this.swaActivationRowCount)); + } + if (this.swaAppendRowCount != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.SWA_APPEND_ROW_COUNT_PROPERTY_NAME, + String.valueOf(this.swaAppendRowCount)); + } return connectionProperties; } @@ -631,6 +691,94 @@ public void setSSLTrustStorePassword(String sslTrustStorePassword) { this.sslTrustStorePassword = sslTrustStorePassword; } + public String getLabels() { + return labels; + } + + public void setLabels(String labels) { + this.labels = labels; + } + + public String getRequestReason() { + return requestReason; + } + + public void setRequestReason(String requestReason) { + this.requestReason = requestReason; + } + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public Integer getJobTimeout() { + return jobTimeout; + } + + public void setJobTimeout(Integer jobTimeout) { + this.jobTimeout = jobTimeout; + } + + public Integer getRetryInitialDelay() { + return retryInitialDelay; + } + + public void setRetryInitialDelay(Integer retryInitialDelay) { + this.retryInitialDelay = retryInitialDelay; + } + + public Integer getRetryMaxDelay() { + return retryMaxDelay; + } + + public void setRetryMaxDelay(Integer retryMaxDelay) { + this.retryMaxDelay = retryMaxDelay; + } + + public Integer getHttpConnectTimeout() { + return httpConnectTimeout; + } + + public void setHttpConnectTimeout(Integer httpConnectTimeout) { + this.httpConnectTimeout = httpConnectTimeout; + } + + public Integer getHttpReadTimeout() { + return httpReadTimeout; + } + + public void setHttpReadTimeout(Integer httpReadTimeout) { + this.httpReadTimeout = httpReadTimeout; + } + + public Long getMaximumBytesBilled() { + return maximumBytesBilled; + } + + public void setMaximumBytesBilled(Long maximumBytesBilled) { + this.maximumBytesBilled = maximumBytesBilled; + } + + public Integer getSwaActivationRowCount() { + return swaActivationRowCount; + } + + public void setSwaActivationRowCount(Integer swaActivationRowCount) { + this.swaActivationRowCount = swaActivationRowCount; + } + + public Integer getSwaAppendRowCount() { + return swaAppendRowCount; + } + + public void setSwaAppendRowCount(Integer swaAppendRowCount) { + this.swaAppendRowCount = swaAppendRowCount; + } + @Override public PrintWriter getLogWriter() { return null; From df71d2cd073e7747700a99422527f35f34f54dad Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 17 Feb 2026 20:30:30 +0000 Subject: [PATCH 03/11] test: update tests for strict parsing --- .../jdbc/BigQueryJdbcUrlUtilityTest.java | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index b16d1a398..72c2c9116 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -47,6 +47,30 @@ public void testParsePropertyWithNoDefault() { assertThat(result).isNull(); } + @Test + public void testParseUrlWithUnknownProperty_throwsException() { + String url = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "UnknownProperty=SomeValue"; + + assertThrows( + BigQueryJdbcRuntimeException.class, + () -> BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId")); + } + + @Test + public void testParseUrlWithTypo_throwsException() { + String url = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "ProjeectId=TypoValue"; + + assertThrows( + BigQueryJdbcRuntimeException.class, + () -> BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId")); + } + @Test public void testParsePropertyWithDefault() { String url = @@ -106,18 +130,18 @@ public void testConnectionPropertiesFromURI() { @Test public void testConnectionPropertiesFromURIMultiline() { String connection_uri = - "bigquery://https://www.googleapis.com/bigquery/v2:443;Multiline=value1\nvalue2\n;"; + "bigquery://https://www.googleapis.com/bigquery/v2:443;OAuthPvtKey=value1\nvalue2\n;"; - assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "Multiline")) + assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "OAuthPvtKey")) .isEqualTo("value1\nvalue2\n"); } @Test public void testConnectionPropertiesFromURIMultilineNoSemicolon() { String connection_uri = - "bigquery://https://www.googleapis.com/bigquery/v2:443;Multiline=value1\nvalue2"; + "bigquery://https://www.googleapis.com/bigquery/v2:443;OAuthPvtKey=value1\nvalue2"; - assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "Multiline")) + assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "OAuthPvtKey")) .isEqualTo("value1\nvalue2"); } @@ -641,19 +665,22 @@ public void testParseListenerPoolSizeInvalidLong() { public void testParseStringListProperty_NullOrEmpty() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;SomeProp="; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes="; List result = BigQueryJdbcUrlUtility.parseStringListProperty(url, "NonExistentProp", "TestClass"); assertEquals(Collections.emptyList(), result); - result = BigQueryJdbcUrlUtility.parseStringListProperty(url, "SomeProp", "TestClass"); + result = + BigQueryJdbcUrlUtility.parseStringListProperty( + url, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Collections.emptyList(), result); String urlWithEmptyList = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;ListProp=,,"; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes=,,"; result = - BigQueryJdbcUrlUtility.parseStringListProperty(urlWithEmptyList, "ListProp", "TestClass"); + BigQueryJdbcUrlUtility.parseStringListProperty( + urlWithEmptyList, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Collections.emptyList(), result); } @@ -661,9 +688,10 @@ public void testParseStringListProperty_NullOrEmpty() { public void testParseStringListProperty_SingleValue() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;ListProp=project1"; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes=project1"; List result = - BigQueryJdbcUrlUtility.parseStringListProperty(url, "ListProp", "TestClass"); + BigQueryJdbcUrlUtility.parseStringListProperty( + url, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Collections.singletonList("project1"), result); } @@ -671,9 +699,10 @@ public void testParseStringListProperty_SingleValue() { public void testParseStringListProperty_MultipleValues() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;ListProp=project1,project2,project3"; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes=project1,project2,project3"; List result = - BigQueryJdbcUrlUtility.parseStringListProperty(url, "ListProp", "TestClass"); + BigQueryJdbcUrlUtility.parseStringListProperty( + url, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Arrays.asList("project1", "project2", "project3"), result); } @@ -681,10 +710,11 @@ public void testParseStringListProperty_MultipleValues() { public void testParseIntProperty_ValidInteger() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;SomeIntProp=123"; + + "OAuthType=2;ProjectId=MyBigQueryProject;HttpConnectTimeout=123"; Integer defaultValue = 0; Integer result = - BigQueryJdbcUrlUtility.parseIntProperty(url, "SomeIntProp", defaultValue, "TestClass"); + BigQueryJdbcUrlUtility.parseIntProperty( + url, "HttpConnectTimeout", defaultValue, "TestClass"); assertEquals(Integer.valueOf(123), result); } @@ -692,10 +722,11 @@ public void testParseIntProperty_ValidInteger() { public void testParseIntProperty_PropertyNotPresent() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;SomeIntProp=123"; + + "OAuthType=2;ProjectId=MyBigQueryProject;"; Integer defaultValue = 42; Integer result = - BigQueryJdbcUrlUtility.parseIntProperty(url, "MissingIntProp", defaultValue, "TestClass"); + BigQueryJdbcUrlUtility.parseIntProperty( + url, "HttpConnectTimeout", defaultValue, "TestClass"); assertEquals(defaultValue, result); } @@ -703,26 +734,26 @@ public void testParseIntProperty_PropertyNotPresent() { public void testParseIntProperty_InvalidIntegerValue() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;InvalidIntProp=abc"; + + "OAuthType=2;ProjectId=MyBigQueryProject;HttpConnectTimeout=abc"; Integer defaultValue = 77; assertThrows( IllegalArgumentException.class, () -> BigQueryJdbcUrlUtility.parseIntProperty( - url, "InvalidIntProp", defaultValue, "TestClass")); + url, "HttpConnectTimeout", defaultValue, "TestClass")); } @Test public void testParseIntProperty_EmptyStringValue() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;EmptyIntProp="; + + "OAuthType=2;ProjectId=MyBigQueryProject;HttpConnectTimeout="; Integer defaultValue = 88; assertThrows( IllegalArgumentException.class, () -> BigQueryJdbcUrlUtility.parseIntProperty( - url, "EmptyIntProp", defaultValue, "TestClass")); + url, "HttpConnectTimeout", defaultValue, "TestClass")); } @Test From 56b7b09b32fede4a66871fb375c1d5a432805e90 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 17 Feb 2026 23:06:47 +0000 Subject: [PATCH 04/11] chore: fix failing tests --- .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 26 ++++++++----------- .../jdbc/BigQueryJdbcUrlUtilityTest.java | 2 +- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index b2e92d611..5f7b92964 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -123,10 +123,7 @@ final class BigQueryJdbcUrlUtility { static final String BYOID_TOKEN_URI_PROPERTY_NAME = "BYOID_TokenUri"; static final String PARTNER_TOKEN_PROPERTY_NAME = "PartnerToken"; private static final Pattern PARTNER_TOKEN_PATTERN = - Pattern.compile( - "(?i)" - + PARTNER_TOKEN_PROPERTY_NAME - + "=\\s*\\(\\s*(GPN:[^;]*?)\\s*(?:;\\s*([^)]*?))?\\s*\\)"); + Pattern.compile("(?i)" + PARTNER_TOKEN_PROPERTY_NAME + "=\\s*\\(([^)]*)\\)"); static final String METADATA_FETCH_THREAD_COUNT_PROPERTY_NAME = "MetaDataFetchThreadCount"; static final int DEFAULT_METADATA_FETCH_THREAD_COUNT_VALUE = 32; static final String RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME = "Timeout"; @@ -616,6 +613,8 @@ final class BigQueryJdbcUrlUtility { map.put(p.toUpperCase(), p); } map.put(PARTNER_TOKEN_PROPERTY_NAME.toUpperCase(), PARTNER_TOKEN_PROPERTY_NAME); + map.put(ENDPOINT_OVERRIDES_PROPERTY_NAME.toUpperCase(), ENDPOINT_OVERRIDES_PROPERTY_NAME); + map.put(PRIVATE_SERVICE_CONNECT_PROPERTY_NAME.toUpperCase(), PRIVATE_SERVICE_CONNECT_PROPERTY_NAME); PROPERTY_NAME_MAP = Collections.unmodifiableMap(map); } @@ -659,9 +658,9 @@ static Map parseUrl(String url) { // Parse PartnerToken separately as it contains ';' StringBuilder urlBuilder = new StringBuilder(urlToParse); String partnerToken = parseAndRemovePartnerTokenProperty(urlBuilder, "parseUrl"); + urlToParse = urlBuilder.toString(); if (partnerToken != null) { map.put(PARTNER_TOKEN_PROPERTY_NAME, partnerToken); - urlToParse = urlBuilder.toString(); } String[] parts = urlToParse.split(";"); @@ -785,17 +784,14 @@ private static String parseAndRemovePartnerTokenProperty( Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(urlBuilder); if (matcher.find()) { - String gpnPart = matcher.group(1); - String environmentPart = matcher.group(2); - StringBuilder partnerToken = new StringBuilder(" ("); - partnerToken.append(gpnPart); - if (environmentPart != null && !environmentPart.trim().isEmpty()) { - partnerToken.append("; "); - partnerToken.append(environmentPart); + String content = matcher.group(1).trim(); + if (content.toUpperCase().startsWith("GPN:")) { + urlBuilder.delete(matcher.start(), matcher.end()); + return " (" + content + ")"; + } else { + urlBuilder.delete(matcher.start(), matcher.end()); + return null; } - partnerToken.append(")"); - urlBuilder.delete(matcher.start(), matcher.end()); - return partnerToken.toString(); } return null; } diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index 72c2c9116..49b400618 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -460,7 +460,7 @@ public void testParsePartnerTokenProperty() { url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + "PartnerToken= ( GPN: partner_name ; test_env ) ;"; - expected = " (GPN: partner_name; test_env)"; + expected = " (GPN: partner_name ; test_env)"; result = BigQueryJdbcUrlUtility.parsePartnerTokenProperty(url, "testParsePartnerTokenProperty"); assertThat(result).isEqualTo(expected); } From fe67f1dca72c33a1f795865d090c9fb31883a0fe Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 18 Feb 2026 00:26:42 +0000 Subject: [PATCH 05/11] chore: throw original `BigQueryJdbcRuntimeException` --- .../cloud/bigquery/jdbc/BigQueryDriver.java | 2 +- .../cloud/bigquery/jdbc/BigQueryDriverTest.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java index e3a6e0b94..68b00cb6c 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java @@ -127,7 +127,7 @@ public Connection connect(String url, Properties info) throws SQLException { try { BigQueryJdbcUrlUtility.parseUrl(connectionUri); } catch (BigQueryJdbcRuntimeException e) { - throw new BigQueryJdbcException(e.getMessage()); + throw new BigQueryJdbcException(e.getMessage(), e); } // LogLevel diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java index 6a33c6144..2d7664f5a 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java @@ -16,7 +16,10 @@ package com.google.cloud.bigquery.jdbc; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; +import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; import java.sql.Connection; import java.sql.DriverPropertyInfo; import java.sql.SQLException; @@ -93,4 +96,17 @@ public void testGetParentLoggerReturnsLogger() { public void testJDBCCompliantReturnsFalse() { assertThat(bigQueryDriver.jdbcCompliant()).isFalse(); } + + @Test + public void testConnectWithInvalidUrlChainsException() { + try { + bigQueryDriver.connect( + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;InvalidProperty=Value", + new Properties()); + fail("Expected SQLException"); + } catch (SQLException e) { + assertThat((Throwable) e).isInstanceOf(BigQueryJdbcException.class); + assertThat(e.getCause()).isInstanceOf(BigQueryJdbcRuntimeException.class); + } + } } From 73c0bcef7494933483a04c55be4cb7b4a5bf45ac Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 18 Feb 2026 00:32:26 +0000 Subject: [PATCH 06/11] chore: add url encoding to appendProperties to prevent injection vulnerabilites --- .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 23 +++++++++++-- .../jdbc/BigQueryJdbcUrlUtilityTest.java | 34 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index 5f7b92964..de4b8c110 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -19,6 +19,10 @@ import com.google.api.client.util.escape.CharEscapers; import com.google.cloud.bigquery.BigQueryOptions; import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -614,7 +618,8 @@ final class BigQueryJdbcUrlUtility { } map.put(PARTNER_TOKEN_PROPERTY_NAME.toUpperCase(), PARTNER_TOKEN_PROPERTY_NAME); map.put(ENDPOINT_OVERRIDES_PROPERTY_NAME.toUpperCase(), ENDPOINT_OVERRIDES_PROPERTY_NAME); - map.put(PRIVATE_SERVICE_CONNECT_PROPERTY_NAME.toUpperCase(), PRIVATE_SERVICE_CONNECT_PROPERTY_NAME); + map.put( + PRIVATE_SERVICE_CONNECT_PROPERTY_NAME.toUpperCase(), PRIVATE_SERVICE_CONNECT_PROPERTY_NAME); PROPERTY_NAME_MAP = Collections.unmodifiableMap(map); } @@ -701,7 +706,14 @@ static String appendPropertiesToURL(String url, String callerClassName, Properti for (Entry entry : properties.entrySet()) { if (entry.getValue() != null && !"".equals(entry.getValue())) { LOG.finest("Appending %s with value %s to URL", entry.getKey(), entry.getValue()); - urlBuilder.append(";").append(entry.getKey()).append("=").append(entry.getValue()); + try { + String encodedValue = + URLEncoder.encode((String) entry.getValue(), StandardCharsets.UTF_8.name()) + .replace("+", "%20"); + urlBuilder.append(";").append(entry.getKey()).append("=").append(encodedValue); + } catch (UnsupportedEncodingException e) { + throw new BigQueryJdbcRuntimeException(e); + } } } return urlBuilder.toString(); @@ -872,7 +884,12 @@ static Map parseOverrideProperties(String url, String callerClas Matcher matcher = pattern.matcher(url); String overridePropertiesString; if (matcher.find() && matcher.groupCount() >= 1) { - overridePropertiesString = matcher.group(2); + try { + overridePropertiesString = + URLDecoder.decode(matcher.group(2), StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new BigQueryJdbcRuntimeException(e); + } } else { return overrideProps; } diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index 49b400618..131a5d42a 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -891,4 +891,38 @@ public void testParseRequestReason() { "testParseRequestReason"); assertEquals("testingRequestReason", requestReason); } + + @Test + public void testAppendPropertiesToURL_propertyWithSemicolon_isEscaped() throws Exception { + String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"; + Properties properties = new Properties(); + String complexValue = "value;ExtraProperty=injection"; + properties.setProperty("ProjectId", complexValue); + + String updatedUrl = BigQueryJdbcUrlUtility.appendPropertiesToURL(url, null, properties); + + Map parsedProperties = BigQueryJdbcUrlUtility.parseUrl(updatedUrl); + + assertThat(parsedProperties.get("ProjectId")).isEqualTo(complexValue); + assertFalse(parsedProperties.containsKey("ExtraProperty")); + } + + @Test + public void testEndpointOverrides_viaProperties() throws Exception { + String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443"; + Properties props = new Properties(); + + String overrides = "OAUTH2=http://localhost:1234,BIGQUERY=http://localhost:9090"; + props.setProperty("EndpointOverrides", overrides); + + String updatedUrl = BigQueryJdbcUrlUtility.appendPropertiesToURL(url, null, props); + + Map parsedOverrides = + BigQueryJdbcUrlUtility.parseOverrideProperties(updatedUrl, null); + + assertThat(parsedOverrides).containsKey("OAUTH2"); + assertThat(parsedOverrides.get("OAUTH2")).isEqualTo("http://localhost:1234"); + assertThat(parsedOverrides).containsKey("BIGQUERY"); + assertThat(parsedOverrides.get("BIGQUERY")).isEqualTo("http://localhost:9090"); + } } From e25caf49996f2520f4cc61698eebdbe2852d5bb0 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 18 Feb 2026 13:29:43 +0000 Subject: [PATCH 07/11] chore: refactor `parseAndRemovePartnerTokenProperty()` --- .../google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index de4b8c110..d4f440606 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -797,12 +797,9 @@ private static String parseAndRemovePartnerTokenProperty( if (matcher.find()) { String content = matcher.group(1).trim(); + urlBuilder.delete(matcher.start(), matcher.end()); if (content.toUpperCase().startsWith("GPN:")) { - urlBuilder.delete(matcher.start(), matcher.end()); return " (" + content + ")"; - } else { - urlBuilder.delete(matcher.start(), matcher.end()); - return null; } } return null; From 9fa4baeb4b55088490dcd5da081e3a1a2a2cd3b0 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 18 Feb 2026 13:30:17 +0000 Subject: [PATCH 08/11] fix: use "ProjectId" instead of "Project_Id" in tests --- .../com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java index 616f3bab9..1ee627b8a 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java @@ -51,7 +51,7 @@ protected static URIBuilder getBaseUri(int authType) { } protected static URIBuilder getBaseUri(int authType, String projectId) { - return getBaseUri(authType).append("PROJECT_ID", projectId); + return getBaseUri(authType).append("ProjectId", projectId); } protected static URIBuilder getUriOAuthServiceAccount() { From 423594ae239d04efeb5b08e1fab4afe126f5bd4e Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 18 Feb 2026 13:42:38 +0000 Subject: [PATCH 09/11] chore: add error message sanitization --- .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 6 ++++-- .../bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index d4f440606..46cf945ec 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -677,14 +677,16 @@ static Map parseUrl(String url) { String key = kv[0].trim(); if (kv.length == 1) { if (!key.isEmpty()) { - throw new BigQueryJdbcRuntimeException("Property '" + key + "' has no value."); + String safeKey = key.length() > 32 ? key.substring(0, 32) + "..." : key; + throw new BigQueryJdbcRuntimeException("Property '" + safeKey + "' has no value."); } } else { String value = kv[1]; // Value might be empty string if "Key=" if (!key.isEmpty()) { String upperCaseKey = key.toUpperCase(); if (!PROPERTY_NAME_MAP.containsKey(upperCaseKey)) { - throw new BigQueryJdbcRuntimeException("Unknown property: " + key); + String safeKey = key.length() > 32 ? key.substring(0, 32) + "..." : key; + throw new BigQueryJdbcRuntimeException("Unknown property: " + safeKey); } map.put(PROPERTY_NAME_MAP.get(upperCaseKey), CharEscapers.decodeUriPath(value)); } diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index 131a5d42a..c67ec670a 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -145,6 +145,21 @@ public void testConnectionPropertiesFromURIMultilineNoSemicolon() { .isEqualTo("value1\nvalue2"); } + @Test + public void testParseUrl_longUnknownProperty_sanitized() { + String longKey = String.join("", Collections.nCopies(50, "a")); + String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + longKey + "=value"; + + BigQueryJdbcRuntimeException e = + assertThrows( + BigQueryJdbcRuntimeException.class, () -> BigQueryJdbcUrlUtility.parseUrl(url)); + + assertThat(e.getMessage()).contains("Unknown property:"); + assertThat(e.getMessage()).contains("..."); + assertThat(e.getMessage()).doesNotContain(longKey); + assertThat(e.getMessage().length()).isLessThan(100); + } + @Test public void testOverridePropertiesFromURICompatibility() { String connection_uri = From 6709e7e3602178ea8a4020fa2b021f1252f9426f Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Sat, 21 Feb 2026 00:13:19 +0000 Subject: [PATCH 10/11] chore: address pr feedback --- .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 109 ++++++++++-------- .../cloud/bigquery/jdbc/DataSource.java | 25 +++- .../jdbc/BigQueryJdbcUrlUtilityTest.java | 2 +- 3 files changed, 80 insertions(+), 56 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index 46cf945ec..612dfa4b2 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -19,14 +19,16 @@ import com.google.api.client.util.escape.CharEscapers; import com.google.cloud.bigquery.BigQueryOptions; import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; +import com.google.common.collect.ImmutableList; +import com.google.common.net.UrlEscapers; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -43,6 +45,14 @@ */ final class BigQueryJdbcUrlUtility { + private static final Map> PARSE_CACHE = + Collections.synchronizedMap( + new LinkedHashMap>(50, 0.75f, true) { + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > 50; // bound cache size + } + }); + // TODO: Add all Connection options static final String ALLOW_LARGE_RESULTS_PROPERTY_NAME = "AllowLargeResults"; static final String LARGE_RESULTS_TABLE_PROPERTY_NAME = "LargeResultTable"; @@ -127,7 +137,10 @@ final class BigQueryJdbcUrlUtility { static final String BYOID_TOKEN_URI_PROPERTY_NAME = "BYOID_TokenUri"; static final String PARTNER_TOKEN_PROPERTY_NAME = "PartnerToken"; private static final Pattern PARTNER_TOKEN_PATTERN = - Pattern.compile("(?i)" + PARTNER_TOKEN_PROPERTY_NAME + "=\\s*\\(([^)]*)\\)"); + Pattern.compile( + "(?i)(?:^|(?<=;))\\s*" + + PARTNER_TOKEN_PROPERTY_NAME + + "\\s*=\\s*\\(([^)]*)\\)\\s*(?=;|$)"); static final String METADATA_FETCH_THREAD_COUNT_PROPERTY_NAME = "MetaDataFetchThreadCount"; static final int DEFAULT_METADATA_FETCH_THREAD_COUNT_VALUE = 32; static final String RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME = "Timeout"; @@ -597,6 +610,12 @@ final class BigQueryJdbcUrlUtility { + " header.") .build()))); + private static final List NETWORK_PROPERTIES = + ImmutableList.of( + PARTNER_TOKEN_PROPERTY_NAME, + ENDPOINT_OVERRIDES_PROPERTY_NAME, + PRIVATE_SERVICE_CONNECT_PROPERTY_NAME); + private static final Map PROPERTY_NAME_MAP; static { @@ -616,10 +635,9 @@ final class BigQueryJdbcUrlUtility { for (String p : BYOID_PROPERTIES) { map.put(p.toUpperCase(), p); } - map.put(PARTNER_TOKEN_PROPERTY_NAME.toUpperCase(), PARTNER_TOKEN_PROPERTY_NAME); - map.put(ENDPOINT_OVERRIDES_PROPERTY_NAME.toUpperCase(), ENDPOINT_OVERRIDES_PROPERTY_NAME); - map.put( - PRIVATE_SERVICE_CONNECT_PROPERTY_NAME.toUpperCase(), PRIVATE_SERVICE_CONNECT_PROPERTY_NAME); + for (String p : NETWORK_PROPERTIES) { + map.put(p.toUpperCase(), p); + } PROPERTY_NAME_MAP = Collections.unmodifiableMap(map); } @@ -648,24 +666,33 @@ static String parseUriProperty(String uri, String property) { * @throws BigQueryJdbcRuntimeException if an unknown property is found or the URL is malformed. */ static Map parseUrl(String url) { + if (url == null) { + return Collections.emptyMap(); + } + return PARSE_CACHE.computeIfAbsent(url, BigQueryJdbcUrlUtility::parseUrlInternal); + } + + private static Map parseUrlInternal(String url) { Map map = new HashMap<>(); if (url == null) { return map; } - int start = url.indexOf(';'); - if (start == -1) { + String[] urlParts = url.split(";", 2); + if (urlParts.length < 2) { return map; } - String urlToParse = url.substring(start + 1); + String urlToParse = urlParts[1]; // Parse PartnerToken separately as it contains ';' - StringBuilder urlBuilder = new StringBuilder(urlToParse); - String partnerToken = parseAndRemovePartnerTokenProperty(urlBuilder, "parseUrl"); - urlToParse = urlBuilder.toString(); - if (partnerToken != null) { - map.put(PARTNER_TOKEN_PROPERTY_NAME, partnerToken); + Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(urlToParse); + if (matcher.find()) { + String partnerToken = matcher.group(1).trim(); + if (partnerToken.toUpperCase().startsWith("GPN:")) { + map.put(PARTNER_TOKEN_PROPERTY_NAME, " (" + partnerToken + ")"); + urlToParse = matcher.replaceFirst(""); + } } String[] parts = urlToParse.split(";"); @@ -674,25 +701,17 @@ static Map parseUrl(String url) { continue; } String[] kv = part.split("=", 2); - String key = kv[0].trim(); - if (kv.length == 1) { - if (!key.isEmpty()) { - String safeKey = key.length() > 32 ? key.substring(0, 32) + "..." : key; - throw new BigQueryJdbcRuntimeException("Property '" + safeKey + "' has no value."); - } - } else { - String value = kv[1]; // Value might be empty string if "Key=" - if (!key.isEmpty()) { - String upperCaseKey = key.toUpperCase(); - if (!PROPERTY_NAME_MAP.containsKey(upperCaseKey)) { - String safeKey = key.length() > 32 ? key.substring(0, 32) + "..." : key; - throw new BigQueryJdbcRuntimeException("Unknown property: " + safeKey); - } - map.put(PROPERTY_NAME_MAP.get(upperCaseKey), CharEscapers.decodeUriPath(value)); - } + String key = kv[0].trim().toUpperCase(); + if (kv.length != 2 || !PROPERTY_NAME_MAP.containsKey(key)) { + String ref = (kv.length == 2) ? key : part; + String safeRef = ref.length() > 32 ? ref.substring(0, 32) + "..." : ref; + throw new BigQueryJdbcRuntimeException( + String.format("Wrong value or unknown setting: %s", safeRef)); } + + map.put(PROPERTY_NAME_MAP.get(key), CharEscapers.decodeUriPath(kv[1])); } - return map; + return Collections.unmodifiableMap(map); } /** @@ -708,14 +727,11 @@ static String appendPropertiesToURL(String url, String callerClassName, Properti for (Entry entry : properties.entrySet()) { if (entry.getValue() != null && !"".equals(entry.getValue())) { LOG.finest("Appending %s with value %s to URL", entry.getKey(), entry.getValue()); - try { - String encodedValue = - URLEncoder.encode((String) entry.getValue(), StandardCharsets.UTF_8.name()) - .replace("+", "%20"); - urlBuilder.append(";").append(entry.getKey()).append("=").append(encodedValue); - } catch (UnsupportedEncodingException e) { - throw new BigQueryJdbcRuntimeException(e); - } + String encodedValue = + UrlEscapers.urlFormParameterEscaper() + .escape((String) entry.getValue()) + .replace("+", "%20"); + urlBuilder.append(";").append(entry.getKey()).append("=").append(encodedValue); } } return urlBuilder.toString(); @@ -790,18 +806,11 @@ public static String parsePartnerTokenProperty(String url, String callerClassNam LOG.finest("++enter++\t" + callerClassName); // This property is expected to be set by partners only. For more details on exact format // supported, refer b/396086960 - return parseAndRemovePartnerTokenProperty(new StringBuilder(url), callerClassName); - } - - private static String parseAndRemovePartnerTokenProperty( - StringBuilder urlBuilder, String callerClassName) { - Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(urlBuilder); - + Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(url); if (matcher.find()) { - String content = matcher.group(1).trim(); - urlBuilder.delete(matcher.start(), matcher.end()); - if (content.toUpperCase().startsWith("GPN:")) { - return " (" + content + ")"; + String partnerToken = matcher.group(1).trim(); + if (partnerToken.toUpperCase().startsWith("GPN:")) { + return " (" + partnerToken + ")"; } } return null; diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java index d5c2ab1a4..0050976f2 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java @@ -78,7 +78,7 @@ public class DataSource implements javax.sql.DataSource { private Integer metadataFetchThreadCount; private String sslTrustStorePath; private String sslTrustStorePassword; - private String labels; + private Map labels; private String requestReason; private Integer timeout; private Integer jobTimeout; @@ -156,7 +156,7 @@ private Properties createProperties() { } if (this.queryProperties != null) { connectionProperties.setProperty( - BigQueryJdbcUrlUtility.QUERY_PROPERTIES_NAME, this.queryProperties.toString()); + BigQueryJdbcUrlUtility.QUERY_PROPERTIES_NAME, serializeMap(this.queryProperties)); } if (this.enableSession != null) { connectionProperties.setProperty( @@ -295,7 +295,8 @@ private Properties createProperties() { String.valueOf(this.sslTrustStorePassword)); } if (this.labels != null) { - connectionProperties.setProperty(BigQueryJdbcUrlUtility.LABELS_PROPERTY_NAME, this.labels); + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.LABELS_PROPERTY_NAME, serializeMap(this.labels)); } if (this.requestReason != null) { connectionProperties.setProperty( @@ -346,6 +347,20 @@ private Properties createProperties() { return connectionProperties; } + private String serializeMap(Map map) { + if (map == null || map.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + return sb.toString(); + } + @Override public Connection getConnection(String username, String password) throws SQLException { LOG.warning( @@ -691,11 +706,11 @@ public void setSSLTrustStorePassword(String sslTrustStorePassword) { this.sslTrustStorePassword = sslTrustStorePassword; } - public String getLabels() { + public Map getLabels() { return labels; } - public void setLabels(String labels) { + public void setLabels(Map labels) { this.labels = labels; } diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index c67ec670a..830e51533 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -154,7 +154,7 @@ public void testParseUrl_longUnknownProperty_sanitized() { assertThrows( BigQueryJdbcRuntimeException.class, () -> BigQueryJdbcUrlUtility.parseUrl(url)); - assertThat(e.getMessage()).contains("Unknown property:"); + assertThat(e.getMessage()).contains("Wrong value or unknown setting: "); assertThat(e.getMessage()).contains("..."); assertThat(e.getMessage()).doesNotContain(longKey); assertThat(e.getMessage().length()).isLessThan(100); From 290d3170367d8f937641ac64e4075d749edcbe55 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Sat, 21 Feb 2026 03:49:04 +0000 Subject: [PATCH 11/11] fix: regex for partnerToken --- .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index 612dfa4b2..ab506b59f 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -138,9 +138,8 @@ protected boolean removeEldestEntry(Map.Entry> eldes static final String PARTNER_TOKEN_PROPERTY_NAME = "PartnerToken"; private static final Pattern PARTNER_TOKEN_PATTERN = Pattern.compile( - "(?i)(?:^|(?<=;))\\s*" - + PARTNER_TOKEN_PROPERTY_NAME - + "\\s*=\\s*\\(([^)]*)\\)\\s*(?=;|$)"); + "(?:^|(?<=;))" + PARTNER_TOKEN_PROPERTY_NAME + "=\\s*((?:\\([^)]*\\)|[^;])*?)(?=(?:;|$))", + Pattern.CASE_INSENSITIVE); static final String METADATA_FETCH_THREAD_COUNT_PROPERTY_NAME = "MetaDataFetchThreadCount"; static final int DEFAULT_METADATA_FETCH_THREAD_COUNT_VALUE = 32; static final String RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME = "Timeout"; @@ -688,11 +687,16 @@ private static Map parseUrlInternal(String url) { // Parse PartnerToken separately as it contains ';' Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(urlToParse); if (matcher.find()) { - String partnerToken = matcher.group(1).trim(); - if (partnerToken.toUpperCase().startsWith("GPN:")) { - map.put(PARTNER_TOKEN_PROPERTY_NAME, " (" + partnerToken + ")"); - urlToParse = matcher.replaceFirst(""); + String rawToken = matcher.group(1).trim(); + String token = + (rawToken.startsWith("(") && rawToken.endsWith(")")) + ? rawToken.substring(1, rawToken.length() - 1).trim() + : rawToken; + + if (token.toUpperCase().startsWith("GPN:")) { + map.put(PARTNER_TOKEN_PROPERTY_NAME, " (" + token + ")"); } + urlToParse = matcher.replaceFirst(""); } String[] parts = urlToParse.split(";"); @@ -808,9 +812,14 @@ public static String parsePartnerTokenProperty(String url, String callerClassNam // supported, refer b/396086960 Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(url); if (matcher.find()) { - String partnerToken = matcher.group(1).trim(); - if (partnerToken.toUpperCase().startsWith("GPN:")) { - return " (" + partnerToken + ")"; + String rawToken = matcher.group(1).trim(); + String token = + (rawToken.startsWith("(") && rawToken.endsWith(")")) + ? rawToken.substring(1, rawToken.length() - 1).trim() + : rawToken; + + if (token.toUpperCase().startsWith("GPN:")) { + return " (" + token + ")"; } } return null;