Updated addon repository URL and improved debug output on download
[supertux.git] / src / addon / addon_manager.cpp
index fe2de83..7266c3e 100644 (file)
@@ -1,5 +1,6 @@
 //  SuperTux - Add-on Manager
 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
+//                2014 Ingo Ruhnke <grumbel@gmail.com>
 //
 //  This program is free software: you can redistribute it and/or modify
 //  it under the terms of the GNU General Public License as published by
 #include <version.h>
 
 #include <algorithm>
+#include <iostream>
 #include <memory>
 #include <physfs.h>
 #include <sstream>
 #include <stdexcept>
+#include <stdio.h>
 #include <sys/stat.h>
 
-#ifdef HAVE_LIBCURL
-#  include <curl/curl.h>
-#  include <curl/easy.h>
-#endif
-
 #include "addon/addon.hpp"
+#include "addon/md5.hpp"
 #include "lisp/list_iterator.hpp"
 #include "lisp/parser.hpp"
+#include "util/file_system.hpp"
+#include "util/log.hpp"
 #include "util/reader.hpp"
 #include "util/writer.hpp"
-#include "util/log.hpp"
 
-#ifdef HAVE_LIBCURL
 namespace {
 
-size_t my_curl_string_append(void *ptr, size_t size, size_t nmemb, void *string_ptr)
+MD5 md5_from_file(const std::string& filename)
 {
-  std::string& s = *static_cast<std::string*>(string_ptr);
-  std::string buf(static_cast<char*>(ptr), size * nmemb);
-  s += buf;
-  log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
-  return size * nmemb;
+  // TODO: this does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
+  //IFileStream ifs(installed_physfs_filename);
+  //std::string md5 = MD5(ifs).hex_digest();
+
+  MD5 md5;
+
+  PHYSFS_file* file = PHYSFS_openRead(filename.c_str());
+  if (!file)
+  {
+    std::ostringstream out;
+    out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
+    throw std::runtime_error(out.str());
+  }
+  else
+  {
+    while (true)
+    {
+      unsigned char buffer[1024];
+      PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
+      if (len <= 0) break;
+      md5.update(buffer, len);
+    }
+    PHYSFS_close(file);
+
+    return md5;
+  }
 }
 
-size_t my_curl_physfs_write(void *ptr, size_t size, size_t nmemb, void *f_p)
+bool has_suffix(const std::string& str, const std::string& suffix)
 {
-  PHYSFS_file* f = static_cast<PHYSFS_file*>(f_p);
-  PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
-  log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
-  return size * written;
+  if (str.length() >= suffix.length())
+    return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
+  else
+    return false;
 }
 
+} // namespace
+
+AddonManager::AddonManager(const std::string& addon_directory,
+                           std::vector<Config::Addon>& addon_config) :
+  m_downloader(),
+  m_addon_directory(addon_directory),
+  m_repository_url("https://raw.githubusercontent.com/SuperTuxTeam/addons/master/index-0_4_0.nfo"),
+  m_addon_config(addon_config),
+  m_installed_addons(),
+  m_repository_addons(),
+  m_has_been_updated(false),
+  m_transfer_status()
+{
+  PHYSFS_mkdir(m_addon_directory.c_str());
+
+  add_installed_addons();
+
+  // FIXME: We should also restore the order here
+  for(auto& addon : m_addon_config)
+  {
+    if (addon.enabled)
+    {
+      try
+      {
+        enable_addon(addon.id);
+      }
+      catch(const std::exception& err)
+      {
+        log_warning << "failed to enable addon from config: " << err.what() << std::endl;
+      }
+    }
+  }
+
+  try
+  {
+    m_repository_addons = parse_addon_infos("/addons/repository.nfo");
+  }
+  catch(const std::exception& err)
+  {
+    log_warning << "parsing repository.nfo failed: " << err.what() << std::endl;
+  }
 }
-#endif
 
