Skip to content

Commit 3603f20

Browse files
authored
add include directive support to PropertyFileConfiguration (#5254)
1 parent 7f13d4f commit 3603f20

File tree

5 files changed

+262
-16
lines changed

5 files changed

+262
-16
lines changed

Util/include/Poco/Util/PropertyFileConfiguration.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "Poco/Util/MapConfiguration.h"
2323
#include <istream>
2424
#include <ostream>
25+
#include <set>
2526

2627

2728
namespace Poco {
@@ -34,6 +35,7 @@ class Util_API PropertyFileConfiguration: public MapConfiguration
3435
///
3536
/// The file syntax is implemented as follows.
3637
/// - a line starting with a hash '#' or exclamation mark '!' is treated as a comment and ignored
38+
/// (with the exception of the !include directive described below)
3739
/// - every other line denotes a property assignment in the form
3840
/// <key> = <value> or
3941
/// <key> : <value>
@@ -50,6 +52,11 @@ class Util_API PropertyFileConfiguration: public MapConfiguration
5052
/// A value can spread across multiple lines if the last character in a line (the character
5153
/// immediately before the carriage return or line feed character) is a single backslash.
5254
///
55+
/// A line of the form
56+
/// !include <path>
57+
/// (where <path> is a relative or absolute file path) includes another properties file.
58+
/// Relative paths are resolved relative to the directory of the including file.
59+
///
5360
/// Property names are case sensitive. Leading and trailing whitespace is
5461
/// removed from both keys and values. A property name can neither contain
5562
/// a colon ':' nor an equal sign '=' character.
@@ -88,7 +95,8 @@ class Util_API PropertyFileConfiguration: public MapConfiguration
8895
~PropertyFileConfiguration() = default;
8996

9097
private:
91-
void parseLine(std::istream& istr);
98+
void loadStream(std::istream& istr, const std::string& basePath, std::set<std::string>& includeStack);
99+
void parseLine(std::istream& istr, const std::string& basePath, std::set<std::string>& includeStack);
92100
static int readChar(std::istream& istr);
93101
};
94102

Util/src/PropertyFileConfiguration.cpp

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
#include "Poco/FileStream.h"
2020
#include "Poco/LineEndingConverter.h"
2121
#include "Poco/Ascii.h"
22-
22+
#include <set>
23+
#include <string_view>
2324

2425
using Poco::trim;
2526
using Poco::Path;
@@ -46,24 +47,39 @@ void PropertyFileConfiguration::load(std::istream& istr)
4647
AbstractConfiguration::ScopedLock lock(*this);
4748

4849
clear();
49-
while (!istr.eof())
50-
{
51-
if (istr.fail())
52-
{
53-
throw Poco::IOException("Broken input stream");
54-
}
55-
parseLine(istr);
56-
}
50+
std::set<std::string> includeStack;
51+
loadStream(istr, "", includeStack);
5752
}
5853

5954

6055
void PropertyFileConfiguration::load(const std::string& path)
6156
{
57+
Poco::Path p(path);
58+
p.makeAbsolute();
59+
std::string basePath = p.parent().toString();
6260
Poco::FileInputStream istr(path);
63-
if (istr.good())
64-
load(istr);
65-
else
61+
if (!istr.good())
6662
throw Poco::OpenFileException(path);
63+
AbstractConfiguration::ScopedLock lock(*this);
64+
clear();
65+
std::set<std::string> includeStack;
66+
includeStack.insert(p.toString());
67+
loadStream(istr, basePath, includeStack);
68+
}
69+
70+
71+
void PropertyFileConfiguration::loadStream(std::istream& istr, const std::string& basePath, std::set<std::string>& includeStack)
72+
{
73+
for (;;)
74+
{
75+
if (!istr.good())
76+
{
77+
if (istr.eof())
78+
break;
79+
throw Poco::IOException("Broken input stream");
80+
}
81+
parseLine(istr, basePath, includeStack);
82+
}
6783
}
6884

6985

@@ -121,18 +137,62 @@ void PropertyFileConfiguration::save(const std::string& path) const
121137
}
122138

123139

124-
void PropertyFileConfiguration::parseLine(std::istream& istr)
140+
void PropertyFileConfiguration::parseLine(std::istream& istr, const std::string& basePath, std::set<std::string>& includeStack)
125141
{
126-
static const int eof = std::char_traits<char>::eof();
142+
constexpr int eof = std::char_traits<char>::eof();
127143

128144
int c = istr.get();
129145
while (c != eof && Poco::Ascii::isSpace(c)) c = istr.get();
130146
if (c != eof)
131147
{
132-
if (c == '#' || c == '!')
148+
if (c == '#')
133149
{
134150
while (c != eof && c != '\n' && c != '\r') c = istr.get();
135151
}
152+
else if (c == '!')
153+
{
154+
std::string line;
155+
line += static_cast<char>(c);
156+
while (c != eof && c != '\n' && c != '\r')
157+
{
158+
c = istr.get();
159+
if (c != eof && c != '\n' && c != '\r')
160+
line += static_cast<char>(c);
161+
}
162+
163+
constexpr std::string_view includeDirective = "!include";
164+
if (line.size() > includeDirective.size() &&
165+
line.compare(0, includeDirective.size(), includeDirective) == 0 &&
166+
Poco::Ascii::isSpace(line[includeDirective.size()]))
167+
{
168+
std::string includePath = Poco::trim(line.substr(includeDirective.size()));
169+
if (includePath.empty())
170+
throw Poco::SyntaxException("Missing path in !include directive");
171+
Poco::Path p(includePath);
172+
if (p.isRelative() && !basePath.empty())
173+
p = Poco::Path(basePath).resolve(p);
174+
p.makeAbsolute();
175+
const std::string absPathStr = p.toString();
176+
177+
if (includeStack.find(absPathStr) != includeStack.end())
178+
{
179+
throw Poco::FileException("Cyclic property file include detected", absPathStr);
180+
}
181+
182+
struct StackGuard
183+
{
184+
std::set<std::string>& stack;
185+
const std::string& key;
186+
StackGuard(std::set<std::string>& s, const std::string& k): stack(s), key(k) { stack.insert(k); }
187+
~StackGuard() { stack.erase(key); }
188+
} guard(includeStack, absPathStr);
189+
190+
Poco::FileInputStream includeIstr(p.toString());
191+
if (!includeIstr.good())
192+
throw Poco::OpenFileException(p.toString());
193+
loadStream(includeIstr, p.parent().toString(), includeStack);
194+
}
195+
}
136196
else
137197
{
138198
std::string key;

Util/testsuite/src/PropertyFileConfigurationTest.cpp

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
#include "Poco/Util/PropertyFileConfiguration.h"
1515
#include "Poco/AutoPtr.h"
1616
#include "Poco/Exception.h"
17+
#include "Poco/TemporaryFile.h"
18+
#include "Poco/FileStream.h"
1719
#include <sstream>
1820
#include <algorithm>
21+
#include "Poco/File.h"
22+
#include "Poco/Path.h"
1923

2024

2125
using Poco::Util::PropertyFileConfiguration;
@@ -24,6 +28,7 @@ using Poco::AutoPtr;
2428
using Poco::NotFoundException;
2529

2630

31+
2732
PropertyFileConfigurationTest::PropertyFileConfigurationTest(const std::string& name): AbstractConfigurationTest(name)
2833
{
2934
}
@@ -119,6 +124,175 @@ void PropertyFileConfigurationTest::testSave()
119124
}
120125

121126

127+
void PropertyFileConfigurationTest::testInclude()
128+
{
129+
// Write an included properties file
130+
Poco::TemporaryFile includedFile;
131+
{
132+
Poco::FileOutputStream ostr(includedFile.path());
133+
ostr << "included.prop1 = includedValue1\n";
134+
ostr << "included.prop2 = includedValue2\n";
135+
}
136+
137+
// Write a main properties file that includes the other using an absolute path
138+
Poco::TemporaryFile mainFile;
139+
{
140+
Poco::FileOutputStream ostr(mainFile.path());
141+
ostr << "main.prop = mainValue\n";
142+
ostr << "!include " << includedFile.path() << "\n";
143+
ostr << "main.prop2 = mainValue2\n";
144+
}
145+
146+
AutoPtr<PropertyFileConfiguration> pConf = new PropertyFileConfiguration(mainFile.path());
147+
148+
assertTrue (pConf->getString("main.prop") == "mainValue");
149+
assertTrue (pConf->getString("main.prop2") == "mainValue2");
150+
assertTrue (pConf->getString("included.prop1") == "includedValue1");
151+
assertTrue (pConf->getString("included.prop2") == "includedValue2");
152+
153+
// Relative include path (same directory, include by filename only)
154+
Poco::TemporaryFile includedFileRel;
155+
{
156+
Poco::FileOutputStream ostr(includedFileRel.path());
157+
ostr << "includedRel.prop1 = includedRelValue1\n";
158+
ostr << "includedRel.prop2 = includedRelValue2\n";
159+
}
160+
161+
Poco::TemporaryFile mainFileRel;
162+
{
163+
Poco::FileOutputStream ostr(mainFileRel.path());
164+
ostr << "mainRel.prop = mainRelValue\n";
165+
// include by filename only; should be resolved relative to mainFileRel
166+
ostr << "!include " << Poco::Path(includedFileRel.path()).getFileName() << "\n";
167+
ostr << "mainRel.prop2 = mainRelValue2\n";
168+
}
169+
170+
AutoPtr<PropertyFileConfiguration> pConfRel = new PropertyFileConfiguration(mainFileRel.path());
171+
172+
assertTrue (pConfRel->getString("mainRel.prop") == "mainRelValue");
173+
assertTrue (pConfRel->getString("mainRel.prop2") == "mainRelValue2");
174+
assertTrue (pConfRel->getString("includedRel.prop1") == "includedRelValue1");
175+
assertTrue (pConfRel->getString("includedRel.prop2") == "includedRelValue2");
176+
177+
// Nested includes: main includes A, A includes B (relative paths)
178+
Poco::TemporaryFile fileB;
179+
{
180+
Poco::FileOutputStream ostr(fileB.path());
181+
ostr << "nestedB.prop = nestedBValue\n";
182+
}
183+
184+
Poco::TemporaryFile fileA;
185+
{
186+
Poco::FileOutputStream ostr(fileA.path());
187+
// A includes B by filename
188+
ostr << "!include " << Poco::Path(fileB.path()).getFileName() << "\n";
189+
ostr << "nestedA.prop = nestedAValue\n";
190+
}
191+
192+
Poco::TemporaryFile mainFileNested;
193+
{
194+
Poco::FileOutputStream ostr(mainFileNested.path());
195+
ostr << "mainNested.prop = mainNestedValue\n";
196+
// main includes A by filename; A then includes B
197+
ostr << "!include " << Poco::Path(fileA.path()).getFileName() << "\n";
198+
}
199+
200+
AutoPtr<PropertyFileConfiguration> pConfNested = new PropertyFileConfiguration(mainFileNested.path());
201+
202+
assertTrue (pConfNested->getString("mainNested.prop") == "mainNestedValue");
203+
assertTrue (pConfNested->getString("nestedA.prop") == "nestedAValue");
204+
assertTrue (pConfNested->getString("nestedB.prop") == "nestedBValue");
205+
206+
// Non-existent include should throw
207+
Poco::TemporaryFile mainFile2;
208+
{
209+
Poco::FileOutputStream ostr(mainFile2.path());
210+
ostr << "prop = value\n";
211+
212+
// Construct a guaranteed-nonexistent include path in the same directory
213+
Poco::Path includePath(mainFile2.path());
214+
includePath.setFileName("nonexistent_include.properties");
215+
Poco::File includeFile(includePath);
216+
int counter = 0;
217+
while (includeFile.exists())
218+
{
219+
includePath.setFileName("nonexistent_include_" + std::to_string(++counter) + ".properties");
220+
includeFile = Poco::File(includePath);
221+
}
222+
223+
ostr << "!include " << includePath.toString() << "\n";
224+
}
225+
try
226+
{
227+
AutoPtr<PropertyFileConfiguration> pConf2 = new PropertyFileConfiguration(mainFile2.path());
228+
fail("must throw");
229+
}
230+
catch (Poco::FileException&)
231+
{
232+
}
233+
234+
// !include with no path should throw SyntaxException
235+
Poco::TemporaryFile mainFile3;
236+
{
237+
Poco::FileOutputStream ostr(mainFile3.path());
238+
ostr << "prop = value\n";
239+
ostr << "!include \n";
240+
}
241+
try
242+
{
243+
AutoPtr<PropertyFileConfiguration> pConf3 = new PropertyFileConfiguration(mainFile3.path());
244+
fail("must throw");
245+
}
246+
catch (Poco::SyntaxException&)
247+
{
248+
}
249+
250+
// !include with tab separator should work
251+
Poco::TemporaryFile includedFileTab;
252+
{
253+
Poco::FileOutputStream ostr(includedFileTab.path());
254+
ostr << "tab.prop = tabValue\n";
255+
}
256+
257+
Poco::TemporaryFile mainFileTab;
258+
{
259+
Poco::FileOutputStream ostr(mainFileTab.path());
260+
ostr << "!include\t" << includedFileTab.path() << "\n";
261+
}
262+
263+
AutoPtr<PropertyFileConfiguration> pConfTab = new PropertyFileConfiguration(mainFileTab.path());
264+
assertTrue (pConfTab->getString("tab.prop") == "tabValue");
265+
266+
// !includeSomething should be treated as a regular comment
267+
Poco::TemporaryFile mainFile4;
268+
{
269+
Poco::FileOutputStream ostr(mainFile4.path());
270+
ostr << "!includeSomething\n";
271+
ostr << "prop = value\n";
272+
}
273+
274+
AutoPtr<PropertyFileConfiguration> pConf4 = new PropertyFileConfiguration(mainFile4.path());
275+
assertTrue (pConf4->getString("prop") == "value");
276+
assertTrue (!pConf4->hasProperty("includeSomething"));
277+
278+
// Self-include should throw (cyclic include detection)
279+
Poco::TemporaryFile selfFile;
280+
{
281+
Poco::FileOutputStream ostr(selfFile.path());
282+
ostr << "prop = value\n";
283+
ostr << "!include " << selfFile.path() << "\n";
284+
}
285+
try
286+
{
287+
AutoPtr<PropertyFileConfiguration> pConf5 = new PropertyFileConfiguration(selfFile.path());
288+
fail("must throw");
289+
}
290+
catch (Poco::FileException&)
291+
{
292+
}
293+
}
294+
295+
122296
AbstractConfiguration::Ptr PropertyFileConfigurationTest::allocConfiguration() const
123297
{
124298
return new PropertyFileConfiguration;
@@ -142,6 +316,7 @@ CppUnit::Test* PropertyFileConfigurationTest::suite()
142316
AbstractConfigurationTest_addTests(pSuite, PropertyFileConfigurationTest);
143317
CppUnit_addTest(pSuite, PropertyFileConfigurationTest, testLoad);
144318
CppUnit_addTest(pSuite, PropertyFileConfigurationTest, testSave);
319+
CppUnit_addTest(pSuite, PropertyFileConfigurationTest, testInclude);
145320

146321
return pSuite;
147322
}

Util/testsuite/src/PropertyFileConfigurationTest.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class PropertyFileConfigurationTest: public AbstractConfigurationTest
2626

2727
void testLoad();
2828
void testSave();
29+
void testInclude();
2930

3031
void setUp();
3132
void tearDown();

XML/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ src/latin1tab.h
1111
src/nametab.h
1212
src/siphash.h
1313
src/utf8tab.h
14+
src/winconfig.h
15+
src/xmlparse.c
1416
src/xmlparse.cpp
1517
src/xmlrole.c
1618
src/xmlrole.h

0 commit comments

Comments
 (0)