text: ////////////////////////////////////////////////////////////////////////////
  //
  // Copyright 2016 Realm Inc.
  //
  // 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 "sync/impl/sync_file.hpp"
  
  #include <realm/util/file.hpp>
  #include <realm/util/time.hpp>
  #include <realm/util/scope_exit.hpp>
  
  #include <iomanip>
  #include <sstream>
  #include <system_error>
  #include <fstream>
  
  #ifdef _WIN32
  #include <io.h>
  #include <fcntl.h>
  
  inline static int mkstemp(char* _template) { return _open(_mktemp(_template), _O_CREAT | _O_TEMPORARY, _S_IREAD | _S_IWRITE); }
  #else
  #include <unistd.h>
  #endif
  
  
  using File = realm::util::File;
  
  namespace realm {
  
  namespace {
  
  uint8_t value_of_hex_digit(char hex_digit)
  {
      if (hex_digit >= '0' && hex_digit <= '9') {
          return hex_digit - '0';
      } else if (hex_digit >= 'A' && hex_digit <= 'F') {
          return 10 + hex_digit - 'A';
      } else if (hex_digit >= 'a' && hex_digit <= 'f') {
          return 10 + hex_digit - 'a';
      } else {
          throw std::invalid_argument("Cannot get the value of a character that isn't a hex digit.");
      }
  }
  
  bool filename_is_reserved(const std::string& filename) {
      return (filename == "." || filename == "..");
  }
  
  bool character_is_unreserved(char character)
  {
      bool is_capital_letter = (character >= 'A' && character <= 'Z');
      bool is_lowercase_letter = (character >= 'a' && character <= 'z');
      bool is_number = (character >= '0' && character <= '9');
      bool is_allowed_symbol = (character == '-' || character == '_' || character == '.');
      return is_capital_letter || is_lowercase_letter || is_number || is_allowed_symbol;
  }
  
  char decoded_char_for(const std::string& percent_encoding, size_t index)
  {
      if (index+2 >= percent_encoding.length()) {
          throw std::invalid_argument("Malformed string: not enough characters after '%' before end of string.");
      }
      REALM_ASSERT(percent_encoding[index] == '%');
      return (16*value_of_hex_digit(percent_encoding[index + 1])) + value_of_hex_digit(percent_encoding[index + 2]);
  }
  
  } // (anonymous namespace)
  
  namespace util {
  
  std::string make_percent_encoded_string(const std::string& raw_string)
  {
      std::string buffer;
      buffer.reserve(raw_string.size());
      for (size_t i=0; i<raw_string.size(); i++) {
          unsigned char character = raw_string[i];
          if (character_is_unreserved(character)) {
              buffer.push_back(character);
          } else {
              buffer.resize(buffer.size() + 3);
              // Format string must resolve to exactly 3 characters.
              sprintf(&buffer.back() - 2, "%%%2X", character);
          }
      }
      return buffer;
  }
  
  std::string make_raw_string(const std::string& percent_encoded_string)
  {
      std::string buffer;
      size_t input_len = percent_encoded_string.length();
      buffer.reserve(input_len);
      size_t idx = 0;
      while (idx < input_len) {
          char current = percent_encoded_string[idx];
          if (current == '%') {
              // Decode. +3.
              buffer.push_back(decoded_char_for(percent_encoded_string, idx));
              idx += 3;
          } else {
              // No need to decode. +1.
              if (!character_is_unreserved(current)) {
                  throw std::invalid_argument("Input string is invalid: contains reserved characters.");
              }
              buffer.push_back(current);
              idx++;
          }
      }
      return buffer;
  }
  
  std::string file_path_by_appending_component(const std::string& path, const std::string& component, FilePathType path_type)
  {
      // FIXME: Does this have to be changed to accomodate Windows platforms?
      std::string buffer;
      buffer.reserve(2 + path.length() + component.length());
      buffer.append(path);
      std::string terminal = "";
      if (path_type == FilePathType::Directory && component[component.length() - 1] != '/') {
          terminal = "/";
      }
      char path_last = path[path.length() - 1];
      char component_first = component[0];
      if (path_last == '/' && component_first == '/') {
          buffer.append(component.substr(1));
          buffer.append(terminal);
      } else if (path_last == '/' || component_first == '/') {
          buffer.append(component);
          buffer.append(terminal);
      } else {
          buffer.append("/");
          buffer.append(component);
          buffer.append(terminal);
      }
      return buffer;
  }
  
  std::string file_path_by_appending_extension(const std::string& path, const std::string& extension)
  {
      std::string buffer;
      buffer.reserve(1 + path.length() + extension.length());
      buffer.append(path);
      char path_last = path[path.length() - 1];
      char extension_first = extension[0];
      if (path_last == '.' && extension_first == '.') {
          buffer.append(extension.substr(1));
      } else if (path_last == '.' || extension_first == '.') {
          buffer.append(extension);
      } else {
          buffer.append(".");
          buffer.append(extension);
      }
      return buffer;
  }
  
  std::string create_timestamped_template(const std::string& prefix, int wildcard_count)
  {
      constexpr int WILDCARD_MAX = 20;
      constexpr int WILDCARD_MIN = 6;
      wildcard_count = std::min(WILDCARD_MAX, std::max(WILDCARD_MIN, wildcard_count));
      std::time_t time = std::time(nullptr);
      std::stringstream stream;
      stream << prefix << "-" << util::format_local_time(time, "%Y%m%d-%H%M%S") << "-" << std::string(wildcard_count, 'X');
      return stream.str();
  }
  
  std::string reserve_unique_file_name(const std::string& path, const std::string& template_string)
  {
      REALM_ASSERT_DEBUG(template_string.find("XXXXXX") != std::string::npos);
      std::string path_buffer = file_path_by_appending_component(path, template_string, FilePathType::File);
      int fd = mkstemp(&path_buffer[0]);
      if (fd < 0) {
          int err = errno;
          throw std::system_error(err, std::system_category());
      }
      // Remove the file so we can use the name for our own file.
  #ifdef _WIN32
      _close(fd);
      _unlink(path_buffer.c_str());
  #else
      close(fd);
      unlink(path_buffer.c_str());
  #endif
      return path_buffer;
  }
  
  } // util
  
  constexpr const char SyncFileManager::c_sync_directory[];
  constexpr const char SyncFileManager::c_utility_directory[];
  constexpr const char SyncFileManager::c_state_directory[];
  constexpr const char SyncFileManager::c_recovery_directory[];
  constexpr const char SyncFileManager::c_metadata_directory[];
  constexpr const char SyncFileManager::c_metadata_realm[];
  
  std::string SyncFileManager::get_special_directory(std::string directory_name) const
  {
      auto dir_path = file_path_by_appending_component(get_base_sync_directory(),
                                                       directory_name,
                                                       util::FilePathType::Directory);
      util::try_make_dir(dir_path);
      return dir_path;
  }
  
  std::string SyncFileManager::get_base_sync_directory() const
  {
      auto sync_path = file_path_by_appending_component(m_base_path,
                                                        c_sync_directory,
                                                        util::FilePathType::Directory);
      util::try_make_dir(sync_path);
      return sync_path;
  }
  
  std::string SyncFileManager::user_directory(const std::string& local_identity) const
  {
      REALM_ASSERT(local_identity.length() > 0);
      std::string escaped = util::make_percent_encoded_string(local_identity);
      if (filename_is_reserved(escaped))
          throw std::invalid_argument("A user can't have an identifier reserved by the filesystem.");
  
      auto user_path = file_path_by_appending_component(get_base_sync_directory(),
                                                        escaped,
                                                        util::FilePathType::Directory);
      util::try_make_dir(user_path);
      return user_path;
  }
  
  void SyncFileManager::remove_user_directory(const std::string& local_identity) const
  {
      REALM_ASSERT(local_identity.length() > 0);
      const auto& escaped = util::make_percent_encoded_string(local_identity);
      if (filename_is_reserved(escaped))
          throw std::invalid_argument("A user can't have an identifier reserved by the filesystem.");
  
      auto user_path = file_path_by_appending_component(get_base_sync_directory(),
                                                        escaped,
                                                        util::FilePathType::Directory);
      util::try_remove_dir_recursive(user_path);
  }
  
  bool SyncFileManager::try_rename_user_directory(const std::string& old_name, const std::string& new_name) const
  {
      REALM_ASSERT_DEBUG(old_name.length() > 0 && new_name.length() > 0);
      const auto& old_name_escaped = util::make_percent_encoded_string(old_name);
      const auto& new_name_escaped = util::make_percent_encoded_string(new_name);
      const std::string& base = get_base_sync_directory();
      if (filename_is_reserved(old_name_escaped) || filename_is_reserved(new_name_escaped))
          throw std::invalid_argument("A user directory can't be renamed using a reserved identifier.");
  
      const auto& old_path = file_path_by_appending_component(base, old_name_escaped, util::FilePathType::Directory);
      const auto& new_path = file_path_by_appending_component(base, new_name_escaped, util::FilePathType::Directory);
  
      try {
          File::move(old_path, new_path);
      } catch (File::NotFound const&) {
          return false;
      }
      return true;
  }
  
  bool SyncFileManager::remove_realm(const std::string& absolute_path) const
  {
      REALM_ASSERT(absolute_path.length() > 0);
      bool success = true;
      // Remove the Realm file (e.g. "example.realm").
      success = File::try_remove(absolute_path);
      // Remove the lock file (e.g. "example.realm.lock").
      auto lock_path = util::file_path_by_appending_extension(absolute_path, "lock");
      success = File::try_remove(lock_path);
      // Remove the management directory (e.g. "example.realm.management").
      auto management_path = util::file_path_by_appending_extension(absolute_path, "management");
      try {
          util::try_remove_dir_recursive(management_path);
      }
      catch (File::AccessError const&) {
          success = false;
      }
      return success;
  }
  
  bool SyncFileManager::copy_realm_file(const std::string& old_path, const std::string& new_path) const
  {
      REALM_ASSERT(old_path.length() > 0);
      try {
          if (File::exists(new_path)) {
              return false;
          }
          File::copy(old_path, new_path);
      }
      catch (File::NotFound const&) {
          return false;
      }
      catch (File::AccessError const&) {
          return false;
      }
      return true;
  }
  
  bool SyncFileManager::remove_realm(const std::string& local_identity, const std::string& raw_realm_path) const
  {
      REALM_ASSERT(local_identity.length() > 0);
      REALM_ASSERT(raw_realm_path.length() > 0);
      if (filename_is_reserved(local_identity) || filename_is_reserved(raw_realm_path))
          throw std::invalid_argument("A user or Realm can't have an identifier reserved by the filesystem.");
  
      auto escaped = util::make_percent_encoded_string(raw_realm_path);
      auto realm_path = util::file_path_by_appending_component(user_directory(local_identity), escaped);
      return remove_realm(realm_path);
  }
  
  std::string SyncFileManager::path(const std::string& local_identity, const std::string& raw_realm_path) const
  {
      REALM_ASSERT(local_identity.length() > 0);
      REALM_ASSERT(raw_realm_path.length() > 0);
      if (filename_is_reserved(local_identity) || filename_is_reserved(raw_realm_path))
          throw std::invalid_argument("A user or Realm can't have an identifier reserved by the filesystem.");
  
      auto escaped = util::make_percent_encoded_string(raw_realm_path);
      return util::file_path_by_appending_component(user_directory(local_identity), escaped);
  }
  
  std::string SyncFileManager::metadata_path() const
  {
      auto dir_path = file_path_by_appending_component(get_utility_directory(),
                                                       c_metadata_directory,
                                                       util::FilePathType::Directory);
      util::try_make_dir(dir_path);
      return util::file_path_by_appending_component(dir_path, c_metadata_realm);
  }
  
  bool SyncFileManager::remove_metadata_realm() const
  {
      auto dir_path = file_path_by_appending_component(get_utility_directory(),
                                                       c_metadata_directory,
                                                       util::FilePathType::Directory);
      try {
          util::try_remove_dir_recursive(dir_path);
          return true;
      }
      catch (File::AccessError const&) {
          return false;
      }
  }
  
  } // realm
  