-AddonManager&
-AddonManager::get_instance()
+AddonManager::~AddonManager()
 {
-  static AddonManager instance;
-  return instance;
+  // sync enabled/disabled addons into the config for saving
+  m_addon_config.clear();
+  for(auto& addon : m_installed_addons)
+  {
+    m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
+  }
 }
 
-AddonManager::AddonManager() :
-  addons(),
-  ignored_addon_filenames()
+Addon&
+AddonManager::get_repository_addon(const AddonId& id)
 {
-#ifdef HAVE_LIBCURL
-  curl_global_init(CURL_GLOBAL_ALL);
-#endif
+  auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
+                         [&id](const std::unique_ptr<Addon>& addon)
+                         {
+                           return addon->get_id() == id;
+                         });
+
+  if (it != m_repository_addons.end())
+  {
+    return **it;
+  }
+  else
+  {
+    throw std::runtime_error("Couldn't find repository Addon with id: " + id);
+  }
 }
 
-AddonManager::~AddonManager()
+Addon&
+AddonManager::get_installed_addon(const AddonId& id)
 {
-#ifdef HAVE_LIBCURL
-  curl_global_cleanup();
-#endif
+  auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
+                         [&id](const std::unique_ptr<Addon>& addon)
+                         {
+                           return addon->get_id() == id;
+                         });
+
+  if (it != m_installed_addons.end())
+  {
+    return **it;
+  }
+  else
+  {
+    throw std::runtime_error("Couldn't find installed Addon with id: " + id);
+  }
+}
 
-  for (std::vector<Addon*>::iterator i = addons.begin(); i != addons.end(); i++) delete *i;
+std::vector<AddonId>
+AddonManager::get_repository_addons() const
+{
+  std::vector<AddonId> results;
+  results.reserve(m_repository_addons.size());
+  std::transform(m_repository_addons.begin(), m_repository_addons.end(),
+                 std::back_inserter(results),
+                 [](const std::unique_ptr<Addon>& addon)
+                 {
+                   return addon->get_id();
+                 });
+  return results;
 }
 
-std::vector<Addon*>
-AddonManager::get_addons()
+
+std::vector<AddonId>
+AddonManager::get_installed_addons() const
 {
-  /*
-    for (std::vector<Addon>::iterator it = installed_addons.begin(); it != installed_addons.end(); ++it) {
-    Addon& addon = *it;
-    if (addon.md5 == "") addon.md5 = calculate_md5(addon);
-    }
-  */
-  return addons;
+  std::vector<AddonId> results;
+  results.reserve(m_installed_addons.size());
+  std::transform(m_installed_addons.begin(), m_installed_addons.end(),
+                 std::back_inserter(results),
+                 [](const std::unique_ptr<Addon>& addon)
+                 {
+                   return addon->get_id();
+                 });
+  return results;
 }
 
-void
-AddonManager::check_online()
+bool
+AddonManager::has_online_support() const
 {
-#ifdef HAVE_LIBCURL
-  char error_buffer[CURL_ERROR_SIZE+1];
-
-  const char* baseUrl = "http://addons.supertux.googlecode.com/git/index-0_3_5.nfo";
-  std::string addoninfos = "";
-
-  CURL *curl_handle;
-  curl_handle = curl_easy_init();
-  curl_easy_setopt(curl_handle, CURLOPT_URL, baseUrl);
-  curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_string_append);
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &addoninfos);
-  curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
-  curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
-  CURLcode result = curl_easy_perform(curl_handle);
-  curl_easy_cleanup(curl_handle);
-
-  if (result != CURLE_OK) {
-    std::string why = error_buffer[0] ? error_buffer : "unhandled error";
-    throw std::runtime_error("Downloading Add-on list failed: " + why);
-  }
+  return true;
+}
 
-  try {
-    lisp::Parser parser;
-    std::stringstream addoninfos_stream(addoninfos);
-    const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
+bool
+AddonManager::has_been_updated() const
+{
+  return m_has_been_updated;
+}
 
-    const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
-    if(!addons_lisp) throw std::runtime_error("Downloaded file is not an Add-on list");
+TransferStatusPtr
+AddonManager::request_check_online()
+{
+  if (m_transfer_status)
+  {
+    throw std::runtime_error("only async request can be made to AddonManager at a time");
+  }
+  else
+  {
+    m_transfer_status = m_downloader.request_download(m_repository_url, "/addons/repository.nfo");
 
-    lisp::ListIterator iter(addons_lisp);
-    while(iter.next()) 
-    {
-      const std::string& token = iter.item();
-      if(token != "supertux-addoninfo") 
+    m_transfer_status->then(
+      [this](bool success)
       {
-        log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
-        continue;
-      }
-      std::auto_ptr<Addon> addon(new Addon());
-      addon->parse(*(iter.lisp()));
-      addon->installed = false;
-      addon->loaded = false;
-
-      // make sure the list of known Add-ons does not already contain this one 
-      bool exists = false;
-      for (std::vector<Addon*>::const_iterator i = addons.begin(); i != addons.end(); i++) {
-        if (**i == *addon) {
-          exists = true; 
-          break; 
+        m_transfer_status = {};
+
+        if (success)
+        {
+          m_repository_addons = parse_addon_infos("/addons/repository.nfo");
+          m_has_been_updated = true;
         }
-      }
+      });
 
-      if (exists) 
-      {
-        // do nothing
-      }
-      else if (addon->suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) 
+    return m_transfer_status;
+  }
+}
+
+void
+AddonManager::check_online()
+{
+  m_downloader.download(m_repository_url, "/addons/repository.nfo");
+  m_repository_addons = parse_addon_infos("/addons/repository.nfo");
+  m_has_been_updated = true;
+}
+
+TransferStatusPtr
+AddonManager::request_install_addon(const AddonId& addon_id)
+{
+  if (m_transfer_status)
+  {
+    throw std::runtime_error("only one addon install request allowed at a time");
+  }
+  else
+  {
+    { // remove addon if it already exists
+      auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
+                             [&addon_id](const std::unique_ptr<Addon>& addon)
+                             {
+                               return addon->get_id() == addon_id;
+                             });
+      if (it != m_installed_addons.end())
       {
-        // make sure the Add-on's file name does not contain weird characters
-        log_warning << "Add-on \"" << addon->title << "\" contains unsafe file name. Skipping." << std::endl;
+        log_debug << "reinstalling addon " << addon_id << std::endl;
+        if ((*it)->is_enabled())
+        {
+          disable_addon((*it)->get_id());
+        }
+        m_installed_addons.erase(it);
       }
       else
       {
-        addons.push_back(addon.release());
+        log_debug << "installing addon " << addon_id << std::endl;
       }
     }
-  } catch(std::exception& e) {
-    std::stringstream msg;
-    msg << "Problem when reading Add-on list: " << e.what();
-    throw std::runtime_error(msg.str());
-  }
 
-#endif
-}
+    Addon& addon = get_repository_addon(addon_id);
 
-void
-AddonManager::install(Addon* addon)
-{
-#ifdef HAVE_LIBCURL
+    std::string install_filename = FileSystem::join(m_addon_directory, addon.get_filename());
 
-  if (addon->installed) throw std::runtime_error("Tried installing installed Add-on");
+    m_transfer_status = m_downloader.request_download(addon.get_url(), install_filename);
 
-  // make sure the Add-on's file name does not contain weird characters
-  if (addon->suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
-    throw std::runtime_error("Add-on has unsafe file name (\""+addon->suggested_filename+"\")");
-  }
+    m_transfer_status->then(
+      [this, install_filename, addon_id](bool success)
+      {
+        m_transfer_status = {};
+
+        if (success)
+        {
+          // complete the addon install
+          Addon& repository_addon = get_repository_addon(addon_id);
+
+          MD5 md5 = md5_from_file(install_filename);
+          if (repository_addon.get_md5() != md5.hex_digest())
+          {
+            if (PHYSFS_delete(install_filename.c_str()) == 0)
+            {
+              log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
+            }
+
+            throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
+          }
+          else
+          {
+            const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
+            if (!realdir)
+            {
+              throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
+            }
+            else
+            {
+              add_installed_archive(install_filename, md5.hex_digest());
+            }
+          }
+        }
+      });
 
-  std::string fileName = addon->suggested_filename;
+    return m_transfer_status;
+  }
+}
 
