Make sure that duplicate Addon install requests don't lead to duplicates in the insta...
[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<std::string>& ignored_addon_ids) :
76   m_downloader(),
77   m_addon_directory(addon_directory),
78   //m_repository_url("http://addons.supertux.googlecode.com/git/index-0_3_5.nfo"),
79   m_repository_url("http://localhost:8000/index-0_4_0.nfo"),
80   m_ignored_addon_ids(ignored_addon_ids),
81   m_installed_addons(),
82   m_repository_addons()
83 {
84   PHYSFS_mkdir(m_addon_directory.c_str());
85
86   add_installed_addons();
87   for(auto& addon : m_installed_addons)
88   {
89     if (std::find(m_ignored_addon_ids.begin(), m_ignored_addon_ids.end(),
90                   addon->get_id()) != m_ignored_addon_ids.end())
91     {
92       enable_addon(addon->get_id());
93     }
94   }
95 }
96
97 AddonManager::~AddonManager()
98 {
99 }
100
101 Addon&
102 AddonManager::get_repository_addon(const AddonId& id)
103 {
104   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
105                          [&id](const std::unique_ptr<Addon>& addon)
106                          {
107                            return addon->get_id() == id;
108                          });
109
110   if (it != m_repository_addons.end())
111   {
112     return **it;
113   }
114   else
115   {
116     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
117   }
118 }
119
120 Addon&
121 AddonManager::get_installed_addon(const AddonId& id)
122 {
123   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
124                          [&id](const std::unique_ptr<Addon>& addon)
125                          {
126                            return addon->get_id() == id;
127                          });
128
129   if (it != m_installed_addons.end())
130   {
131     return **it;
132   }
133   else
134   {
135     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
136   }
137 }
138
139 std::vector<AddonId>
140 AddonManager::get_repository_addons() const
141 {
142   std::vector<AddonId> results;
143   results.reserve(m_repository_addons.size());
144   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
145                  std::back_inserter(results),
146                  [](const std::unique_ptr<Addon>& addon)
147                  {
148                    return addon->get_id();
149                  });
150   return results;
151 }
152
153
154 std::vector<AddonId>
155 AddonManager::get_installed_addons() const
156 {
157   std::vector<AddonId> results;
158   results.reserve(m_installed_addons.size());
159   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
160                  std::back_inserter(results),
161                  [](const std::unique_ptr<Addon>& addon)
162                  {
163                    return addon->get_id();
164                  });
165   return results;
166 }
167
168 bool
169 AddonManager::has_online_support() const
170 {
171   return true;
172 }
173
174 void
175 AddonManager::check_online()
176 {
177   std::string addoninfos = m_downloader.download(m_repository_url);
178   m_repository_addons = parse_addon_infos(addoninfos);
179 }
180
181 void
182 AddonManager::install_addon(const AddonId& addon_id)
183 {
184   { // remove addon if it already exists
185     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
186                            [&addon_id](const std::unique_ptr<Addon>& addon)
187                            {
188                              return addon->get_id() == addon_id;
189                            });
190     if (it != m_installed_addons.end())
191     {
192       log_debug << "reinstalling addon " << addon_id << std::endl;
193       if ((*it)->is_enabled())
194       {
195         disable_addon((*it)->get_id());
196       }
197       m_installed_addons.erase(it);
198     }
199     else
200     {
201       log_debug << "installing addon " << addon_id << std::endl;
202     }
203   }
204
205   Addon& repository_addon = get_repository_addon(addon_id);
206
207   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
208
209   m_downloader.download(repository_addon.get_http_url(), install_filename);
210
211   MD5 md5 = md5_from_file(install_filename);
212   if (repository_addon.get_md5() != md5.hex_digest())
213   {
214     if (PHYSFS_delete(install_filename.c_str()) == 0)
215     {
216       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
217     }
218
219     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
220   }
221   else
222   {
223     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
224     if (!realdir)
225     {
226       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
227     }
228     else
229     {
230       add_installed_archive(install_filename);
231     }
232   }
233 }
234
235 void
236 AddonManager::uninstall_addon(const AddonId& addon_id)
237 {
238   log_debug << "uninstalling addon " << addon_id << std::endl;
239   Addon& addon = get_installed_addon(addon_id);
240   if (addon.is_enabled())
241   {
242     disable_addon(addon_id);
243   }
244   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
245   PHYSFS_delete(addon.get_install_filename().c_str());
246   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
247                                           [&addon](const std::unique_ptr<Addon>& rhs)
248                                           {
249                                             return addon.get_id() == rhs->get_id();
250                                           }),
251                            m_installed_addons.end());
252 }
253
254 void
255 AddonManager::enable_addon(const AddonId& addon_id)
256 {
257   log_debug << "enabling addon " << addon_id << std::endl;
258   Addon& addon = get_installed_addon(addon_id);
259   if (addon.is_enabled())
260   {
261     log_warning << "Tried enabling already enabled Add-on" << std::endl;
262   }
263   else
264   {
265     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
266     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
267     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
268     {
269       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
270                   << PHYSFS_getLastError() << std::endl;
271     }
272     else
273     {
274       addon.set_enabled(true);
275     }
276   }
277 }
278
279 void
280 AddonManager::disable_addon(const AddonId& addon_id)
281 {
282   log_debug << "disabling addon " << addon_id << std::endl;
283   Addon& addon = get_installed_addon(addon_id);
284   if (!addon.is_enabled())
285   {
286     log_warning << "Tried disabling already disabled Add-On" << std::endl;
287   }
288   else
289   {
290     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
291     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
292     {
293       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
294                   << PHYSFS_getLastError() << std::endl;
295     }
296     else
297     {
298       addon.set_enabled(false);
299     }
300   }
301 }
302
303 std::vector<std::string>
304 AddonManager::scan_for_archives() const
305 {
306   std::vector<std::string> archives;
307
308   // Search for archives and add them to the search path
309   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
310     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
311        PHYSFS_freeList);
312   for(char** i = rc.get(); *i != 0; ++i)
313   {
314     if (has_suffix(*i, ".zip"))
315     {
316       std::string archive = FileSystem::join(m_addon_directory, *i);
317       if (PHYSFS_exists(archive.c_str()))
318       {
319         archives.push_back(archive);
320       }
321     }
322   }
323
324   return archives;
325 }
326
327 std::string
328 AddonManager::scan_for_info(const std::string& archive_os_path) const
329 {
330   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
331     rc2(PHYSFS_enumerateFiles("/"),
332         PHYSFS_freeList);
333   for(char** j = rc2.get(); *j != 0; ++j)
334   {
335     log_debug << "enumerating: " << std::string(*j) << std::endl;
336     if (has_suffix(*j, ".nfo"))
337     {
338       std::string nfo_filename = FileSystem::join("/", *j);
339
340       // make sure it's in the current archive_os_path
341       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
342       if (!realdir)
343       {
344         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
345       }
346       else
347       {
348         log_debug << "compare: " << realdir << " " << archive_os_path << std::endl;
349         if (realdir == archive_os_path)
350         {
351           return nfo_filename;
352         }
353       }
354     }
355   }
356
357   return std::string();
358 }
359
360 void
361 AddonManager::add_installed_archive(const std::string& archive)
362 {
363   const char* realdir = PHYSFS_getRealDir(archive.c_str());
364   if (!realdir)
365   {
366     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
367                 << PHYSFS_getLastError() << std::endl;
368   }
369   else
370   {
371     std::string os_path = FileSystem::join(realdir, archive);
372
373     PHYSFS_addToSearchPath(os_path.c_str(), 0);
374
375     std::string nfo_filename = scan_for_info(os_path);
376
377     if (nfo_filename.empty())
378     {
379       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
380     }
381     else
382     {
383       try
384       {
385         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
386         addon->set_install_filename(os_path);
387         m_installed_addons.push_back(std::move(addon));
388       }
389       catch (const std::runtime_error& e)
390       {
391         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
392       }
393     }
394
395     PHYSFS_removeFromSearchPath(os_path.c_str());
396   }
397 }
398
399 void
400 AddonManager::add_installed_addons()
401 {
402   auto archives = scan_for_archives();
403
404   for(auto archive : archives)
405   {
406     add_installed_archive(archive);
407   }
408 }
409
410 AddonManager::AddonList
411 AddonManager::parse_addon_infos(const std::string& addoninfos) const
412 {
413   AddonList m_addons;
414
415   try
416   {
417     lisp::Parser parser;
418     std::stringstream addoninfos_stream(addoninfos);
419     const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
420     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
421     if(!addons_lisp)
422     {
423       throw std::runtime_error("Downloaded file is not an Add-on list");
424     }
425     else
426     {
427       lisp::ListIterator iter(addons_lisp);
428       while(iter.next())
429       {
430         const std::string& token = iter.item();
431         if(token != "supertux-addoninfo")
432         {
433           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
434         }
435         else
436         {
437           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
438           m_addons.push_back(std::move(addon));
439         }
440       }
441
442       return m_addons;
443     }
444   }
445   catch(const std::exception& e)
446   {
447     std::stringstream msg;
448     msg << "Problem when reading Add-on list: " << e.what();
449     throw std::runtime_error(msg.str());
450   }
451
452   return m_addons;
453 }
454
455 /* EOF */