chromium/chromeos/components/onc/variable_expander.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/components/onc/variable_expander.h"

#include <algorithm>
#include <string_view>

#include "base/containers/flat_map.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/syslog_logging.h"
#include "base/values.h"

namespace {

// Expects ",n" or ",n,m" in |range|. Puts n into |start| and m into |count| if
// present. Returns true if |range| was well formatted and parsing the numbers
// succeeded.
bool ParseRange(std::string_view range, size_t* start, size_t* count) {
  std::vector<std::string_view> parts = base::SplitStringPiece(
      range, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  DCHECK(!parts.empty());
  if (!parts[0].empty())
    return false;
  if (parts.size() > 1 && !base::StringToSizeT(parts[1], start))
    return false;
  if (parts.size() > 2 && !base::StringToSizeT(parts[2], count))
    return false;
  if (parts.size() > 3)
    return false;
  return true;
}

// Expands all occurrences of a variable with name |variable_name| to
// |replacement| or parts of it. This method handles the following variants:
//   - ${variable_name}           -> |replacement|
//   - ${variable_name,pos}       -> |replacement.substr(pos)|
//   - ${variable_name,pos,count} -> |replacement.substr(pos,count)|
// Strictly enforces the format (up to whitespace), e.g.
// ${variable_name ,  2 , 9  } works, but ${variable_name,2o,9e} doesn't.
// Returns true if no error occurred.
bool Expand(std::string_view variable_name,
            std::string_view replacement,
            std::string* str) {
  std::string token = base::StrCat({"${", variable_name});
  size_t token_start = 0;
  bool no_error = true;
  int count = 0;
  while ((token_start = str->find(token, token_start)) != std::string::npos) {
    // Exit if |token| is found too often.
    if (++count > 100) {
      LOG(ERROR) << "Maximum replacement count exceeded for " << variable_name
                 << " in string '" << *str << "'";
      no_error = false;
      break;
    }

    // Find the closing braces.
    const size_t range_start = token_start + token.size();
    const size_t range_end = str->find("}", range_start);
    if (range_end == std::string::npos) {
      LOG(ERROR) << "Closing braces not found for " << variable_name
                 << " in string '" << *str << "'";
      ++token_start;
      no_error = false;
      continue;
    }

    // Full variable, e.g. ${machine_name} or ${machine_name,8,3}.
    DCHECK_GE(range_end, range_start);
    const std::string_view full_token =
        std::string_view(*str).substr(token_start, range_end + 1 - token_start);

    // Determine if the variable defines a range, e.g. ${machine_name,8,3}.
    size_t replacement_start = 0;
    size_t replacement_count = std::string::npos;
    if (range_end > range_start) {
      const std::string_view range =
          std::string_view(*str).substr(range_start, range_end - range_start);
      if (!ParseRange(range, &replacement_start, &replacement_count)) {
        LOG(ERROR) << "Invalid range definition for " << variable_name
                   << " in string '" << *str << "'";
        token_start += full_token.size();
        no_error = false;
        continue;
      }
    }

    const std::string_view replacement_part = replacement.substr(
        std::min(replacement_start, replacement.size()), replacement_count);
    // Don't use ReplaceSubstringsAfterOffset here, it can lead to a doubling
    // of tokens, see VariableExpanderTest.DoesNotRecurse test.
    base::ReplaceFirstSubstringAfterOffset(str, token_start, full_token,
                                           replacement_part);
    token_start += replacement_part.size();
  }
  return no_error;
}

}  // namespace

namespace chromeos {

VariableExpander::VariableExpander(
    base::flat_map<std::string, std::string> variables)
    : variables_(std::move(variables)) {}

VariableExpander::~VariableExpander() = default;

bool VariableExpander::ExpandString(std::string* str) const {
  bool no_error = true;
  for (const auto& kv : variables_)
    no_error &= Expand(kv.first, kv.second, str);
  return no_error;
}

bool VariableExpander::ExpandValue(base::Value* value) const {
  bool no_error = true;
  switch (value->type()) {
    case base::Value::Type::STRING: {
      std::string expanded_str = value->GetString();
      no_error &= ExpandString(&expanded_str);
      *value = base::Value(expanded_str);
      break;
    }

    case base::Value::Type::DICT: {
      for (const auto child : value->GetDict()) {
        no_error &= ExpandValue(&child.second);
      }
      break;
    }

    case base::Value::Type::LIST: {
      for (base::Value& child : value->GetList())
        no_error &= ExpandValue(&child);
      break;
    }

    case base::Value::Type::BOOLEAN:
    case base::Value::Type::INTEGER:
    case base::Value::Type::DOUBLE:
    case base::Value::Type::BINARY:
    case base::Value::Type::NONE: {
      // Nothing to do here.
      break;
    }
  }
  return no_error;
}

}  // namespace chromeos