[gn] Don't read entire build.ninja into memory in gn gen/clean

This switches ExtractRegenerationCommands to use std::istream rather
than reading the whole file just to use the first ~10 lines.

Change-Id: I2a77c24541edd20d3f5e20130199855148e304fc
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/14321
Reviewed-by: Brett Wilson <brettw@chromium.org>
diff --git a/src/gn/commands.cc b/src/gn/commands.cc
index 5ff7ca8..3840761 100644
--- a/src/gn/commands.cc
+++ b/src/gn/commands.cc
@@ -4,6 +4,7 @@
 
 #include "gn/commands.h"
 
+#include <fstream>
 #include <optional>
 
 #include "base/command_line.h"
@@ -492,25 +493,25 @@
 
   // Write a stripped down build.ninja file with just the commands needed
   // for ninja to call GN and regenerate ninja files.
-  base::FilePath build_ninja_file(settings->GetFullPath(
+  base::FilePath build_ninja_path(settings->GetFullPath(
       SourceFile(settings->build_dir().value() + "build.ninja")));
-  std::string build_ninja_contents;
-  if (!base::ReadFileToString(build_ninja_file, &build_ninja_contents)) {
-    // Couldn't parse the build.ninja file.
-    Err(Location(), "Couldn't read build.ninja in this directory.",
+  std::ifstream build_ninja_file(build_ninja_path.value());
+  if (!build_ninja_file) {
+    // Couldn't open the build.ninja file.
+    Err(Location(), "Couldn't open build.ninja in this directory.",
         "Try running \"gn gen\" on it and then re-running \"gn clean\".")
         .PrintToStdout();
     return false;
   }
   std::string build_commands =
-      NinjaBuildWriter::ExtractRegenerationCommands(build_ninja_contents);
+      NinjaBuildWriter::ExtractRegenerationCommands(build_ninja_file);
   if (build_commands.empty()) {
     Err(Location(), "Unexpected build.ninja contents in this directory.",
         "Try running \"gn gen\" on it and then re-running \"gn clean\".")
         .PrintToStdout();
     return false;
   }
-  if (util::WriteFileAtomically(build_ninja_file, build_commands.data(),
+  if (util::WriteFileAtomically(build_ninja_path, build_commands.data(),
                                 static_cast<int>(build_commands.size())) ==
       -1) {
     Err(Location(), std::string("Failed to write build.ninja."))
diff --git a/src/gn/ninja_build_writer.cc b/src/gn/ninja_build_writer.cc
index 90a296b..05db217 100644
--- a/src/gn/ninja_build_writer.cc
+++ b/src/gn/ninja_build_writer.cc
@@ -13,7 +13,6 @@
 
 #include "base/command_line.h"
 #include "base/files/file_util.h"
-#include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "gn/build_settings.h"
@@ -302,13 +301,10 @@
 
 // static
 std::string NinjaBuildWriter::ExtractRegenerationCommands(
-    const std::string& build_ninja_in) {
-  std::vector<std::string_view> lines = base::SplitStringPiece(
-      build_ninja_in, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
-
+    std::istream& build_ninja_in) {
   std::ostringstream out;
   int num_blank_lines = 0;
-  for (const auto& line : lines) {
+  for (std::string line; std::getline(build_ninja_in, line);) {
     out << line << '\n';
     if (line.empty())
       ++num_blank_lines;
diff --git a/src/gn/ninja_build_writer.h b/src/gn/ninja_build_writer.h
index 54a80aa..c21769a 100644
--- a/src/gn/ninja_build_writer.h
+++ b/src/gn/ninja_build_writer.h
@@ -56,8 +56,7 @@
   // commands::PrepareForRegeneration.
   //
   // On error, returns an empty string.
-  static std::string ExtractRegenerationCommands(
-      const std::string& build_ninja_in);
+  static std::string ExtractRegenerationCommands(std::istream& build_ninja_in);
 
   bool Run(Err* err);
 
diff --git a/src/gn/ninja_build_writer_unittest.cc b/src/gn/ninja_build_writer_unittest.cc
index 453ff03..f8abaf2 100644
--- a/src/gn/ninja_build_writer_unittest.cc
+++ b/src/gn/ninja_build_writer_unittest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <fstream>
 #include <sstream>
 
 #include "base/command_line.h"
@@ -211,7 +212,7 @@
 
   std::vector<const Target*> targets = {&target_foo};
 
-  std::ostringstream ninja_out;
+  std::stringstream ninja_out;
   std::ostringstream depfile_out;
 
   NinjaBuildWriter writer(setup.build_settings(), used_toolchains, targets,
@@ -242,7 +243,7 @@
   EXPECT_SNIPPET(ninja_out_str, expected_default);
 
   std::string commands =
-      NinjaBuildWriter::ExtractRegenerationCommands(ninja_out_str);
+      NinjaBuildWriter::ExtractRegenerationCommands(ninja_out);
   EXPECT_SNIPPET(commands, expected_rule_gn);
   EXPECT_SNIPPET(commands, expected_build_ninja_stamp);
   EXPECT_SNIPPET(commands, expected_build_ninja);
@@ -254,6 +255,22 @@
 #undef EXPECT_NO_SNIPPET
 }
 
+TEST_F(NinjaBuildWriterTest, ExtractRegenerationCommands_DefaultStream) {
+  std::ifstream ninja_in;
+  EXPECT_EQ(NinjaBuildWriter::ExtractRegenerationCommands(ninja_in), "");
+}
+
+TEST_F(NinjaBuildWriterTest, ExtractRegenerationCommands_StreamError) {
+  std::ifstream ninja_in("/does/not/exist");
+  EXPECT_EQ(NinjaBuildWriter::ExtractRegenerationCommands(ninja_in), "");
+}
+
+TEST_F(NinjaBuildWriterTest, ExtractRegenerationCommands_IncompleteNinja) {
+  std::stringstream ninja_in;
+  ninja_in << "foo\nbar\nbaz\nbif\n";
+  EXPECT_EQ(NinjaBuildWriter::ExtractRegenerationCommands(ninja_in), "");
+}
+
 TEST_F(NinjaBuildWriterTest, SpaceInDepfile) {
   TestWithScope setup;
   Err err;