-  // make sure its file doesn't already exist
-  if (PHYSFS_exists(fileName.c_str())) {
-    fileName = addon->stored_md5 + "_" + addon->suggested_filename;
-    if (PHYSFS_exists(fileName.c_str())) {
-      throw std::runtime_error("Add-on of suggested filename already exists (\""+addon->suggested_filename+"\", \""+fileName+"\")");
+void
+AddonManager::install_addon(const AddonId& addon_id)
+{
+  { // remove addon if it already exists
+    auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
+                           [&addon_id](const std::unique_ptr<Addon>& addon)
+                           {
+                             return addon->get_id() == addon_id;
+                           });
+    if (it != m_installed_addons.end())
+    {
+      log_debug << "reinstalling addon " << addon_id << std::endl;
+      if ((*it)->is_enabled())
+      {
+        disable_addon((*it)->get_id());
+      }
+      m_installed_addons.erase(it);
+    }
+    else
+    {
+      log_debug << "installing addon " << addon_id << std::endl;
     }
   }
 
-  char error_buffer[CURL_ERROR_SIZE+1];
+  Addon& repository_addon = get_repository_addon(addon_id);
 
-  char* url = (char*)malloc(addon->http_url.length() + 1);
-  strncpy(url, addon->http_url.c_str(), addon->http_url.length() + 1);
+  std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
 
-  PHYSFS_file* f = PHYSFS_openWrite(fileName.c_str());
+  m_downloader.download(repository_addon.get_url(), install_filename);
 
-  log_debug << "Downloading \"" << url << "\"" << std::endl;
-
-  CURL *curl_handle;
-  curl_handle = curl_easy_init();
-  curl_easy_setopt(curl_handle, CURLOPT_URL, url);
-  curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_physfs_write);
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, f);
-  curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
-  curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
-  CURLcode result = curl_easy_perform(curl_handle);
-  curl_easy_cleanup(curl_handle);
-
-  PHYSFS_close(f);
-
-  free(url);
+  MD5 md5 = md5_from_file(install_filename);
+  if (repository_addon.get_md5() != md5.hex_digest())
+  {
+    if (PHYSFS_delete(install_filename.c_str()) == 0)
+    {
+      log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
+    }
 
-  if (result != CURLE_OK) {
-    PHYSFS_delete(fileName.c_str());
-    std::string why = error_buffer[0] ? error_buffer : "unhandled error";
-    throw std::runtime_error("Downloading Add-on failed: " + why);
+    throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
   }
-
-  addon->installed = true;
-  addon->installed_physfs_filename = fileName;
-  static const std::string writeDir = PHYSFS_getWriteDir();
-  static const std::string dirSep = PHYSFS_getDirSeparator();
-  addon->installed_absolute_filename = writeDir + dirSep + fileName;
-  addon->loaded = false;
-
-  if (addon->get_md5() != addon->stored_md5) {
-    addon->installed = false;
-    PHYSFS_delete(fileName.c_str());
-    std::string why = "MD5 checksums differ"; 
-    throw std::runtime_error("Downloading Add-on failed: " + why);
+  else
+  {
+    const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
+    if (!realdir)
+    {
+      throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
+    }
+    else
+    {
+      add_installed_archive(install_filename, md5.hex_digest());
+    }
   }
-
-  log_debug << "Finished downloading \"" << addon->installed_absolute_filename << "\". Enabling Add-on." << std::endl;
-
-  enable(addon);
-
-#else
-  (void) addon;
-#endif
-
 }
 
 void
-AddonManager::remove(Addon* addon)
+AddonManager::uninstall_addon(const AddonId& addon_id)
 {
-  if (!addon->installed) throw std::runtime_error("Tried removing non-installed Add-on");
-
-  //FIXME: more checks
-
-  // make sure the Add-on's file name does not contain weird characters
-  if (addon->installed_physfs_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
-    throw std::runtime_error("Add-on has unsafe file name (\""+addon->installed_physfs_filename+"\")");
+  log_debug << "uninstalling addon " << addon_id << std::endl;
+  Addon& addon = get_installed_addon(addon_id);
+  if (addon.is_enabled())
+  {
+    disable_addon(addon_id);
   }
-
-  unload(addon);
-
-  log_debug << "deleting file \"" << addon->installed_absolute_filename << "\"" << std::endl;
-  PHYSFS_delete(addon->installed_absolute_filename.c_str());
-  addon->installed = false;
-
-  // FIXME: As we don't know anything more about it (e.g. where to get it), remove it from list of known Add-ons
+  log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
+  PHYSFS_delete(addon.get_install_filename().c_str());
+  m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
+                                          [&addon](const std::unique_ptr<Addon>& rhs)
+                                          {
+                                            return addon.get_id() == rhs->get_id();
+                                          }),
+                           m_installed_addons.end());
 }
 
 void
