| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f210a15 commit 2df7ce9
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ const common = require('../common'); | |||
| 4 | 4 | const assert = require('node:assert'); | |
| 5 | 5 | const path = require('node:path'); | |
| 6 | 6 | const { describe, it } = require('node:test'); | |
| 7 | + const { parseEnv } = require('node:util'); | ||
| 7 | 8 | const fixtures = require('../common/fixtures'); | |
| 8 | 9 | ||
| 9 | 10 | const validEnvFilePath = '../fixtures/dotenv/valid.env'; | |
@@ -200,4 +201,61 @@ describe('.env supports edge cases', () => { | |||
| 200 | 201 | assert.strictEqual(child.code, 9); | |
| 201 | 202 | assert.match(child.stderr, /bad option: --env-file-ABCD/); | |
| 202 | 203 | }); | |
| 204 | + | ||
| 205 | + it('should handle invalid multiline syntax', () => { | ||
| 206 | + const result = parseEnv([ | ||
| 207 | + 'foo', | ||
| 208 | + '', | ||
| 209 | + 'bar', | ||
| 210 | + 'baz=whatever', | ||
| 211 | + 'VALID_AFTER_INVALID=test', | ||
| 212 | + 'multiple_invalid', | ||
| 213 | + 'lines_without_equals', | ||
| 214 | + 'ANOTHER_VALID=value', | ||
| 215 | + ].join('\n')); | ||
| 216 | + | ||
| 217 | + assert.deepStrictEqual(result, { | ||
| 218 | + baz: 'whatever', | ||
| 219 | + VALID_AFTER_INVALID: 'test', | ||
| 220 | + ANOTHER_VALID: 'value', | ||
| 221 | + }); | ||
| 222 | + }); | ||
| 223 | + | ||
| 224 | + it('should handle trimming of keys and values correctly', () => { | ||
| 225 | + const result = parseEnv([ | ||
| 226 | + ' KEY_WITH_SPACES_BEFORE= value_with_spaces_before_and_after ', | ||
| 227 | + 'KEY_WITH_TABS_BEFORE\t=\tvalue_with_tabs_before_and_after\t', | ||
| 228 | + 'KEY_WITH_SPACES_AND_TABS\t = \t value_with_spaces_and_tabs \t', | ||
| 229 | + ' KEY_WITH_SPACES_ONLY =value', | ||
| 230 | + 'KEY_WITH_NO_VALUE=', | ||
| 231 | + 'KEY_WITH_SPACES_AFTER= value ', | ||
| 232 | + 'KEY_WITH_SPACES_AND_COMMENT=value # this is a comment', | ||
| 233 | + 'KEY_WITH_ONLY_COMMENT=# this is a comment', | ||
| 234 | + 'KEY_WITH_EXPORT=export value', | ||
| 235 | + ' export KEY_WITH_EXPORT_AND_SPACES = value ', | ||
| 236 | + ].join('\n')); | ||
| 237 | + | ||
| 238 | + assert.deepStrictEqual(result, { | ||
| 239 | + KEY_WITH_SPACES_BEFORE: 'value_with_spaces_before_and_after', | ||
| 240 | + KEY_WITH_TABS_BEFORE: 'value_with_tabs_before_and_after', | ||
| 241 | + KEY_WITH_SPACES_AND_TABS: 'value_with_spaces_and_tabs', | ||
| 242 | + KEY_WITH_SPACES_ONLY: 'value', | ||
| 243 | + KEY_WITH_NO_VALUE: '', | ||
| 244 | + KEY_WITH_ONLY_COMMENT: '', | ||
| 245 | + KEY_WITH_SPACES_AFTER: 'value', | ||
| 246 | + KEY_WITH_SPACES_AND_COMMENT: 'value', | ||
| 247 | + KEY_WITH_EXPORT: 'export value', | ||
| 248 | + KEY_WITH_EXPORT_AND_SPACES: 'value', | ||
| 249 | + }); | ||
| 250 | + }); | ||
| 251 | + | ||
| 252 | + it('should handle a comment in a valid value', () => { | ||
| 253 | + const result = parseEnv([ | ||
| 254 | + 'KEY_WITH_COMMENT_IN_VALUE="value # this is a comment"', | ||
| 255 | + ].join('\n')); | ||
| 256 | + | ||
| 257 | + assert.deepStrictEqual(result, { | ||
| 258 | + KEY_WITH_COMMENT_IN_VALUE: 'value # this is a comment', | ||
| 259 | + }); | ||
| 260 | + }); | ||
| 203 | 261 | }); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments