Skip to content

Commit 4e37acd

Browse files
Merge branch '4943-conf-line-continuation' into ai_server
2 parents b7ff817 + 5608266 commit 4e37acd

5 files changed

Lines changed: 151 additions & 20 deletions

File tree

scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,27 @@ require ZoneMinder::Database;
128128
}
129129
open( my $CONFIG, '<', $config_file )
130130
or croak("Can't open config file '$config_file': $!");
131-
foreach my $str ( <$CONFIG> ) {
132-
next if ( $str =~ /^\s*$/ );
133-
next if ( $str =~ /^\s*#/ );
134-
my ( $name, $value ) = $str =~ /^\s*([^=\s]+)\s*=\s*[\'"]*(.*?)[\'"]*\s*$/;
131+
my $logical = '';
132+
while ( my $str = <$CONFIG> ) {
133+
$str =~ s/\r?\n\z//;
134+
# A trailing backslash (with optional whitespace before the newline)
135+
# means the value continues on the next physical line. Continuation
136+
# lines have their leading whitespace stripped so users can indent.
137+
if ( $logical ne '' ) {
138+
$str =~ s/^[ \t]+//;
139+
}
140+
if ( $str =~ s/\\[ \t]*\z// ) {
141+
$logical .= $str;
142+
next;
143+
}
144+
$logical .= $str;
145+
my $line = $logical;
146+
$logical = '';
147+
next if ( $line =~ /^\s*$/ );
148+
next if ( $line =~ /^\s*#/ );
149+
my ( $name, $value ) = $line =~ /^\s*([^=\s]+)\s*=\s*[\'"]*(.*?)[\'"]*\s*$/;
135150
if ( !$name ) {
136-
print(STDERR "Warning, bad line in $config_file: $str\n");
151+
print(STDERR "Warning, bad line in $config_file: $line\n");
137152
next;
138153
} # end if
139154
$name = uc $name;
@@ -143,7 +158,7 @@ require ZoneMinder::Database;
143158
#print(STDERR "Warning, known config option name $name in $config_file\n");
144159
#}
145160
$Config{$name} = $value;
146-
} # end foreach config line
161+
} # end while config line
147162
close($CONFIG);
148163
} # end sub process_configfile
149164

src/zm_config.cpp

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
#include <cerrno>
2626
#include <cstring>
2727
#include <dirent.h>
28+
#include <fstream>
2829
#include <glob.h>
30+
#include <string>
31+
#include <vector>
2932

3033
// Note that Error and Debug calls won't actually go anywhere unless you
3134
// set the relevant ENV vars because the logger gets it's setting from the
@@ -105,18 +108,34 @@ void zmLoadDBConfig() {
105108
}
106109

107110
void process_configfile(char const *configFile) {
108-
FILE *cfg;
109-
char line[512];
110-
if ( (cfg = fopen(configFile, "r")) == nullptr ) {
111+
std::ifstream cfg(configFile);
112+
if ( !cfg.is_open() ) {
111113
Fatal("Can't open %s: %s", configFile, strerror(errno));
112114
return;
113115
}
114-
while ( fgets(line, sizeof(line), cfg) != nullptr ) {
115-
char *line_ptr = line;
116+
std::string raw;
117+
while ( std::getline(cfg, raw) ) {
118+
// Tolerate Windows line endings.
119+
if ( !raw.empty() && raw.back() == '\r' ) raw.pop_back();
120+
121+
// Backslash before the newline means the value continues on the next
122+
// physical line. Trailing whitespace before the '\' is allowed.
123+
// Continuation lines have their leading whitespace stripped so callers
124+
// can indent for readability without it leaking into the value.
125+
while ( true ) {
126+
size_t last_non_ws = raw.find_last_not_of(" \t");
127+
if ( last_non_ws == std::string::npos || raw[last_non_ws] != '\\' ) break;
128+
raw.erase(last_non_ws);
129+
std::string next;
130+
if ( !std::getline(cfg, next) ) break;
131+
if ( !next.empty() && next.back() == '\r' ) next.pop_back();
132+
size_t lead = next.find_first_not_of(" \t");
133+
if ( lead != std::string::npos ) raw.append(next, lead, std::string::npos);
134+
}
116135

117-
// Trim off any cr/lf line endings
118-
int chomp_len = strcspn(line_ptr, "\r\n");
119-
line_ptr[chomp_len] = '\0';
136+
std::vector<char> line(raw.begin(), raw.end());
137+
line.push_back('\0');
138+
char *line_ptr = line.data();
120139

121140
// Remove leading white space
122141
int white_len = strspn(line_ptr, " \t");
@@ -135,7 +154,7 @@ void process_configfile(char const *configFile) {
135154
// Now look for the '=' in the middle of the line
136155
temp_ptr = strchr(line_ptr, '=');
137156
if ( !temp_ptr ) {
138-
Warning("Invalid data in %s: '%s'", configFile, line);
157+
Warning("Invalid data in %s: '%s'", configFile, line.data());
139158
continue;
140159
}
141160

@@ -204,7 +223,6 @@ void process_configfile(char const *configFile) {
204223
// Warning( "Invalid parameter '%s' in %s", name_ptr, ZM_CONFIG );
205224
}
206225
} // end foreach line of the config
207-
fclose(cfg);
208226
}
209227

210228
StaticConfig staticConfig;

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
include(Catch)
1313

1414
set(TEST_SOURCES
15+
zm_config.cpp
1516
zm_db_schema.cpp
1617
zm_box.cpp
1718
zm_comms.cpp

tests/zm_config.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include "zm_catch2.h"
19+
20+
#include "zm_config.h"
21+
22+
#include <cstdio>
23+
#include <cstdlib>
24+
#include <fstream>
25+
#include <string>
26+
#include <unistd.h>
27+
28+
namespace {
29+
30+
// Writes `body` to a fresh /tmp file and returns its path; caller unlinks.
31+
std::string WriteTempConf(const std::string &body) {
32+
char path[] = "/tmp/zm_conf_test_XXXXXX";
33+
int fd = mkstemp(path);
34+
REQUIRE(fd >= 0);
35+
ssize_t n = write(fd, body.data(), body.size());
36+
REQUIRE(n == static_cast<ssize_t>(body.size()));
37+
close(fd);
38+
return std::string(path);
39+
}
40+
41+
} // namespace
42+
43+
TEST_CASE("process_configfile: backslash continuation joins lines") {
44+
const std::string body =
45+
"ZM_SERVER_NAME = first\\\n"
46+
" second\\\n"
47+
" third\n";
48+
std::string path = WriteTempConf(body);
49+
50+
staticConfig.SERVER_NAME.clear();
51+
process_configfile(path.c_str());
52+
unlink(path.c_str());
53+
54+
// Leading whitespace on continuation lines is trimmed.
55+
REQUIRE(staticConfig.SERVER_NAME == "firstsecondthird");
56+
}
57+
58+
TEST_CASE("process_configfile: bare backslash inside value is preserved") {
59+
// No newline after the backslash, so it is not a continuation marker.
60+
const std::string body = "ZM_DIR_EXPORTS = C:\\Users\\zm\n";
61+
std::string path = WriteTempConf(body);
62+
63+
staticConfig.DIR_EXPORTS.clear();
64+
process_configfile(path.c_str());
65+
unlink(path.c_str());
66+
67+
REQUIRE(staticConfig.DIR_EXPORTS == "C:\\Users\\zm");
68+
}
69+
70+
TEST_CASE("process_configfile: lines longer than the legacy 512-byte cap survive") {
71+
// A 1500-byte value would have been truncated by the old fgets() buffer.
72+
std::string long_value(1500, 'a');
73+
const std::string body = "ZM_DB_HOST = " + long_value + "\n";
74+
std::string path = WriteTempConf(body);
75+
76+
staticConfig.DB_HOST.clear();
77+
process_configfile(path.c_str());
78+
unlink(path.c_str());
79+
80+
REQUIRE(staticConfig.DB_HOST == long_value);
81+
}

web/includes/config.php.in

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,30 @@ function process_configfile($configFile) {
304304
$config = array();
305305
if (is_readable($configFile)) {
306306
$cfg = fopen($configFile, 'r') or error_log('Could not open config file: '.$configFile);
307+
$logical = '';
307308
while ( !feof($cfg) ) {
308-
$str = fgets($cfg, 512);
309-
if ( preg_match('/^\s*(#.*)?$/', $str) ) {
309+
$str = fgets($cfg);
310+
if ($str === false) break;
311+
$str = preg_replace('/\r?\n\z/', '', $str);
312+
// Trailing backslash (with optional whitespace) continues the value on
313+
// the next physical line. Leading whitespace on continuation lines is
314+
// stripped so users can indent for readability.
315+
if ($logical !== '') {
316+
$str = ltrim($str, " \t");
317+
}
318+
if (preg_match('/^(.*?)\\\\[ \t]*$/', $str, $m)) {
319+
$logical .= $m[1];
320+
continue;
321+
}
322+
$logical .= $str;
323+
$line = $logical;
324+
$logical = '';
325+
if ( preg_match('/^\s*(#.*)?$/', $line) ) {
310326
continue;
311-
} else if ( preg_match('/^\s*([^=\s]+)\s*=\s*[\'"]*(.*?)[\'"]*\s*$/', $str, $matches) ) {
327+
} else if ( preg_match('/^\s*([^=\s]+)\s*=\s*[\'"]*(.*?)[\'"]*\s*$/', $line, $matches) ) {
312328
$config[$matches[1]] = ['Name'=>$matches[1], 'Value'=>$matches[2]];
313329
} else {
314-
error_log("Malformed line in config $configFile\n$str");
330+
error_log("Malformed line in config $configFile\n$line");
315331
}
316332
}
317333
fclose($cfg);

0 commit comments

Comments
 (0)