/* * Copyright (C) 2016 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ module asgen.result; import std.stdio; import std.string; import std.array : empty; import std.conv : to; import std.json; import appstream.Component; import asgen.hint; import asgen.utils : buildCptGlobalID; import asgen.backends.interfaces; import asgen.config : Config; /** * Helper function for GeneratorResult.finalize() */ extern(C) int evaluateCustomEntry (void *keyPtr, void *value, void *userData) { auto key = (cast(const(char)*) keyPtr).fromStringz; auto conf = *cast(Config*) userData; if (key in conf.allowedCustomKeys) return false; // FALSE, do not delete // remove invalid key return true; } final class GeneratorResult { private: Component[string] cpts; string[Component] cptGCID; string[string] mdataHashes; HintList[string] hints; public: immutable string pkid; immutable string pkgname; Package pkg; public: this (Package pkg) { this.pkid = pkg.id; this.pkgname = pkg.name; this.pkg = pkg; } @safe bool packageIsIgnored () pure { return (cpts.length == 0) && (hints.length == 0); } @safe Component getComponent (string id) pure { auto ptr = (id in cpts); if (ptr is null) return null; return *ptr; } @trusted Component[] getComponents () pure { return cpts.values (); } @trusted bool isIgnored (Component cpt) { return getComponent (cpt.getId ()) is null; } @trusted void updateComponentGCID (Component cpt, string data) { import std.digest.md; auto cid = cpt.getId (); if (data.empty) { cptGCID[cpt] = buildCptGlobalID (cid, "???-NO_CHECKSUM-???"); return; } auto oldHashP = (cid in mdataHashes); string oldHash = ""; if (oldHashP !is null) oldHash = *oldHashP; auto hash = md5Of (oldHash ~ data); auto checksum = toHexString (hash); auto newHash = to!string (checksum); mdataHashes[cid] = newHash; cptGCID[cpt] = buildCptGlobalID (cid, newHash); } @trusted void addComponent (Component cpt, string data = "") { string cid = cpt.getId (); if (cid.empty) throw new Exception ("Can not add component without ID to results set."); cpt.setPkgnames ([this.pkgname]); cpts[cid] = cpt; updateComponentGCID (cpt, data); } @safe void dropComponent (string cid) pure { auto cpt = getComponent (cid); if (cpt is null) return; cpts.remove (cid); cptGCID.remove (cpt); } /** * Add an issue hint to this result. * Params: * id = The component-id or component itself this tag is assigned to. * tag = The hint tag. * params = Dictionary of parameters to insert into the issue report. * Returns: * True if the hint did not cause the removal of the component, False otherwise. **/ @trusted bool addHint (T) (T id, string tag, string[string] params) if (is(T == string) || is(T == Component) || is(T == typeof(null))) { static if (is(T == string)) { immutable cid = id; } else { static if (is(T == typeof(null))) immutable cid = "general"; else immutable cid = id.getId (); } auto hint = new GeneratorHint (tag, cid); hint.setVars (params); hints[cid] ~= hint; // we stop dealing with this component when we encounter a fatal // error. if (hint.isError) { dropComponent (cid); return false; } return true; } /** * Add an issue hint to this result. * Params: * id = The component-id or component itself this tag is assigned to. * tag = The hint tag. * msg = An error message to add to the report. * Returns: * True if the hint did not cause the removal of the component, False otherwise. **/ @safe bool addHint (T) (T id, string tag, string msg = null) { string[string] vars; if (msg !is null) vars = ["msg": msg]; return addHint (id, tag, vars); } /** * Create JSON metadata for the hints found for the package * associacted with this GeneratorResult. */ string hintsToJson () { if (hints.length == 0) return null; // is this really the only way you can set a type for JSONValue? auto map = JSONValue (["null": 0]); map.object.remove ("null"); foreach (cid; hints.byKey ()) { auto cptHints = hints[cid]; auto hintNodes = JSONValue ([0, 0]); hintNodes.array = []; foreach (GeneratorHint hint; cptHints) { hintNodes.array ~= hint.toJsonNode (); } map.object[cid] = hintNodes; } auto root = JSONValue (["package": JSONValue (pkid), "hints": map]); return toJSON (&root, true); } /** * Drop invalid components and components with errors. */ void finalize () { auto conf = Config.get (); // we need to duplicate the associative array, because the addHint() function // may remove entries from "cpts", breaking our foreach loop. foreach (cpt; cpts.dup.byValue) { auto ckind = cpt.getKind; cpt.setActiveLocale ("C"); if (ckind == ComponentKind.UNKNOWN) if (!addHint (cpt, "metainfo-unknown-type")) continue; if ((!cpt.hasBundle) && (cpt.getPkgnames.empty)) if (!addHint (cpt, "no-install-candidate")) continue; if (cpt.getName.empty) if (!addHint (cpt, "metainfo-no-name")) continue; if (cpt.getSummary.empty) if (!addHint (cpt, "metainfo-no-summary")) continue; // desktop apps get extra treatment (more validation, addition of fallback long-description) if (ckind == ComponentKind.DESKTOP_APP) { // checks specific for .desktop and web apps if (cpt.getIcons ().len == 0) if (!addHint (cpt, "gui-app-without-icon")) continue; // desktop-application components are required to have a category if (cpt.getCategories ().len <= 0) if (!addHint (cpt, "no-valid-category")) continue; // inject package descriptions, if needed auto flags = cpt.getValueFlags; cpt.setValueFlags (flags | AsValueFlags.NO_TRANSLATION_FALLBACK); cpt.setActiveLocale ("C"); if (cpt.getDescription.empty) { // component doesn't have a long description, add one from // the packaging. auto desc_added = false; foreach (ref lang, ref desc; pkg.description) { cpt.setDescription (desc, lang); desc_added = true; } if (desc_added) if (!addHint (cpt, "description-from-package")) continue; } // check if we can add a launchable here if ((cpt.getLaunchable (LaunchableKind.DESKTOP_ID) is null) && (cpt.getId.endsWith (".desktop"))) { import appstream.Launchable; auto launch = new Launchable; launch.setKind (LaunchableKind.DESKTOP_ID); launch.addEntry (cpt.getId ()); cpt.addLaunchable (launch); } } // finally, filter custom tags auto customHashTable = cpt.getCustom (); auto noCustomKeysAllowed = conf.allowedCustomKeys.length == 0; if (customHashTable.size > 0) { import glib.c.types; if (noCustomKeysAllowed) { // if we don't allow any custom keys, we can delete them faster customHashTable.removeAll (); continue; } // filter the custom values customHashTable.foreachRemove (&evaluateCustomEntry, &conf); } } } /** * Return the number of components we've found. **/ @safe ulong componentsCount () pure { return cpts.length; } /** * Return the number of hints that have been emitted. **/ @safe ulong hintsCount () pure { return hints.length; } @safe string gcidForComponent (Component cpt) pure { auto cgp = (cpt in cptGCID); if (cgp is null) return null; return *cgp; } @trusted string[] getGCIDs () pure { return cptGCID.values (); } } unittest { import asgen.backends.dummy.dummypkg; writeln ("TEST: ", "GeneratorResult"); auto pkg = new DummyPackage ("foobar", "1.0", "amd64"); auto res = new GeneratorResult (pkg); auto vars = ["rainbows": "yes", "unicorns": "no", "storage": "towel"]; res.addHint ("org.freedesktop.foobar.desktop", "just-a-unittest", vars); res.addHint ("org.freedesktop.awesome-bar.desktop", "metainfo-chocolate-missing", "Nothing is good without chocolate. Add some."); res.addHint ("org.freedesktop.awesome-bar.desktop", "metainfo-does-not-frobnicate", "Frobnicate functionality is missing."); writeln (res.hintsToJson ()); }