-AddonManager::disable(Addon* addon)
+AddonManager::enable_addon(const AddonId& addon_id)
 {
-  unload(addon);
-
-  std::string fileName = addon->installed_physfs_filename;
-  if (std::find(ignored_addon_filenames.begin(), ignored_addon_filenames.end(), fileName) == ignored_addon_filenames.end()) {
-    ignored_addon_filenames.push_back(fileName);
+  log_debug << "enabling addon " << addon_id << std::endl;
+  Addon& addon = get_installed_addon(addon_id);
+  if (addon.is_enabled())
+  {
+    log_warning << "Tried enabling already enabled Add-on" << std::endl;
+  }
+  else
+  {
+    log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
+    //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
+    if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
+    {
+      log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
+                  << PHYSFS_getLastError() << std::endl;
+    }
+    else
+    {
+      addon.set_enabled(true);
+    }
   }
 }
 
 void
-AddonManager::enable(Addon* addon)
+AddonManager::disable_addon(const AddonId& addon_id)
 {
-  load(addon);
-
-  std::string fileName = addon->installed_physfs_filename;
-  std::vector<std::string>::iterator i = std::find(ignored_addon_filenames.begin(), ignored_addon_filenames.end(), fileName);
-  if (i != ignored_addon_filenames.end()) {
-    ignored_addon_filenames.erase(i);
+  log_debug << "disabling addon " << addon_id << std::endl;
+  Addon& addon = get_installed_addon(addon_id);
+  if (!addon.is_enabled())
+  {
+    log_warning << "Tried disabling already disabled Add-On" << std::endl;
+  }
+  else
+  {
+    log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
+    if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
+    {
+      log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
+                  << PHYSFS_getLastError() << std::endl;
+    }
+    else
+    {
+      addon.set_enabled(false);
+    }
   }
 }
 
-void
-AddonManager::unload(Addon* addon)
+std::vector<std::string>
+AddonManager::scan_for_archives() const
 {
-  if (!addon->installed) throw std::runtime_error("Tried unloading non-installed Add-on");
-  if (!addon->loaded) return;
+  std::vector<std::string> archives;
 
-  log_debug << "Removing archive \"" << addon->installed_absolute_filename << "\" from search path" << std::endl;
-  if (PHYSFS_removeFromSearchPath(addon->installed_absolute_filename.c_str()) == 0) {
-    log_warning << "Could not remove " << addon->installed_absolute_filename << " from search path. Ignoring." << std::endl;
-    return;
+  // Search for archives and add them to the search path
+  std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
+    rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
+       PHYSFS_freeList);
+  for(char** i = rc.get(); *i != 0; ++i)
+  {
+    if (has_suffix(*i, ".zip"))
+    {
+      std::string archive = FileSystem::join(m_addon_directory, *i);
+      if (PHYSFS_exists(archive.c_str()))
+      {
+        archives.push_back(archive);
+      }
+    }
   }
 
-  addon->loaded = false;
+  return archives;
 }
 
-void
-AddonManager::load(Addon* addon)
+std::string
+AddonManager::scan_for_info(const std::string& archive_os_path) const
 {
-  if (!addon->installed) throw std::runtime_error("Tried loading non-installed Add-on");
-  if (addon->loaded) return;
+  std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
+    rc2(PHYSFS_enumerateFiles("/"),
+        PHYSFS_freeList);
+  for(char** j = rc2.get(); *j != 0; ++j)
+  {
+    if (has_suffix(*j, ".nfo"))
+    {
+      std::string nfo_filename = FileSystem::join("/", *j);
 
-  log_debug << "Adding archive \"" << addon->installed_absolute_filename << "\" to search path" << std::endl;
-  if (PHYSFS_addToSearchPath(addon->installed_absolute_filename.c_str(), 0) == 0) {
-    log_warning << "Could not add " << addon->installed_absolute_filename << " to search path. Ignoring." << std::endl;
-    return;
+      // make sure it's in the current archive_os_path
+      const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
+      if (!realdir)
+      {
+        log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
+      }
+      else
+      {
+        if (realdir == archive_os_path)
+        {
+          return nfo_filename;
+        }
+      }
+    }
   }
 
-  addon->loaded = true;
+  return std::string();
 }
 
 void
-AddonManager::load_addons()
+AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
 {
-  // unload all Addons and forget about them
-  for (std::vector<Addon*>::iterator i = addons.begin(); i != addons.end(); i++) {
-    if ((*i)->installed && (*i)->loaded) unload(*i);
-    delete *i;
+  const char* realdir = PHYSFS_getRealDir(archive.c_str());
+  if (!realdir)
+  {
+    log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
+                << PHYSFS_getLastError() << std::endl;
   }
-  addons.clear();
+  else
+  {
+    std::string os_path = FileSystem::join(realdir, archive);
 
-  // Search for archives and add them to the search path
-  char** rc = PHYSFS_enumerateFiles("/");
-
-  for(char** i = rc; *i != 0; ++i) {
-
-    // get filename of potential archive
-    std::string fileName = *i;
-
-    const std::string archiveDir = PHYSFS_getRealDir(fileName.c_str());
-    static const std::string dirSep = PHYSFS_getDirSeparator();
-    std::string fullFilename = archiveDir + dirSep + fileName;
-
-    /*
-    // make sure it's in the writeDir
-    static const std::string writeDir = PHYSFS_getWriteDir();
-    if (fileName.compare(0, writeDir.length(), writeDir) != 0) continue;
-    */
-
-    // make sure it looks like an archive
-    static const std::string archiveExt = ".zip";
-    if (fullFilename.compare(fullFilename.length()-archiveExt.length(), archiveExt.length(), archiveExt) != 0) continue;
+    PHYSFS_addToSearchPath(os_path.c_str(), 0);
 
-    // make sure it exists
-    struct stat stats;
-    if (stat(fullFilename.c_str(), &stats) != 0) continue;
+    std::string nfo_filename = scan_for_info(os_path);
 
-    // make sure it's an actual file
-    if (!S_ISREG(stats.st_mode)) continue;
-
-    log_debug << "Found archive \"" << fullFilename << "\"" << std::endl;
-
-    // add archive to search path
-    PHYSFS_addToSearchPath(fullFilename.c_str(), 0);
-
-    // Search for infoFiles
-    std::string infoFileName = "";
-    char** rc2 = PHYSFS_enumerateFiles("/");
-    for(char** i = rc2; *i != 0; ++i) {
+    if (nfo_filename.empty())
+    {
+      log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
+    }
+    else
+    {
+      try
+      {
+        std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
+        addon->set_install_filename(os_path, md5);
+        m_installed_addons.push_back(std::move(addon));
+      }
+      catch (const std::runtime_error& e)
+      {
+        log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
+      }
+    }
 
-      // get filename of potential infoFile
-      std::string potentialInfoFileName = *i;
+    PHYSFS_removeFromSearchPath(os_path.c_str());
+  }
+}
 
-      // make sure it looks like an infoFile
-      static const std::string infoExt = ".nfo";
-      if (potentialInfoFileName.length() <= infoExt.length())
-        continue;
+void
+AddonManager::add_installed_addons()
+{
+  auto archives = scan_for_archives();
 
-      if (potentialInfoFileName.compare(potentialInfoFileName.length()-infoExt.length(), infoExt.length(), infoExt) != 0)
-        continue;
+  for(auto archive : archives)
+  {
+    MD5 md5 = md5_from_file(archive);
+    add_installed_archive(archive, md5.hex_digest());
+  }
+}
 
-      // make sure it's in the current archive
-      std::string infoFileDir = PHYSFS_getRealDir(potentialInfoFileName.c_str());
-      if (infoFileDir != fullFilename) continue;
+AddonManager::AddonList
+AddonManager::parse_addon_infos(const std::string& filename) const
+{
+  AddonList m_addons;
 
-      // found infoFileName
-      infoFileName = potentialInfoFileName;
-      break;
+  try
+  {
+    lisp::Parser parser;
+    const lisp::Lisp* root = parser.parse(filename);
+    const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
+    if(!addons_lisp)
+    {
+      throw std::runtime_error("Downloaded file is not an Add-on list");
     }
-    PHYSFS_freeList(rc2);
-
-    // if we have an infoFile, it's an Addon
-    if (infoFileName != "") {
-      try {
-        Addon* addon = new Addon();
-        addon->parse(infoFileName);
-        addon->installed = true;
-        addon->installed_physfs_filename = fileName;
-        addon->installed_absolute_filename = fullFilename;
-        addon->loaded = true;
-        addons.push_back(addon);
-
-        // check if the Addon is disabled 
-        if (std::find(ignored_addon_filenames.begin(), ignored_addon_filenames.end(), fileName) != ignored_addon_filenames.end()) {
-          unload(addon);
+    else
+    {
+      lisp::ListIterator iter(addons_lisp);
+      while(iter.next())
+      {
+        const std::string& token = iter.item();
+        if(token != "supertux-addoninfo")
+        {
+          log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
+        }
+        else
+        {
+          std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
+          m_addons.push_back(std::move(addon));
         }
-
-      } catch (const std::runtime_error& e) {
-        log_warning << "Could not load add-on info for " << fullFilename << ", loading as unmanaged:" << e.what() << std::endl;
       }
-    }
 
+      return m_addons;
+    }
+  }
+  catch(const std::exception& e)
+  {
+    std::stringstream msg;
+    msg << "Problem when reading Add-on list: " << e.what();
+    throw std::runtime_error(msg.str());
   }
 
-  PHYSFS_freeList(rc);
-}
-
-void
-AddonManager::read(const Reader& lisp)
-{
-  lisp.get("disabled-addons", ignored_addon_filenames); 
+  return m_addons;
 }
 
 void
-AddonManager::write(lisp::Writer& writer)
+AddonManager::update()
 {
-  writer.write("disabled-addons", ignored_addon_filenames); 
+  m_downloader.update();
 }
 
 /* EOF */