1 // Aseprite
2 // Copyright (C) 2017-2018  David Capello
3 //
4 // This program is distributed under the terms of
5 // the End-User License Agreement for Aseprite.
6 
7 #ifdef HAVE_CONFIG_H
8 #include "config.h"
9 #endif
10 
11 #include "app/extensions.h"
12 
13 #include "app/ini_file.h"
14 #include "app/load_matrix.h"
15 #include "app/pref/preferences.h"
16 #include "app/resource_finder.h"
17 #include "base/exception.h"
18 #include "base/file_handle.h"
19 #include "base/fs.h"
20 #include "base/fstream_path.h"
21 #include "base/unique_ptr.h"
22 #include "render/dithering_matrix.h"
23 
24 #include "archive.h"
25 #include "archive_entry.h"
26 #include "json11.hpp"
27 
28 #include <fstream>
29 #include <queue>
30 #include <sstream>
31 #include <string>
32 
33 namespace app {
34 
35 namespace {
36 
37 const char* kPackageJson = "package.json";
38 const char* kInfoJson = "__info.json";
39 const char* kAsepriteDefaultThemeExtensionName = "aseprite-theme";
40 
41 class ReadArchive {
42 public:
ReadArchive(const std::string & filename)43   ReadArchive(const std::string& filename)
44     : m_arch(nullptr), m_open(false) {
45     m_arch = archive_read_new();
46     archive_read_support_format_zip(m_arch);
47 
48     m_file = base::open_file(filename, "rb");
49     if (!m_file)
50       throw base::Exception("Error loading file %s",
51                             filename.c_str());
52 
53     int err;
54     if ((err = archive_read_open_FILE(m_arch, m_file.get())))
55       throw base::Exception("Error uncompressing extension\n%s (%d)",
56                             archive_error_string(m_arch), err);
57 
58     m_open = true;
59   }
60 
~ReadArchive()61   ~ReadArchive() {
62     if (m_arch) {
63       if (m_open)
64         archive_read_close(m_arch);
65       archive_read_free(m_arch);
66     }
67   }
68 
readEntry()69   archive_entry* readEntry() {
70     archive_entry* entry;
71     int err = archive_read_next_header(m_arch, &entry);
72 
73     if (err == ARCHIVE_EOF)
74       return nullptr;
75 
76     if (err != ARCHIVE_OK)
77       throw base::Exception("Error uncompressing extension\n%s",
78                             archive_error_string(m_arch));
79 
80     return entry;
81   }
82 
copyDataTo(archive * out)83   int copyDataTo(archive* out) {
84     const void* buf;
85     size_t size;
86     int64_t offset;
87     for (;;) {
88       int err = archive_read_data_block(m_arch, &buf, &size, &offset);
89       if (err == ARCHIVE_EOF)
90         break;
91       if (err != ARCHIVE_OK)
92         return err;
93 
94       err = archive_write_data_block(out, buf, size, offset);
95       if (err != ARCHIVE_OK) {
96         throw base::Exception("Error writing data blocks\n%s (%d)",
97                               archive_error_string(out), err);
98         return err;
99       }
100     }
101     return ARCHIVE_OK;
102   }
103 
copyDataTo(std::ostream & dst)104   int copyDataTo(std::ostream& dst) {
105     const void* buf;
106     size_t size;
107     int64_t offset;
108     for (;;) {
109       int err = archive_read_data_block(m_arch, &buf, &size, &offset);
110       if (err == ARCHIVE_EOF)
111         break;
112       if (err != ARCHIVE_OK)
113         return err;
114       dst.write((const char*)buf, size);
115     }
116     return ARCHIVE_OK;
117   }
118 
119 private:
120   base::FileHandle m_file;
121   archive* m_arch;
122   bool m_open;
123 };
124 
125 class WriteArchive {
126 public:
WriteArchive()127   WriteArchive()
128    : m_arch(nullptr)
129    , m_open(false) {
130     m_arch = archive_write_disk_new();
131     m_open = true;
132   }
133 
~WriteArchive()134   ~WriteArchive() {
135     if (m_arch) {
136       if (m_open)
137         archive_write_close(m_arch);
138       archive_write_free(m_arch);
139     }
140   }
141 
writeEntry(ReadArchive & in,archive_entry * entry)142   void writeEntry(ReadArchive& in, archive_entry* entry) {
143     int err = archive_write_header(m_arch, entry);
144     if (err != ARCHIVE_OK)
145       throw base::Exception("Error writing file into disk\n%s (%d)",
146                             archive_error_string(m_arch), err);
147 
148     in.copyDataTo(m_arch);
149     err = archive_write_finish_entry(m_arch);
150     if (err != ARCHIVE_OK)
151       throw base::Exception("Error saving the last part of a file entry in disk\n%s (%d)",
152                             archive_error_string(m_arch), err);
153   }
154 
155 private:
156   archive* m_arch;
157   bool m_open;
158 };
159 
read_json_file(const std::string & path,json11::Json & json)160 void read_json_file(const std::string& path, json11::Json& json)
161 {
162   std::string jsonText, line;
163   std::ifstream in(FSTREAM_PATH(path), std::ifstream::binary);
164   while (std::getline(in, line)) {
165     jsonText += line;
166     jsonText.push_back('\n');
167   }
168   std::string err;
169   json = json11::Json::parse(jsonText, err);
170   if (!err.empty())
171     throw base::Exception("Error parsing JSON file: %s\n",
172                           err.c_str());
173 }
174 
write_json_file(const std::string & path,const json11::Json & json)175 void write_json_file(const std::string& path, const json11::Json& json)
176 {
177   std::string text;
178   json.dump(text);
179   std::ofstream out(FSTREAM_PATH(path), std::ifstream::binary);
180   out.write(text.c_str(), text.size());
181 }
182 
183 } // anonymous namespace
184 
matrix() const185 const render::DitheringMatrix& Extension::DitheringMatrixInfo::matrix() const
186 {
187   if (!m_matrix) {
188     m_matrix = new render::DitheringMatrix;
189     load_dithering_matrix_from_sprite(m_path, *m_matrix);
190   }
191   return *m_matrix;
192 }
193 
destroyMatrix()194 void Extension::DitheringMatrixInfo::destroyMatrix()
195 {
196   if (m_matrix)
197     delete m_matrix;
198 }
199 
Extension(const std::string & path,const std::string & name,const std::string & version,const std::string & displayName,const bool isEnabled,const bool isBuiltinExtension)200 Extension::Extension(const std::string& path,
201                      const std::string& name,
202                      const std::string& version,
203                      const std::string& displayName,
204                      const bool isEnabled,
205                      const bool isBuiltinExtension)
206   : m_path(path)
207   , m_name(name)
208   , m_version(version)
209   , m_displayName(displayName)
210   , m_isEnabled(isEnabled)
211   , m_isInstalled(true)
212   , m_isBuiltinExtension(isBuiltinExtension)
213 {
214 }
215 
~Extension()216 Extension::~Extension()
217 {
218   // Delete all matrices
219   for (auto& it : m_ditheringMatrices)
220     it.second.destroyMatrix();
221 }
222 
addLanguage(const std::string & id,const std::string & path)223 void Extension::addLanguage(const std::string& id, const std::string& path)
224 {
225   m_languages[id] = path;
226 }
227 
addTheme(const std::string & id,const std::string & path)228 void Extension::addTheme(const std::string& id, const std::string& path)
229 {
230   m_themes[id] = path;
231 }
232 
addPalette(const std::string & id,const std::string & path)233 void Extension::addPalette(const std::string& id, const std::string& path)
234 {
235   m_palettes[id] = path;
236 }
237 
addDitheringMatrix(const std::string & id,const std::string & path,const std::string & name)238 void Extension::addDitheringMatrix(const std::string& id,
239                                    const std::string& path,
240                                    const std::string& name)
241 {
242   DitheringMatrixInfo info(path, name);
243   m_ditheringMatrices[id] = info;
244 }
245 
canBeDisabled() const246 bool Extension::canBeDisabled() const
247 {
248   return (m_isEnabled &&
249           //!isCurrentTheme() &&
250           !isDefaultTheme()); // Default theme cannot be disabled or uninstalled
251 }
252 
canBeUninstalled() const253 bool Extension::canBeUninstalled() const
254 {
255   return (!m_isBuiltinExtension &&
256           // We can uninstall the current theme (e.g. to upgrade it)
257           //!isCurrentTheme() &&
258           !isDefaultTheme());
259 }
260 
enable(const bool state)261 void Extension::enable(const bool state)
262 {
263   // Do nothing
264   if (m_isEnabled == state)
265     return;
266 
267   set_config_bool("extensions", m_name.c_str(), state);
268   flush_config_file();
269 
270   m_isEnabled = state;
271 }
272 
uninstall()273 void Extension::uninstall()
274 {
275   if (!m_isInstalled)
276     return;
277 
278   ASSERT(canBeUninstalled());
279   if (!canBeUninstalled())
280     return;
281 
282   TRACE("EXT: Uninstall extension '%s' from '%s'...\n",
283         m_name.c_str(), m_path.c_str());
284 
285   // Remove all files inside the extension path
286   uninstallFiles(m_path);
287   ASSERT(!base::is_directory(m_path));
288 
289   m_isEnabled = false;
290   m_isInstalled = false;
291 }
292 
uninstallFiles(const std::string & path)293 void Extension::uninstallFiles(const std::string& path)
294 {
295 #if 1 // Read the list of files to be uninstalled from __info.json file
296 
297   std::string infoFn = base::join_path(path, kInfoJson);
298   if (!base::is_file(infoFn))
299     throw base::Exception("Cannot remove extension, '%s' file doesn't exist",
300                           infoFn.c_str());
301 
302   json11::Json json;
303   read_json_file(infoFn, json);
304 
305   base::paths installedDirs;
306 
307   for (const auto& value : json["installedFiles"].array_items()) {
308     std::string fn = base::join_path(path, value.string_value());
309     if (base::is_file(fn)) {
310       TRACE("EXT: Deleting file '%s'\n", fn.c_str());
311       base::delete_file(fn);
312     }
313     else if (base::is_directory(fn)) {
314       installedDirs.push_back(fn);
315     }
316   }
317 
318   std::sort(installedDirs.begin(),
319             installedDirs.end(),
320             [](const std::string& a,
321                const std::string& b) {
322               return b.size() < a.size();
323             });
324 
325   for (const auto& dir : installedDirs) {
326     TRACE("EXT: Deleting directory '%s'\n", dir.c_str());
327     base::remove_directory(dir);
328   }
329 
330   TRACE("EXT: Deleting file '%s'\n", infoFn.c_str());
331   base::delete_file(infoFn);
332 
333   TRACE("EXT: Deleting extension directory '%s'\n", path.c_str());
334   base::remove_directory(path);
335 
336 #else // The following code delete the whole "path",
337       // we prefer the __info.json approach.
338 
339   for (auto& item : base::list_files(path)) {
340     std::string fn = base::join_path(path, item);
341     if (base::is_file(fn)) {
342       TRACE("EXT: Deleting file '%s'\n", fn.c_str());
343       base::delete_file(fn);
344     }
345     else if (base::is_directory(fn)) {
346       uninstallFiles(fn);
347     }
348   }
349 
350   TRACE("EXT: Deleting directory '%s'\n", path.c_str());
351   base::remove_directory(path);
352 
353 #endif
354 }
355 
isCurrentTheme() const356 bool Extension::isCurrentTheme() const
357 {
358   auto it = m_themes.find(Preferences::instance().theme.selected());
359   return (it != m_themes.end());
360 }
361 
isDefaultTheme() const362 bool Extension::isDefaultTheme() const
363 {
364   return (name() == kAsepriteDefaultThemeExtensionName);
365 }
366 
Extensions()367 Extensions::Extensions()
368 {
369   // Create and get the user extensions directory
370   {
371     ResourceFinder rf2;
372     rf2.includeUserDir("extensions/.");
373     m_userExtensionsPath = rf2.getFirstOrCreateDefault();
374     m_userExtensionsPath = base::normalize_path(m_userExtensionsPath);
375     if (!m_userExtensionsPath.empty() &&
376         m_userExtensionsPath.back() == '.') {
377       m_userExtensionsPath = base::get_file_path(m_userExtensionsPath);
378     }
379     LOG("EXT: User extensions path '%s'\n", m_userExtensionsPath.c_str());
380   }
381 
382   ResourceFinder rf;
383   rf.includeUserDir("extensions");
384   rf.includeDataDir("extensions");
385 
386   // Load extensions from data/ directory on all possible locations
387   // (installed folder and user folder)
388   while (rf.next()) {
389     auto extensionsDir = rf.filename();
390 
391     if (base::is_directory(extensionsDir)) {
392       for (auto fn : base::list_files(extensionsDir)) {
393         const auto dir = base::join_path(extensionsDir, fn);
394         if (!base::is_directory(dir))
395           continue;
396 
397         const bool isBuiltinExtension =
398           (m_userExtensionsPath != base::get_file_path(dir));
399 
400         auto fullFn = base::join_path(dir, kPackageJson);
401         fullFn = base::normalize_path(fullFn);
402 
403         LOG("EXT: Loading extension '%s'...\n", fullFn.c_str());
404         if (!base::is_file(fullFn)) {
405           LOG("EXT: File '%s' not found\n", fullFn.c_str());
406           continue;
407         }
408 
409         try {
410           loadExtension(dir, fullFn, isBuiltinExtension);
411         }
412         catch (const std::exception& ex) {
413           LOG("EXT: Error loading JSON file: %s\n",
414               ex.what());
415         }
416       }
417     }
418   }
419 }
420 
~Extensions()421 Extensions::~Extensions()
422 {
423   for (auto ext : m_extensions)
424     delete ext;
425 }
426 
languagePath(const std::string & langId)427 std::string Extensions::languagePath(const std::string& langId)
428 {
429   for (auto ext : m_extensions) {
430     if (!ext->isEnabled())      // Ignore disabled extensions
431       continue;
432 
433     auto it = ext->languages().find(langId);
434     if (it != ext->languages().end())
435       return it->second;
436   }
437   return std::string();
438 }
439 
themePath(const std::string & themeId)440 std::string Extensions::themePath(const std::string& themeId)
441 {
442   for (auto ext : m_extensions) {
443     if (!ext->isEnabled())      // Ignore disabled extensions
444       continue;
445 
446     auto it = ext->themes().find(themeId);
447     if (it != ext->themes().end())
448       return it->second;
449   }
450   return std::string();
451 }
452 
palettePath(const std::string & palId)453 std::string Extensions::palettePath(const std::string& palId)
454 {
455   for (auto ext : m_extensions) {
456     if (!ext->isEnabled())      // Ignore disabled extensions
457       continue;
458 
459     auto it = ext->palettes().find(palId);
460     if (it != ext->palettes().end())
461       return it->second;
462   }
463   return std::string();
464 }
465 
palettes() const466 ExtensionItems Extensions::palettes() const
467 {
468   ExtensionItems palettes;
469   for (auto ext : m_extensions) {
470     if (!ext->isEnabled())      // Ignore disabled themes
471       continue;
472 
473     for (auto item : ext->palettes())
474       palettes[item.first] = item.second;
475   }
476   return palettes;
477 }
478 
ditheringMatrix(const std::string & matrixId)479 const render::DitheringMatrix* Extensions::ditheringMatrix(const std::string& matrixId)
480 {
481   for (auto ext : m_extensions) {
482     if (!ext->isEnabled())      // Ignore disabled themes
483       continue;
484 
485     auto it = ext->m_ditheringMatrices.find(matrixId);
486     if (it != ext->m_ditheringMatrices.end())
487       return &it->second.matrix();
488   }
489   return nullptr;
490 }
491 
ditheringMatrices()492 std::vector<Extension::DitheringMatrixInfo> Extensions::ditheringMatrices()
493 {
494   std::vector<Extension::DitheringMatrixInfo> result;
495   for (auto ext : m_extensions) {
496     if (!ext->isEnabled())      // Ignore disabled themes
497       continue;
498 
499     for (auto it : ext->m_ditheringMatrices)
500       result.push_back(it.second);
501   }
502   return result;
503 }
504 
enableExtension(Extension * extension,const bool state)505 void Extensions::enableExtension(Extension* extension, const bool state)
506 {
507   extension->enable(state);
508   generateExtensionSignals(extension);
509 }
510 
uninstallExtension(Extension * extension)511 void Extensions::uninstallExtension(Extension* extension)
512 {
513   extension->uninstall();
514   generateExtensionSignals(extension);
515 
516   auto it = std::find(m_extensions.begin(),
517                       m_extensions.end(), extension);
518   ASSERT(it != m_extensions.end());
519   if (it != m_extensions.end())
520     m_extensions.erase(it);
521 
522   delete extension;
523 }
524 
getCompressedExtensionInfo(const std::string & zipFn)525 ExtensionInfo Extensions::getCompressedExtensionInfo(const std::string& zipFn)
526 {
527   ExtensionInfo info;
528   info.dstPath =
529     base::join_path(m_userExtensionsPath,
530                     base::get_file_title(zipFn));
531 
532   // First of all we read the package.json file inside the .zip to
533   // know 1) the extension name, 2) that the .json file can be parsed
534   // correctly, 3) the final destination directory.
535   ReadArchive in(zipFn);
536   archive_entry* entry;
537   while ((entry = in.readEntry()) != nullptr) {
538     const std::string entryFn = archive_entry_pathname(entry);
539     if (base::get_file_name(entryFn) != kPackageJson)
540       continue;
541 
542     info.commonPath = base::get_file_path(entryFn);
543     if (!info.commonPath.empty() &&
544         entryFn.size() > info.commonPath.size()) {
545       info.commonPath.push_back(entryFn[info.commonPath.size()]);
546     }
547 
548     std::stringstream out;
549     in.copyDataTo(out);
550 
551     std::string err;
552     auto json = json11::Json::parse(out.str(), err);
553     if (err.empty()) {
554       info.name = json["name"].string_value();
555       info.version = json["version"].string_value();
556       info.dstPath = base::join_path(m_userExtensionsPath, info.name);
557     }
558     break;
559   }
560   return info;
561 }
562 
installCompressedExtension(const std::string & zipFn,const ExtensionInfo & info)563 Extension* Extensions::installCompressedExtension(const std::string& zipFn,
564                                                   const ExtensionInfo& info)
565 {
566   base::paths installedFiles;
567 
568   // Uncompress zipFn in info.dstPath
569   {
570     ReadArchive in(zipFn);
571     WriteArchive out;
572 
573     archive_entry* entry;
574     while ((entry = in.readEntry()) != nullptr) {
575       // Fix the entry filename to write the file in the disk
576       std::string fn = archive_entry_pathname(entry);
577 
578       LOG("EXT: Original filename in zip <%s>...\n", fn.c_str());
579 
580       if (!info.commonPath.empty()) {
581         // Check mismatch with package.json common path
582         if (fn.compare(0, info.commonPath.size(), info.commonPath) != 0)
583           continue;
584 
585         fn.erase(0, info.commonPath.size());
586         if (fn.empty())
587           continue;
588       }
589 
590       installedFiles.push_back(fn);
591 
592       const std::string fullFn = base::join_path(info.dstPath, fn);
593 #if _WIN32
594       archive_entry_copy_pathname_w(entry, base::from_utf8(fullFn).c_str());
595 #else
596       archive_entry_set_pathname(entry, fullFn.c_str());
597 #endif
598 
599       LOG("EXT: Uncompressing file <%s> to <%s>\n",
600           fn.c_str(), fullFn.c_str());
601 
602       out.writeEntry(in, entry);
603     }
604   }
605 
606   // Save the list of installed files in "__info.json" file
607   {
608     json11::Json::object obj;
609     obj["installedFiles"] = json11::Json(installedFiles);
610     json11::Json json(obj);
611 
612     const std::string fullFn = base::join_path(info.dstPath, kInfoJson);
613     LOG("EXT: Saving list of installed files in <%s>\n", fullFn.c_str());
614     write_json_file(fullFn, json);
615   }
616 
617   // Load the extension
618   Extension* extension = loadExtension(
619     info.dstPath,
620     base::join_path(info.dstPath, kPackageJson),
621     false);
622   if (!extension)
623     throw base::Exception("Error adding the new extension");
624 
625   // Generate signals
626   NewExtension(extension);
627   generateExtensionSignals(extension);
628 
629   return extension;
630 }
631 
loadExtension(const std::string & path,const std::string & fullPackageFilename,const bool isBuiltinExtension)632 Extension* Extensions::loadExtension(const std::string& path,
633                                      const std::string& fullPackageFilename,
634                                      const bool isBuiltinExtension)
635 {
636   json11::Json json;
637   read_json_file(fullPackageFilename, json);
638   auto name = json["name"].string_value();
639   auto version = json["version"].string_value();
640   auto displayName = json["displayName"].string_value();
641 
642   LOG("EXT: Extension '%s' loaded\n", name.c_str());
643 
644   base::UniquePtr<Extension> extension(
645     new Extension(path,
646                   name,
647                   version,
648                   displayName,
649                   // Extensions are enabled by default
650                   get_config_bool("extensions", name.c_str(), true),
651                   isBuiltinExtension));
652 
653   auto contributes = json["contributes"];
654   if (contributes.is_object()) {
655     // Languages
656     auto languages = contributes["languages"];
657     if (languages.is_array()) {
658       for (const auto& lang : languages.array_items()) {
659         std::string langId = lang["id"].string_value();
660         std::string langPath = lang["path"].string_value();
661 
662         // The path must be always relative to the extension
663         langPath = base::join_path(path, langPath);
664 
665         LOG("EXT: New language '%s' in '%s'\n",
666             langId.c_str(),
667             langPath.c_str());
668 
669         extension->addLanguage(langId, langPath);
670       }
671     }
672 
673     // Themes
674     auto themes = contributes["themes"];
675     if (themes.is_array()) {
676       for (const auto& theme : themes.array_items()) {
677         std::string themeId = theme["id"].string_value();
678         std::string themePath = theme["path"].string_value();
679 
680         // The path must be always relative to the extension
681         themePath = base::join_path(path, themePath);
682 
683         LOG("EXT: New theme '%s' in '%s'\n",
684             themeId.c_str(),
685             themePath.c_str());
686 
687         extension->addTheme(themeId, themePath);
688       }
689     }
690 
691     // Palettes
692     auto palettes = contributes["palettes"];
693     if (palettes.is_array()) {
694       for (const auto& palette : palettes.array_items()) {
695         std::string palId = palette["id"].string_value();
696         std::string palPath = palette["path"].string_value();
697 
698         // The path must be always relative to the extension
699         palPath = base::join_path(path, palPath);
700 
701         LOG("EXT: New palette '%s' in '%s'\n",
702             palId.c_str(),
703             palPath.c_str());
704 
705         extension->addPalette(palId, palPath);
706       }
707     }
708 
709     // Dithering matrices
710     auto ditheringMatrices = contributes["ditheringMatrices"];
711     if (ditheringMatrices.is_array()) {
712       for (const auto& ditheringMatrix : ditheringMatrices.array_items()) {
713         std::string matId = ditheringMatrix["id"].string_value();
714         std::string matPath = ditheringMatrix["path"].string_value();
715         std::string matName = ditheringMatrix["name"].string_value();
716         if (matName.empty())
717           matName = matId;
718 
719         // The path must be always relative to the extension
720         matPath = base::join_path(path, matPath);
721 
722         LOG("EXT: New dithering matrix '%s' in '%s'\n",
723             matId.c_str(),
724             matPath.c_str());
725 
726         extension->addDitheringMatrix(matId, matPath, matName);
727       }
728     }
729   }
730 
731   if (extension)
732     m_extensions.push_back(extension.get());
733   return extension.release();
734 }
735 
generateExtensionSignals(Extension * extension)736 void Extensions::generateExtensionSignals(Extension* extension)
737 {
738   if (extension->hasLanguages()) LanguagesChange(extension);
739   if (extension->hasThemes()) ThemesChange(extension);
740   if (extension->hasPalettes()) PalettesChange(extension);
741   if (extension->hasDitheringMatrices()) DitheringMatricesChange(extension);
742 }
743 
744 } // namespace app
745