pw_file: FlatFileSystem service implementation

Provides an optional implementation for the FileSystem RPC service that
enumerates a list of files as a "flat" file system.

Change-Id: Icfabbec17ba9d458aba8652d22342d3697ca3a07
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/60401
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Armando Montanez <amontanez@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index f48f7c9..a86c9c8 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -280,6 +280,7 @@
       "$dir_pw_containers:tests",
       "$dir_pw_cpu_exception_cortex_m:tests",
       "$dir_pw_crypto:tests",
+      "$dir_pw_file:tests",
       "$dir_pw_function:tests",
       "$dir_pw_fuzzer:tests",
       "$dir_pw_hdlc:tests",
diff --git a/pw_file/BUILD.bazel b/pw_file/BUILD.bazel
new file mode 100644
index 0000000..7eda65c
--- /dev/null
+++ b/pw_file/BUILD.bazel
@@ -0,0 +1,60 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+#     https://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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+    "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+proto_library(
+    name = "proto",
+    srcs = ["file.proto"],
+)
+
+pw_cc_library(
+    name = "flat_file_system",
+    srcs = [
+        "flat_file_system.cc",
+    ],
+    hdrs = [
+        "public/pw_file/flat_file_system.h",
+    ],
+    deps = [
+        ":proto",
+        "//pw_bytes",
+        "//pw_result",
+        "//pw_rpc/raw:method",
+        "//pw_status",
+    ],
+)
+
+pw_cc_test(
+    name = "flat_file_system_test",
+    srcs = [
+        "flat_file_system_test.cc",
+    ],
+    deps = [
+        ":flat_file_system",
+        ":proto",
+        "//pw_bytes",
+        "//pw_protobuf",
+        "//pw_rpc/raw:test_method_context",
+        "//pw_status",
+    ],
+)
diff --git a/pw_file/BUILD.gn b/pw_file/BUILD.gn
index f12c20c..13404d8 100644
--- a/pw_file/BUILD.gn
+++ b/pw_file/BUILD.gn
@@ -16,6 +16,11 @@
 
 import("$dir_pw_docgen/docs.gni")
 import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_includes") {
+  include_dirs = [ "public" ]
+}
 
 pw_proto_library("proto") {
   sources = [ "file.proto" ]
@@ -23,7 +28,39 @@
   deps = [ "$dir_pw_protobuf:common_protos" ]
 }
 
+pw_source_set("flat_file_system") {
+  public_deps = [
+    ":proto.pwpb",
+    ":proto.raw_rpc",
+    "$dir_pw_rpc/raw:method",
+    dir_pw_assert,
+    dir_pw_bytes,
+    dir_pw_log,
+    dir_pw_result,
+    dir_pw_status,
+  ]
+  public_configs = [ ":public_includes" ]
+  public = [ "public/pw_file/flat_file_system.h" ]
+  sources = [ "flat_file_system.cc" ]
+}
+
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
   inputs = [ "file.proto" ]
 }
+
+pw_test_group("tests") {
+  tests = [ ":flat_file_system_test" ]
+}
+
+pw_test("flat_file_system_test") {
+  deps = [
+    ":flat_file_system",
+    ":proto.pwpb",
+    "$dir_pw_rpc/raw:test_method_context",
+    dir_pw_bytes,
+    dir_pw_protobuf,
+    dir_pw_status,
+  ]
+  sources = [ "flat_file_system_test.cc" ]
+}
diff --git a/pw_file/docs.rst b/pw_file/docs.rst
index 3bfd29d..43f3aa2 100644
--- a/pw_file/docs.rst
+++ b/pw_file/docs.rst
@@ -29,3 +29,17 @@
 .. literalinclude:: file.proto
   :language: protobuf
   :lines: 14-
