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

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

Initial release

File size: 14.2 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.utils;
21@safe:
22
23import std.stdio : File, write, writeln;
24import std.string;
25import std.ascii : letters, digits;
26import std.conv : to;
27import std.random : randomSample;
28import std.range : chain;
29import std.algorithm : startsWith;
30import std.array : appender, empty;
31import std.path : buildPath, dirName, buildNormalizedPath;
32import std.typecons : Nullable;
33import std.datetime : Clock, parseRFC822DateTime, SysTime;
34static import std.file;
35
36import appstream.Component;
37import appstream.Icon;
38
39import asgen.logging;
40
41public immutable DESKTOP_GROUP = "Desktop Entry";
42
43public immutable GENERIC_BUFFER_SIZE = 2048;
44
45struct ImageSize
46{
47    uint width;
48    uint height;
49
50    this (uint w, uint h)
51    {
52        width = w;
53        height = h;
54    }
55
56    this (uint s)
57    {
58        width = s;
59        height = s;
60    }
61
62    string toString () const
63    {
64        return format ("%sx%s", width, height);
65    }
66
67    uint toInt () const
68    {
69        if (width > height)
70            return width;
71        return height;
72    }
73
74    int opCmp (const ImageSize s) const
75    {
76        // only compares width, should be enough for now
77        if (this.width > s.width)
78            return 1;
79        if (this.width == s.width)
80            return 0;
81        return -1;
82    }
83}
84
85/**
86 * Generate a random alphanumeric string.
87 */
88@trusted
89string randomString (uint len)
90{
91    auto asciiLetters = to! (dchar[]) (letters);
92    auto asciiDigits = to! (dchar[]) (digits);
93
94    if (len == 0)
95        len = 1;
96
97    auto res = to!string (randomSample (chain (asciiLetters, asciiDigits), len));
98    return res;
99}
100
101/**
102 * Check if the locale is a valid locale which we want to include
103 * in the resulting metadata. Some locales added just for testing
104 * by upstreams should be filtered out.
105 */
106@safe
107bool localeValid (string locale) pure
108{
109    switch (locale) {
110        case "x-test":
111        case "xx":
112            return false;
113        default:
114            return true;
115    }
116}
117
118/**
119 * Check if the given string is a top-level domain name.
120 * The TLD list of AppStream is incomplete, but it will
121 * cover 99% of all cases.
122 * (in a short check on Debian, it covered all TLDs in use there)
123 */
124@trusted
125bool isTopLevelDomain (const string value) pure
126{
127    import asgen.bindings.appstream_utils;
128    if (value.empty)
129        return false;
130    return as_utils_is_tld (value.toStringz);
131}
132
133/**
134 * Build a global component ID.
135 *
136 * The global-id is used as a global, unique identifier for this component.
137 * (while the component-ID is local, e.g. for one suite).
138 * Its primary usecase is to identify a media directory on the filesystem which is
139 * associated with this component.
140 **/
141@trusted
142string buildCptGlobalID (string cid, string checksum, bool allowNoChecksum = false) pure
143in { assert (cid.length >= 2); }
144body
145{
146    if (cid is null)
147        return null;
148    if ((!allowNoChecksum) && (checksum is null))
149            return null;
150    if (checksum is null)
151        checksum = "";
152
153    // check whether we can build the gcid by using the reverse domain name,
154    // or whether we should use the simple standard splitter.
155    auto reverseDomainSplit = false;
156    immutable parts = cid.split (".");
157    if (parts.length > 2) {
158        // check if we have a valid TLD. If so, use the reverse-domain-name splitting.
159        if (isTopLevelDomain (parts[0]))
160            reverseDomainSplit = true;
161    }
162
163    string gcid;
164    if (reverseDomainSplit)
165        gcid = "%s/%s/%s/%s".format (parts[0].toLower(), parts[1], join (parts[2..$], "."), checksum);
166    else
167        gcid = "%s/%s/%s/%s".format (cid[0].toLower(), cid[0..2].toLower(), cid, checksum);
168
169    return gcid;
170}
171
172/**
173 * Get the component-id back from a global component-id.
174 */
175@trusted
176string getCidFromGlobalID (string gcid) pure
177{
178    import asgen.bindings.appstream_utils;
179
180    auto parts = gcid.split ("/");
181    if (parts.length != 4)
182        return null;
183    if (isTopLevelDomain (parts[0])) {
184        return join (parts[0..3], ".");
185    }
186
187    return parts[2];
188}
189
190@trusted
191void hardlink (const string srcFname, const string destFname)
192{
193    import core.sys.posix.unistd;
194    import core.stdc.string;
195    import core.stdc.errno;
196
197    immutable res = link (srcFname.toStringz, destFname.toStringz);
198    if (res != 0)
199        throw new std.file.FileException ("Unable to create link: %s".format (errno.strerror));
200}
201
202/**
203 * Copy a directory using multiple threads.
204 * This function follows symbolic links,
205 * and replaces them with actual directories
206 * in destDir.
207 *
208 * Params:
209 *      srcDir = Source directory to copy.
210 *      destDir = Path to the destination directory.
211 *      useHardlinks = Use hardlinks instead of copying files.
212 */
213void copyDir (in string srcDir, in string destDir, bool useHardlinks = false) @trusted
214{
215    import std.file;
216    import std.path;
217    import std.parallelism;
218    import std.array : appender;
219
220    auto deSrc = DirEntry (srcDir);
221    auto files = appender!(string[]);
222
223    if (!exists (destDir)) {
224        mkdirRecurse (destDir);
225    }
226
227        auto deDest = DirEntry (destDir);
228    if(!deDest.isDir ()) {
229        throw new FileException (deDest.name, " is not a directory");
230    }
231
232    immutable destRoot = deDest.name ~ '/';
233
234    if (!deSrc.isDir ()) {
235        if (useHardlinks)
236            hardlink (deSrc.name, destRoot);
237        else
238            std.file.copy (deSrc.name, destRoot);
239    } else {
240        auto srcLen = deSrc.name.length;
241        if (!std.file.exists (destRoot))
242            mkdir (destRoot);
243
244        // make an array of the regular files and create the directory structure
245        // Since it is SpanMode.breadth, we can just use mkdir
246        foreach (DirEntry e; dirEntries (deSrc.name, SpanMode.breadth, true)) {
247            if (attrIsDir (e.attributes)) {
248                auto childDir = destRoot ~ e.name[srcLen..$];
249                mkdir (childDir);
250            } else {
251                files ~= e.name;
252            }
253        }
254
255        // parallel foreach for regular files
256        foreach (fn; taskPool.parallel (files.data, 100)) {
257            immutable destFn = destRoot ~ fn[srcLen..$];
258
259            if (useHardlinks)
260                hardlink (fn, destFn);
261            else
262                std.file.copy (fn, destFn);
263        }
264    }
265}
266
267/**
268 * Escape XML characters.
269 */
270@safe
271S escapeXml (S) (S s) pure
272{
273    string r;
274    size_t lastI;
275    auto result = appender!S ();
276
277    foreach (i, c; s) {
278        switch (c) {
279            case '&':  r = "&amp;"; break;
280            case '"':  r = "&quot;"; break;
281            case '\'': r = "&apos;"; break;
282            case '<':  r = "&lt;"; break;
283            case '>':  r = "&gt;"; break;
284            default: continue;
285        }
286
287        // Replace with r
288        result.put (s[lastI .. i]);
289        result.put (r);
290        lastI = i + 1;
291    }
292
293    if (!result.data.ptr)
294        return s;
295    result.put (s[lastI .. $]);
296    return result.data;
297}
298
299/**
300 * Get full path for an AppStream generator data file.
301 */
302@safe
303string getDataPath (string fname)
304{
305    import std.path;
306    auto exeDir = dirName (std.file.thisExePath ());
307
308    if (exeDir.startsWith ("/usr"))
309        return buildPath ("/usr/share/appstream", fname);
310
311    auto resPath = buildNormalizedPath (exeDir, "..", "data", fname);
312    if (!std.file.exists (resPath))
313        return buildPath ("/usr/share/appstream", fname);
314
315    return resPath;
316}
317
318/**
319 * Check if a path exists and is a directory.
320 */
321bool existsAndIsDir (string path) @safe
322{
323    if (std.file.exists (path))
324        if (std.file.isDir (path))
325            return true;
326    return false;
327}
328
329/**
330 * Convert a string array into a byte array.
331 */
332ubyte[] stringArrayToByteArray (string[] strArray) pure @trusted
333{
334    auto res = appender!(ubyte[]);
335    res.reserve (strArray.length * 2); // make a guess, we will likely need much more space
336
337    foreach (ref s; strArray) {
338        res ~= cast(ubyte[]) s;
339    }
340
341    return res.data;
342}
343
344/**
345 * Check if string contains a remote URI.
346 */
347@safe
348bool isRemote (const string uri)
349{
350    import std.regex;
351
352    auto uriregex = ctRegex!(`^(https?|ftps?)://`);
353    auto match = matchFirst (uri, uriregex);
354
355    return (!match.empty);
356}
357
358private immutable(Nullable!SysTime) download (const string url, ref File dest, const uint retryCount = 5) @trusted
359in { assert (url.isRemote); }
360body
361{
362    import core.time;
363    import std.net.curl : CurlException, HTTP, FTP;
364
365    Nullable!SysTime ret;
366
367    size_t onReceiveCb (File f, ubyte[] data)
368    {
369        f.rawWrite (data);
370        return data.length;
371    }
372
373    /* the curl library is stupid; you can't make an AutoProtocol to set timeouts */
374    logDebug ("Downloading %s", url);
375    try {
376        if (url.startsWith ("http")) {
377            auto downloader = HTTP (url);
378            downloader.connectTimeout = dur!"seconds" (30);
379            downloader.dataTimeout = dur!"seconds" (30);
380            downloader.onReceive = (data) => onReceiveCb (dest, data);
381            downloader.perform();
382            if ("last-modified" in downloader.responseHeaders) {
383                    auto lastmodified = downloader.responseHeaders["last-modified"];
384                    ret = parseRFC822DateTime(lastmodified);
385            }
386        } else {
387            auto downloader = FTP (url);
388            downloader.connectTimeout = dur!"seconds" (30);
389            downloader.dataTimeout = dur!"seconds" (30);
390            downloader.onReceive = (data) => onReceiveCb (dest, data);
391            downloader.perform();
392        }
393        logDebug ("Downloaded %s", url);
394    } catch (CurlException e) {
395        if (retryCount > 0) {
396            logDebug ("Failed to download %s, will retry %d more %s",
397                      url,
398                      retryCount,
399                      retryCount > 1 ? "times" : "time");
400            download (url, dest, retryCount - 1);
401        } else {
402            throw e;
403        }
404    }
405
406    return ret;
407}
408
409/**
410 * Download or open `path` and return it as a string array.
411 *
412 * Params:
413 *      path = The path to access.
414 *
415 * Returns: The data if successful.
416 */
417string[] getFileContents (const string path, const uint retryCount = 5) @trusted
418{
419    import core.stdc.stdlib : free;
420    import core.sys.posix.stdio : fclose, open_memstream;
421
422    char * ptr = null;
423    scope (exit) free (ptr);
424
425    size_t sz = 0;
426
427    if (path.isRemote) {
428        {
429            auto f = open_memstream (&ptr, &sz);
430            scope (exit) fclose (f);
431            auto file = File.wrapFile (f);
432            download (path, file, retryCount);
433        }
434
435        return to!string (ptr.fromStringz).splitLines;
436    } else {
437        if (!std.file.exists (path))
438            throw new Exception ("No such file '%s'", path);
439
440        return std.file.readText (path).splitLines;
441    }
442}
443
444/**
445 * Download `url` to `dest`.
446 *
447 * Params:
448 *      url = The URL to download.
449 *      dest = The location for the downloaded file.
450 *      retryCount = Number of times to retry on timeout.
451 */
452void downloadFile (const string url, const string dest, const uint retryCount = 5) @trusted
453in  { assert (url.isRemote); }
454out { assert (std.file.exists (dest)); }
455body
456{
457    import std.file;
458
459    if (dest.exists) {
460        logDebug ("Already downloaded '%s' into '%s', won't redownload", url, dest);
461        return;
462    }
463
464    mkdirRecurse (dest.dirName);
465
466    auto f = File (dest, "wb");
467    scope (failure) remove (dest);
468
469    auto time = download (url, f, retryCount);
470
471    f.close ();
472    if (!time.isNull)
473        setTimes (dest, Clock.currTime, time);
474}
475
476/**
477 * Get path of the directory with test samples.
478 * The function will look for test data in the current
479 * working directory.
480 */
481string
482getTestSamplesDir () @trusted
483{
484    import std.path : getcwd;
485
486    auto path = buildPath (getcwd (), "test", "samples");
487    if (std.file.exists (path))
488        return path;
489    path = buildNormalizedPath (getcwd (), "..", "test", "samples");
490
491    return path;
492}
493
494/**
495 * Return stock icon for this component, or null if it does not
496 * have one.
497 */
498@system
499Nullable!Icon componentGetStockIcon (ref Component cpt)
500{
501    Nullable!Icon res;
502    auto iconsArr = cpt.getIcons ();
503
504    for (uint i = 0; i < iconsArr.len; i++) {
505        // cast array data to D AsIcon and keep a reference to the C struct
506        auto icon = new Icon (cast (AsIcon*) iconsArr.index (i));
507        if (icon.getKind() == IconKind.STOCK) {
508            res = icon;
509            return res;
510        }
511    }
512
513    return res;
514}
515
516unittest
517{
518    writeln ("TEST: ", "GCID");
519
520    assert (buildCptGlobalID ("foobar.desktop", "DEADBEEF") == "f/fo/foobar.desktop/DEADBEEF");
521    assert (buildCptGlobalID ("org.gnome.yelp.desktop", "DEADBEEF") == "org/gnome/yelp.desktop/DEADBEEF");
522    assert (buildCptGlobalID ("noto-cjk.font", "DEADBEEF") == "n/no/noto-cjk.font/DEADBEEF");
523    assert (buildCptGlobalID ("io.sample.awesomeapp.sdk", "ABAD1DEA") == "io/sample/awesomeapp.sdk/ABAD1DEA");
524
525    assert (buildCptGlobalID ("io.sample.awesomeapp.sdk", null, true) == "io/sample/awesomeapp.sdk/");
526
527    assert (getCidFromGlobalID ("f/fo/foobar.desktop/DEADBEEF") == "foobar.desktop");
528    assert (getCidFromGlobalID ("org/gnome/yelp.desktop/DEADBEEF") == "org.gnome.yelp.desktop");
529
530    assert (ImageSize (80, 40).toString () == "80x40");
531    assert (ImageSize (1024, 420).toInt () == 1024);
532    assert (ImageSize (1024, 800) > ImageSize (64, 32));
533    assert (ImageSize (48) < ImageSize (64));
534
535    assert (stringArrayToByteArray (["A", "b", "C", "ö", "8"]) == [65, 98, 67, 195, 182, 56]);
536
537    assert (isRemote ("http://test.com"));
538    assert (isRemote ("https://example.org"));
539    assert (!isRemote ("/srv/mirror"));
540    assert (!isRemote ("file:///srv/test"));
541}
Note: See TracBrowser for help on using the repository browser.