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