|
1 | 1 | #include "node_dotenv.h"
|
| 2 | +#include <regex> |
2 | 3 | #include <unordered_set>
|
3 | 4 | #include "env-inl.h"
|
4 | 5 | #include "node_file.h"
|
@@ -87,137 +88,58 @@ Local<Object> Dotenv::ToObject(Environment* env) const {
|
87 | 88 | return result;
|
88 | 89 | }
|
89 | 90 |
|
90 |
| -std::string_view trim_spaces(std::string_view input) { |
91 |
| - if (input.empty()) return ""; |
92 |
| - if (input.front() == ' ') { |
93 |
| - input.remove_prefix(input.find_first_not_of(' ')); |
94 |
| - } |
95 |
| - if (!input.empty() && input.back() == ' ') { |
96 |
| - input = input.substr(0, input.find_last_not_of(' ') + 1); |
97 |
| - } |
98 |
| - return input; |
| 91 | +const std::regex whitespace_regex{ |
| 92 | + "(^\\s+|\\s+$)", |
| 93 | + std::regex_constants::ECMAScript | std::regex_constants::multiline}; |
| 94 | + |
| 95 | +std::string trim_spaces(const std::string& input) { |
| 96 | + return std::regex_replace(input, whitespace_regex, ""); |
99 | 97 | }
|
100 | 98 |
|
| 99 | +const std::regex env_regex{ |
| 100 | + R"((?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$))", |
| 101 | + std::regex_constants::ECMAScript | std::regex_constants::multiline}; |
| 102 | + |
| 103 | +const std::regex quotes_regex{ |
| 104 | + R"(^(['"`])([\s\S]*)\1$)", |
| 105 | + std::regex_constants::ECMAScript | std::regex_constants::multiline}; |
| 106 | +const std::regex newline_regex{ |
| 107 | + R"(\\n)", |
| 108 | + std::regex_constants::ECMAScript | std::regex_constants::multiline}; |
| 109 | +const std::regex cr_regex{ |
| 110 | + R"(\\r)", |
| 111 | + std::regex_constants::ECMAScript | std::regex_constants::multiline}; |
| 112 | + |
101 | 113 | void Dotenv::ParseContent(const std::string_view input) {
|
102 | 114 | std::string lines(input);
|
103 | 115 |
|
104 | 116 | // Handle windows newlines "\r\n": remove "\r" and keep only "\n"
|
105 | 117 | lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());
|
106 | 118 |
|
107 |
| - std::string_view content = lines; |
108 |
| - content = trim_spaces(content); |
109 |
| - |
110 |
| - std::string_view key; |
111 |
| - std::string_view value; |
| 119 | + std::string content = trim_spaces(lines); |
112 | 120 |
|
113 |
| - while (!content.empty()) { |
114 |
| - // Skip empty lines and comments |
115 |
| - if (content.front() == '\n' || content.front() == '#') { |
116 |
| - auto newline = content.find('\n'); |
117 |
| - if (newline != std::string_view::npos) { |
118 |
| - content.remove_prefix(newline + 1); |
119 |
| - continue; |
120 |
| - } |
121 |
| - } |
| 121 | + std::string key; |
| 122 | + std::string value; |
122 | 123 |
|
123 |
| - // If there is no equal character, then ignore everything |
124 |
| - auto equal = content.find('='); |
125 |
| - if (equal == std::string_view::npos) { |
126 |
| - break; |
127 |
| - } |
| 124 | + auto matches_begin = |
| 125 | + std::sregex_iterator(content.cbegin(), content.cend(), env_regex); |
| 126 | + auto matches_end = std::sregex_iterator(); |
128 | 127 |
|
129 |
| - key = content.substr(0, equal); |
130 |
| - content.remove_prefix(equal + 1); |
131 |
| - key = trim_spaces(key); |
132 |
| - content = trim_spaces(content); |
| 128 | + for (std::regex_iterator i = matches_begin; i != matches_end; ++i) { |
| 129 | + key = i->str(1); |
| 130 | + value = i->str(2); |
| 131 | + value = trim_spaces(value); |
133 | 132 |
|
134 |
| - if (key.empty()) { |
135 |
| - break; |
136 |
| - } |
| 133 | + const auto has_opening_double_quote = value.size() > 0 && value[0] == '\"'; |
| 134 | + value = std::regex_replace(value, quotes_regex, "$2"); |
137 | 135 |
|
138 |
| - // Remove export prefix from key |
139 |
| - if (key.starts_with("export ")) { |
140 |
| - key.remove_prefix(7); |
| 136 | + if (has_opening_double_quote) { |
| 137 | + // replace escaped newlines and carriage returns |
| 138 | + value = std::regex_replace(value, newline_regex, "\n"); |
| 139 | + value = std::regex_replace(value, cr_regex, "\r"); |
141 | 140 | }
|
142 | 141 |
|
143 |
| - // SAFETY: Content is guaranteed to have at least one character |
144 |
| - if (content.empty()) { |
145 |
| - // In case the last line is a single key without value |
146 |
| - // Example: KEY= (without a newline at the EOF) |
147 |
| - store_.insert_or_assign(std::string(key), ""); |
148 |
| - break; |
149 |
| - } |
150 |
| - |
151 |
| - // Expand new line if \n it's inside double quotes |
152 |
| - // Example: EXPAND_NEWLINES = 'expand\nnew\nlines' |
153 |
| - if (content.front() == '"') { |
154 |
| - auto closing_quote = content.find(content.front(), 1); |
155 |
| - if (closing_quote != std::string_view::npos) { |
156 |
| - value = content.substr(1, closing_quote - 1); |
157 |
| - std::string multi_line_value = std::string(value); |
158 |
| - |
159 |
| - size_t pos = 0; |
160 |
| - while ((pos = multi_line_value.find("\\n", pos)) != |
161 |
| - std::string_view::npos) { |
162 |
| - multi_line_value.replace(pos, 2, "\n"); |
163 |
| - pos += 1; |
164 |
| - } |
165 |
| - |
166 |
| - store_.insert_or_assign(std::string(key), multi_line_value); |
167 |
| - content.remove_prefix(content.find('\n', closing_quote + 1)); |
168 |
| - continue; |
169 |
| - } |
170 |
| - } |
171 |
| - |
172 |
| - // Check if the value is wrapped in quotes, single quotes or backticks |
173 |
| - if ((content.front() == '\'' || content.front() == '"' || |
174 |
| - content.front() == '`')) { |
175 |
| - auto closing_quote = content.find(content.front(), 1); |
176 |
| - |
177 |
| - // Check if the closing quote is not found |
178 |
| - // Example: KEY="value |
179 |
| - if (closing_quote == std::string_view::npos) { |
180 |
| - // Check if newline exist. If it does, take the entire line as the value |
181 |
| - // Example: KEY="value\nKEY2=value2 |
182 |
| - // The value pair should be `"value` |
183 |
| - auto newline = content.find('\n'); |
184 |
| - if (newline != std::string_view::npos) { |
185 |
| - value = content.substr(0, newline); |
186 |
| - store_.insert_or_assign(std::string(key), value); |
187 |
| - content.remove_prefix(newline); |
188 |
| - } |
189 |
| - } else { |
190 |
| - // Example: KEY="value" |
191 |
| - value = content.substr(1, closing_quote - 1); |
192 |
| - store_.insert_or_assign(std::string(key), value); |
193 |
| - // Select the first newline after the closing quotation mark |
194 |
| - // since there could be newline characters inside the value. |
195 |
| - content.remove_prefix(content.find('\n', closing_quote + 1)); |
196 |
| - } |
197 |
| - } else { |
198 |
| - // Regular key value pair. |
199 |
| - // Example: `KEY=this is value` |
200 |
| - auto newline = content.find('\n'); |
201 |
| - |
202 |
| - if (newline != std::string_view::npos) { |
203 |
| - value = content.substr(0, newline); |
204 |
| - auto hash_character = value.find('#'); |
205 |
| - // Check if there is a comment in the line |
206 |
| - // Example: KEY=value # comment |
207 |
| - // The value pair should be `value` |
208 |
| - if (hash_character != std::string_view::npos) { |
209 |
| - value = content.substr(0, hash_character); |
210 |
| - } |
211 |
| - content.remove_prefix(newline); |
212 |
| - } else { |
213 |
| - // In case the last line is a single key/value pair |
214 |
| - // Example: KEY=VALUE (without a newline at the EOF) |
215 |
| - value = content.substr(0); |
216 |
| - } |
217 |
| - |
218 |
| - value = trim_spaces(value); |
219 |
| - store_.insert_or_assign(std::string(key), value); |
220 |
| - } |
| 142 | + store_.insert_or_assign(key, value); |
221 | 143 | }
|
222 | 144 | }
|
223 | 145 |
|
|
0 commit comments