Introduce base::StringSplitter

Also improve FsMount tests

Test: perfetto_unittests --getst_filter=StringSplitterTest*
Bug: 73625480
Change-Id: Ib542af6eaa8f27f0aac01c81632d6230b4d17f19
diff --git a/Android.bp b/Android.bp
index 12469e5..15f4e94 100644
--- a/Android.bp
+++ b/Android.bp
@@ -33,6 +33,7 @@
     ":perfetto_src_ipc_wire_protocol_gen",
     "src/base/file_utils.cc",
     "src/base/page_allocator.cc",
+    "src/base/string_splitter.cc",
     "src/base/thread_checker.cc",
     "src/base/unix_task_runner.cc",
     "src/base/watchdog.cc",
@@ -130,6 +131,7 @@
     "src/base/android_task_runner.cc",
     "src/base/file_utils.cc",
     "src/base/page_allocator.cc",
+    "src/base/string_splitter.cc",
     "src/base/thread_checker.cc",
     "src/base/unix_task_runner.cc",
     "src/base/watchdog.cc",
@@ -240,6 +242,7 @@
     "src/base/android_task_runner.cc",
     "src/base/file_utils.cc",
     "src/base/page_allocator.cc",
+    "src/base/string_splitter.cc",
     "src/base/test/test_task_runner.cc",
     "src/base/test/vm_test_utils.cc",
     "src/base/thread_checker.cc",
@@ -2947,6 +2950,7 @@
     ":perfetto_src_ipc_wire_protocol_gen",
     "src/base/file_utils.cc",
     "src/base/page_allocator.cc",
+    "src/base/string_splitter.cc",
     "src/base/thread_checker.cc",
     "src/base/unix_task_runner.cc",
     "src/base/watchdog.cc",
@@ -3059,6 +3063,8 @@
     "src/base/page_allocator.cc",
     "src/base/page_allocator_unittest.cc",
     "src/base/scoped_file_unittest.cc",
+    "src/base/string_splitter.cc",
+    "src/base/string_splitter_unittest.cc",
     "src/base/task_runner_unittest.cc",
     "src/base/test/test_task_runner.cc",
     "src/base/test/vm_test_utils.cc",
diff --git a/include/perfetto/base/BUILD.gn b/include/perfetto/base/BUILD.gn
index 2847412..1a4c10a 100644
--- a/include/perfetto/base/BUILD.gn
+++ b/include/perfetto/base/BUILD.gn
@@ -19,6 +19,7 @@
     "logging.h",
     "page_allocator.h",
     "scoped_file.h",
+    "string_splitter.h",
     "task_runner.h",
     "thread_checker.h",
     "unix_task_runner.h",
