1 /** @file package.cpp  Package containing metadata, data, and/or files.
2  *
3  * @authors Copyright (c) 2014-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  *
5  * @par License
6  * LGPL: http://www.gnu.org/licenses/lgpl.html
7  *
8  * <small>This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU Lesser General Public License as published by
10  * the Free Software Foundation; either version 3 of the License, or (at your
11  * option) any later version. This program is distributed in the hope that it
12  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14  * General Public License for more details. You should have received a copy of
15  * the GNU Lesser General Public License along with this program; if not, see:
16  * http://www.gnu.org/licenses</small>
17  */
18 
19 #include "de/Package"
20 #include "de/App"
21 #include "de/DotPath"
22 #include "de/LogBuffer"
23 #include "de/PackageLoader"
24 #include "de/Process"
25 #include "de/Script"
26 #include "de/ScriptSystem"
27 #include "de/ScriptedInfo"
28 #include "de/TextValue"
29 #include "de/TimeValue"
30 
31 #include <QRegularExpression>
32 
33 namespace de {
34 
35 String const Package::VAR_PACKAGE      ("package");
36 String const Package::VAR_PACKAGE_ID   ("package.ID");
37 String const Package::VAR_PACKAGE_ALIAS("package.alias");
38 String const Package::VAR_PACKAGE_TITLE("package.title");
39 String const Package::VAR_ID           ("ID");
40 String const Package::VAR_TITLE        ("title");
41 String const Package::VAR_VERSION      ("version");
42 
43 static String const PACKAGE_VERSION    ("package.version");
44 static String const PACKAGE_ORDER      ("package.__order__");
45 static String const PACKAGE_IMPORT_PATH("package.importPath");
46 static String const PACKAGE_REQUIRES   ("package.requires");
47 static String const PACKAGE_RECOMMENDS ("package.recommends");
48 static String const PACKAGE_EXTRAS     ("package.extras");
49 static String const PACKAGE_PATH       ("package.path");
50 static String const PACKAGE_TAGS       ("package.tags");
51 
52 static String const VAR_ID  ("ID");
53 static String const VAR_PATH("path");
54 static String const VAR_TAGS("tags");
55 
Asset(Record const & rec)56 Package::Asset::Asset(Record const &rec) : RecordAccessor(rec) {}
57 
Asset(Record const * rec)58 Package::Asset::Asset(Record const *rec) : RecordAccessor(rec) {}
59 
absolutePath(String const & name) const60 String Package::Asset::absolutePath(String const &name) const
61 {
62     // For the context, we'll accept either the variable's own record or the package
63     // metadata.
64     Record const *context = &accessedRecord().parentRecordForMember(name);
65     if (!context->has(ScriptedInfo::VAR_SOURCE))
66     {
67         context = &accessedRecord();
68     }
69     return ScriptedInfo::absolutePathInContext(*context, gets(name));
70 }
71 
DENG2_PIMPL(Package)72 DENG2_PIMPL(Package)
73 {
74     SafePtr<const File> file;
75     Version version; // version of the loaded package
76 
77     Impl(Public *i, File const *f)
78         : Base(i)
79         , file(f)
80     {
81         if (file)
82         {
83             // Check the file name first, then metadata.
84             version = split(versionedIdentifierForFile(*file)).second;
85             if (!version.isValid())
86             {
87                 version = Version(metadata(*file).gets(VAR_VERSION, String()));
88             }
89         }
90     }
91 
92     void verifyFile() const
93     {
94         if (!file)
95         {
96             throw SourceError("Package::verifyFile", "Package's source file missing");
97         }
98     }
99 
100     StringList importPaths() const
101     {
102         StringList paths;
103         if (self().objectNamespace().has(PACKAGE_IMPORT_PATH))
104         {
105             ArrayValue const &imp = self().objectNamespace().geta(PACKAGE_IMPORT_PATH);
106             DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, imp.elements())
107             {
108                 Path importPath = (*i)->asText();
109                 if (!importPath.isAbsolute())
110                 {
111                     // Relative to the package root, and must exist.
112                     importPath = self().root().locate<File const >(importPath).path();
113                 }
114                 paths << importPath;
115             }
116         }
117         return paths;
118     }
119 
120     Record &packageInfo()
121     {
122         return self().objectNamespace().subrecord(VAR_PACKAGE);
123     }
124 };
125 
Package(File const & file)126 Package::Package(File const &file) : d(new Impl(this, &file))
127 {}
128 
~Package()129 Package::~Package()
130 {}
131 
file() const132 File const &Package::file() const
133 {
134     d->verifyFile();
135     return *d->file;
136 }
137 
sourceFile() const138 File const &Package::sourceFile() const
139 {
140     return FS::locate<const File>(objectNamespace().gets(PACKAGE_PATH));
141 }
142 
sourceFileExists() const143 bool Package::sourceFileExists() const
144 {
145     return d->file && FS::tryLocate<const File>(objectNamespace().gets(PACKAGE_PATH));
146 }
147 
root() const148 Folder const &Package::root() const
149 {
150     d->verifyFile();
151     if (const Folder *f = maybeAs<Folder>(&d->file->target()))
152     {
153         return *f;
154     }
155     return *sourceFile().parent();
156 }
157 
objectNamespace()158 Record &Package::objectNamespace()
159 {
160     d->verifyFile();
161     return const_cast<File *>(d->file.get())->objectNamespace();
162 }
163 
objectNamespace() const164 Record const &Package::objectNamespace() const
165 {
166     return const_cast<Package *>(this)->objectNamespace();
167 }
168 
identifier() const169 String Package::identifier() const
170 {
171     d->verifyFile();
172     return identifierForFile(*d->file);
173 }
174 
version() const175 Version Package::version() const
176 {
177     return d->version;
178 }
179 
assets() const180 Package::Assets Package::assets() const
181 {
182     return ScriptedInfo::allBlocksOfType(QStringLiteral("asset"), d->packageInfo());
183 }
184 
executeFunction(String const & name)185 bool Package::executeFunction(String const &name)
186 {
187     Record &pkgInfo = d->packageInfo();
188     if (pkgInfo.has(name))
189     {
190         // The global namespace for this function is the package's info namespace.
191         Process::scriptCall(Process::IgnoreResult, pkgInfo, name);
192         return true;
193     }
194     return false;
195 }
196 
setOrder(int ordinal)197 void Package::setOrder(int ordinal)
198 {
199     objectNamespace().set(PACKAGE_ORDER, ordinal);
200 }
201 
order() const202 int Package::order() const
203 {
204     return objectNamespace().geti(PACKAGE_ORDER);
205 }
206 
findPartialPath(String const & path,FileIndex::FoundFiles & found) const207 void Package::findPartialPath(String const &path, FileIndex::FoundFiles &found) const
208 {
209     App::fileSystem().nameIndex().findPartialPath(identifier(), path, found);
210 }
211 
didLoad()212 void Package::didLoad()
213 {
214     // The package's own import paths come into effect when loaded.
215     foreach (String imp, d->importPaths())
216     {
217         App::scriptSystem().addModuleImportPath(imp);
218     }
219 
220     executeFunction("onLoad");
221 }
222 
aboutToUnload()223 void Package::aboutToUnload()
224 {
225     executeFunction("onUnload");
226 
227     foreach (String imp, d->importPaths())
228     {
229         App::scriptSystem().removeModuleImportPath(imp);
230     }
231 
232     // Not loaded any more, so doesn't have an ordinal.
233     delete objectNamespace().remove(PACKAGE_ORDER);
234 }
235 
parseMetadata(File & packageFile)236 void Package::parseMetadata(File &packageFile) // static
237 {
238     static String const TIMESTAMP("__timestamp__");
239 
240     if (Folder *folder = maybeAs<Folder>(packageFile))
241     {
242         File *initializerScript = folder->tryLocateFile(QStringLiteral("__init__.ds"));
243         if (!initializerScript) initializerScript = folder->tryLocateFile(QStringLiteral("__init__.de")); // old extension
244         File *metadataInfo      = folder->tryLocateFile(QStringLiteral("Info.dei"));
245         if (!metadataInfo) metadataInfo = folder->tryLocateFile(QStringLiteral("Info")); // alternate name
246         Time parsedAt           = Time::invalidTime();
247         bool needParse          = true;
248 
249         if (!metadataInfo && !initializerScript) return; // Nothing to do.
250 
251         // If the metadata has already been parsed, we may not need to do much.
252         // The package's information is stored in a subrecord.
253         if (packageFile.objectNamespace().has(VAR_PACKAGE))
254         {
255             Record &metadata = packageFile.objectNamespace().subrecord(VAR_PACKAGE);
256             if (metadata.has(TIMESTAMP))
257             {
258                 // Already parsed.
259                 needParse = false;
260 
261                 // Only parse if the source has changed.
262                 if (auto const *time = maybeAs<TimeValue>(metadata.get(TIMESTAMP)))
263                 {
264                     needParse =
265                             (metadataInfo      && metadataInfo->status().modifiedAt      > time->time()) ||
266                             (initializerScript && initializerScript->status().modifiedAt > time->time());
267                 }
268             }
269             if (!needParse) return;
270         }
271 
272         // The package identifier and path are automatically set.
273         Record &metadata = initializeMetadata(packageFile);
274 
275         // Check for a ScriptedInfo source.
276         if (metadataInfo)
277         {
278             ScriptedInfo script(&metadata);
279             script.parse(*metadataInfo);
280 
281             parsedAt = metadataInfo->status().modifiedAt;
282         }
283 
284         // Check for an initialization script.
285         if (initializerScript)
286         {
287             Script script(*initializerScript);
288             Process proc(&metadata);
289             proc.run(script);
290             proc.execute();
291 
292             if (parsedAt.isValid() && initializerScript->status().modifiedAt > parsedAt)
293             {
294                 parsedAt = initializerScript->status().modifiedAt;
295             }
296         }
297 
298         metadata.addTime(TIMESTAMP, parsedAt);
299 
300         if (LogBuffer::get().isEnabled(LogEntry::Dev | LogEntry::XVerbose | LogEntry::Resource))
301         {
302             LOGDEV_RES_XVERBOSE("Parsed metadata of '%s':\n" _E(m),
303                     identifierForFile(packageFile)
304                     << packageFile.objectNamespace().asText());
305         }
306     }
307 }
308 
validateMetadata(Record const & packageInfo)309 void Package::validateMetadata(Record const &packageInfo)
310 {
311     if (!packageInfo.has(VAR_ID))
312     {
313         throw NotPackageError("Package::validateMetadata", "Not a package");
314     }
315 
316     // A domain is required in all package identifiers.
317     DotPath const ident(packageInfo.gets(VAR_ID));
318 
319     if (ident.segmentCount() <= 1)
320     {
321         throw ValidationError("Package::validateMetadata",
322                               QString("Identifier of package \"%1\" must specify a domain")
323                               .arg(packageInfo.gets("path")));
324     }
325 
326     String const &topLevelDomain = ident.segment(0).toString();
327     if (topLevelDomain == QStringLiteral("feature") ||
328         topLevelDomain == QStringLiteral("asset"))
329     {
330         // Functional top-level domains cannot be used as package identifiers (only aliases).
331         throw ValidationError("Package::validateMetadata",
332                               QString("Package \"%1\" has an invalid domain: functional top-level "
333                                       "domains can only be used as aliases")
334                               .arg(packageInfo.gets("path")));
335     }
336 
337     static String const required[] = { "title", "version", "license", VAR_TAGS };
338     for (auto const &req : required)
339     {
340         if (!packageInfo.has(req))
341         {
342             throw IncompleteMetadataError("Package::validateMetadata",
343                                           QString("Package \"%1\" does not have '%2' in its metadata")
344                                           .arg(packageInfo.gets("path"))
345                                           .arg(req));
346         }
347     }
348 
349     static QRegularExpression const regexReservedTags("\\b(loaded)\\b");
350     auto match = regexReservedTags.match(packageInfo.gets(VAR_TAGS));
351     if (match.hasMatch())
352     {
353         throw ValidationError("Package::validateMetadata",
354                               QString("Package \"%1\" has a tag that is reserved for internal use (%2)")
355                               .arg(packageInfo.gets("path"))
356                               .arg(match.captured(1)));
357     }
358 }
359 
initializeMetadata(File & packageFile,String const & id)360 Record &Package::initializeMetadata(File &packageFile, String const &id)
361 {
362     if (!packageFile.objectNamespace().has(VAR_PACKAGE))
363     {
364         packageFile.objectNamespace().addSubrecord(VAR_PACKAGE);
365     }
366 
367     Record &metadata = packageFile.objectNamespace().subrecord(VAR_PACKAGE);
368     metadata.set(VAR_ID,   id.isEmpty()? identifierForFile(packageFile) : id);
369     metadata.set(VAR_PATH, packageFile.path());
370     return metadata;
371 }
372 
metadata(File const & packageFile)373 Record const &Package::metadata(File const &packageFile)
374 {
375     return packageFile.objectNamespace().subrecord(VAR_PACKAGE);
376 }
377 
tags(File const & packageFile)378 QStringList Package::tags(File const &packageFile)
379 {
380     return tags(packageFile.objectNamespace().gets(PACKAGE_TAGS));
381 }
382 
matchTags(File const & packageFile,String const & tagRegExp)383 bool Package::matchTags(File const &packageFile, String const &tagRegExp)
384 {
385     return QRegExp(tagRegExp).indexIn(packageFile.objectNamespace().gets(PACKAGE_TAGS, "")) >= 0;
386 }
387 
tags(String const & tagsString)388 QStringList Package::tags(String const &tagsString)
389 {
390     return tagsString.split(' ', QString::SkipEmptyParts);
391 }
392 
393 StringList Package::requires(File const &packageFile)
394 {
395     return packageFile.objectNamespace().getStringList(PACKAGE_REQUIRES);
396 }
397 
addRequiredPackage(File & packageFile,String const & id)398 void Package::addRequiredPackage(File &packageFile, String const &id)
399 {
400     packageFile.objectNamespace().appendToArray(PACKAGE_REQUIRES, new TextValue(id));
401 }
402 
hasOptionalContent(String const & packageId)403 bool Package::hasOptionalContent(String const &packageId)
404 {
405     if (File const *file = PackageLoader::get().select(packageId))
406     {
407         return hasOptionalContent(*file);
408     }
409     return false;
410 }
411 
hasOptionalContent(File const & packageFile)412 bool Package::hasOptionalContent(File const &packageFile)
413 {
414     Record const &meta = packageFile.objectNamespace();
415     return meta.has(PACKAGE_RECOMMENDS) || meta.has(PACKAGE_EXTRAS);
416 }
417 
stripAfterFirstUnderscore(String str)418 static String stripAfterFirstUnderscore(String str)
419 {
420     int pos = str.indexOf('_');
421     if (pos > 0) return str.left(pos);
422     return str;
423 }
424 
extractIdentifier(String str)425 static String extractIdentifier(String str)
426 {
427     return stripAfterFirstUnderscore(str.fileNameWithoutExtension());
428 }
429 
split(String const & identifier_version)430 std::pair<String, Version> Package::split(String const &identifier_version)
431 {
432     std::pair<String, Version> idVer;
433 
434     if (identifier_version.contains(QChar('_')))
435     {
436         idVer.first  = stripAfterFirstUnderscore(identifier_version);
437         idVer.second = Version(identifier_version.substr(idVer.first.size() + 1));
438     }
439     else
440     {
441         idVer.first  = identifier_version;
442     }
443     return idVer;
444 }
445 
splitToHumanReadable(String const & identifier_version)446 String Package::splitToHumanReadable(String const &identifier_version)
447 {
448     auto const id_ver = split(identifier_version);
449     return QObject::tr("%1 " _E(C) "(%2)" _E(.))
450             .arg(id_ver.first)
451             .arg(id_ver.second.isValid()? QObject::tr("version %1").arg(id_ver.second.fullNumber())
452                                         : QObject::tr("any version"));
453 }
454 
equals(String const & id1,String const & id2)455 bool Package::equals(String const &id1, String const &id2)
456 {
457     return split(id1).first == split(id2).first;
458 }
459 
identifierForFile(File const & file)460 String Package::identifierForFile(File const &file)
461 {
462     // The ID may be specified in the metadata.
463     if (auto const *pkgId = file.objectNamespace().tryFind(VAR_PACKAGE_ID))
464     {
465         return pkgId->value().asText();
466     }
467 
468     // Form the prefix if there are enclosing packs as parents.
469     String prefix;
470     Folder const *parent = file.parent();
471     while (parent && parent->extension() == ".pack")
472     {
473         prefix = extractIdentifier(parent->name()) + "." + prefix;
474         parent = parent->parent();
475     }
476     return prefix + extractIdentifier(file.name());
477 }
478 
versionedIdentifierForFile(File const & file)479 String Package::versionedIdentifierForFile(File const &file)
480 {
481     String id = identifierForFile(file);
482     if (id.isEmpty()) return String();
483     auto const id_ver = split(file.name().fileNameWithoutExtension());
484     if (id_ver.second.isValid())
485     {
486         return String("%1_%2").arg(id).arg(id_ver.second.fullNumber());
487     }
488     // The version may be specified in metadata.
489     if (auto const *pkgVer = file.objectNamespace().tryFind(PACKAGE_VERSION))
490     {
491         return String("%1_%2").arg(id).arg(Version(pkgVer->value().asText()).fullNumber());
492     }
493     return id;
494 }
495 
versionForFile(File const & file)496 Version Package::versionForFile(File const &file)
497 {
498     return split(versionedIdentifierForFile(file)).second;
499 }
500 
containerOfFile(File const & file)501 File const *Package::containerOfFile(File const &file)
502 {
503     // Find the containing package.
504     File const *i = file.parent();
505     while (i && i->extension() != ".pack")
506     {
507         i = i->parent();
508     }
509     return i;
510 }
511 
identifierForContainerOfFile(File const & file)512 String Package::identifierForContainerOfFile(File const &file)
513 {
514     // Find the containing package.
515     File const *c = containerOfFile(file);
516     return c? identifierForFile(*c) : "";
517 }
518 
containerOfFileModifiedAt(File const & file)519 Time Package::containerOfFileModifiedAt(File const &file)
520 {
521     File const *c = containerOfFile(file);
522     if (!c) return file.status().modifiedAt;
523     return c->status().modifiedAt;
524 }
525 
526 } // namespace de
527