Repository: nodejs/node
Pull request: src: safely remove the last line from dotenv

Refs: https://github.com/nodejs/node/issues/55925

When _GLIBCXX_ASSERTIONS is enabled, passing npos to remove_prefix triggers an assertion error. Modified to avoid calling remove_prefix when npos is encountered.

```
#0  0x0000ffffa799e020 in __pthread_kill_implementation () from /lib64/libc.so.6
#1  0x0000ffffa794b140 [PAC] in raise () from /lib64/libc.so.6
#2  0x0000ffffa79359c8 [PAC] in abort () from /lib64/libc.so.6
#3  0x0000ffffa7cc20c8 [PAC] in std::__glibcxx_assert_fail(char const*, int, char const*, char const*) () from /lib64/libstdc++.so.6
#4  0x000000000195c634 [PAC] in std::basic_string_view<char, std::char_traits<char> >::remove_prefix (this=0xffffd0a72938, __n=18446744073709551615) at /usr/include/c++/14/string_view:297
#5  0x000000000195b98c [PAC] in node::Dotenv::ParseContent (this=0x8c3adf8 <node::per_process::dotenv_file>, input="API_KEY=\"inaaa\"") at ../../src/node_dotenv.cc:185
#6  0x000000000195be60 [PAC] in node::Dotenv::ParsePath (this=0x8c3adf8 <node::per_process::dotenv_file>, path=".env") at ../../src/node_dotenv.cc:276
#7  0x00000000018dceb4 [PAC] in node::InitializeNodeWithArgsInternal (argv=0xe11eda0, exec_argv=0xe11edb8, errors=0xe11edd0, flags=node::ProcessInitializationFlags::kNoFlags) at ../../src/node.cc:860
#8  0x00000000018dd650 [PAC] in node::InitializeOncePerProcessInternal (args=std::vector of length 2, capacity 2 = {...}, flags=node::ProcessInitializationFlags::kNoFlags) at ../../src/node.cc:1003
#9  0x00000000018de97c [PAC] in node::StartInternal (argc=2, argv=0xe11ece0) at ../../src/node.cc:1432
#10 0x00000000018ded04 [PAC] in node::Start (argc=2, argv=0xffffd0a753f8) at ../../src/node.cc:1491
#11 0x0000000003935f54 [PAC] in main (argc=2, argv=0xffffd0a753f8) at ../../src/[node_main.cc:97](http://node_main.cc:97/)
```

In the case of Windows, a debug build is likely to emit a warning when npos is passed to remove_prefix.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: src/node_dotenv.cc
patch: @@ -182,7 +182,10 @@ void Dotenv::ParseContent(const std::string_view input) {
         }
 
         store_.insert_or_assign(std::string(key), multi_line_value);
-        content.remove_prefix(content.find('\n', closing_quote + 1));
+        auto newline = content.find('\n', closing_quote + 1);
+        if (newline != std::string_view::npos) {
+          content.remove_prefix(newline);
+        }
         continue;
       }
     }
@@ -210,7 +213,10 @@ void Dotenv::ParseContent(const std::string_view input) {
         store_.insert_or_assign(std::string(key), value);
         // Select the first newline after the closing quotation mark
         // since there could be newline characters inside the value.
-        content.remove_prefix(content.find('\n', closing_quote + 1));
+        auto newline = content.find('\n', closing_quote + 1);
+        if (newline != std::string_view::npos) {
+          content.remove_prefix(newline);
+        }
       }
     } else {
       // Regular key value pair.

filename: test/fixtures/dotenv/no-final-newline-single-quotes.env
patch: @@ -0,0 +1 @@
+BASIC='basic'
\ No newline at end of file

filename: test/fixtures/dotenv/no-final-newline.env
patch: @@ -0,0 +1 @@
+BASIC="basic"
\ No newline at end of file

filename: test/parallel/test-dotenv-edge-cases.js
patch: @@ -8,6 +8,8 @@ const fixtures = require('../common/fixtures');
 
 const validEnvFilePath = '../fixtures/dotenv/valid.env';
 const nodeOptionsEnvFilePath = '../fixtures/dotenv/node-options.env';
+const noFinalNewlineEnvFilePath = '../fixtures/dotenv/no-final-newline.env';
+const noFinalNewlineSingleQuotesEnvFilePath = '../fixtures/dotenv/no-final-newline-single-quotes.env';
 
 describe('.env supports edge cases', () => {
   it('supports multiple declarations, including optional ones', async () => {
@@ -148,4 +150,24 @@ describe('.env supports edge cases', () => {
     assert.strictEqual(child.stderr, '');
     assert.strictEqual(child.code, 0);
   });
+
+  it('should handle file without a final newline', async () => {
+    const code = `
+      require('assert').strictEqual(process.env.BASIC, 'basic');
+    `.trim();
+    const child = await common.spawnPromisified(
+      process.execPath,
+      [ `--env-file=${path.resolve(__dirname, noFinalNewlineEnvFilePath)}`, '--eval', code ],
+    );
+
+    const SingleQuotesChild = await common.spawnPromisified(
+      process.execPath,
+      [ `--env-file=${path.resolve(__dirname, noFinalNewlineSingleQuotesEnvFilePath)}`, '--eval', code ],
+    );
+
+    assert.strictEqual(child.stderr, '');
+    assert.strictEqual(child.code, 0);
+    assert.strictEqual(SingleQuotesChild.stderr, '');
+    assert.strictEqual(SingleQuotesChild.code, 0);
+  });
 });