Skip to content

Commit 0309227

Browse files
committed
Keep the last value when collapsing duplicate config lines
SaveConfig collapsed duplicate lines to the first matching entry's value, but Options::SetOption overwrites the entry in place while parsing, so the last line for a name is the value nzbget actually runs on. Healing a damaged config therefore discarded the live value and fell back to the option default on the next start - a disabled news server came back enabled. Resolve duplicates the same way the loader does: keep the first occurrence's name and position, but the last occurrence's value. Applied to both the replace and append paths. Extend the regression tests to assert the surviving value, not just the line count.
1 parent 2da9d5a commit 0309227

2 files changed

Lines changed: 66 additions & 4 deletions

File tree

daemon/extension/ScriptConfig.cpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ bool ScriptConfig::SaveConfig(Options::OptEntries* optEntries)
9393
std::vector<CString> config;
9494
std::set<CString, CaseInsensitiveLess> writtenOptions;
9595

96+
// Options::SetOption overwrites the existing entry while parsing, so when a
97+
// name occurs on several lines the last one is the value nzbget runs on.
98+
// Collapsing duplicates must preserve that value, otherwise saving a damaged
99+
// config silently changes settings.
100+
auto findLastValue = [optEntries](const char* name) -> const char*
101+
{
102+
for (auto it = optEntries->rbegin(); it != optEntries->rend(); ++it)
103+
{
104+
if (!strcasecmp(it->GetName(), name))
105+
{
106+
return it->GetValue();
107+
}
108+
}
109+
return nullptr;
110+
};
111+
96112
// read config file into memory array
97113
int fileLen = (int)FileSystem::FileSize(g_Options->GetConfigFilename()) + 1;
98114
CString content;
@@ -119,10 +135,12 @@ bool ScriptConfig::SaveConfig(Options::OptEntries* optEntries)
119135
{
120136
Options::OptEntry* optEntry = optEntries->FindOption(optname);
121137
// write each option only once, dropping duplicate lines accumulated
122-
// in the config file by earlier versions (issue #588)
138+
// in the config file by earlier versions (issue #588); keep the
139+
// first occurrence's name and position but the last occurrence's
140+
// value, so the collapsed line is the one nzbget was running on
123141
if (optEntry && writtenOptions.find(optEntry->GetName()) == writtenOptions.end())
124142
{
125-
infile.Print("%s=%s\n", optEntry->GetName(), optEntry->GetValue());
143+
infile.Print("%s=%s\n", optEntry->GetName(), findLastValue(optEntry->GetName()));
126144
writtenOptions.insert(optEntry->GetName());
127145
}
128146
}
@@ -139,7 +157,7 @@ bool ScriptConfig::SaveConfig(Options::OptEntries* optEntries)
139157
std::set<CString, CaseInsensitiveLess>::iterator fit = writtenOptions.find(optEntry.GetName());
140158
if (fit == writtenOptions.end())
141159
{
142-
infile.Print("%s=%s\n", optEntry.GetName(), optEntry.GetValue());
160+
infile.Print("%s=%s\n", optEntry.GetName(), findLastValue(optEntry.GetName()));
143161
writtenOptions.insert(optEntry.GetName());
144162
}
145163
}

tests/extension/ScriptConfig.cpp

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ BOOST_AUTO_TEST_CASE(SaveConfigDoesNotAppendDuplicateOptionNames)
6565
contents << firstFile.rdbuf();
6666
firstContents = contents.str();
6767
}
68-
BOOST_CHECK_EQUAL(firstContents, "# existing config\nExtension.Option=first\n");
68+
// one line only, carrying the last entry's value - Options::SetOption
69+
// resolves duplicates the same way, so the file matches what nzbget loads
70+
BOOST_CHECK_EQUAL(firstContents, "# existing config\nExtension.Option=second\n");
6971

7072
BOOST_REQUIRE(scriptConfig.SaveConfig(&optEntries));
7173
std::string secondContents;
@@ -116,4 +118,46 @@ BOOST_AUTO_TEST_CASE(SaveConfigRemovesDuplicateLinesFromDamagedConfig)
116118
"Server2.Host=news.example.com\n");
117119
}
118120

121+
// When a config file carries duplicate lines for one option, Options::SetOption
122+
// overwrites the same entry while parsing, so the LAST line is the value nzbget
123+
// actually runs on. Collapsing the duplicates must preserve that value,
124+
// otherwise saving a damaged config silently changes settings (issue #588).
125+
BOOST_AUTO_TEST_CASE(SaveConfigKeepsLastValueWhenConfigHasDuplicateLines)
126+
{
127+
TempConfigFile configFile;
128+
{
129+
std::ofstream output(configFile.path);
130+
output << "# existing config\n"
131+
<< "Server2.Active=yes\n"
132+
<< "Server2.Name=xsusenet\n"
133+
<< "server2.active=no\n";
134+
}
135+
136+
OptionsGuard optionsGuard;
137+
Options options("nzbget", configFile.path.string().c_str(), true, nullptr, nullptr);
138+
g_Options = &options;
139+
140+
// as loadconfig reports it: raw file lines, duplicates included
141+
Options::OptEntries optEntries;
142+
optEntries.emplace_back("Server2.Active", "yes");
143+
optEntries.emplace_back("Server2.Name", "xsusenet");
144+
optEntries.emplace_back("server2.active", "no");
145+
ScriptConfig scriptConfig;
146+
147+
BOOST_REQUIRE(scriptConfig.SaveConfig(&optEntries));
148+
std::string contents;
149+
{
150+
std::ifstream file(configFile.path);
151+
std::stringstream buffer;
152+
buffer << file.rdbuf();
153+
contents = buffer.str();
154+
}
155+
// one line per option, keeping the first occurrence's name and position
156+
// but the last occurrence's value - exactly what Options::SetOption does
157+
BOOST_CHECK_EQUAL(contents,
158+
"# existing config\n"
159+
"Server2.Active=no\n"
160+
"Server2.Name=xsusenet\n");
161+
}
162+
119163
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)