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

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

Initial release

File size: 11.1 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.handlers.desktopparser;
21
22import std.path : baseName;
23import std.uni : toLower;
24import std.string : format, indexOf, chomp, lastIndexOf, toStringz;
25import std.array : split, empty;
26import std.algorithm : startsWith, endsWith, strip, stripRight;
27import std.stdio;
28import std.typecons : scoped;
29
30import glib.KeyFile;
31import appstream.Component;
32import appstream.Provided;
33import appstream.Icon;
34static import std.regex;
35
36import asgen.result;
37import asgen.utils;
38import asgen.config : Config, FormatVersion;
39
40private string getLocaleFromKey (string key)
41{
42    if (!localeValid (key))
43        return null;
44    auto si = key.indexOf ("[");
45
46    // check if this key is language-specific, if not assume untranslated.
47    if (si <= 0)
48        return "C";
49
50    auto locale = key[si+1..$-1];
51    // drop UTF-8 suffixes
52    locale = chomp (locale, ".utf-8");
53    locale = chomp (locale, ".UTF-8");
54
55    auto delim = locale.lastIndexOf ('.');
56    if (delim > 0) {
57        // looks like we need to drop another encoding suffix
58        // (but we need to make sure it actually is one)
59        auto enc = locale[delim+1..$];
60        if ((enc !is null) && (enc.toLower ().startsWith ("iso"))) {
61            locale = locale[0..delim];
62        }
63    }
64
65    return locale;
66}
67
68private string getValue (KeyFile kf, string key)
69{
70    string val;
71    try {
72        val = kf.getString (DESKTOP_GROUP, key);
73    } catch {
74        val = null;
75    }
76
77    // some dumb .desktop files contain non-printable characters. If we are in XML mode,
78    // this will hard-break the XML reader at a later point, so we need to clean this up
79    // and replace these characters with a nice questionmark, so someone will clean them up.
80    // TODO: Maybe even emit an issue hint if a non-printable chacater is found?
81    auto re = std.regex.ctRegex!(r"[\x00\x08\x0B\x0C\x0E-\x1F]", "g");
82    val = std.regex.replaceAll (val, re, "#?#");
83
84    return val;
85}
86
87/**
88 * Filter out some useless categories which we don't want to have in the
89 * AppStream metadata.
90 */
91private string[] filterCategories (Component cpt, GeneratorResult gres, const(string[]) cats)
92{
93    import asgen.bindings.appstream_utils;
94
95    string[] rescats;
96    foreach (string cat; cats) {
97        switch (cat) {
98            case "GTK":
99            case "Qt":
100            case "GNOME":
101            case "KDE":
102            case "GUI":
103            case "Application":
104                break;
105            default:
106                if (!cat.empty && !cat.toLower.startsWith ("x-")) {
107                    if (as_utils_is_category_name (cat.toStringz))
108                        rescats ~= cat;
109                    else
110                        gres.addHint (cpt, "category-name-invalid", ["category": cat]);
111                }
112
113        }
114    }
115
116    return rescats;
117}
118
119Component parseDesktopFile (GeneratorResult gres, string fname, string data, bool ignore_nodisplay = false)
120{
121    auto fnameBase = baseName (fname);
122
123    auto df = scoped!KeyFile ();
124    try {
125        df.loadFromData (data, -1, GKeyFileFlags.KEEP_TRANSLATIONS);
126    } catch (Exception e) {
127        // there was an error
128        gres.addHint (fnameBase, "desktop-file-error", e.msg);
129        return null;
130    }
131
132    try {
133        // check if we should ignore this .desktop file
134        auto dtype = df.getString (DESKTOP_GROUP, "Type");
135        if ((dtype.toLower () != "application") && (dtype.toLower () != "zomando"))  {
136            // ignore this file, it isn't describing an application
137            return null;
138        }
139    } catch {}
140
141    try {
142        auto nodisplay = df.getString (DESKTOP_GROUP, "NoDisplay");
143        if ((!ignore_nodisplay) && (nodisplay.toLower () == "true")) {
144                // we ignore this .desktop file, shouldn't be displayed
145                return null;
146        }
147    } catch {}
148
149    try {
150        auto asignore = df.getString (DESKTOP_GROUP, "X-AppStream-Ignore");
151        if (asignore.toLower () == "true") {
152            // this .desktop file should be excluded from AppStream metadata
153            return null;
154        }
155    } catch {
156        // we don't care if non-essential tags are missing.
157        // if they are not there, the file should be processed.
158    }
159
160    /* check this is a valid desktop file */
161        if (!df.hasGroup (DESKTOP_GROUP)) {
162        gres.addHint (fnameBase,
163                     "desktop-file-error",
164                     format ("Desktop file '%s' is not a valid desktop file.", fname));
165        return null;
166        }
167
168    // make sure we have a valid component to work on
169    auto cpt = gres.getComponent (fnameBase);
170    if (cpt is null) {
171        // try with the shortname as well
172        if (fnameBase.endsWith (".desktop")) {
173            auto fnameBaseNoext = fnameBase[0..$-8];
174            cpt = gres.getComponent (fnameBaseNoext);
175        }
176    }
177
178    if (cpt is null) {
179        cpt = new Component ();
180        // strip .desktop suffix if the reverse-domain-name scheme is followed and we build for
181        // a high AppStream version.
182        if (Config.get ().formatVersion >= FormatVersion.V0_10) {
183            immutable parts = fnameBase.split (".");
184            if (isTopLevelDomain (parts[0]))
185                cpt.setId (fnameBase[0..$-8]);
186            else
187                cpt.setId (fnameBase);
188        } else {
189            cpt.setId (fnameBase);
190        }
191        cpt.setKind (ComponentKind.DESKTOP_APP);
192        gres.addComponent (cpt);
193    }
194
195    void checkDesktopString (string fieldId, string str)
196    {
197        if (((str.startsWith ("\"")) && (str.endsWith ("\""))) ||
198            ((str.startsWith ("\'")) && (str.endsWith ("\'")))) {
199                gres.addHint (cpt, "metainfo-quoted-value", ["value": str, "field": fieldId]);
200            }
201    }
202
203    immutable hadExistingCptName = !cpt.getName ().empty;
204    immutable hadExistingCptSummary = !cpt.getSummary ().empty;
205
206    size_t dummy;
207    auto keys = df.getKeys (DESKTOP_GROUP, dummy);
208    foreach (string key; keys) {
209        string locale;
210        locale = getLocaleFromKey (key);
211        if (locale is null)
212            continue;
213
214        if (key.startsWith ("Name")) {
215            if (hadExistingCptName)
216                continue;
217
218            immutable val = getValue (df, key);
219            checkDesktopString (key, val);
220            /* run backend specific hooks */
221            auto translations = gres.pkg.getDesktopFileTranslations (df, val);
222            translations[locale] = val;
223            foreach (key, value; translations)
224                cpt.setName (value, key);
225        } else if (key.startsWith ("Comment")) {
226            if (hadExistingCptSummary)
227                continue;
228
229            immutable val = getValue (df, key);
230            checkDesktopString (key, val);
231            auto translations = gres.pkg.getDesktopFileTranslations (df, val);
232            translations[locale] = val;
233
234            foreach (ref key, ref value; translations)
235                cpt.setSummary (value, key);
236        } else if (key == "Categories") {
237            auto value = getValue (df, key);
238            auto cats = value.split (";");
239            cats = filterCategories (cpt, gres, cats);
240            if (cats.empty)
241                continue;
242
243            foreach (ref c; cats)
244                cpt.addCategory (c);
245        } else if (key.startsWith ("Keywords")) {
246            auto val = getValue (df, key);
247            auto translations = gres.pkg.getDesktopFileTranslations (df, val);
248            translations[locale] = val;
249
250            foreach (ref key, ref value; translations) {
251                auto kws = value.split (";").stripRight ("");
252                if (kws.empty)
253                    continue;
254                cpt.setKeywords (kws, key);
255            }
256        } else if (key == "MimeType") {
257            auto value = getValue (df, key);
258            immutable mts = value.split (";");
259            if (mts.empty)
260                continue;
261
262            Provided prov = cpt.getProvidedForKind (ProvidedKind.MIMETYPE);
263            if (prov is null) {
264                prov = new Provided ();
265                prov.setKind (ProvidedKind.MIMETYPE);
266            }
267
268            foreach (ref mt; mts) {
269                if (!mt.empty)
270                    prov.addItem (mt);
271            }
272            cpt.addProvided (prov);
273        } else if (key == "Icon") {
274            auto icon = new Icon ();
275            icon.setKind (IconKind.STOCK);
276            icon.setName (getValue (df, key));
277            cpt.addIcon (icon);
278        }
279    }
280
281    return cpt;
282}
283
284unittest
285{
286    import std.stdio: writeln;
287    import asgen.backends.dummy.dummypkg;
288    writeln ("TEST: ", ".desktop file parser");
289
290    auto data = """
291[Desktop Entry]
292Name=FooBar
293Name[de_DE]=FööBär
294Comment=A foo-ish bar.
295Keywords=Flubber;Test;Meh;
296Keywords[de_DE]=Goethe;Schiller;Kant;
297""";
298
299    auto pkg = new DummyPackage ("pkg", "1.0", "amd64");
300    auto res = new GeneratorResult (pkg);
301    auto cpt = parseDesktopFile (res, "foobar.desktop", data, false);
302    assert (cpt !is null);
303
304    cpt = res.getComponent ("foobar.desktop");
305    assert (cpt !is null);
306
307    assert (cpt.getName () == "FooBar");
308    assert (cpt.getKeywords () == ["Flubber", "Test", "Meh"]);
309
310    cpt.setActiveLocale ("de_DE");
311    assert (cpt.getName () == "FööBär");
312    assert (cpt.getKeywords () == ["Goethe", "Schiller", "Kant"]);
313
314    // test component-id trimming
315    res = new GeneratorResult (pkg);
316    cpt = parseDesktopFile (res, "org.example.foobar.desktop", data, false);
317    assert (cpt !is null);
318
319    cpt = res.getComponent ("org.example.foobar");
320    assert (cpt !is null);
321
322    // test preexisting component
323    res = new GeneratorResult (pkg);
324    auto ecpt = new Component ();
325    ecpt.setKind (ComponentKind.DESKTOP_APP);
326    ecpt.setId ("org.example.foobar");
327    ecpt.setName ("TestX", "C");
328    ecpt.setSummary ("Summary of TestX", "C");
329    res.addComponent (ecpt);
330
331    cpt = parseDesktopFile (res, "org.example.foobar.desktop", data, false);
332    assert (cpt !is null);
333    cpt = res.getComponent ("org.example.foobar");
334    assert (cpt !is null);
335
336    assert (cpt.getName () == "TestX");
337    assert (cpt.getSummary () == "Summary of TestX");
338    assert (cpt.getKeywords () == ["Flubber", "Test", "Meh"]);
339
340    // legacy
341    Config.get ().formatVersion = FormatVersion.V0_8;
342    res = new GeneratorResult (pkg);
343    cpt = parseDesktopFile (res, "org.example.foobar.desktop", data, false);
344    assert (cpt !is null);
345
346    cpt = res.getComponent ("org.example.foobar.desktop");
347    assert (cpt !is null);
348    Config.get ().formatVersion = FormatVersion.V0_10;
349}
Note: See TracBrowser for help on using the repository browser.