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