Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2004-2026 ZNC, see the NOTICE file for details. |
3 | | * |
4 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | | * you may not use this file except in compliance with the License. |
6 | | * You may obtain a copy of the License at |
7 | | * |
8 | | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | | * |
10 | | * Unless required by applicable law or agreed to in writing, software |
11 | | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | | * See the License for the specific language governing permissions and |
14 | | * limitations under the License. |
15 | | */ |
16 | | |
17 | | #include <znc/Config.h> |
18 | | #include <znc/FileUtils.h> |
19 | | #include <stack> |
20 | | #include <sstream> |
21 | | |
22 | | struct ConfigStackEntry { |
23 | | CString sTag; |
24 | | CString sName; |
25 | | CConfig Config; |
26 | | |
27 | | ConfigStackEntry(const CString& Tag, const CString Name) |
28 | 0 | : sTag(Tag), sName(Name), Config() {} |
29 | | }; |
30 | | |
31 | 0 | CConfigEntry::CConfigEntry() : m_pSubConfig(nullptr) {} |
32 | | |
33 | | CConfigEntry::CConfigEntry(const CConfig& Config) |
34 | 0 | : m_pSubConfig(new CConfig(Config)) {} |
35 | | |
36 | 0 | CConfigEntry::CConfigEntry(const CConfigEntry& other) : m_pSubConfig(nullptr) { |
37 | 0 | if (other.m_pSubConfig) m_pSubConfig = new CConfig(*other.m_pSubConfig); |
38 | 0 | } |
39 | | |
40 | 0 | CConfigEntry::~CConfigEntry() { delete m_pSubConfig; } |
41 | | |
42 | 0 | CConfigEntry& CConfigEntry::operator=(const CConfigEntry& other) { |
43 | 0 | delete m_pSubConfig; |
44 | 0 | if (other.m_pSubConfig) |
45 | 0 | m_pSubConfig = new CConfig(*other.m_pSubConfig); |
46 | 0 | else |
47 | 0 | m_pSubConfig = nullptr; |
48 | 0 | return *this; |
49 | 0 | } |
50 | | |
51 | 0 | bool CConfig::Parse(CFile& file, CString& sErrorMsg) { |
52 | 0 | CString sLine; |
53 | 0 | unsigned int uLineNum = 0; |
54 | 0 | CConfig* pActiveConfig = this; |
55 | 0 | std::stack<ConfigStackEntry> ConfigStack; |
56 | 0 | bool bCommented = false; // support for /**/ style comments |
57 | |
|
58 | 0 | if (!file.Seek(0)) { |
59 | 0 | sErrorMsg = "Could not seek to the beginning of the config."; |
60 | 0 | return false; |
61 | 0 | } |
62 | | |
63 | 0 | while (file.ReadLine(sLine)) { |
64 | 0 | uLineNum++; |
65 | |
|
66 | 0 | #define ERROR(arg) \ |
67 | 0 | do { \ |
68 | 0 | std::stringstream stream; \ |
69 | 0 | stream << "Error on line " << uLineNum << ": " << arg; \ |
70 | 0 | sErrorMsg = stream.str(); \ |
71 | 0 | m_SubConfigNameSets.clear(); \ |
72 | 0 | m_SubConfigs.clear(); \ |
73 | 0 | m_ConfigEntries.clear(); \ |
74 | 0 | return false; \ |
75 | 0 | } while (0) |
76 | | |
77 | | // Remove all leading spaces and trailing line endings |
78 | 0 | sLine.TrimLeft(); |
79 | 0 | sLine.TrimRight("\r\n"); |
80 | |
|
81 | 0 | if (bCommented || sLine.StartsWith("/*")) { |
82 | | /* Does this comment end on the same line again? */ |
83 | 0 | bCommented = (!sLine.EndsWith("*/")); |
84 | |
|
85 | 0 | continue; |
86 | 0 | } |
87 | | |
88 | 0 | if ((sLine.empty()) || (sLine.StartsWith("#")) || |
89 | 0 | (sLine.StartsWith("//"))) { |
90 | 0 | continue; |
91 | 0 | } |
92 | | |
93 | 0 | if ((sLine.StartsWith("<")) && (sLine.EndsWith(">"))) { |
94 | 0 | sLine.LeftChomp(); |
95 | 0 | sLine.RightChomp(); |
96 | 0 | sLine.Trim(); |
97 | |
|
98 | 0 | CString sTag = sLine.Token(0); |
99 | 0 | CString sValue = sLine.Token(1, true); |
100 | |
|
101 | 0 | sTag.Trim(); |
102 | 0 | sValue.Trim(); |
103 | |
|
104 | 0 | if (sTag.TrimPrefix("/")) { |
105 | 0 | if (!sValue.empty()) |
106 | 0 | ERROR("Malformated closing tag. Expected \"</" << sTag |
107 | 0 | << ">\"."); |
108 | 0 | if (ConfigStack.empty()) |
109 | 0 | ERROR("Closing tag \"" << sTag << "\" which is not open."); |
110 | | |
111 | 0 | const struct ConfigStackEntry& entry = ConfigStack.top(); |
112 | 0 | CConfig myConfig(entry.Config); |
113 | 0 | CString sName(entry.sName); |
114 | |
|
115 | 0 | if (!sTag.Equals(entry.sTag)) |
116 | 0 | ERROR("Closing tag \"" << sTag << "\" which is not open."); |
117 | | |
118 | | // This breaks entry |
119 | 0 | ConfigStack.pop(); |
120 | |
|
121 | 0 | if (ConfigStack.empty()) |
122 | 0 | pActiveConfig = this; |
123 | 0 | else |
124 | 0 | pActiveConfig = &ConfigStack.top().Config; |
125 | |
|
126 | 0 | const auto sTagLower = sTag.AsLower(); |
127 | 0 | auto& nameset = pActiveConfig->m_SubConfigNameSets[sTagLower]; |
128 | |
|
129 | 0 | if (nameset.find(sName) != nameset.end()) |
130 | 0 | ERROR("Duplicate entry for tag \"" << sTag << "\" name \"" |
131 | 0 | << sName << "\"."); |
132 | | |
133 | 0 | nameset.insert(sName); |
134 | 0 | pActiveConfig->m_SubConfigs[sTagLower].emplace_back(sName, |
135 | 0 | myConfig); |
136 | 0 | } else { |
137 | 0 | if (sValue.empty()) |
138 | 0 | ERROR("Empty block name at begin of block."); |
139 | 0 | ConfigStack.push(ConfigStackEntry(sTag.AsLower(), sValue)); |
140 | 0 | pActiveConfig = &ConfigStack.top().Config; |
141 | 0 | } |
142 | | |
143 | 0 | continue; |
144 | 0 | } |
145 | | |
146 | | // If we have a regular line, figure out where it goes |
147 | 0 | CString sName = sLine.Token(0, false, "="); |
148 | 0 | CString sValue = sLine.Token(1, true, "="); |
149 | | |
150 | | // Only remove the first space, people might want |
151 | | // leading spaces (e.g. in the MOTD). |
152 | 0 | sValue.TrimPrefix(" "); |
153 | | |
154 | | // We don't have any names with spaces, trim all |
155 | | // leading/trailing spaces. |
156 | 0 | sName.Trim(); |
157 | |
|
158 | 0 | if (sName.empty() || sValue.empty()) ERROR("Malformed line"); |
159 | | |
160 | 0 | CString sNameLower = sName.AsLower(); |
161 | 0 | pActiveConfig->m_ConfigEntries[sNameLower].push_back(sValue); |
162 | 0 | } |
163 | | |
164 | 0 | if (bCommented) ERROR("Comment not closed at end of file."); |
165 | | |
166 | 0 | if (!ConfigStack.empty()) { |
167 | 0 | const CString& sTag = ConfigStack.top().sTag; |
168 | 0 | ERROR( |
169 | 0 | "Not all tags are closed at the end of the file. Inner-most open " |
170 | 0 | "tag is \"" |
171 | 0 | << sTag << "\"."); |
172 | 0 | } |
173 | | |
174 | 0 | return true; |
175 | 0 | } |
176 | | |
177 | 0 | void CConfig::Write(CFile& File, unsigned int iIndentation) { |
178 | 0 | CString sIndentation = CString(iIndentation, '\t'); |
179 | |
|
180 | 0 | auto SingleLine = [](const CString& s) { |
181 | 0 | return s.Replace_n("\r", "").Replace_n("\n", ""); |
182 | 0 | }; |
183 | |
|
184 | 0 | for (const auto& it : m_ConfigEntries) { |
185 | 0 | for (const CString& sValue : it.second) { |
186 | 0 | File.Write(SingleLine(sIndentation + it.first + " = " + sValue) + |
187 | 0 | "\n"); |
188 | 0 | } |
189 | 0 | } |
190 | |
|
191 | 0 | for (const auto& it : m_SubConfigs) { |
192 | 0 | for (const auto& it2 : it.second) { |
193 | 0 | File.Write("\n"); |
194 | |
|
195 | 0 | File.Write(SingleLine(sIndentation + "<" + it.first + " " + |
196 | 0 | it2.first + ">") + |
197 | 0 | "\n"); |
198 | 0 | it2.second.m_pSubConfig->Write(File, iIndentation + 1); |
199 | 0 | File.Write(SingleLine(sIndentation + "</" + it.first + ">") + "\n"); |
200 | 0 | } |
201 | 0 | } |
202 | 0 | } |