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

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-configuration.git


The following commit(s) were added to refs/heads/master by this push:
     new d3c40d7  [CONFIGURATION-760] Properties file using cyclical includes 
cause a StackOverflowError instead of detecting the misconfiguration. (#35)
d3c40d7 is described below

commit d3c40d779168c09cacc22481c7b5b1bb69cf6c4e
Author: Gary Gregory <garydgreg...@users.noreply.github.com>
AuthorDate: Thu Sep 12 16:16:27 2019 -0400

    [CONFIGURATION-760] Properties file using cyclical includes cause a 
StackOverflowError instead of detecting the misconfiguration. (#35)
---
 src/changes/changes.xml                            |  3 +
 .../configuration2/PropertiesConfiguration.java    | 40 ++++++----
 .../PropertiesConfigurationLayout.java             |  8 +-
 .../TestPropertiesConfiguration.java               | 88 ++++++++++++++++++++--
 .../TestPropertiesConfigurationLayout.java         |  6 +-
 .../include-cyclical-back-to-root.properties       | 16 ++++
 .../resources/include-cyclical-root.properties     | 16 ++++
 .../resources/include-cyclical-step.properties     | 16 ++++
 .../include-include-cyclical-reference.properties  | 16 ++++
 9 files changed, 186 insertions(+), 23 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 149f142..5724ccc 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -72,6 +72,9 @@
       <action dev="ggregory" type="update" issue="CONFIGURATION-759" 
due-to="Gary Gregory">
         Update Spring from 4.3.24.RELEASE to 4.3.25.RELEASE.
       </action>
+      <action dev="ggregory" type="fix" issue="CONFIGURATION-760" due-to="Gary 
Gregory">
+        Properties file using cyclical includes cause a StackOverflowError 
instead of detecting the misconfiguration.
+      </action>
     </release>
     <release version="2.5" date="2019-05-23"
              description="Minor release with new features and updated 
dependencies.">
diff --git 
a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java 
b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
index 438bba8..4df6827 100644
--- 
a/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
+++ 
b/src/main/java/org/apache/commons/configuration2/PropertiesConfiguration.java
@@ -27,6 +27,7 @@ import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Deque;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -643,11 +644,12 @@ public class PropertiesConfiguration extends 
BaseConfiguration
      *
      * @param key the property key
      * @param value the property value
+     * @param seenStack the stack of seen include URLs
      * @return a flag whether this is a normal property
      * @throws ConfigurationException if an error occurs
      * @since 1.3
      */
-    boolean propertyLoaded(final String key, final String value)
+    boolean propertyLoaded(final String key, final String value, final 
Deque<URL> seenStack)
             throws ConfigurationException
     {
         boolean result;
@@ -661,7 +663,7 @@ public class PropertiesConfiguration extends 
BaseConfiguration
                         getListDelimiterHandler().split(value, true);
                 for (final String f : files)
                 {
-                    loadIncludeFile(interpolate(f), false);
+                    loadIncludeFile(interpolate(f), false, seenStack);
                 }
             }
             result = false;
@@ -676,7 +678,7 @@ public class PropertiesConfiguration extends 
BaseConfiguration
                         getListDelimiterHandler().split(value, true);
                 for (final String f : files)
                 {
-                    loadIncludeFile(interpolate(f), true);
+                    loadIncludeFile(interpolate(f), true, seenStack);
                 }
             }
             result = false;
@@ -1853,9 +1855,11 @@ public class PropertiesConfiguration extends 
BaseConfiguration
      *
      * @param fileName the name of the file to load
      * @param optional whether or not the {@code fileName} is optional
+     * @param seenStack Stack of seen include URLs
      * @throws ConfigurationException if loading fails
      */
-    private void loadIncludeFile(final String fileName, final boolean 
optional) throws ConfigurationException
+    private void loadIncludeFile(final String fileName, final boolean 
optional, final Deque<URL> seenStack)
+            throws ConfigurationException
     {
         if (locator == null)
         {
@@ -1881,11 +1885,8 @@ public class PropertiesConfiguration extends 
BaseConfiguration
 
         if (url == null)
         {
-            if (getIncludeListener() != null)
-            {
-                getIncludeListener().accept(new ConfigurationException(
-                        "Cannot resolve include file " + fileName, new 
FileNotFoundException(fileName)));
-            }
+            getIncludeListener().accept(new ConfigurationException("Cannot 
resolve include file " + fileName,
+                    new FileNotFoundException(fileName)));
         }
         else
         {
@@ -1896,14 +1897,25 @@ public class PropertiesConfiguration extends 
BaseConfiguration
             {
                 try
                 {
-                    fh.load(url);
+                    // Check for cycles
+                    if (seenStack.contains(url))
+                    {
+                        throw new ConfigurationException(
+                                String.format("Cycle detected loading %s, seen 
stack: %s", url, seenStack));
+                    }
+                    seenStack.add(url);
+                    try
+                    {
+                        fh.load(url);
+                    }
+                    finally
+                    {
+                        seenStack.pop();
+                    }
                 }
                 catch (ConfigurationException e)
                 {
-                    if (getIncludeListener() != null)
-                    {
-                        getIncludeListener().accept(e);
-                    }
+                    getIncludeListener().accept(e);
                 }
             }
             finally
diff --git 
a/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java
 
b/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java
index dffff50..4da001f 100644
--- 
a/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java
+++ 
b/src/main/java/org/apache/commons/configuration2/PropertiesConfigurationLayout.java
@@ -19,6 +19,8 @@ package org.apache.commons.configuration2;
 import java.io.IOException;
 import java.io.Reader;
 import java.io.Writer;
+import java.net.URL;
+import java.util.ArrayDeque;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -138,6 +140,9 @@ public class PropertiesConfigurationLayout implements 
EventListener<Configuratio
     /** Stores the force single line flag. */
     private boolean forceSingleLine;
 
+    /** Seen includes. */
+    private final ArrayDeque<URL> seenStack = new ArrayDeque<>();
+
     /**
      * Creates a new, empty instance of {@code PropertiesConfigurationLayout}.
      */
@@ -486,7 +491,7 @@ public class PropertiesConfigurationLayout implements 
EventListener<Configuratio
             while (reader.nextProperty())
             {
                 if (config.propertyLoaded(reader.getPropertyName(),
-                        reader.getPropertyValue()))
+                        reader.getPropertyValue(), seenStack))
                 {
                     final boolean contained = layoutData.containsKey(reader
                             .getPropertyName());
@@ -653,6 +658,7 @@ public class PropertiesConfigurationLayout implements 
EventListener<Configuratio
      */
     private void clear()
     {
+        seenStack.clear();
         layoutData.clear();
         setHeaderComment(null);
         setFooterComment(null);
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
 
b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
index bd1e532..5b5e8f5 100644
--- 
a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
+++ 
b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfiguration.java
@@ -49,6 +49,7 @@ import java.net.URLStreamHandler;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -73,8 +74,8 @@ import org.apache.commons.configuration2.io.DefaultFileSystem;
 import org.apache.commons.configuration2.io.FileHandler;
 import org.apache.commons.configuration2.io.FileSystem;
 import org.apache.commons.lang3.mutable.MutableObject;
+import org.junit.Assert;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -1147,8 +1148,82 @@ public class TestPropertiesConfiguration
     }
 
     @Test
-    @Ignore("PropertiesConfiguration does NOT detect cyclical references.")
-    public void testIncludeLoadAllCycliclaReference() throws Exception
+    public void testIncludeLoadCyclicalReferenceFail() throws Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-cyclical-reference.properties");
+        try {
+            handler.load();
+            Assert.fail("Expected " + Configuration.class.getCanonicalName());
+        } catch (ConfigurationException e) {
+            // expected
+            // e.printStackTrace();
+        }
+        assertNull(pc.getString("keyA"));
+    }
+
+    @Test
+    public void testIncludeLoadCyclicalMultiStepReferenceFail() throws 
Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-cyclical-root.properties");
+        try {
+            handler.load();
+            Assert.fail("Expected " + Configuration.class.getCanonicalName());
+        } catch (ConfigurationException e) {
+            // expected
+            // e.printStackTrace();
+        }
+        assertNull(pc.getString("keyA"));
+    }
+
+    @Test
+    public void testIncludeLoadCyclicalMultiStepReferenceIgnore() throws 
Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-cyclical-root.properties");
+        handler.load();
+        assertEquals("valueA", pc.getString("keyA"));
+    }
+
+    @Test
+    public void testIncludeIncludeLoadCyclicalReferenceFail() throws Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-include-cyclical-reference.properties");
+        try {
+            handler.load();
+            Assert.fail("Expected " + Configuration.class.getCanonicalName());
+        } catch (ConfigurationException e) {
+            // expected
+            // e.printStackTrace();
+        }
+        assertNull(pc.getString("keyA"));
+    }
+
+    @Test
+    public void testIncludeIncludeLoadCyclicalReferenceIgnore() throws 
Exception
+    {
+        final PropertiesConfiguration pc = new PropertiesConfiguration();
+        pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
+        final FileHandler handler = new FileHandler(pc);
+        handler.setBasePath(testBasePath);
+        handler.setFileName("include-include-cyclical-reference.properties");
+        handler.load();
+        assertEquals("valueA", pc.getString("keyA"));
+    }
+
+    @Test
+    public void testIncludeLoadCyclicalReferenceIgnore() throws Exception
     {
         final PropertiesConfiguration pc = new PropertiesConfiguration();
         pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
@@ -1231,7 +1306,7 @@ public class TestPropertiesConfiguration
     {
         final DummyLayout layout = new DummyLayout();
         conf.setLayout(layout);
-        conf.propertyLoaded("layoutLoadedProperty", "yes");
+        conf.propertyLoaded("layoutLoadedProperty", "yes", null);
         assertEquals("Layout's load() was called", 0, layout.loadCalls);
         assertEquals("Property not added", "yes", 
conf.getString("layoutLoadedProperty"));
     }
@@ -1244,7 +1319,8 @@ public class TestPropertiesConfiguration
     {
         final DummyLayout layout = new DummyLayout();
         conf.setLayout(layout);
-        conf.propertyLoaded(PropertiesConfiguration.getInclude(), 
"testClasspath.properties,testEqual.properties");
+        conf.propertyLoaded(PropertiesConfiguration.getInclude(), 
"testClasspath.properties,testEqual.properties",
+                new ArrayDeque<>());
         assertEquals("Layout's load() was not correctly called", 2, 
layout.loadCalls);
         assertFalse("Property was added", 
conf.containsKey(PropertiesConfiguration.getInclude()));
     }
@@ -1259,7 +1335,7 @@ public class TestPropertiesConfiguration
         final DummyLayout layout = new DummyLayout();
         conf.setLayout(layout);
         conf.setIncludesAllowed(false);
-        conf.propertyLoaded(PropertiesConfiguration.getInclude(), 
"testClassPath.properties,testEqual.properties");
+        conf.propertyLoaded(PropertiesConfiguration.getInclude(), 
"testClassPath.properties,testEqual.properties", null);
         assertEquals("Layout's load() was called", 0, layout.loadCalls);
         assertFalse("Property was added", 
conf.containsKey(PropertiesConfiguration.getInclude()));
     }
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfigurationLayout.java
 
b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfigurationLayout.java
index 6b63d33..f5912ac 100644
--- 
a/src/test/java/org/apache/commons/configuration2/TestPropertiesConfigurationLayout.java
+++ 
b/src/test/java/org/apache/commons/configuration2/TestPropertiesConfigurationLayout.java
@@ -26,6 +26,8 @@ import static org.junit.Assert.fail;
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.net.URL;
+import java.util.Deque;
 import java.util.Iterator;
 
 import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
@@ -830,12 +832,12 @@ public class TestPropertiesConfigurationLayout
          * load() call on the layout is invoked.
          */
         @Override
-        boolean propertyLoaded(final String key, final String value)
+        boolean propertyLoaded(final String key, final String value, 
Deque<URL> seenStack)
                 throws ConfigurationException
         {
             if (builder == null)
             {
-                return super.propertyLoaded(key, value);
+                return super.propertyLoaded(key, value, seenStack);
             }
             if (PropertiesConfiguration.getInclude().equals(key))
             {
diff --git a/src/test/resources/include-cyclical-back-to-root.properties 
b/src/test/resources/include-cyclical-back-to-root.properties
new file mode 100644
index 0000000..71a5b8d
--- /dev/null
+++ b/src/test/resources/include-cyclical-back-to-root.properties
@@ -0,0 +1,16 @@
+#   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.
+include = src/test/resources/include-cyclical-root.properties
+keyA = valueA
diff --git a/src/test/resources/include-cyclical-root.properties 
b/src/test/resources/include-cyclical-root.properties
new file mode 100644
index 0000000..5e18ec4
--- /dev/null
+++ b/src/test/resources/include-cyclical-root.properties
@@ -0,0 +1,16 @@
+#   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.
+include = src/test/resources/include-cyclical-step.properties
+keyA = valueA
diff --git a/src/test/resources/include-cyclical-step.properties 
b/src/test/resources/include-cyclical-step.properties
new file mode 100644
index 0000000..edee5d5
--- /dev/null
+++ b/src/test/resources/include-cyclical-step.properties
@@ -0,0 +1,16 @@
+#   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.
+include = src/test/resources/include-cyclical-back-to-root.properties
+keyA = valueA
diff --git a/src/test/resources/include-include-cyclical-reference.properties 
b/src/test/resources/include-include-cyclical-reference.properties
new file mode 100644
index 0000000..7a5ccf4
--- /dev/null
+++ b/src/test/resources/include-include-cyclical-reference.properties
@@ -0,0 +1,16 @@
+#   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.
+include = src/test/resources/include-cyclical-reference.properties
+keyA = valueA

Reply via email to