Save and parse repository index when AddonManager loads
[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   if (!file)
54   {
55     std::ostringstream out;
56     out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
57     throw std::runtime_error(out.str());
58   }
59   else
60   {
61     while (true)
62     {
63       PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
64       if (len <= 0) break;
65       md5.update(buffer, len);
66     }
67     PHYSFS_close(file);
68
69     return md5;
70   }
71 }
72
73 bool has_suffix(const std::string& str, const std::string& suffix)
74 {
75   if (str.length() >= suffix.length())
76     return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
77   else
78     return false;
79 }
80
81 } // namespace
82
83 AddonManager::AddonManager(const std::string& addon_directory,
84                            std::vector<Config::Addon>& addon_config) :
85   m_downloader(),
86   m_addon_directory(addon_directory),
87   m_repository_url("http://addons.supertux.googlecode.com/git/index-0_4_0.nfo"),
88   m_addon_config(addon_config),
89   m_installed_addons(),
90   m_repository_addons(),
91   m_has_been_updated(false),
92   m_transfer_status()
93 {
94   PHYSFS_mkdir(m_addon_directory.c_str());
95
96   add_installed_addons();
97
98   // FIXME: We should also restore the order here
99   for(auto& addon : m_addon_config)
100   {
101     if (addon.enabled)
102     {
103       try
104       {
105         enable_addon(addon.id);
106       }
107       catch(const std::exception& err)
108       {
109         log_warning << "failed to enable addon from config: " << err.what() << std::endl;
110       }
111     }
112   }
113
114   try
115   {
116     m_repository_addons = parse_addon_infos("/addons/repository.nfo");
117   }
118   catch(const std::exception& err)
119   {
120     log_warning << "parsing repository.nfo failed: " << err.what() << std::endl;
121   }
122 }
123
124 AddonManager::~AddonManager()
125 {
126   // sync enabled/disabled addons into the config for saving
127   m_addon_config.clear();
128   for(auto& addon : m_installed_addons)
129   {
130     m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
131   }
132 }
133
134 Addon&
135 AddonManager::get_repository_addon(const AddonId& id)
136 {
137   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
138                          [&id](const std::unique_ptr<Addon>& addon)
139                          {
140                            return addon->get_id() == id;
141                          });
142
143   if (it != m_repository_addons.end())
144   {
145     return **it;
146   }
147   else
148   {
149     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
150   }
151 }
152
153 Addon&
154 AddonManager::get_installed_addon(const AddonId& id)
155 {
156   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
157                          [&id](const std::unique_ptr<Addon>& addon)
158                          {
159                            return addon->get_id() == id;
160                          });
161
162   if (it != m_installed_addons.end())
163   {
164     return **it;
165   }
166   else
167   {
168     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
169   }
170 }
171
172 std::vector<AddonId>
173 AddonManager::get_repository_addons() const
174 {
175   std::vector<AddonId> results;
176   results.reserve(m_repository_addons.size());
177   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
178                  std::back_inserter(results),
179                  [](const std::unique_ptr<Addon>& addon)
180                  {
181                    return addon->get_id();
182                  });
183   return results;
184 }
185
186
187 std::vector<AddonId>
188 AddonManager::get_installed_addons() const
189 {
190   std::vector<AddonId> results;
191   results.reserve(m_installed_addons.size());
192   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
193                  std::back_inserter(results),
194                  [](const std::unique_ptr<Addon>& addon)
195                  {
196                    return addon->get_id();
197                  });
198   return results;
199 }
200
201 bool
202 AddonManager::has_online_support() const
203 {
204   return true;
205 }
206
207 bool
208 AddonManager::has_been_updated() const
209 {
210   return m_has_been_updated;
211 }
212
213 TransferStatusPtr
214 AddonManager::request_check_online()
215 {
216   if (m_transfer_status)
217   {
218     throw std::runtime_error("only async request can be made to AddonManager at a time");
219   }
220   else
221   {
222     m_transfer_status = m_downloader.request_download(m_repository_url, "/addons/repository.nfo");
223
224     m_transfer_status->then([this]{
225         m_repository_addons = parse_addon_infos("/addons/repository.nfo");
226         m_has_been_updated = true;
227
228         m_transfer_status = {};
229       });
230
231     return m_transfer_status;
232   }
233 }
234
235 void
236 AddonManager::check_online()
237 {
238   m_downloader.download(m_repository_url, "/addons/repository.nfo");
239   m_repository_addons = parse_addon_infos("/addons/repository.nfo");
240   m_has_been_updated = true;
241 }
242
243 TransferStatusPtr
244 AddonManager::request_install_addon(const AddonId& addon_id)
245 {
246   if (m_transfer_status)
247   {
248     throw std::runtime_error("only one addon install request allowed at a time");
249   }
250   else
251   {
252     { // remove addon if it already exists
253       auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
254                              [&addon_id](const std::unique_ptr<Addon>& addon)
255                              {
256                                return addon->get_id() == addon_id;
257                              });
258       if (it != m_installed_addons.end())
259       {
260         log_debug << "reinstalling addon " << addon_id << std::endl;
261         if ((*it)->is_enabled())
262         {
263           disable_addon((*it)->get_id());
264         }
265         m_installed_addons.erase(it);
266       }
267       else
268       {
269         log_debug << "installing addon " << addon_id << std::endl;
270       }
271     }
272
273     Addon& addon = get_repository_addon(addon_id);
274
275     std::string install_filename = FileSystem::join(m_addon_directory, addon.get_filename());
276
277     m_transfer_status = m_downloader.request_download(addon.get_url(), install_filename);
278
279     m_transfer_status->then(
280       [this, install_filename, addon_id]
281       {
282         // complete the addon install
283         Addon& repository_addon = get_repository_addon(addon_id);
284
285         MD5 md5 = md5_from_file(install_filename);
286         if (repository_addon.get_md5() != md5.hex_digest())
287         {
288           if (PHYSFS_delete(install_filename.c_str()) == 0)
289           {
290             log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
291           }
292
293           throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
294         }
295         else
296         {
297           const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
298           if (!realdir)
299           {
300             throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
301           }
302           else
303           {
304             add_installed_archive(install_filename, md5.hex_digest());
305           }
306         }
307
308         m_transfer_status = {};
309       });
310
311     return m_transfer_status;
312   }
313 }
314
315 void
316 AddonManager::install_addon(const AddonId& addon_id)
317 {
318   { // remove addon if it already exists
319     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
320                            [&addon_id](const std::unique_ptr<Addon>& addon)
321                            {
322                              return addon->get_id() == addon_id;
323                            });
324     if (it != m_installed_addons.end())
325     {
326       log_debug << "reinstalling addon " << addon_id << std::endl;
327       if ((*it)->is_enabled())
328       {
329         disable_addon((*it)->get_id());
330       }
331       m_installed_addons.erase(it);
332     }
333     else
334     {
335       log_debug << "installing addon " << addon_id << std::endl;
336     }
337   }
338
339   Addon& repository_addon = get_repository_addon(addon_id);
340
341   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
342
343   m_downloader.download(repository_addon.get_url(), install_filename);
344
345   MD5 md5 = md5_from_file(install_filename);
346   if (repository_addon.get_md5() != md5.hex_digest())
347   {
348     if (PHYSFS_delete(install_filename.c_str()) == 0)
349     {
350       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
351     }
352
353     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
354   }
355   else
356   {
357     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
358     if (!realdir)
359     {
360       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
361     }
362     else
363     {
364       add_installed_archive(install_filename, md5.hex_digest());
365     }
366   }
367 }
368
369 void
370 AddonManager::uninstall_addon(const AddonId& addon_id)
371 {
372   log_debug << "uninstalling addon " << addon_id << std::endl;
373   Addon& addon = get_installed_addon(addon_id);
374   if (addon.is_enabled())
375   {
376     disable_addon(addon_id);
377   }
378   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
379   PHYSFS_delete(addon.get_install_filename().c_str());
380   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
381                                           [&addon](const std::unique_ptr<Addon>& rhs)
382                                           {
383                                             return addon.get_id() == rhs->get_id();
384                                           }),
385                            m_installed_addons.end());
386 }
387
388 void
389 AddonManager::enable_addon(const AddonId& addon_id)
390 {
391   log_debug << "enabling addon " << addon_id << std::endl;
392   Addon& addon = get_installed_addon(addon_id);
393   if (addon.is_enabled())
394   {
395     log_warning << "Tried enabling already enabled Add-on" << std::endl;
396   }
397   else
398   {
399     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
400     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
401     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
402     {
403       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
404                   << PHYSFS_getLastError() << std::endl;
405     }
406     else
407     {
408       addon.set_enabled(true);
409     }
410   }
411 }
412
413 void
414 AddonManager::disable_addon(const AddonId& addon_id)
415 {
416   log_debug << "disabling addon " << addon_id << std::endl;
417   Addon& addon = get_installed_addon(addon_id);
418   if (!addon.is_enabled())
419   {
420     log_warning << "Tried disabling already disabled Add-On" << std::endl;
421   }
422   else
423   {
424     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
425     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
426     {
427       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
428                   << PHYSFS_getLastError() << std::endl;
429     }
430     else
431     {
432       addon.set_enabled(false);
433     }
434   }
435 }
436
437 std::vector<std::string>
438 AddonManager::scan_for_archives() const
439 {
440   std::vector<std::string> archives;
441
442   // Search for archives and add them to the search path
443   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
444     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
445        PHYSFS_freeList);
446   for(char** i = rc.get(); *i != 0; ++i)
447   {
448     if (has_suffix(*i, ".zip"))
449     {
450       std::string archive = FileSystem::join(m_addon_directory, *i);
451       if (PHYSFS_exists(archive.c_str()))
452       {
453         archives.push_back(archive);
454       }
455     }
456   }
457
458   return archives;
459 }
460
461 std::string
462 AddonManager::scan_for_info(const std::string& archive_os_path) const
463 {
464   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
465     rc2(PHYSFS_enumerateFiles("/"),
466         PHYSFS_freeList);
467   for(char** j = rc2.get(); *j != 0; ++j)
468   {
469     if (has_suffix(*j, ".nfo"))
470     {
471       std::string nfo_filename = FileSystem::join("/", *j);
472
473       // make sure it's in the current archive_os_path
474       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
475       if (!realdir)
476       {
477         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
478       }
479       else
480       {
481         if (realdir == archive_os_path)
482         {
483           return nfo_filename;
484         }
485       }
486     }
487   }
488
489   return std::string();
490 }
491
492 void
493 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
494 {
495   const char* realdir = PHYSFS_getRealDir(archive.c_str());
496   if (!realdir)
497   {
498     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
499                 << PHYSFS_getLastError() << std::endl;
500   }
501   else
502   {
503     std::string os_path = FileSystem::join(realdir, archive);
504
505     PHYSFS_addToSearchPath(os_path.c_str(), 0);
506
507     std::string nfo_filename = scan_for_info(os_path);
508
509     if (nfo_filename.empty())
510     {
511       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
512     }
513     else
514     {
515       try
516       {
517         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
518         addon->set_install_filename(os_path, md5);
519         m_installed_addons.push_back(std::move(addon));
520       }
521       catch (const std::runtime_error& e)
522       {
523         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
524       }
525     }
526
527     PHYSFS_removeFromSearchPath(os_path.c_str());
528   }
529 }
530
531 void
532 AddonManager::add_installed_addons()
533 {
534   auto archives = scan_for_archives();
535
536   for(auto archive : archives)
537   {
538     MD5 md5 = md5_from_file(archive);
539     add_installed_archive(archive, md5.hex_digest());
540   }
541 }
542
543 AddonManager::AddonList
544 AddonManager::parse_addon_infos(const std::string& filename) const
545 {
546   AddonList m_addons;
547
548   try
549   {
550     lisp::Parser parser;
551     const lisp::Lisp* root = parser.parse(filename);
552     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
553     if(!addons_lisp)
554     {
555       throw std::runtime_error("Downloaded file is not an Add-on list");
556     }
557     else
558     {
559       lisp::ListIterator iter(addons_lisp);
560       while(iter.next())
561       {
562         const std::string& token = iter.item();
563         if(token != "supertux-addoninfo")
564         {
565           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
566         }
567         else
568         {
569           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
570           m_addons.push_back(std::move(addon));
571         }
572       }
573
574       return m_addons;
575     }
576   }
577   catch(const std::exception& e)
578   {
579     std::stringstream msg;
580     msg << "Problem when reading Add-on list: " << e.what();
581     throw std::runtime_error(msg.str());
582   }
583
584   return m_addons;
585 }
586
587 void
588 AddonManager::update()
589 {
590   m_downloader.update();
591 }
592
593 void
594 AddonManager::abort_install()
595 {
596   log_info << "addon install aborted" << std::endl;
597
598   m_downloader.abort(m_transfer_status->id);
599
600   m_transfer_status = {};
601 }
602
603 /* EOF */