@@ -102,8 +102,11 @@ MaybeLocal<Object> Dotenv::ToObject(Environment* env) const {
102
102
return scope.Escape (result);
103
103
}
104
104
105
- // Removes space characters (spaces, tabs and newlines) from
106
- // the start and end of a given input string
105
+ // Removes leading and trailing spaces from a string_view.
106
+ // Returns an empty string_view if the input is empty.
107
+ // Example:
108
+ // trim_spaces(" hello ") -> "hello"
109
+ // trim_spaces("") -> ""
107
110
std::string_view trim_spaces (std::string_view input) {
108
111
if (input.empty ()) return " " ;
109
112
@@ -135,47 +138,62 @@ void Dotenv::ParseContent(const std::string_view input) {
135
138
while (!content.empty ()) {
136
139
// Skip empty lines and comments
137
140
if (content.front () == ' \n ' || content.front () == ' #' ) {
141
+ // Check if the first character of the content is a newline or a hash
138
142
auto newline = content.find (' \n ' );
139
143
if (newline != std::string_view::npos) {
140
- content.remove_prefix (newline + 1 );
141
- continue ;
142
- }
143
- }
144
-
145
- // If there is no equal character, then ignore everything
146
- auto equal = content.find (' =' );
147
- if (equal == std::string_view::npos) {
148
- auto newline = content.find (' \n ' );
149
- if (newline != std::string_view::npos) {
150
- // If we used `newline` only,
151
- // the '\n' might remain and cause an empty-line parse
144
+ // Remove everything up to and including the newline character
152
145
content.remove_prefix (newline + 1 );
153
146
} else {
147
+ // If no newline is found, clear the content
154
148
content = {};
155
149
}
156
- // No valid data here, skip to next line
150
+
151
+ // Skip the remaining code in the loop and continue with the next
152
+ // iteration.
157
153
continue ;
158
154
}
159
155
160
- key = content.substr (0 , equal);
161
- content.remove_prefix (equal + 1 );
156
+ // Find the next equals sign or newline in a single pass.
157
+ // This optimizes the search by avoiding multiple iterations.
158
+ auto equal_or_newline = content.find_first_of (" =\n " );
159
+
160
+ // If we found nothing or found a newline before equals, the line is invalid
161
+ if (equal_or_newline == std::string_view::npos ||
162
+ content.at (equal_or_newline) == ' \n ' ) {
163
+ if (equal_or_newline != std::string_view::npos) {
164
+ content.remove_prefix (equal_or_newline + 1 );
165
+ content = trim_spaces (content);
166
+ continue ;
167
+ }
168
+ break ;
169
+ }
170
+
171
+ // We found an equals sign, extract the key
172
+ key = content.substr (0 , equal_or_newline);
173
+ content.remove_prefix (equal_or_newline + 1 );
162
174
key = trim_spaces (key);
163
175
164
- // If the value is not present (e.g. KEY=) set is to an empty string
176
+ // If the value is not present (e.g. KEY=) set it to an empty string
165
177
if (content.empty () || content.front () == ' \n ' ) {
166
178
store_.insert_or_assign (std::string (key), " " );
167
179
continue ;
168
180
}
169
181
170
182
content = trim_spaces (content);
171
183
172
- if (key.empty ()) {
173
- break ;
174
- }
184
+ // Skip lines with empty keys after trimming spaces.
185
+ // Examples of invalid keys that would be skipped:
186
+ // =value
187
+ // " "=value
188
+ if (key.empty ()) continue ;
175
189
176
- // Remove export prefix from key
190
+ // Remove export prefix from key and ensure proper spacing.
191
+ // Example: export FOO=bar -> FOO=bar
177
192
if (key.starts_with (" export " )) {
178
193
key.remove_prefix (7 );
194
+ // Trim spaces after removing export prefix to handle cases like:
195
+ // export FOO=bar
196
+ key = trim_spaces (key);
179
197
}
180
198
181
199
// SAFETY: Content is guaranteed to have at least one character
@@ -194,6 +212,7 @@ void Dotenv::ParseContent(const std::string_view input) {
194
212
value = content.substr (1 , closing_quote - 1 );
195
213
std::string multi_line_value = std::string (value);
196
214
215
+ // Replace \n with actual newlines in double-quoted strings
197
216
size_t pos = 0 ;
198
217
while ((pos = multi_line_value.find (" \\ n" , pos)) !=
199
218
std::string_view::npos) {
@@ -206,15 +225,17 @@ void Dotenv::ParseContent(const std::string_view input) {
206
225
if (newline != std::string_view::npos) {
207
226
content.remove_prefix (newline + 1 );
208
227
} else {
228
+ // In case the last line is a single key/value pair
229
+ // Example: KEY=VALUE (without a newline at the EOF
209
230
content = {};
210
231
}
211
232
continue ;
212
233
}
213
234
}
214
235
215
- // Check if the value is wrapped in quotes, single quotes or backticks
216
- if (( content.front () == ' \' ' || content.front () == ' "' ||
217
- content.front () == ' `' ) ) {
236
+ // Handle quoted values (single quotes, double quotes, backticks)
237
+ if (content.front () == ' \' ' || content.front () == ' "' ||
238
+ content.front () == ' `' ) {
218
239
auto closing_quote = content.find (content.front (), 1 );
219
240
220
241
// Check if the closing quote is not found
@@ -228,13 +249,16 @@ void Dotenv::ParseContent(const std::string_view input) {
228
249
value = content.substr (0 , newline);
229
250
store_.insert_or_assign (std::string (key), value);
230
251
content.remove_prefix (newline + 1 );
252
+ } else {
253
+ // No newline - take rest of content
254
+ value = content;
255
+ store_.insert_or_assign (std::string (key), value);
256
+ break ;
231
257
}
232
258
} else {
233
- // Example: KEY="value"
259
+ // Found closing quote - take content between quotes
234
260
value = content.substr (1 , closing_quote - 1 );
235
261
store_.insert_or_assign (std::string (key), value);
236
- // Select the first newline after the closing quotation mark
237
- // since there could be newline characters inside the value.
238
262
auto newline = content.find (' \n ' , closing_quote + 1 );
239
263
if (newline != std::string_view::npos) {
240
264
// Use +1 to discard the '\n' itself => next line
@@ -257,13 +281,13 @@ void Dotenv::ParseContent(const std::string_view input) {
257
281
// Example: KEY=value # comment
258
282
// The value pair should be `value`
259
283
if (hash_character != std::string_view::npos) {
260
- value = content .substr (0 , hash_character);
284
+ value = value .substr (0 , hash_character);
261
285
}
262
- store_.insert_or_assign (std::string (key), trim_spaces (value));
286
+ value = trim_spaces (value);
287
+ store_.insert_or_assign (std::string (key), std::string (value));
263
288
content.remove_prefix (newline + 1 );
264
289
} else {
265
- // In case the last line is a single key/value pair
266
- // Example: KEY=VALUE (without a newline at the EOF)
290
+ // Last line without newline
267
291
value = content;
268
292
auto hash_char = value.find (' #' );
269
293
if (hash_char != std::string_view::npos) {
@@ -272,9 +296,9 @@ void Dotenv::ParseContent(const std::string_view input) {
272
296
store_.insert_or_assign (std::string (key), trim_spaces (value));
273
297
content = {};
274
298
}
275
-
276
- store_.insert_or_assign (std::string (key), trim_spaces (value));
277
299
}
300
+
301
+ content = trim_spaces (content);
278
302
}
279
303
}
280
304
0 commit comments