+
+------------------------------
+Flat FileSystem implementation
+------------------------------
+This module provides the ``FlatFileSystemService``, an optional implementation
+of the FileSystem RPC service with a virtual interface that allows different
+data storage implementations to expose logical files. As the name implies, the
+file system is treated as a flat file system; it does not support any
+directory-like interactions.
+
+The ``FlatFileSystemService`` implementation requires a static, fixed list of
+``FileSystemEntry`` pointers. Each ``FileSystemEntry`` represents a potential
+file, and acts as an interface boundary that is backed by some kind of storage
+mechanism (e.g. ``BlobStore``, ``PersistentBuffer``).
diff --git a/pw_file/flat_file_system.cc b/pw_file/flat_file_system.cc
new file mode 100644
index 0000000..d347903
--- /dev/null
+++ b/pw_file/flat_file_system.cc
@@ -0,0 +1,175 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+
+#define PW_LOG_MODULE_NAME "FS"
+
+#include "pw_file/flat_file_system.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+#include <string_view>
+
+#include "pw_assert/check.h"
+#include "pw_bytes/span.h"
+#include "pw_file/file.pwpb.h"
+#include "pw_log/log.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_protobuf/serialized_size.h"
+#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::file {
+
+using FileSystemEntry = FlatFileSystemService::FileSystemEntry;
+
+Status FlatFileSystemService::EnumerateFile(
+    FileSystemEntry& entry,
+    pw::file::ListResponse::StreamEncoder& output_encoder) {
+  StatusWithSize sws = entry.Name(file_name_buffer_);
+  if (!sws.ok()) {
+    return sws.status();
+  }
+  {
+    pw::file::Path::StreamEncoder encoder = output_encoder.GetPathsEncoder();
+
+    encoder.WritePath(reinterpret_cast<const char*>(file_name_buffer_.data()),
+                      sws.size());
+    encoder.WriteSizeBytes(entry.SizeBytes());
+    encoder.WritePermissions(entry.Permissions());
+    encoder.WriteFileId(entry.FileId());
+  }
+  return output_encoder.status();
+}
+
+void FlatFileSystemService::EnumerateAllFiles(RawServerWriter& writer) {
+  for (FileSystemEntry* entry : entries_) {
+    PW_DCHECK_NOTNULL(entry);
+    // For now, don't try to pack entries.
+    pw::file::ListResponse::MemoryEncoder encoder(writer.PayloadBuffer());
+    if (Status status = EnumerateFile(*entry, encoder); !status.ok()) {
+      PW_LOG_ERROR("Failed to enumerate file (id: %u) with status %d",
+                   static_cast<unsigned>(entry->FileId()),
+                   static_cast<int>(status.code()));
+      continue;
+    }
+
+    Status write_status = writer.Write(encoder);
+    if (!write_status.ok()) {
+      writer.Finish(write_status);
+      return;
+    }
+  }
+  writer.Finish(OkStatus());
+}
+
+void FlatFileSystemService::List(ServerContext&,
+                                 ConstByteSpan request,
+                                 RawServerWriter& writer) {
+  protobuf::Decoder decoder(request);
+  // If a file name was provided, try and find and enumerate the file.
+  while (decoder.Next().ok()) {
+    if (decoder.FieldNumber() !=
+        static_cast<uint32_t>(pw::file::ListRequest::Fields::PATH)) {
+      continue;
+    }
+
+    std::string_view file_name_view;
+    if (!decoder.ReadString(&file_name_view).ok() ||
+        file_name_view.length() == 0) {
+      writer.Finish(Status::DataLoss());
+      return;
+    }
+
+    // Find and enumerate the file requested.
+    Result<FileSystemEntry*> result = FindFile(file_name_view);
+    if (!result.ok()) {
+      writer.Finish(result.status());
+      return;
+    }
+
+    pw::file::ListResponse::MemoryEncoder encoder(writer.PayloadBuffer());
+    Status proto_encode_status = EnumerateFile(*result.value(), encoder);
+    if (!proto_encode_status.ok()) {
+      writer.Finish(proto_encode_status);
+      return;
+    }
+
+    writer.Finish(writer.Write(encoder));
+    return;
+  }
+
+  // If no path was provided in the ListRequest, just enumerate everything.
+  EnumerateAllFiles(writer);
+}
+
+StatusWithSize FlatFileSystemService::Delete(ServerContext&,
+                                             ConstByteSpan request,
+                                             ByteSpan) {
+  protobuf::Decoder decoder(request);
+  while (decoder.Next().ok()) {
+    if (decoder.FieldNumber() !=
+        static_cast<uint32_t>(pw::file::DeleteRequest::Fields::PATH)) {
+      continue;
+    }
+
+    std::string_view file_name_view;
+    if (!decoder.ReadString(&file_name_view).ok()) {
+      return StatusWithSize(Status::DataLoss(), 0);
+    }
+    return StatusWithSize(FindAndDeleteFile(file_name_view), 0);
+  }
+  return StatusWithSize(Status::InvalidArgument(), 0);
+}
+
+Result<FileSystemEntry*> FlatFileSystemService::FindFile(
+    std::string_view file_name) {
+  Status search_status;
+  for (FileSystemEntry* entry : entries_) {
+    PW_DCHECK_NOTNULL(entry);
+    StatusWithSize sws = entry->Name(file_name_buffer_);
+
+    // If there not an exact file name length match, don't try and check against
+    // a prefix.
+    if (!sws.ok() || file_name.length() != sws.size()) {
+      if (sws.status() != Status::NotFound()) {
+        PW_LOG_ERROR("Failed to read file name (id: %u) with status %d",
+                     static_cast<unsigned>(entry->FileId()),
+                     static_cast<int>(sws.status().code()));
+      }
+      continue;
+    }
+
+    if (memcmp(file_name.data(), file_name_buffer_.data(), file_name.size()) ==
+        0) {
+      return entry;
+    }
+  }
+
+  search_status.Update(Status::NotFound());
+  return search_status;
+}
+
+Status FlatFileSystemService::FindAndDeleteFile(std::string_view file_name) {
+  Result<FileSystemEntry*> result = FindFile(file_name);
+  if (!result.ok()) {
+    return result.status();
+  }
+
+  return result.value()->Delete();
+}
+
+}  // namespace pw::file
diff --git a/pw_file/flat_file_system_test.cc b/pw_file/flat_file_system_test.cc
new file mode 100644
index 0000000..80c91a4
--- /dev/null
+++ b/pw_file/flat_file_system_test.cc
@@ -0,0 +1,233 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+//     https://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 "pw_file/flat_file_system.h"
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_file/file.pwpb.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_rpc/raw/test_method_context.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::file {
+namespace {
+
+class FakeFile : public FlatFileSystemService::FileSystemEntry {
+ public:
+  constexpr FakeFile(std::string_view file_name, size_t size, uint32_t file_id)
+      : name_(file_name), size_(size), file_id_(file_id) {}
+
+  StatusWithSize Name(ByteSpan dest) override {
+    if (name_.empty()) {
+      return StatusWithSize(Status::NotFound(), 0);
+    }
+
+    size_t bytes_to_copy = std::min(dest.size_bytes(), name_.size());
+    memcpy(dest.data(), name_.data(), bytes_to_copy);
+    if (bytes_to_copy != name_.size()) {
+      return StatusWithSize(Status::ResourceExhausted(), bytes_to_copy);
+    }
+
+    return StatusWithSize(OkStatus(), bytes_to_copy);
+  }
+
+  size_t SizeBytes() override { return size_; }
+
+  FlatFileSystemService::FileSystemEntry::FilePermissions Permissions()
+      override {
+    return FlatFileSystemService::FileSystemEntry::FilePermissions::NONE;
+  }
+
+  Status Delete() override { return Status::Unimplemented(); }
+
+  FlatFileSystemService::FileSystemEntry::Id FileId() override {
+    return file_id_;
+  }
+
+ private:
+  std::string_view name_;
+  size_t size_;
+  uint32_t file_id_;
+};
+
+bool FileSystemEntryHasName(FlatFileSystemService::FileSystemEntry* entry) {
+  std::array<std::byte, 4> expected_name;
+  StatusWithSize file_name_sws = entry->Name(expected_name);
+  return file_name_sws.size() != 0;
+}
+
+// Compares a serialized Path message to a flat file system entry.
+void ComparePathToEntry(ConstByteSpan serialized_path,
+                        FlatFileSystemService::FileSystemEntry* entry) {
+  std::array<std::byte, 64> expected_name;
+  StatusWithSize file_name_sws = entry->Name(expected_name);
+
+  // A partial name read shouldn't happen.
+  ASSERT_EQ(OkStatus(), file_name_sws.status());
+
+  protobuf::Decoder decoder(serialized_path);
+  while (decoder.Next().ok()) {
+    switch (decoder.FieldNumber()) {
+      case static_cast<uint32_t>(pw::file::Path::Fields::PATH): {
+        std::string_view serialized_name;
+        EXPECT_EQ(OkStatus(), decoder.ReadString(&serialized_name));
+        size_t name_bytes_to_read =
+            std::min(serialized_name.size(), file_name_sws.size());
+        EXPECT_EQ(0,
+                  memcmp(expected_name.data(),
+                         serialized_name.data(),
+                         name_bytes_to_read));
+        break;
+      }
+
+      case static_cast<uint32_t>(pw::file::Path::Fields::PERMISSIONS): {
+        uint32_t seralized_permissions;
+        EXPECT_EQ(OkStatus(), decoder.ReadUint32(&seralized_permissions));
+        EXPECT_EQ(static_cast<uint32_t>(entry->Permissions()),
+                  seralized_permissions);
+        break;
+      }
+
+      case static_cast<uint32_t>(pw::file::Path::Fields::SIZE_BYTES): {
+        uint32_t serialized_file_size;
+        EXPECT_EQ(OkStatus(), decoder.ReadUint32(&serialized_file_size));
+        EXPECT_EQ(static_cast<uint32_t>(entry->SizeBytes()),
+                  serialized_file_size);
+        break;
+      }
+
+      case static_cast<uint32_t>(pw::file::Path::Fields::FILE_ID): {
+        uint32_t serialized_file_id;
+        EXPECT_EQ(OkStatus(), decoder.ReadUint32(&serialized_file_id));
+        EXPECT_EQ(static_cast<uint32_t>(entry->FileId()), serialized_file_id);
+        break;
+      }
+
+      default:
+        // unexpected result.
+        // TODO something here.
+        break;
+    }
+  }
+}
+
+size_t ValidateExpectedPaths(
+    std::span<FlatFileSystemService::FileSystemEntry*> flat_file_system,
+    const Vector<ByteSpan>& results) {
+  size_t serialized_path_entry_count = 0;
+  size_t file_system_index = 0;
+  for (ConstByteSpan response : results) {
+    protobuf::Decoder decoder(response);
+    while (decoder.Next().ok()) {
+      constexpr uint32_t kListResponsePathsFieldNumber =
+          static_cast<uint32_t>(pw::file::ListResponse::Fields::PATHS);
+      EXPECT_EQ(decoder.FieldNumber(), kListResponsePathsFieldNumber);
+      if (decoder.FieldNumber() != kListResponsePathsFieldNumber) {
+        return 0;
+      }
+
+      serialized_path_entry_count++;
+
+      // Skip any file system entries without names.
+      while (!FileSystemEntryHasName(flat_file_system[file_system_index])) {
+        file_system_index++;
+        EXPECT_GT(flat_file_system.size(), file_system_index);
+      }
+
+      // There's a 1:1 mapping in the same order for all files that have a name.
+      ConstByteSpan serialized_path;
+      EXPECT_EQ(OkStatus(), decoder.ReadBytes(&serialized_path));
+      ComparePathToEntry(serialized_path,
+                         flat_file_system[file_system_index++]);
+    }
+  }
+  return serialized_path_entry_count;
+}
+
+TEST(FlatFileSystem, List_NoFiles) {
+  std::array<std::byte, 1> file_name_buffer;
+
+  PW_RAW_TEST_METHOD_CONTEXT(FlatFileSystemService, List)
+  ctx(std::span<FlatFileSystemService::FileSystemEntry*>(), file_name_buffer);
+  ctx.call(ConstByteSpan());
+
+  EXPECT_TRUE(ctx.done());
+  EXPECT_EQ(OkStatus(), ctx.status());
+  EXPECT_EQ(0u, ctx.responses().size());
+}
+
+TEST(FlatFileSystem, List_OneFile) {
+  std::array<std::byte, 20> file_name_buffer;
+  FakeFile file{"compressed.zip.gz", 2, 1231};
+  std::array<FlatFileSystemService::FileSystemEntry*, 1> static_file_system{
+      &file};
+
+  PW_RAW_TEST_METHOD_CONTEXT(FlatFileSystemService, List)
+  ctx(static_file_system, file_name_buffer);
+  ctx.call(ConstByteSpan());
+
+  EXPECT_EQ(1u, ValidateExpectedPaths(static_file_system, ctx.responses()));
+}
+
+TEST(FlatFileSystem, List_ThreeFiles) {
+  std::array<std::byte, 10> file_name_buffer;
+  std::array<FakeFile, 3> files{
+      {{"SNAP_001", 372, 9}, {"tokens.csv", 808, 15038202}, {"a.txt", 0, 2}}};
+  std::array<FlatFileSystemService::FileSystemEntry*, 3> static_file_system{
+      &files[0], &files[1], &files[2]};
+
+  PW_RAW_TEST_METHOD_CONTEXT(FlatFileSystemService, List)
+  ctx(static_file_system, file_name_buffer);
+  ctx.call(ConstByteSpan());
+
+  EXPECT_EQ(3u, ValidateExpectedPaths(static_file_system, ctx.responses()));
+}
+
+TEST(FlatFileSystem, List_UnnamedFile) {
+  std::array<std::byte, 10> file_name_buffer;
+  FakeFile file{"", 0, 0};
+  std::array<FlatFileSystemService::FileSystemEntry*, 1> static_file_system{
+      &file};
+
+  PW_RAW_TEST_METHOD_CONTEXT(FlatFileSystemService, List)
+  ctx(static_file_system, file_name_buffer);
+  ctx.call(ConstByteSpan());
+
+  EXPECT_EQ(0u, ValidateExpectedPaths(static_file_system, ctx.responses()));
+}
+
+TEST(FlatFileSystem, List_FileMissingName) {
+  std::array<std::byte, 10> file_name_buffer;
+  std::array<FakeFile, 3> files{
+      {{"SNAP_001", 372, 9}, {"", 808, 15038202}, {"a.txt", 0, 2}}};
+  std::array<FlatFileSystemService::FileSystemEntry*, 3> static_file_system{
+      &files[0], &files[1], &files[2]};
+
+  PW_RAW_TEST_METHOD_CONTEXT(FlatFileSystemService, List)
+  ctx(static_file_system, file_name_buffer);
+  ctx.call(ConstByteSpan());
+
+  EXPECT_EQ(2u, ValidateExpectedPaths(static_file_system, ctx.responses()));
+}
+
+}  // namespace
+}  // namespace pw::file
diff --git a/pw_file/public/pw_file/flat_file_system.h b/pw_file/public/pw_file/flat_file_system.h
new file mode 100644
index 0000000..59a0e45
--- /dev/null
+++ b/pw_file/public/pw_file/flat_file_system.h
@@ -0,0 +1,100 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+#include <string_view>
+
+#include "pw_bytes/span.h"
+#include "pw_file/file.pwpb.h"
+#include "pw_file/file.raw_rpc.pb.h"
+#include "pw_result/result.h"
+#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::file {
+
+// This implements the pw.file.FileSystem RPC service. This implementation
+// has a strict limitation that everything is treated as if the file system
+// was "flat" (i.e. no directories). This means there's no concept of logical
+// directories, despite any "path like" naming that may be employed by a user.
+class FlatFileSystemService
+    : public generated::FileSystem<FlatFileSystemService> {
+ public:
+  class FileSystemEntry {
+   public:
+    using FilePermissions = pw::file::Path::Permissions;
+    using Id = uint32_t;
+
+    FileSystemEntry() = default;
+    virtual ~FileSystemEntry() = default;
+
+    // All readable files MUST be named, and names must be globally unique to
+    // prevent ambiguity. Unnamed file entries will NOT be enumerated by a
+    // FlatFileSystemService.
+    //
+    // Returns:
+    //   OK - Successfully read file name to `dest`.
+    //   NOT_FOUND - No file to enumerate for this entry.
+    //   RESOURCE_EXHAUSTED - `dest` buffer too small to fit the full file name.
+    virtual StatusWithSize Name(ByteSpan dest) = 0;
+
+    virtual size_t SizeBytes() = 0;
+    virtual FilePermissions Permissions() = 0;
+
+    // Deleting a file, if allowed, should cause a
+    virtual Status Delete() = 0;
+
+    // File IDs must be globally unique, and map to a pw_transfer
+    // TransferService read/write handler.
+    virtual Id FileId() = 0;
+  };
+
+  // Constructs a flat file system from a static list of file entries.
+  //
+  // Args:
+  //   entry_list - A list of pointers to all FileSystemEntry objects that may
+  //     contain files. These pointers may not be null. The span's underlying
+  //     buffer must outlive this object.
+  //   file_name_buffer - Used internally by this class to find and enumerate
+  //     files. Should be large enough to hold the longest expected file name.
+  //     The span's underlying buffer must outlive this object.
+  FlatFileSystemService(std::span<FileSystemEntry*> entry_list,
+                        ByteSpan file_name_buffer)
+      : file_name_buffer_(file_name_buffer), entries_(entry_list) {}
+
+  // Method definitions for pw.file.FileSystem.
+  void List(ServerContext&, ConstByteSpan request, RawServerWriter& writer);
+
+  // Returns:
+  //   OK - File successfully deleted.
+  //   NOT_FOUND - Could not find
+  StatusWithSize Delete(ServerContext&, ConstByteSpan request, ByteSpan);
+
+ private:
+  Result<FileSystemEntry*> FindFile(std::string_view file_name);
+  Status FindAndDeleteFile(std::string_view file_name);
+
+  Status EnumerateFile(FileSystemEntry& entry,
+                       pw::file::ListResponse::StreamEncoder& output_encoder);
+  void EnumerateAllFiles(RawServerWriter& writer);
+
+  ByteSpan file_name_buffer_;
+  std::span<FileSystemEntry*> entries_;
+};
+
+}  // namespace pw::file