Refactor channel remapping out of LogReader

This change takes all the logic to rename and remap channels in
LogReader and places it in a new class: ConfigRemapper. LogReader still
retains the same remapping/renaming API for backwards compatibility, but
now the args are basically just passed along to ConfigRemapper. This was
done to allow configs to be remapped without a corresponding log file.

Change-Id: Ia32d8a3e640e94af0b52397ccd322149588d6da4
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/events/logging/config_remapper.cc b/aos/events/logging/config_remapper.cc
new file mode 100644
index 0000000..21f45d5
--- /dev/null
+++ b/aos/events/logging/config_remapper.cc
@@ -0,0 +1,679 @@
+#include "aos/events/logging/config_remapper.h"
+
+#include <vector>
+
+#include "absl/strings/escaping.h"
+#include "flatbuffers/flatbuffers.h"
+
+#include "aos/events/logging/logger_generated.h"
+#include "aos/flatbuffer_merge.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/network/multinode_timestamp_filter.h"
+#include "aos/network/remote_message_generated.h"
+#include "aos/network/remote_message_schema.h"
+#include "aos/network/team_number.h"
+#include "aos/network/timestamp_channel.h"
+
+namespace aos {
+using message_bridge::RemoteMessage;
+
+namespace {
+// Checks if the specified channel name/type exists in the config and, depending
+// on the value of conflict_handling, calls conflict_handler or just dies.
+template <typename F>
+void CheckAndHandleRemapConflict(
+    std::string_view new_name, std::string_view new_type,
+    const Configuration *config,
+    ConfigRemapper::RemapConflict conflict_handling, F conflict_handler) {
+  const Channel *existing_channel =
+      configuration::GetChannel(config, new_name, new_type, "", nullptr, true);
+  if (existing_channel != nullptr) {
+    switch (conflict_handling) {
+      case ConfigRemapper::RemapConflict::kDisallow:
+        LOG(FATAL)
+            << "Channel "
+            << configuration::StrippedChannelToString(existing_channel)
+            << " is already used--you can't remap an original channel to it.";
+        break;
+      case ConfigRemapper::RemapConflict::kCascade:
+        VLOG(1) << "Automatically remapping "
+                << configuration::StrippedChannelToString(existing_channel)
+                << " to avoid conflicts.";
+        conflict_handler();
+        break;
+    }
+  }
+}
+}  // namespace
+
+namespace configuration {
+// We don't really want to expose this publicly, but log reader doesn't really
+// want to re-implement it.
+void HandleMaps(const flatbuffers::Vector<flatbuffers::Offset<Map>> *maps,
+                std::string *name, std::string_view type, const Node *node);
+}  // namespace configuration
+
+bool CompareChannels(const Channel *c,
+                     ::std::pair<std::string_view, std::string_view> p) {
+  int name_compare = c->name()->string_view().compare(p.first);
+  if (name_compare == 0) {
+    return c->type()->string_view() < p.second;
+  } else if (name_compare < 0) {
+    return true;
+  } else {
+    return false;
+  }
+}
+
+bool EqualsChannels(const Channel *c,
+                    ::std::pair<std::string_view, std::string_view> p) {
+  return c->name()->string_view() == p.first &&
+         c->type()->string_view() == p.second;
+}
+// Copies the channel, removing the schema as we go.  If new_name is provided,
+// it is used instead of the name inside the channel.  If new_type is provided,
+// it is used instead of the type in the channel.
+flatbuffers::Offset<Channel> CopyChannel(const Channel *c,
+                                         std::string_view new_name,
+                                         std::string_view new_type,
+                                         flatbuffers::FlatBufferBuilder *fbb) {
+  CHECK_EQ(Channel::MiniReflectTypeTable()->num_elems, 14u)
+      << ": Merging logic needs to be updated when the number of channel "
+         "fields changes.";
+
+  flatbuffers::Offset<flatbuffers::String> name_offset =
+      fbb->CreateSharedString(new_name.empty() ? c->name()->string_view()
+                                               : new_name);
+  flatbuffers::Offset<flatbuffers::String> type_offset =
+      fbb->CreateSharedString(new_type.empty() ? c->type()->str() : new_type);
+  flatbuffers::Offset<flatbuffers::String> source_node_offset =
+      c->has_source_node() ? fbb->CreateSharedString(c->source_node()->str())
+                           : 0;
+
+  flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Connection>>>
+      destination_nodes_offset =
+          RecursiveCopyVectorTable(c->destination_nodes(), fbb);
+
+  flatbuffers::Offset<
+      flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>>
+      logger_nodes_offset = CopyVectorSharedString(c->logger_nodes(), fbb);
+
+  Channel::Builder channel_builder(*fbb);
+  channel_builder.add_name(name_offset);
+  channel_builder.add_type(type_offset);
+  if (c->has_frequency()) {
+    channel_builder.add_frequency(c->frequency());
+  }
+  if (c->has_max_size()) {
+    channel_builder.add_max_size(c->max_size());
+  }
+  if (c->has_num_senders()) {
+    channel_builder.add_num_senders(c->num_senders());
+  }
+  if (c->has_num_watchers()) {
+    channel_builder.add_num_watchers(c->num_watchers());
+  }
+  if (!source_node_offset.IsNull()) {
+    channel_builder.add_source_node(source_node_offset);
+  }
+  if (!destination_nodes_offset.IsNull()) {
+    channel_builder.add_destination_nodes(destination_nodes_offset);
+  }
+  if (c->has_logger()) {
+    channel_builder.add_logger(c->logger());
+  }
+  if (!logger_nodes_offset.IsNull()) {
+    channel_builder.add_logger_nodes(logger_nodes_offset);
+  }
+  if (c->has_read_method()) {
+    channel_builder.add_read_method(c->read_method());
+  }
+  if (c->has_num_readers()) {
+    channel_builder.add_num_readers(c->num_readers());
+  }
+  if (c->has_channel_storage_duration()) {
+    channel_builder.add_channel_storage_duration(c->channel_storage_duration());
+  }
+  return channel_builder.Finish();
+}
+
+ConfigRemapper::ConfigRemapper(const Configuration *config,
+                               const Configuration *replay_config,
+                               const logger::ReplayChannels *replay_channels)
+    : remapped_configuration_(config),
+      original_configuration_(config),
+      replay_configuration_(replay_config),
+      replay_channels_(replay_channels) {
+  MakeRemappedConfig();
+
+  // If any remote timestamp channel was not marked NOT_LOGGED, then remap that
+  // channel to avoid the redundant logged data. Also, this loop handles the
+  // MessageHeader to RemoteMessae name change.
+  // Note: This path is mainly for backwards compatibility reasons, and should
+  // not be necessary for any new logs.
+  for (const Node *node : configuration::GetNodes(original_configuration())) {
+    message_bridge::ChannelTimestampFinder finder(original_configuration(),
+                                                  "log_reader", node);
+
+    absl::btree_set<std::string_view> remote_nodes;
+
+    for (const Channel *channel : *original_configuration()->channels()) {
+      if (!configuration::ChannelIsSendableOnNode(channel, node)) {
+        continue;
+      }
+      if (!channel->has_destination_nodes()) {
+        continue;
+      }
+      for (const Connection *connection : *channel->destination_nodes()) {
+        if (configuration::ConnectionDeliveryTimeIsLoggedOnNode(connection,
+                                                                node)) {
+          // Start by seeing if the split timestamp channels are being used for
+          // this message.
+          const Channel *timestamp_channel = configuration::GetChannel(
+              original_configuration(),
+              finder.SplitChannelName(channel, connection),
+              RemoteMessage::GetFullyQualifiedName(), "", node, true);
+
+          if (timestamp_channel != nullptr) {
+            // If for some reason a timestamp channel is not NOT_LOGGED (which
+            // is unusual), then remap the channel so that the replayed channel
+            // doesn't overlap with the special separate replay we do for
+            // timestamps.
+            if (timestamp_channel->logger() != LoggerConfig::NOT_LOGGED) {
+              RemapOriginalChannel<RemoteMessage>(
+                  timestamp_channel->name()->string_view(), node);
+            }
+            continue;
+          }
+
+          // Otherwise collect this one up as a node to look for a combined
+          // channel from.  It is more efficient to compare nodes than channels.
+          LOG(WARNING) << "Failed to find channel "
+                       << finder.SplitChannelName(channel, connection)
+                       << " on node " << FlatbufferToJson(node);
+          remote_nodes.insert(connection->name()->string_view());
+        }
+      }
+    }
+
+    std::vector<const Node *> timestamp_logger_nodes =
+        configuration::TimestampNodes(original_configuration(), node);
+    for (const std::string_view remote_node : remote_nodes) {
+      const std::string channel = finder.CombinedChannelName(remote_node);
+
+      // See if the log file is an old log with logger::MessageHeader channels
+      // in it, or a newer log with RemoteMessage.  If we find an older log,
+      // rename the type too along with the name.
+      if (HasChannel<logger::MessageHeader>(channel, node)) {
+        CHECK(!HasChannel<RemoteMessage>(channel, node))
+            << ": Can't have both a logger::MessageHeader and RemoteMessage "
+               "remote "
+               "timestamp channel.";
+        // In theory, we should check NOT_LOGGED like RemoteMessage and be more
+        // careful about updating the config, but there are fewer and fewer logs
+        // with logger::MessageHeader remote messages, so it isn't worth the
+        // effort.
+        RemapOriginalChannel<logger::MessageHeader>(
+            channel, node, "/original", "aos.message_bridge.RemoteMessage");
+      } else {
+        CHECK(HasChannel<RemoteMessage>(channel, node))
+            << ": Failed to find {\"name\": \"" << channel << "\", \"type\": \""
+            << RemoteMessage::GetFullyQualifiedName() << "\"} for node "
+            << node->name()->string_view();
+        // Only bother to remap if there's something on the channel.  We can
+        // tell if the channel was marked NOT_LOGGED or not.  This makes the
+        // config not change un-necesarily when we replay a log with NOT_LOGGED
+        // messages.
+        if (HasOriginalChannel<RemoteMessage>(channel, node)) {
+          RemapOriginalChannel<RemoteMessage>(channel, node);
+        }
+      }
+    }
+  }
+  if (replay_configuration_) {
+    CHECK_EQ(configuration::MultiNode(remapped_configuration()),
+             configuration::MultiNode(replay_configuration_))
+        << ": Log file and replay config need to both be multi or single "
+           "node.";
+  }
+}
+
+ConfigRemapper::~ConfigRemapper() {
+  // Zero out some buffers. It's easy to do use-after-frees on these, so make
+  // it more obvious.
+  if (remapped_configuration_buffer_) {
+    remapped_configuration_buffer_->Wipe();
+  }
+}
+
+const Configuration *ConfigRemapper::original_configuration() const {
+  return original_configuration_;
+}
+
+const Configuration *ConfigRemapper::remapped_configuration() const {
+  return remapped_configuration_;
+}
+
+void ConfigRemapper::set_configuration(const Configuration *configuration) {
+  remapped_configuration_ = configuration;
+}
+
+std::vector<const Channel *> ConfigRemapper::RemappedChannels() const {
+  std::vector<const Channel *> result;
+  result.reserve(remapped_channels_.size());
+  for (auto &pair : remapped_channels_) {
+    const Channel *const original_channel =
+        CHECK_NOTNULL(original_configuration()->channels()->Get(pair.first));
+
+    auto channel_iterator = std::lower_bound(
+        remapped_configuration_->channels()->cbegin(),
+        remapped_configuration_->channels()->cend(),
+        std::make_pair(std::string_view(pair.second.remapped_name),
+                       original_channel->type()->string_view()),
+        CompareChannels);
+
+    CHECK(channel_iterator != remapped_configuration_->channels()->cend());
+    CHECK(EqualsChannels(
+        *channel_iterator,
+        std::make_pair(std::string_view(pair.second.remapped_name),
+                       original_channel->type()->string_view())));
+    result.push_back(*channel_iterator);
+  }
+  return result;
+}
+
+const Channel *ConfigRemapper::RemapChannel(const EventLoop *event_loop,
+                                            const Node *node,
+                                            const Channel *channel) {
+  std::string_view channel_name = channel->name()->string_view();
+  std::string_view channel_type = channel->type()->string_view();
+  const int channel_index =
+      configuration::ChannelIndex(original_configuration(), channel);
+  // If the channel is remapped, find the correct channel name to use.
+  if (remapped_channels_.count(channel_index) > 0) {
+    VLOG(3) << "Got remapped channel on "
+            << configuration::CleanedChannelToString(channel);
+    channel_name = remapped_channels_[channel_index].remapped_name;
+  }
+
+  VLOG(2) << "Going to remap channel " << channel_name << " " << channel_type;
+  const Channel *remapped_channel = configuration::GetChannel(
+      remapped_configuration(), channel_name, channel_type,
+      event_loop ? event_loop->name() : "log_reader", node);
+
+  CHECK(remapped_channel != nullptr)
+      << ": Unable to send {\"name\": \"" << channel_name << "\", \"type\": \""
+      << channel_type << "\"} because it is not in the provided configuration.";
+
+  return remapped_channel;
+}
+
+void ConfigRemapper::RemapOriginalChannel(std::string_view name,
+                                          std::string_view type,
+                                          std::string_view add_prefix,
+                                          std::string_view new_type,
+                                          RemapConflict conflict_handling) {
+  RemapOriginalChannel(name, type, nullptr, add_prefix, new_type,
+                       conflict_handling);
+}
+
+void ConfigRemapper::RemapOriginalChannel(std::string_view name,
+                                          std::string_view type,
+                                          const Node *node,
+                                          std::string_view add_prefix,
+                                          std::string_view new_type,
+                                          RemapConflict conflict_handling) {
+  if (node != nullptr) {
+    VLOG(1) << "Node is " << FlatbufferToJson(node);
+  }
+  if (replay_channels_ != nullptr) {
+    CHECK(std::find(replay_channels_->begin(), replay_channels_->end(),
+                    std::make_pair(std::string{name}, std::string{type})) !=
+          replay_channels_->end())
+        << "Attempted to remap channel " << name << " " << type
+        << " which is not included in the replay channels passed to "
+           "ConfigRemapper.";
+  }
+  const Channel *remapped_channel =
+      configuration::GetChannel(original_configuration(), name, type, "", node);
+  CHECK(remapped_channel != nullptr) << ": Failed to find {\"name\": \"" << name
+                                     << "\", \"type\": \"" << type << "\"}";
+  VLOG(1) << "Original {\"name\": \"" << name << "\", \"type\": \"" << type
+          << "\"}";
+  VLOG(1) << "Remapped "
+          << configuration::StrippedChannelToString(remapped_channel);
+
+  // We want to make /spray on node 0 go to /0/spray by snooping the maps.  And
+  // we want it to degrade if the heuristics fail to just work.
+  //
+  // The easiest way to do this is going to be incredibly specific and verbose.
+  // Look up /spray, to /0/spray.  Then, prefix the result with /original to get
+  // /original/0/spray.  Then, create a map from /original/spray to
+  // /original/0/spray for just the type we were asked for.
+  if (name != remapped_channel->name()->string_view()) {
+    MapT new_map;
+    new_map.match = std::make_unique<ChannelT>();
+    new_map.match->name = absl::StrCat(add_prefix, name);
+    new_map.match->type = type;
+    if (node != nullptr) {
+      new_map.match->source_node = node->name()->str();
+    }
+    new_map.rename = std::make_unique<ChannelT>();
+    new_map.rename->name =
+        absl::StrCat(add_prefix, remapped_channel->name()->string_view());
+    maps_.emplace_back(std::move(new_map));
+  }
+
+  // Then remap the original channel to the prefixed channel.
+  const size_t channel_index =
+      configuration::ChannelIndex(original_configuration(), remapped_channel);
+  CHECK_EQ(0u, remapped_channels_.count(channel_index))
+      << "Already remapped channel "
+      << configuration::CleanedChannelToString(remapped_channel);
+
+  RemappedChannel remapped_channel_struct;
+  remapped_channel_struct.remapped_name =
+      std::string(add_prefix) +
+      std::string(remapped_channel->name()->string_view());
+  remapped_channel_struct.new_type = new_type;
+  const std::string_view remapped_type = new_type.empty() ? type : new_type;
+  CheckAndHandleRemapConflict(
+      remapped_channel_struct.remapped_name, remapped_type,
+      remapped_configuration_, conflict_handling,
+      [this, &remapped_channel_struct, remapped_type, node, add_prefix,
+       conflict_handling]() {
+        RemapOriginalChannel(remapped_channel_struct.remapped_name,
+                             remapped_type, node, add_prefix, "",
+                             conflict_handling);
+      });
+  remapped_channels_[channel_index] = std::move(remapped_channel_struct);
+  MakeRemappedConfig();
+}
+
+void ConfigRemapper::RenameOriginalChannel(const std::string_view name,
+                                           const std::string_view type,
+                                           const std::string_view new_name,
+                                           const std::vector<MapT> &add_maps) {
+  RenameOriginalChannel(name, type, nullptr, new_name, add_maps);
+}
+
+void ConfigRemapper::RenameOriginalChannel(const std::string_view name,
+                                           const std::string_view type,
+                                           const Node *const node,
+                                           const std::string_view new_name,
+                                           const std::vector<MapT> &add_maps) {
+  if (node != nullptr) {
+    VLOG(1) << "Node is " << FlatbufferToJson(node);
+  }
+  // First find the channel and rename it.
+  const Channel *remapped_channel =
+      configuration::GetChannel(original_configuration(), name, type, "", node);
+  CHECK(remapped_channel != nullptr) << ": Failed to find {\"name\": \"" << name
+                                     << "\", \"type\": \"" << type << "\"}";
+  VLOG(1) << "Original {\"name\": \"" << name << "\", \"type\": \"" << type
+          << "\"}";
+  VLOG(1) << "Remapped "
+          << configuration::StrippedChannelToString(remapped_channel);
+
+  const size_t channel_index =
+      configuration::ChannelIndex(original_configuration(), remapped_channel);
+  CHECK_EQ(0u, remapped_channels_.count(channel_index))
+      << "Already remapped channel "
+      << configuration::CleanedChannelToString(remapped_channel);
+
+  RemappedChannel remapped_channel_struct;
+  remapped_channel_struct.remapped_name = new_name;
+  remapped_channel_struct.new_type.clear();
+  remapped_channels_[channel_index] = std::move(remapped_channel_struct);
+
+  // Then add any provided maps.
+  for (const MapT &map : add_maps) {
+    maps_.push_back(map);
+  }
+
+  // Finally rewrite the config.
+  MakeRemappedConfig();
+}
+
+void ConfigRemapper::MakeRemappedConfig() {
+  // If no remapping occurred and we are using the original config, then there
+  // is nothing interesting to do here.
+  if (remapped_channels_.empty() && replay_configuration_ == nullptr) {
+    remapped_configuration_ = original_configuration();
+    return;
+  }
+  // Config to copy Channel definitions from. Use the specified
+  // replay_configuration_ if it has been provided.
+  const Configuration *const base_config = replay_configuration_ == nullptr
+                                               ? original_configuration()
+                                               : replay_configuration_;
+
+  // Create a config with all the channels, but un-sorted/merged.  Collect up
+  // the schemas while we do this.  Call MergeConfiguration to sort everything,
+  // and then merge it all in together.
+
+  // This is the builder that we use for the config containing all the new
+  // channels.
+  flatbuffers::FlatBufferBuilder fbb;
+  fbb.ForceDefaults(true);
+  std::vector<flatbuffers::Offset<Channel>> channel_offsets;
+
+  CHECK_EQ(Channel::MiniReflectTypeTable()->num_elems, 14u)
+      << ": Merging logic needs to be updated when the number of channel "
+         "fields changes.";
+
+  // List of schemas.
+  std::map<std::string_view, FlatbufferVector<reflection::Schema>> schema_map;
+  // Make sure our new RemoteMessage schema is in there for old logs without it.
+  schema_map.insert(std::make_pair(
+      message_bridge::RemoteMessage::GetFullyQualifiedName(),
+      FlatbufferVector<reflection::Schema>(FlatbufferSpan<reflection::Schema>(
+          message_bridge::RemoteMessageSchema()))));
+
+  // Reconstruct the remapped channels.
+  for (auto &pair : remapped_channels_) {
+    const Channel *const c = CHECK_NOTNULL(configuration::GetChannel(
+        base_config, original_configuration()->channels()->Get(pair.first), "",
+        nullptr));
+    channel_offsets.emplace_back(
+        CopyChannel(c, pair.second.remapped_name, "", &fbb));
+
+    if (c->has_destination_nodes()) {
+      for (const Connection *connection : *c->destination_nodes()) {
+        switch (connection->timestamp_logger()) {
+          case LoggerConfig::LOCAL_LOGGER:
+          case LoggerConfig::NOT_LOGGED:
+            // There is no timestamp channel associated with this, so ignore it.
+            break;
+
+          case LoggerConfig::REMOTE_LOGGER:
+          case LoggerConfig::LOCAL_AND_REMOTE_LOGGER:
+            // We want to make a split timestamp channel regardless of what type
+            // of log this used to be.  No sense propagating the single
+            // timestamp channel.
+
+            CHECK(connection->has_timestamp_logger_nodes());
+            for (const flatbuffers::String *timestamp_logger_node :
+                 *connection->timestamp_logger_nodes()) {
+              const Node *node =
+                  configuration::GetNode(original_configuration(),
+                                         timestamp_logger_node->string_view());
+              message_bridge::ChannelTimestampFinder finder(
+                  original_configuration(), "log_reader", node);
+
+              // We are assuming here that all the maps are setup correctly to
+              // handle arbitrary timestamps.  Apply the maps for this node to
+              // see what name this ends up with.
+              std::string name = finder.SplitChannelName(
+                  pair.second.remapped_name, c->type()->str(), connection);
+              std::string unmapped_name = name;
+              configuration::HandleMaps(original_configuration()->maps(), &name,
+                                        "aos.message_bridge.RemoteMessage",
+                                        node);
+              CHECK_NE(name, unmapped_name)
+                  << ": Remote timestamp channel was not remapped, this is "
+                     "very fishy";
+              flatbuffers::Offset<flatbuffers::String> channel_name_offset =
+                  fbb.CreateString(name);
+              flatbuffers::Offset<flatbuffers::String> channel_type_offset =
+                  fbb.CreateString("aos.message_bridge.RemoteMessage");
+              flatbuffers::Offset<flatbuffers::String> source_node_offset =
+                  fbb.CreateString(timestamp_logger_node->string_view());
+
+              // Now, build a channel.  Don't log it, 2 senders, and match the
+              // source frequency.
+              Channel::Builder channel_builder(fbb);
+              channel_builder.add_name(channel_name_offset);
+              channel_builder.add_type(channel_type_offset);
+              channel_builder.add_source_node(source_node_offset);
+              channel_builder.add_logger(LoggerConfig::NOT_LOGGED);
+              channel_builder.add_num_senders(2);
+              if (c->has_frequency()) {
+                channel_builder.add_frequency(c->frequency());
+              }
+              if (c->has_channel_storage_duration()) {
+                channel_builder.add_channel_storage_duration(
+                    c->channel_storage_duration());
+              }
+              channel_offsets.emplace_back(channel_builder.Finish());
+            }
+            break;
+        }
+      }
+    }
+  }
+
+  // Now reconstruct the original channels, translating types as needed
+  for (const Channel *c : *base_config->channels()) {
+    // Search for a mapping channel.
+    std::string_view new_type = "";
+    for (auto &pair : remapped_channels_) {
+      const Channel *const remapped_channel =
+          original_configuration()->channels()->Get(pair.first);
+      if (remapped_channel->name()->string_view() == c->name()->string_view() &&
+          remapped_channel->type()->string_view() == c->type()->string_view()) {
+        new_type = pair.second.new_type;
+        break;
+      }
+    }
+
+    // Copy everything over.
+    channel_offsets.emplace_back(CopyChannel(c, "", new_type, &fbb));
+
+    // Add the schema if it doesn't exist.
+    if (schema_map.find(c->type()->string_view()) == schema_map.end()) {
+      CHECK(c->has_schema());
+      schema_map.insert(std::make_pair(c->type()->string_view(),
+                                       RecursiveCopyFlatBuffer(c->schema())));
+    }
+  }
+
+  // The MergeConfiguration API takes a vector, not a map.  Convert.
+  std::vector<FlatbufferVector<reflection::Schema>> schemas;
+  while (!schema_map.empty()) {
+    schemas.emplace_back(std::move(schema_map.begin()->second));
+    schema_map.erase(schema_map.begin());
+  }
+
+  // Create the Configuration containing the new channels that we want to add.
+  const flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Channel>>>
+      channels_offset =
+          channel_offsets.empty() ? 0 : fbb.CreateVector(channel_offsets);
+
+  // Copy over the old maps.
+  std::vector<flatbuffers::Offset<Map>> map_offsets;
+  if (base_config->maps()) {
+    for (const Map *map : *base_config->maps()) {
+      map_offsets.emplace_back(RecursiveCopyFlatBuffer(map, &fbb));
+    }
+  }
+
+  // Now create the new maps.  These are second so they take effect first.
+  for (const MapT &map : maps_) {
+    CHECK(!map.match->name.empty());
+    const flatbuffers::Offset<flatbuffers::String> match_name_offset =
+        fbb.CreateString(map.match->name);
+    flatbuffers::Offset<flatbuffers::String> match_type_offset;
+    if (!map.match->type.empty()) {
+      match_type_offset = fbb.CreateString(map.match->type);
+    }
+    flatbuffers::Offset<flatbuffers::String> match_source_node_offset;
+    if (!map.match->source_node.empty()) {
+      match_source_node_offset = fbb.CreateString(map.match->source_node);
+    }
+    CHECK(!map.rename->name.empty());
+    const flatbuffers::Offset<flatbuffers::String> rename_name_offset =
+        fbb.CreateString(map.rename->name);
+    Channel::Builder match_builder(fbb);
+    match_builder.add_name(match_name_offset);
+    if (!match_type_offset.IsNull()) {
+      match_builder.add_type(match_type_offset);
+    }
+    if (!match_source_node_offset.IsNull()) {
+      match_builder.add_source_node(match_source_node_offset);
+    }
+    const flatbuffers::Offset<Channel> match_offset = match_builder.Finish();
+
+    Channel::Builder rename_builder(fbb);
+    rename_builder.add_name(rename_name_offset);
+    const flatbuffers::Offset<Channel> rename_offset = rename_builder.Finish();
+
+    Map::Builder map_builder(fbb);
+    map_builder.add_match(match_offset);
+    map_builder.add_rename(rename_offset);
+    map_offsets.emplace_back(map_builder.Finish());
+  }
+
+  flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Map>>>
+      maps_offsets = map_offsets.empty() ? 0 : fbb.CreateVector(map_offsets);
+
+  // And copy everything else over.
+  flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Node>>>
+      nodes_offset = RecursiveCopyVectorTable(base_config->nodes(), &fbb);
+
+  flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Application>>>
+      applications_offset =
+          RecursiveCopyVectorTable(base_config->applications(), &fbb);
+
+  // Now insert everything else in unmodified.
+  ConfigurationBuilder configuration_builder(fbb);
+  if (!channels_offset.IsNull()) {
+    configuration_builder.add_channels(channels_offset);
+  }
+  if (!maps_offsets.IsNull()) {
+    configuration_builder.add_maps(maps_offsets);
+  }
+  if (!nodes_offset.IsNull()) {
+    configuration_builder.add_nodes(nodes_offset);
+  }
+  if (!applications_offset.IsNull()) {
+    configuration_builder.add_applications(applications_offset);
+  }
+
+  if (base_config->has_channel_storage_duration()) {
+    configuration_builder.add_channel_storage_duration(
+        base_config->channel_storage_duration());
+  }
+
+  CHECK_EQ(Configuration::MiniReflectTypeTable()->num_elems, 6u)
+      << ": Merging logic needs to be updated when the number of configuration "
+         "fields changes.";
+
+  fbb.Finish(configuration_builder.Finish());
+
+  // Clean it up and return it!  By using MergeConfiguration here, we'll
+  // actually get a deduplicated config for free too.
+  FlatbufferDetachedBuffer<Configuration> new_merged_config =
+      configuration::MergeConfiguration(
+          FlatbufferDetachedBuffer<Configuration>(fbb.Release()));
+
+  remapped_configuration_buffer_ =
+      std::make_unique<FlatbufferDetachedBuffer<Configuration>>(
+          configuration::MergeConfiguration(new_merged_config, schemas));
+
+  remapped_configuration_ = &remapped_configuration_buffer_->message();
+
+  // TODO(austin): Lazily re-build to save CPU?
+}
+
+}  // namespace aos