This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit b7d637e105ff8800c98e050a75dc1a22586c0522 Author: opencode <[email protected]> AuthorDate: Wed Oct 14 17:31:19 2026 +0200 Decode service binding entry files as UTF-8 getProperty() decoded the binding file bytes with new String(byte[],int, int), which uses the JVM default character set. Service binding entries under SERVICE_BINDING_ROOT are text files that follow the UTF-8 convention (Kubernetes secret projections encode stringData as UTF-8) and the resolved values flow into configuration such as SSLHostConfig certificate key passwords, so on a JVM whose default character set is not UTF-8 every non-ASCII character was replaced with a question mark, producing a silently wrong credential. Decode the bytes as UTF-8 explicitly. Since the service binding specification fixes no encoding, the class Javadoc now states that entry files are decoded as UTF-8 and that invalid bytes become U+FFFD. Adds TestServiceBindingPropertySource covering the UTF-8 decoding of multi-byte characters (with and without the chomp: prefix), the full chomp matrix and the null results for unknown bindings and malformed keys. The environment variable cannot be set from the tests, so a package-private constructor pins the binding root for the test class; the public no-argument path is unchanged and still reads the environment on every resolution. Note that the tests pass on the default-UTF-8 test JVM regardless of the fix; they fail against the previous code when the default character set is not UTF-8, and they pin the contract either way. --- .../digester/ServiceBindingPropertySource.java | 27 +++++-- .../digester/TestServiceBindingPropertySource.java | 91 ++++++++++++++++++++++ 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java index be53e1a100..a3ba4ae9ed 100644 --- a/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java +++ b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java @@ -17,6 +17,7 @@ package org.apache.tomcat.util.digester; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -62,6 +63,10 @@ import org.apache.tomcat.util.IntrospectionUtils; * convenience for hand-edited files/values where removing a trailing newline is difficult, and trailing whitespace * changes the meaning of the value. * </p> + * <p> + * Binding entry files are decoded as UTF-8. Bytes that are not valid UTF-8 are replaced with the Unicode replacement + * character (U+FFFD). + * </p> * How to configure: * * <pre> @@ -85,11 +90,23 @@ import org.apache.tomcat.util.IntrospectionUtils; public class ServiceBindingPropertySource implements IntrospectionUtils.PropertySource { /** - * Default constructor. + * Default constructor. The binding root is taken from the {@code SERVICE_BINDING_ROOT} environment variable when + * an expression is resolved. */ public ServiceBindingPropertySource() { + this(null); + } + + /* + * Test seam: pins the binding root so the tests do not need to set the environment variable. A null value selects + * the environment variable at resolution time. + */ + ServiceBindingPropertySource(String serviceBindingRoot) { + this.serviceBindingRoot = serviceBindingRoot; } + private final String serviceBindingRoot; + private static final String SERVICE_BINDING_ROOT_ENV_VAR = "SERVICE_BINDING_ROOT"; /** @@ -106,8 +123,8 @@ public class ServiceBindingPropertySource implements IntrospectionUtils.Property @Override public String getProperty(String key) { // get the root to search from - String serviceBindingRoot = System.getenv(SERVICE_BINDING_ROOT_ENV_VAR); - if (serviceBindingRoot == null) { + String root = serviceBindingRoot != null ? serviceBindingRoot : System.getenv(SERVICE_BINDING_ROOT_ENV_VAR); + if (root == null) { return null; } @@ -123,7 +140,7 @@ public class ServiceBindingPropertySource implements IntrospectionUtils.Property return null; } - Path path = Paths.get(serviceBindingRoot, parts[0], parts[1]); + Path path = Paths.get(root, parts[0], parts[1]); if (!path.toFile().exists()) { return null; @@ -145,7 +162,7 @@ public class ServiceBindingPropertySource implements IntrospectionUtils.Property } } - return new String(bytes, 0, length); + return new String(bytes, 0, length, StandardCharsets.UTF_8); } catch (IOException ioe) { // Treat as not found return null; diff --git a/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java b/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java new file mode 100644 index 0000000000..3a7e8d43fa --- /dev/null +++ b/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java @@ -0,0 +1,91 @@ +/* + * 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.tomcat.util.digester; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class TestServiceBindingPropertySource { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + + private File writeBinding(String name, String key, String content) throws Exception { + File bindingFolder = new File(temporaryFolder.getRoot(), name); + Assert.assertTrue(bindingFolder.mkdirs() || bindingFolder.isDirectory()); + File file = new File(bindingFolder, key); + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + return file; + } + + + @Test + public void testPropertyValueIsDecodedAsUtf8() throws Exception { + String value = "p\u00e4ssw\u00f6rd-\u00fcnicode-\u2603"; + writeBinding("my-binding", "keyPassword", value); + writeBinding("my-binding", "withNewline", value + "\n"); + + ServiceBindingPropertySource source = + new ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath()); + + Assert.assertEquals(value, source.getProperty("my-binding.keyPassword")); + Assert.assertEquals(value, source.getProperty("chomp:my-binding.withNewline")); + } + + + @Test + public void testChompTrimsSingleTrailingLineBreak() throws Exception { + writeBinding("my-binding", "lf", "value\n"); + writeBinding("my-binding", "crlf", "value\r\n"); + writeBinding("my-binding", "cr", "value\r"); + writeBinding("my-binding", "none", "value"); + writeBinding("my-binding", "blankline", "value\n\n"); + writeBinding("my-binding", "trailingspace", "value \n"); + + ServiceBindingPropertySource source = + new ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath()); + + Assert.assertEquals("value", source.getProperty("chomp:my-binding.lf")); + Assert.assertEquals("value", source.getProperty("chomp:my-binding.crlf")); + Assert.assertEquals("value", source.getProperty("chomp:my-binding.cr")); + Assert.assertEquals("value", source.getProperty("chomp:my-binding.none")); + Assert.assertEquals("value\n", source.getProperty("chomp:my-binding.blankline")); + Assert.assertEquals("value ", source.getProperty("chomp:my-binding.trailingspace")); + } + + + @Test + public void testUnknownBindingsResolveToNull() throws Exception { + writeBinding("my-binding", "known", "value"); + + ServiceBindingPropertySource source = + new ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath()); + + Assert.assertNull(source.getProperty("my-binding.unknown")); + Assert.assertNull(source.getProperty("unknown-binding.known")); + // A key without exactly one dot is outside the <binding-name>.<key> contract + Assert.assertNull(source.getProperty("no-dot")); + Assert.assertNull(source.getProperty("too.many.dots")); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