diff --git a/include/perfetto/base/string_splitter.h b/include/perfetto/base/string_splitter.h
new file mode 100644
index 0000000..cee1a27
--- /dev/null
+++ b/include/perfetto/base/string_splitter.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef INCLUDE_PERFETTO_BASE_STRING_SPLITTER_H_
+#define INCLUDE_PERFETTO_BASE_STRING_SPLITTER_H_
+
+#include <string>
+
+namespace perfetto {
+namespace base {
+
+// C++ version of strtok(). Splits a string without making copies or any heap
+// allocations. Destructs the original string passed in input.
+// Supports the special case of using \0 as a delimiter.
+// The token returned in output are valid as long as the input string is valid.
+class StringSplitter {
+ public:
+  // Can take ownership of the string if passed via std::move(), e.g.:
+  // StringSplitter(std::move(str), '\n');
+  StringSplitter(std::string, char delimiter);
+
+  // Splits a C-string. The input string will be forcefully null-terminated (so
+  // str[size - 1] should be == '\0' or the last char will be truncated).
+  StringSplitter(char* str, size_t size, char delimiter);
+
+  // Splits the current token from an outer StringSplitter instance. This is to
+  // chain splitters as follows:
+  // for (base::StringSplitter lines(x, '\n'); ss.Next();)
+  //   for (base::StringSplitter words(&lines, ' '); words.Next();)
+  StringSplitter(StringSplitter*, char delimiter);
+
+  // Returns true if a token is found (in which case it will be stored in
+  // cur_token()), false if no more tokens are found.
+  bool Next();
+
+  // Returns the current token iff last call to Next() returned true. In this
+  // case it guarantees that the returned string is always null terminated.
+  // In all other cases (before the 1st call to Next() and after Next() returns
+  // false) returns nullptr.
+  char* cur_token() { return cur_; }
+
+  // Returns the length of the current token (excluding the null terminator).
+  size_t cur_token_size() const { return cur_size_; }
+
+ private:
+  StringSplitter(const StringSplitter&) = delete;
+  StringSplitter& operator=(const StringSplitter&) = delete;
+  void Initialize(char* str, size_t size);
+
+  std::string str_;
+  char* cur_;
+  size_t cur_size_;
+  char* next_;
+  char* end_;  // STL-style, points one past the last char.
+  const char delimiter_;
+};
+
+}  // namespace base
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_BASE_STRING_SPLITTER_H_
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index 9fc5460..2698002 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -24,6 +24,7 @@
   sources = [
     "file_utils.cc",
     "page_allocator.cc",
+    "string_splitter.cc",
     "thread_checker.cc",
     "unix_task_runner.cc",
   ]
@@ -99,6 +100,7 @@
   sources = [
     "page_allocator_unittest.cc",
     "scoped_file_unittest.cc",
+    "string_splitter_unittest.cc",
     "task_runner_unittest.cc",
     "thread_checker_unittest.cc",
     "utils_unittest.cc",
diff --git a/src/base/string_splitter.cc b/src/base/string_splitter.cc
new file mode 100644
index 0000000..aab47b1
--- /dev/null
+++ b/src/base/string_splitter.cc
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "perfetto/base/string_splitter.h"
+
+#include <utility>
+
+#include "perfetto/base/logging.h"
+
+namespace perfetto {
+namespace base {
+
+StringSplitter::StringSplitter(std::string str, char delimiter)
+    : str_(std::move(str)), delimiter_(delimiter) {
+  // It's legal to access str[str.size()] in C++11 (it always returns \0),
+  // hence the +1 (which becomes just size() after the -1 in Initialize()).
+  Initialize(&str_[0], str_.size() + 1);
+}
+
+StringSplitter::StringSplitter(char* str, size_t size, char delimiter)
+    : delimiter_(delimiter) {
+  Initialize(str, size);
+}
+
+StringSplitter::StringSplitter(StringSplitter* outer, char delimiter)
+    : delimiter_(delimiter) {
+  Initialize(outer->cur_token(), outer->cur_token_size() + 1);
+}
+
+void StringSplitter::Initialize(char* str, size_t size) {
+  PERFETTO_DCHECK(!size || str);
+  next_ = str;
+  end_ = str + size;
+  cur_ = nullptr;
+  cur_size_ = 0;
+  if (size)
+    next_[size - 1] = '\0';
+}
+
+bool StringSplitter::Next() {
+  for (; next_ < end_; next_++) {
+    if (*next_ == delimiter_)
+      continue;
+    cur_ = next_;
+    for (;; next_++) {
+      if (*next_ == delimiter_) {
+        cur_size_ = static_cast<size_t>(next_ - cur_);
+        *(next_++) = '\0';
+        break;
+      }
+      if (*next_ == '\0') {
+        cur_size_ = static_cast<size_t>(next_ - cur_);
+        next_ = end_;
+        break;
+      }
+    }
+    if (*cur_)
+      return true;
+    break;
+  }
+  cur_ = nullptr;
+  cur_size_ = 0;
+  return false;
+}
+
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/base/string_splitter_unittest.cc b/src/base/string_splitter_unittest.cc
new file mode 100644
index 0000000..c3a3796
--- /dev/null
+++ b/src/base/string_splitter_unittest.cc
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "perfetto/base/string_splitter.h"
+
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace perfetto {
+namespace base {
+namespace {
+
+using testing::ElementsAreArray;
+
+TEST(StringSplitterTest, StdString) {
+  {
+    StringSplitter ss("", 'x');
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+  }
+  {
+    StringSplitter ss(std::string(), 'x');
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+  }
+  {
+    StringSplitter ss("a", 'x');
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("a", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(0u, ss.cur_token_size());
+  }
+  {
+    StringSplitter ss("abc", 'x');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("abc", ss.cur_token());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_FALSE(ss.Next());
+  }
+  {
+    StringSplitter ss("ab,", ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("ab", ss.cur_token());
+    EXPECT_EQ(2u, ss.cur_token_size());
+    EXPECT_FALSE(ss.Next());
+  }
+  {
+    StringSplitter ss(",ab,", ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("ab", ss.cur_token());
+    EXPECT_EQ(2u, ss.cur_token_size());
+    EXPECT_FALSE(ss.Next());
+  }
+  {
+    StringSplitter ss("a,b,c", ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(1u, ss.cur_token_size());
+    EXPECT_STREQ("a", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("b", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("c", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+  }
+  {
+    StringSplitter ss("a,b,c,", ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("a", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("b", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("c", ss.cur_token());
+
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+  }
+  {
+    StringSplitter ss(",,a,,b,,,,c,,,", ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("a", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("b", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("c", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+    }
+  }
+  {
+    StringSplitter ss(",,", ',');
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(nullptr, ss.cur_token());
+      EXPECT_EQ(0u, ss.cur_token_size());
+    }
+  }
+  {
+    StringSplitter ss(",,foo", ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("foo", ss.cur_token());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_FALSE(ss.Next());
+  }
+}
+
+TEST(StringSplitterTest, CString) {
+  {
+    char buf[] = "\0x\0";
+    StringSplitter ss(buf, sizeof(buf), ',');
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+  }
+  {
+    char buf[] = "foo\nbar\n\nbaz\n";
+    StringSplitter ss(buf, sizeof(buf), '\n');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("foo", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("bar", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("baz", ss.cur_token());
+
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(0u, ss.cur_token_size());
+  }
+  {
+    char buf[] = "";
+    StringSplitter ss(buf, 0, ',');
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+  }
+  {
+    char buf[] = "\0";
+    StringSplitter ss(buf, 1, ',');
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+    EXPECT_EQ(0u, ss.cur_token_size());
+  }
+  {
+    char buf[] = ",,foo,bar\0,baz";
+    StringSplitter ss(buf, sizeof(buf), ',');
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("foo", ss.cur_token());
+    EXPECT_EQ(3u, ss.cur_token_size());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("bar", ss.cur_token());
+    EXPECT_EQ(3u, ss.cur_token_size());
+
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+    }
+  }
+  {
+    char buf[] = ",,a\0,b,";
+    StringSplitter ss(buf, sizeof(buf), ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("a", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(nullptr, ss.cur_token());
+      EXPECT_EQ(0u, ss.cur_token_size());
+    }
+  }
+  {
+    char buf[] = ",a,\0b";
+    StringSplitter ss(buf, sizeof(buf), ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("a", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+  {
+    char buf[] = ",a\0\0,x\0\0b";
+    StringSplitter ss(buf, sizeof(buf), ',');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("a", ss.cur_token());
+    EXPECT_EQ(1u, ss.cur_token_size());
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+}
+
+TEST(StringSplitterTest, SplitOnNUL) {
+  {
+    StringSplitter ss(std::string(""), '\0');
+    EXPECT_FALSE(ss.Next());
+    EXPECT_EQ(nullptr, ss.cur_token());
+  }
+  {
+    std::string str;
+    str.resize(48);
+    memcpy(&str[0], "foo\0", 4);
+    memcpy(&str[4], "bar\0", 4);
+    memcpy(&str[20], "baz", 3);
+    StringSplitter ss(std::move(str), '\0');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("foo", ss.cur_token());
+    EXPECT_EQ(3u, ss.cur_token_size());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("bar", ss.cur_token());
+    EXPECT_EQ(3u, ss.cur_token_size());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_STREQ("baz", ss.cur_token());
+    EXPECT_EQ(3u, ss.cur_token_size());
+
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+  {
+    char buf[] = "foo\0bar\0baz\0";
+    StringSplitter ss(buf, sizeof(buf), '\0');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("foo", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("bar", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("baz", ss.cur_token());
+
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+  {
+    char buf[] = "\0\0foo\0\0\0\0bar\0baz\0\0";
+    StringSplitter ss(buf, sizeof(buf), '\0');
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("foo", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("bar", ss.cur_token());
+
+    EXPECT_TRUE(ss.Next());
+    EXPECT_EQ(3u, ss.cur_token_size());
+    EXPECT_STREQ("baz", ss.cur_token());
+
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+  {
+    char buf[] = "";
+    StringSplitter ss(buf, 0, '\0');
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+  {
+    char buf[] = "\0";
+    StringSplitter ss(buf, 1, '\0');
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+  {
+    char buf[] = "\0\0";
+    StringSplitter ss(buf, 2, '\0');
+    for (int i = 0; i < 3; i++) {
+      EXPECT_FALSE(ss.Next());
+      EXPECT_EQ(0u, ss.cur_token_size());
+      EXPECT_EQ(nullptr, ss.cur_token());
+    }
+  }
+}
+
+TEST(StringSplitterTest, NestedUsage) {
+  char text[] = R"(
+l1w1 l1w2 l1w3
+
+,l,2,w,1   l,2,,w,,2,,
+)";
+  std::vector<std::string> all_lines;
+  std::vector<std::string> all_words;
+  std::vector<std::string> all_tokens;
+  for (StringSplitter lines(text, sizeof(text), '\n'); lines.Next();) {
+    all_lines.push_back(lines.cur_token());
+    for (StringSplitter words(&lines, ' '); words.Next();) {
+      all_words.push_back(words.cur_token());
+      for (StringSplitter tokens(&words, ','); tokens.Next();) {
+        all_tokens.push_back(tokens.cur_token());
+      }
+    }
+  }
+  EXPECT_THAT(all_lines,
+              ElementsAreArray({"l1w1 l1w2 l1w3", ",l,2,w,1   l,2,,w,,2,,"}));
+  EXPECT_THAT(all_words, ElementsAreArray({"l1w1", "l1w2", "l1w3", ",l,2,w,1",
+                                           "l,2,,w,,2,,"}));
+  EXPECT_THAT(all_tokens, ElementsAreArray({"l1w1", "l1w2", "l1w3", "l", "2",
+                                            "w", "1", "l", "2", "w", "2"}));
+}  // namespace
+
+}  // namespace
+}  // namespace base
+}  // namespace perfetto
diff --git a/src/ftrace_reader/format_parser.cc b/src/ftrace_reader/format_parser.cc
index ff516e7..744ba56 100644
--- a/src/ftrace_reader/format_parser.cc
+++ b/src/ftrace_reader/format_parser.cc
@@ -25,6 +25,7 @@
 #include <string>
 #include <vector>
 
+#include "perfetto/base/string_splitter.h"
 #include "perfetto/base/utils.h"
 
 namespace perfetto {
@@ -80,9 +81,6 @@
 }
 
 bool ParseFtraceEvent(const std::string& input, FtraceEvent* output) {
-  std::unique_ptr<char[], base::FreeDeleter> input_copy(strdup(input.c_str()));
-  char* s = input_copy.get();
-
   char buffer[MAX_FIELD_LENGTH + 1];
 
   bool has_id = false;
@@ -93,7 +91,8 @@
   std::vector<FtraceEvent::Field> common_fields;
   std::vector<FtraceEvent::Field> fields;
 
-  for (char* line = strtok(s, "\n"); line; line = strtok(nullptr, "\n")) {
+  for (base::StringSplitter ss(input, '\n'); ss.Next();) {
+    const char* line = ss.cur_token();
     if (!has_id && sscanf(line, "ID: %d", &id) == 1) {
       has_id = true;
       continue;
diff --git a/src/process_stats/procfs_utils.cc b/src/process_stats/procfs_utils.cc
index e94716f..0411e20 100644
--- a/src/process_stats/procfs_utils.cc
+++ b/src/process_stats/procfs_utils.cc
@@ -11,6 +11,7 @@
 #include "file_utils.h"
 
 #include "perfetto/base/logging.h"
+#include "perfetto/base/string_splitter.h"
 
 using file_utils::ForEachPidInProcPath;
 using file_utils::ReadProcFile;
@@ -53,17 +54,6 @@
   return atoi(line + strlen(status_string));
 }
 
-inline std::vector<std::string> SplitOnNull(const char* input, size_t size) {
-  std::vector<std::string> output;
-  const char* end = input + size;
-  do {
-    // This works because it will only push the string up to a null character.
-    output.push_back(std::string(input));
-    input += output.back().size() + 1;
-  } while (input[0] != 0 && input < end);
-  return output;
-}
-
 }  // namespace
 
 int ReadTgid(int pid) {
@@ -86,7 +76,9 @@
     process->cmdline.push_back(name);
     process->in_kernel = true;
   } else {
-    process->cmdline = SplitOnNull(cmdline_buf, sizeof(cmdline_buf));
+    using perfetto::base::StringSplitter;
+    for (StringSplitter ss(cmdline_buf, sizeof(cmdline_buf), '\0'); ss.Next();)
+      process->cmdline.push_back(ss.cur_token());
     ReadExePath(pid, process->exe, sizeof(process->exe));
     process->is_app = IsApp(process->cmdline[0].c_str(), process->exe);
   }
diff --git a/src/traced/probes/filesystem/fs_mount.cc b/src/traced/probes/filesystem/fs_mount.cc
index 1187974..1db29ba 100644
--- a/src/traced/probes/filesystem/fs_mount.cc
+++ b/src/traced/probes/filesystem/fs_mount.cc
@@ -23,51 +23,31 @@
 
 #include "perfetto/base/file_utils.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/base/string_splitter.h"
 
 namespace perfetto {
 
-namespace {
-constexpr const char kMountsPath[] = "/proc/mounts";
-
-std::vector<std::string> split(const std::string& text, char s) {
-  std::vector<std::string> result;
-  size_t start = 0;
-  size_t end = 0;
-  do {
-    end = text.find(s, start);
-    if (end == std::string::npos)
-      end = text.size();
-    std::string sub = text.substr(start, end - start);
-    if (!sub.empty())
-      result.emplace_back(std::move(sub));
-    start = end + 1;
-  } while (start < text.size());
-  return result;
-}
-}  // namespace
-
-std::multimap<BlockDeviceID, std::string> ParseMounts() {
+std::multimap<BlockDeviceID, std::string> ParseMounts(const char* path) {
   std::string data;
-  if (!base::ReadFile(kMountsPath, &data)) {
-    PERFETTO_ELOG("Failed to read %s.", kMountsPath);
+  if (!base::ReadFile(path, &data)) {
+    PERFETTO_ELOG("Failed to read %s", path);
     return {};
   }
   std::multimap<BlockDeviceID, std::string> device_to_mountpoints;
-  std::vector<std::string> lines = split(data, '\n');
-  struct stat buf {};
-  for (const std::string& line : lines) {
-    std::vector<std::string> words = split(line, ' ');
-    if (words.size() < 2) {
-      PERFETTO_DLOG("Encountered incomplete row in %s: %s.", kMountsPath,
-                    line.c_str());
+
+  for (base::StringSplitter lines(std::move(data), '\n'); lines.Next();) {
+    base::StringSplitter words(&lines, ' ');
+    if (!words.Next() || !words.Next()) {
+      PERFETTO_DLOG("Invalid mount point: %s.", lines.cur_token());
       continue;
     }
-    std::string& mountpoint = words[1];
-    if (stat(mountpoint.c_str(), &buf) == -1) {
+    const char* mountpoint = words.cur_token();
+    struct stat buf {};
+    if (stat(mountpoint, &buf) == -1) {
       PERFETTO_PLOG("stat");
       continue;
     }
-    device_to_mountpoints.emplace(buf.st_dev, std::move(mountpoint));
+    device_to_mountpoints.emplace(buf.st_dev, mountpoint);
   }
   return device_to_mountpoints;
 }
diff --git a/src/traced/probes/filesystem/fs_mount.h b/src/traced/probes/filesystem/fs_mount.h
index f59971c..8d4c49a 100644
--- a/src/traced/probes/filesystem/fs_mount.h
+++ b/src/traced/probes/filesystem/fs_mount.h
@@ -27,7 +27,10 @@
 // On ARM, st_dev is not dev_t but unsigned long long.
 using BlockDeviceID = decltype(stat::st_dev);
 
-std::multimap<BlockDeviceID, std::string> ParseMounts();
+constexpr char kMountsPath[] = "/proc/mounts";
+
+std::multimap<BlockDeviceID, std::string> ParseMounts(
+    const char* path = kMountsPath);
 
 }  // namespace perfetto
 
diff --git a/src/traced/probes/filesystem/fs_mount_unittest.cc b/src/traced/probes/filesystem/fs_mount_unittest.cc
index b6c395a..de5526c 100644
--- a/src/traced/probes/filesystem/fs_mount_unittest.cc
+++ b/src/traced/probes/filesystem/fs_mount_unittest.cc
@@ -16,8 +16,16 @@
 
 #include "src/traced/probes/filesystem/fs_mount.h"
 
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "perfetto/base/build_config.h"
+#include "perfetto/base/scoped_file.h"
+#include "perfetto/base/utils.h"
 
 namespace perfetto {
 namespace {
@@ -25,12 +33,38 @@
 using testing::Contains;
 using testing::Pair;
 
-TEST(InodeTest, ParseMounts) {
-  auto mounts = ParseMounts();
-  struct stat buf;
+TEST(FsMountTest, ParseRealMounts) {
+  std::multimap<BlockDeviceID, std::string> mounts = ParseMounts();
+  struct stat buf = {};
   ASSERT_NE(stat("/proc", &buf), -1);
   EXPECT_THAT(mounts, Contains(Pair(buf.st_dev, "/proc")));
 }
 
+TEST(FsMountTest, ParseSyntheticMounts) {
+  const char kMounts[] = R"(
+procfs /proc proc rw,nosuid,nodev,noexec,relatime 0 0
+#INVALIDLINE
+sysfs / sysfs rw,nosuid,nodev,noexec,relatime 0 0
+)";
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+  char tmp_path[PATH_MAX] = "/data/local/tmp/fake_mounts.XXXXXX";
+#else
+  char tmp_path[PATH_MAX] = "/tmp/fake_mounts.XXXXXX";
+#endif
+
+  base::ScopedFile tmp_fd(mkstemp(tmp_path));
+  ASSERT_GT(*tmp_fd, -1);
+  base::ignore_result(write(*tmp_fd, kMounts, sizeof(kMounts)));
+  tmp_fd.reset();
+
+  std::multimap<BlockDeviceID, std::string> mounts = ParseMounts(tmp_path);
+  unlink(tmp_path);
+  struct stat proc_stat = {}, root_stat = {};
+  ASSERT_NE(stat("/proc", &proc_stat), -1);
+  ASSERT_NE(stat("/", &root_stat), -1);
+  EXPECT_THAT(mounts, Contains(Pair(proc_stat.st_dev, "/proc")));
+  EXPECT_THAT(mounts, Contains(Pair(root_stat.st_dev, "/")));
+}  // namespace
+
 }  // namespace
 }  // namespace perfetto