source: appstream-generator/src/asgen/result.d @ 4841

Last change on this file since 4841 was 4841, checked in by Juanma, 2 years ago

Initial release

File size: 10.0 KB
Line 
1/*
2 * Copyright (C) 2016 Matthias Klumpp <matthias@tenstral.net>
3 *
4 * Licensed under the GNU Lesser General Public License Version 3
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 3 of the license, or
9 * (at your option) any later version.
10 *
11 * This software is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
18 */
19
20module asgen.result;
21
22import std.stdio;
23import std.string;
24import std.array : empty;
25import std.conv : to;
26import std.json;
27import appstream.Component;
28
29import asgen.hint;
30import asgen.utils : buildCptGlobalID;
31import asgen.backends.interfaces;
32import asgen.config : Config;
33
34
35/**
36 * Helper function for GeneratorResult.finalize()
37 */
38extern(C)
39int evaluateCustomEntry (void *keyPtr, void *value, void *userData)
40{
41    auto key = (cast(const(char)*) keyPtr).fromStringz;
42    auto conf = *cast(Config*) userData;
43
44    if (key in conf.allowedCustomKeys)
45        return false; // FALSE, do not delete
46
47    // remove invalid key
48    return true;
49}
50
51final class GeneratorResult
52{
53
54private:
55    Component[string] cpts;
56    string[Component] cptGCID;
57    string[string] mdataHashes;
58    HintList[string] hints;
59
60public:
61    immutable string pkid;
62    immutable string pkgname;
63    Package pkg;
64
65public:
66
67    this (Package pkg)
68    {
69        this.pkid = pkg.id;
70        this.pkgname = pkg.name;
71        this.pkg = pkg;
72    }
73
74    @safe
75    bool packageIsIgnored () pure
76    {
77        return (cpts.length == 0) && (hints.length == 0);
78    }
79
80    @safe
81    Component getComponent (string id) pure
82    {
83        auto ptr = (id in cpts);
84        if (ptr is null)
85            return null;
86        return *ptr;
87    }
88
89    @trusted
90    Component[] getComponents () pure
91    {
92        return cpts.values ();
93    }
94
95    @trusted
96    bool isIgnored (Component cpt)
97    {
98        return getComponent (cpt.getId ()) is null;
99    }
100
101    @trusted
102    void updateComponentGCID (Component cpt, string data)
103    {
104        import std.digest.md;
105
106        auto cid = cpt.getId ();
107        if (data.empty) {
108            cptGCID[cpt] = buildCptGlobalID (cid, "???-NO_CHECKSUM-???");
109            return;
110        }
111
112        auto oldHashP = (cid in mdataHashes);
113        string oldHash = "";
114        if (oldHashP !is null)
115            oldHash = *oldHashP;
116
117        auto hash = md5Of (oldHash ~ data);
118        auto checksum = toHexString (hash);
119        auto newHash = to!string (checksum);
120
121        mdataHashes[cid] = newHash;
122        cptGCID[cpt] = buildCptGlobalID (cid, newHash);
123    }
124
125    @trusted
126    void addComponent (Component cpt, string data = "")
127    {
128        string cid = cpt.getId ();
129        if (cid.empty)
130            throw new Exception ("Can not add component without ID to results set.");
131
132        cpt.setPkgnames ([this.pkgname]);
133        cpts[cid] = cpt;
134        updateComponentGCID (cpt, data);
135    }
136
137    @safe
138    void dropComponent (string cid) pure
139    {
140        auto cpt = getComponent (cid);
141        if (cpt is null)
142            return;
143        cpts.remove (cid);
144        cptGCID.remove (cpt);
145    }
146
147    /**
148     * Add an issue hint to this result.
149     * Params:
150     *      id = The component-id or component itself this tag is assigned to.
151     *      tag    = The hint tag.
152     *      params = Dictionary of parameters to insert into the issue report.
153     * Returns:
154     *      True if the hint did not cause the removal of the component, False otherwise.
155     **/
156    @trusted
157    bool addHint (T) (T id, string tag, string[string] params)
158        if (is(T == string) || is(T == Component) || is(T == typeof(null)))
159    {
160        static if (is(T == string)) {
161            immutable cid = id;
162        } else {
163            static if (is(T == typeof(null)))
164                immutable cid = "general";
165            else
166                immutable cid = id.getId ();
167        }
168
169        auto hint = new GeneratorHint (tag, cid);
170        hint.setVars (params);
171        hints[cid] ~= hint;
172
173        // we stop dealing with this component when we encounter a fatal
174        // error.
175        if (hint.isError) {
176            dropComponent (cid);
177            return false;
178        }
179
180        return true;
181    }
182
183    /**
184     * Add an issue hint to this result.
185     * Params:
186     *      id = The component-id or component itself this tag is assigned to.
187     *      tag = The hint tag.
188     *      msg = An error message to add to the report.
189     * Returns:
190     *      True if the hint did not cause the removal of the component, False otherwise.
191     **/
192    @safe
193    bool addHint (T) (T id, string tag, string msg = null)
194    {
195        string[string] vars;
196        if (msg !is null)
197            vars = ["msg": msg];
198        return addHint (id, tag, vars);
199    }
200
201    /**
202     * Create JSON metadata for the hints found for the package
203     * associacted with this GeneratorResult.
204     */
205    string hintsToJson ()
206    {
207        if (hints.length == 0)
208            return null;
209
210        // is this really the only way you can set a type for JSONValue?
211        auto map = JSONValue (["null": 0]);
212        map.object.remove ("null");
213
214        foreach (cid; hints.byKey ()) {
215            auto cptHints = hints[cid];
216            auto hintNodes = JSONValue ([0, 0]);
217            hintNodes.array = [];
218            foreach (GeneratorHint hint; cptHints) {
219                hintNodes.array ~= hint.toJsonNode ();
220            }
221
222            map.object[cid] = hintNodes;
223        }
224
225        auto root = JSONValue (["package": JSONValue (pkid), "hints": map]);
226        return toJSON (&root, true);
227    }
228
229    /**
230     * Drop invalid components and components with errors.
231     */
232    void finalize ()
233    {
234        auto conf = Config.get ();
235
236        // we need to duplicate the associative array, because the addHint() function
237        // may remove entries from "cpts", breaking our foreach loop.
238        foreach (cpt; cpts.dup.byValue) {
239            auto ckind = cpt.getKind;
240            cpt.setActiveLocale ("C");
241
242            if (ckind == ComponentKind.UNKNOWN)
243                if (!addHint (cpt, "metainfo-unknown-type"))
244                    continue;
245
246            if ((!cpt.hasBundle) && (cpt.getPkgnames.empty))
247                if (!addHint (cpt, "no-install-candidate"))
248                    continue;
249
250            if (cpt.getName.empty)
251                if (!addHint (cpt, "metainfo-no-name"))
252                    continue;
253
254            if (cpt.getSummary.empty)
255                if (!addHint (cpt, "metainfo-no-summary"))
256                    continue;
257
258            // desktop apps get extra treatment (more validation, addition of fallback long-description)
259            if (ckind == ComponentKind.DESKTOP_APP) {
260                // checks specific for .desktop and web apps
261                if (cpt.getIcons ().len == 0)
262                    if (!addHint (cpt, "gui-app-without-icon"))
263                        continue;
264
265                // desktop-application components are required to have a category
266                if (cpt.getCategories ().len <= 0)
267                    if (!addHint (cpt, "no-valid-category"))
268                        continue;
269
270                // inject package descriptions, if needed
271                auto flags = cpt.getValueFlags;
272                cpt.setValueFlags (flags | AsValueFlags.NO_TRANSLATION_FALLBACK);
273
274                cpt.setActiveLocale ("C");
275                if (cpt.getDescription.empty) {
276                    // component doesn't have a long description, add one from
277                    // the packaging.
278                    auto desc_added = false;
279                    foreach (ref lang, ref desc; pkg.description) {
280                            cpt.setDescription (desc, lang);
281                            desc_added = true;
282                    }
283                    if (desc_added)
284                        if (!addHint (cpt, "description-from-package"))
285                            continue;
286                }
287            }
288
289            // finally, filter custom tags
290            auto customHashTable = cpt.getCustom ();
291            auto noCustomKeysAllowed = conf.allowedCustomKeys.length == 0;
292            if (customHashTable.size > 0) {
293                import gi.glibtypes;
294
295                if (noCustomKeysAllowed) {
296                    // if we don't allow any custom keys, we can delete them faster
297                    customHashTable.removeAll ();
298                    continue;
299                }
300
301                // filter the custom values
302                customHashTable.foreachRemove (&evaluateCustomEntry, &conf);
303            }
304        }
305    }
306
307    /**
308     * Return the number of components we've found.
309     **/
310    @safe
311    ulong componentsCount () pure
312    {
313        return cpts.length;
314    }
315
316    /**
317     * Return the number of hints that have been emitted.
318     **/
319    @safe
320    ulong hintsCount () pure
321    {
322        return hints.length;
323    }
324
325    @safe
326    string gcidForComponent (Component cpt) pure
327    {
328        auto cgp = (cpt in cptGCID);
329        if (cgp is null)
330            return null;
331        return *cgp;
332    }
333
334    @trusted
335    string[] getGCIDs () pure
336    {
337        return cptGCID.values ();
338    }
339
340}
341
342unittest
343{
344    import asgen.backends.dummy.dummypkg;
345    writeln ("TEST: ", "GeneratorResult");
346
347    auto pkg = new DummyPackage ("foobar", "1.0", "amd64");
348    auto res = new GeneratorResult (pkg);
349
350    auto vars = ["rainbows": "yes", "unicorns": "no", "storage": "towel"];
351    res.addHint ("org.freedesktop.foobar.desktop", "just-a-unittest", vars);
352    res.addHint ("org.freedesktop.awesome-bar.desktop", "metainfo-chocolate-missing", "Nothing is good without chocolate. Add some.");
353    res.addHint ("org.freedesktop.awesome-bar.desktop", "metainfo-does-not-frobnicate", "Frobnicate functionality is missing.");
354
355    writeln (res.hintsToJson ());
356}
Note: See TracBrowser for help on using the repository browser.