ab56662e8c9f635ee4211078d0dedf7d935cb4aa
[supertux.git] / src / addon / addon_manager.cpp
1 //  SuperTux - Add-on Manager
2 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
3 //                2014 Ingo Ruhnke <grumbel@gmail.com>
4 //
5 //  This program is free software: you can redistribute it and/or modify
6 //  it under the terms of the GNU General Public License as published by
7 //  the Free Software Foundation, either version 3 of the License, or
8 //  (at your option) any later version.
9 //
10 //  This program is distributed in the hope that it will be useful,
11 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 //  GNU General Public License for more details.
14 //
15 //  You should have received a copy of the GNU General Public License
16 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 #include "addon/addon_manager.hpp"
19
20 #include <config.h>
21 #include <version.h>
22
23 #include <algorithm>
24 #include <iostream>
25 #include <memory>
26 #include <physfs.h>
27 #include <sstream>
28 #include <stdexcept>
29 #include <stdio.h>
30 #include <sys/stat.h>
31
32 #include "addon/addon.hpp"
33 #include "addon/md5.hpp"
34 #include "lisp/list_iterator.hpp"
35 #include "lisp/parser.hpp"
36 #include "util/file_system.hpp"
37 #include "util/log.hpp"
38 #include "util/reader.hpp"
39 #include "util/writer.hpp"
40
41 namespace {
42
43 MD5 md5_from_file(const std::string& filename)
44 {
45   // TODO: this does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
46   //IFileStream ifs(installed_physfs_filename);
47   //std::string md5 = MD5(ifs).hex_digest();
48
49   MD5 md5;
50
51   unsigned char buffer[1024];
52   PHYSFS_file* file = PHYSFS_openRead(filename.c_str());
53   while (true)
54   {
55     PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
56     if (len <= 0) break;
57     md5.update(buffer, len);
58   }
59   PHYSFS_close(file);
60
61   return md5;
62 }
63
64 bool has_suffix(const std::string& str, const std::string& suffix)
65 {
66   if (str.length() >= suffix.length())
67     return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
68   else
69     return false;
70 }
71
72 } // namespace
73
74 AddonManager::AddonManager(const std::string& addon_directory,
75                            std::vector<Config::Addon>& addon_config) :
76   m_downloader(),
77   m_addon_directory(addon_directory),
78   m_repository_url("http://addons.supertux.googlecode.com/git/index-0_4_0.nfo"),
79   m_addon_config(addon_config),
80   m_installed_addons(),
81   m_repository_addons(),
82   m_has_been_updated(false)
83 {
84   PHYSFS_mkdir(m_addon_directory.c_str());
85
86   add_installed_addons();
87
88   // FIXME: We should also restore the order here
89   for(auto& addon : m_addon_config)
90   {
91     if (addon.enabled)
92     {
93       try
94       {
95         enable_addon(addon.id);
96       }
97       catch(const std::exception& err)
98       {
99         log_warning << "failed to enable addon from config: " << err.what() << std::endl;
100       }
101     }
102   }
103 }
104
105 AddonManager::~AddonManager()
106 {
107   // sync enabled/disabled addons into the config for saving
108   m_addon_config.clear();
109   for(auto& addon : m_installed_addons)
110   {
111     m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
112   }
113 }
114
115 Addon&
116 AddonManager::get_repository_addon(const AddonId& id)
117 {
118   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
119                          [&id](const std::unique_ptr<Addon>& addon)
120                          {
121                            return addon->get_id() == id;
122                          });
123
124   if (it != m_repository_addons.end())
125   {
126     return **it;
127   }
128   else
129   {
130     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
131   }
132 }
133
134 Addon&
135 AddonManager::get_installed_addon(const AddonId& id)
136 {
137   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
138                          [&id](const std::unique_ptr<Addon>& addon)
139                          {
140                            return addon->get_id() == id;
141                          });
142
143   if (it != m_installed_addons.end())
144   {
145     return **it;
146   }
147   else
148   {
149     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
150   }
151 }
152
153 std::vector<AddonId>
154 AddonManager::get_repository_addons() const
155 {
156   std::vector<AddonId> results;
157   results.reserve(m_repository_addons.size());
158   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
159                  std::back_inserter(results),
160                  [](const std::unique_ptr<Addon>& addon)
161                  {
162                    return addon->get_id();
163                  });
164   return results;
165 }
166
167
168 std::vector<AddonId>
169 AddonManager::get_installed_addons() const
170 {
171   std::vector<AddonId> results;
172   results.reserve(m_installed_addons.size());
173   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
174                  std::back_inserter(results),
175                  [](const std::unique_ptr<Addon>& addon)
176                  {
177                    return addon->get_id();
178                  });
179   return results;
180 }
181
182 bool
183 AddonManager::has_online_support() const
184 {
185   return true;
186 }
187
188 bool
189 AddonManager::has_been_updated() const
190 {
191   return m_has_been_updated;
192 }
193
194 void
195 AddonManager::check_online()
196 {
197   std::string addoninfos = m_downloader.download(m_repository_url);
198   m_repository_addons = parse_addon_infos(addoninfos);
199   m_has_been_updated = true;
200 }
201
202 void
203 AddonManager::install_addon(const AddonId& addon_id)
204 {
205   { // remove addon if it already exists
206     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
207                            [&addon_id](const std::unique_ptr<Addon>& addon)
208                            {
209                              return addon->get_id() == addon_id;
210                            });
211     if (it != m_installed_addons.end())
212     {
213       log_debug << "reinstalling addon " << addon_id << std::endl;
214       if ((*it)->is_enabled())
215       {
216         disable_addon((*it)->get_id());
217       }
218       m_installed_addons.erase(it);
219     }
220     else
221     {
222       log_debug << "installing addon " << addon_id << std::endl;
223     }
224   }
225
226   Addon& repository_addon = get_repository_addon(addon_id);
227
228   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
229
230   m_downloader.download(repository_addon.get_url(), install_filename);
231
232   MD5 md5 = md5_from_file(install_filename);
233   if (repository_addon.get_md5() != md5.hex_digest())
234   {
235     if (PHYSFS_delete(install_filename.c_str()) == 0)
236     {
237       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
238     }
239
240     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
241   }
242   else
243   {
244     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
245     if (!realdir)
246     {
247       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
248     }
249     else
250     {
251       add_installed_archive(install_filename, md5.hex_digest());
252     }
253   }
254 }
255
256 void
257 AddonManager::uninstall_addon(const AddonId& addon_id)
258 {
259   log_debug << "uninstalling addon " << addon_id << std::endl;
260   Addon& addon = get_installed_addon(addon_id);
261   if (addon.is_enabled())
262   {
263     disable_addon(addon_id);
264   }
265   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
266   PHYSFS_delete(addon.get_install_filename().c_str());
267   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
268                                           [&addon](const std::unique_ptr<Addon>& rhs)
269                                           {
270                                             return addon.get_id() == rhs->get_id();
271                                           }),
272                            m_installed_addons.end());
273 }
274
275 void
276 AddonManager::enable_addon(const AddonId& addon_id)
277 {
278   log_debug << "enabling addon " << addon_id << std::endl;
279   Addon& addon = get_installed_addon(addon_id);
280   if (addon.is_enabled())
281   {
282     log_warning << "Tried enabling already enabled Add-on" << std::endl;
283   }
284   else
285   {
286     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
287     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
288     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
289     {
290       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
291                   << PHYSFS_getLastError() << std::endl;
292     }
293     else
294     {
295       addon.set_enabled(true);
296     }
297   }
298 }
299
300 void
301 AddonManager::disable_addon(const AddonId& addon_id)
302 {
303   log_debug << "disabling addon " << addon_id << std::endl;
304   Addon& addon = get_installed_addon(addon_id);
305   if (!addon.is_enabled())
306   {
307     log_warning << "Tried disabling already disabled Add-On" << std::endl;
308   }
309   else
310   {
311     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
312     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
313     {
314       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
315                   << PHYSFS_getLastError() << std::endl;
316     }
317     else
318     {
319       addon.set_enabled(false);
320     }
321   }
322 }
323
324 std::vector<std::string>
325 AddonManager::scan_for_archives() const
326 {
327   std::vector<std::string> archives;
328
329   // Search for archives and add them to the search path
330   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
331     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
332        PHYSFS_freeList);
333   for(char** i = rc.get(); *i != 0; ++i)
334   {
335     if (has_suffix(*i, ".zip"))
336     {
337       std::string archive = FileSystem::join(m_addon_directory, *i);
338       if (PHYSFS_exists(archive.c_str()))
339       {
340         archives.push_back(archive);
341       }
342     }
343   }
344
345   return archives;
346 }
347
348 std::string
349 AddonManager::scan_for_info(const std::string& archive_os_path) const
350 {
351   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
352     rc2(PHYSFS_enumerateFiles("/"),
353         PHYSFS_freeList);
354   for(char** j = rc2.get(); *j != 0; ++j)
355   {
356     if (has_suffix(*j, ".nfo"))
357     {
358       std::string nfo_filename = FileSystem::join("/", *j);
359
360       // make sure it's in the current archive_os_path
361       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
362       if (!realdir)
363       {
364         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
365       }
366       else
367       {
368         if (realdir == archive_os_path)
369         {
370           return nfo_filename;
371         }
372       }
373     }
374   }
375
376   return std::string();
377 }
378
379 void
380 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
381 {
382   const char* realdir = PHYSFS_getRealDir(archive.c_str());
383   if (!realdir)
384   {
385     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
386                 << PHYSFS_getLastError() << std::endl;
387   }
388   else
389   {
390     std::string os_path = FileSystem::join(realdir, archive);
391
392     PHYSFS_addToSearchPath(os_path.c_str(), 0);
393
394     std::string nfo_filename = scan_for_info(os_path);
395
396     if (nfo_filename.empty())
397     {
398       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
399     }
400     else
401     {
402       try
403       {
404         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
405         addon->set_install_filename(os_path, md5);
406         m_installed_addons.push_back(std::move(addon));
407       }
408       catch (const std::runtime_error& e)
409       {
410         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
411       }
412     }
413
414     PHYSFS_removeFromSearchPath(os_path.c_str());
415   }
416 }
417
418 void
419 AddonManager::add_installed_addons()
420 {
421   auto archives = scan_for_archives();
422
423   for(auto archive : archives)
424   {
425     MD5 md5 = md5_from_file(archive);
426     add_installed_archive(archive, md5.hex_digest());
427   }
428 }
429
430 AddonManager::AddonList
431 AddonManager::parse_addon_infos(const std::string& addoninfos) const
432 {
433   AddonList m_addons;
434
435   try
436   {
437     lisp::Parser parser;
438     std::stringstream addoninfos_stream(addoninfos);
439     const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
440     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
441     if(!addons_lisp)
442     {
443       throw std::runtime_error("Downloaded file is not an Add-on list");
444     }
445     else
446     {
447       lisp::ListIterator iter(addons_lisp);
448       while(iter.next())
449       {
450         const std::string& token = iter.item();
451         if(token != "supertux-addoninfo")
452         {
453           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
454         }
455         else
456         {
457           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
458           m_addons.push_back(std::move(addon));
459         }
460       }
461
462       return m_addons;
463     }
464   }
465   catch(const std::exception& e)
466   {
467     std::stringstream msg;
468     msg << "Problem when reading Add-on list: " << e.what();
469     throw std::runtime_error(msg.str());
470   }
471
472   return m_addons;
473 }
474
475 /* EOF */