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

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

Initial release

File size: 16.7 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.zarchive;
21
22import std.stdio;
23import std.string;
24import std.regex;
25import std.conv : to;
26import std.path : buildNormalizedPath;
27import std.array : appender;
28import std.typecons : RefCounted, RefCountedAutoInitialize;
29import std.concurrency : Generator, yield;
30static import std.file;
31
32import asgen.logging;
33import asgen.utils : GENERIC_BUFFER_SIZE;
34
35import asgen.bindings.libarchive;
36
37private immutable DEFAULT_BLOCK_SIZE = 65536;
38
39enum ArchiveType
40{
41    GZIP,
42    XZ
43}
44
45private const(char)[] getArchiveErrorMessage (archive *ar)
46{
47    return fromStringz (archive_error_string (ar));
48}
49
50private string readArchiveData (archive *ar, string name = null)
51{
52    archive_entry *ae;
53    int ret;
54    size_t size;
55    char[GENERIC_BUFFER_SIZE] buff;
56    auto data = appender!string;
57
58    ret = archive_read_next_header (ar, &ae);
59
60    if (ret == ARCHIVE_EOF)
61        return data.data;
62
63    if (ret != ARCHIVE_OK) {
64        if (name is null)
65            throw new Exception (format ("Unable to read header of compressed data: %s", getArchiveErrorMessage (ar)));
66        else
67            throw new Exception (format ("Unable to read header of compressed file '%s': %s", name, getArchiveErrorMessage (ar)));
68    }
69
70    while (true) {
71        size = archive_read_data (ar, cast(void*) buff, GENERIC_BUFFER_SIZE);
72        if (size < 0) {
73            if (name is null)
74                throw new Exception (format ("Failed to read compressed data: %s", getArchiveErrorMessage (ar)));
75            else
76                throw new Exception (format ("Failed to read data from '%s': %s", name, getArchiveErrorMessage (ar)));
77        }
78
79        if (size == 0)
80            break;
81
82        data ~= buff[0..size];
83    }
84
85    return data.data;
86}
87
88string decompressFile (string fname)
89{
90    int ret;
91
92    archive *ar = archive_read_new ();
93    scope(exit) archive_read_free (ar);
94
95    archive_read_support_format_raw (ar);
96    archive_read_support_format_empty (ar);
97    archive_read_support_filter_all (ar);
98
99    ret = archive_read_open_filename (ar, toStringz (fname), DEFAULT_BLOCK_SIZE);
100    if (ret != ARCHIVE_OK)
101        throw new Exception (format ("Unable to open compressed file '%s': %s", fname, getArchiveErrorMessage (ar)));
102
103    return readArchiveData (ar, fname);
104}
105
106string decompressData (ubyte[] data)
107{
108    int ret;
109
110    archive *ar = archive_read_new ();
111    scope(exit) archive_read_free (ar);
112
113    archive_read_support_filter_all (ar);
114    archive_read_support_format_empty (ar);
115    archive_read_support_format_raw (ar);
116
117    auto dSize = ubyte.sizeof * data.length;
118    ret = archive_read_open_memory (ar, cast(void*) data, dSize);
119    if (ret != ARCHIVE_OK)
120        throw new Exception (format ("Unable to open compressed data: %s", getArchiveErrorMessage (ar)));
121
122    return readArchiveData (ar);
123}
124
125struct ArchiveDecompressor
126{
127
128private:
129    string archive_fname;
130
131    const(ubyte)[] readEntry (archive *ar)
132    {
133        const void *buff = null;
134        size_t size = 0UL;
135        long offset = 0;
136        auto res = appender!(ubyte[]);
137
138        while (archive_read_data_block (ar, &buff, &size, &offset) == ARCHIVE_OK) {
139            res ~= cast(ubyte[]) buff[0..size];
140        }
141
142        return res.data;
143        }
144
145    void extractEntryTo (archive *ar, string fname)
146    {
147        const void *buff = null;
148        size_t size = 0UL;
149        long offset = 0;
150        long output_offset = 0;
151
152        auto f = File (fname, "w"); // open for writing
153
154        while (archive_read_data_block (ar, &buff, &size, &offset) == ARCHIVE_OK) {
155            if (offset > output_offset) {
156                f.seek (offset - output_offset, SEEK_CUR);
157                output_offset = offset;
158            }
159            while (size > 0) {
160                auto bytes_to_write = size;
161                if (bytes_to_write > DEFAULT_BLOCK_SIZE)
162                    bytes_to_write = DEFAULT_BLOCK_SIZE;
163
164                f.rawWrite (buff[0..bytes_to_write]);
165                output_offset += bytes_to_write;
166                size -= bytes_to_write;
167            }
168        }
169    }
170
171    archive *openArchive ()
172    {
173        archive *ar = archive_read_new ();
174
175        archive_read_support_filter_all (ar);
176        archive_read_support_format_all (ar);
177
178        auto ret = archive_read_open_filename (ar, archive_fname.toStringz, DEFAULT_BLOCK_SIZE);
179        if (ret != ARCHIVE_OK)
180            throw new Exception (format ("Unable to open compressed file '%s': %s",
181                                 archive_fname,
182                                 getArchiveErrorMessage (ar)));
183
184        return ar;
185    }
186
187    bool pathMatches (string path1, string path2) {
188        import std.path;
189
190        if (path1 == path2)
191            return true;
192
193        auto path1Abs = buildNormalizedPath ("/", path1);
194        auto path2Abs = buildNormalizedPath ("/", path2);
195
196        if (path1Abs == path2Abs)
197            return true;
198
199        return false;
200    }
201
202public:
203
204    struct ArchiveEntry
205    {
206        string fname;
207        const(ubyte)[] data;
208    }
209
210    void open (string fname)
211    {
212        archive_fname = fname;
213    }
214
215    bool isOpen ()
216    {
217        return archive_fname !is null;
218    }
219
220    bool extractFileTo (string fname, string fdest)
221    {
222        archive_entry *en;
223
224        auto ar = openArchive ();
225        scope(exit) archive_read_free (ar);
226
227        while (archive_read_next_header (ar, &en) == ARCHIVE_OK) {
228            auto pathname = fromStringz (archive_entry_pathname (en));
229
230            if (pathMatches (fname, to!string (pathname))) {
231                this.extractEntryTo (ar, fdest);
232                return true;
233                    } else {
234                archive_read_data_skip (ar);
235            }
236        }
237
238        return false;
239    }
240
241    void extractArchive (const string dest)
242    in { assert (std.file.isDir (dest)); }
243    body
244    {
245        import std.path;
246        archive_entry *en;
247
248        auto ar = openArchive ();
249        scope(exit) archive_read_free (ar);
250
251        while (archive_read_next_header (ar, &en) == ARCHIVE_OK) {
252            auto pathname = buildPath (dest, archive_entry_pathname (en).fromStringz);
253            /* at the moment we only handle directories and files */
254            if (archive_entry_filetype (en) == AE_IFDIR) {
255                if (!std.file.exists (pathname))
256                    std.file.mkdir (pathname);
257                continue;
258            }
259
260            if (archive_entry_filetype (en) == AE_IFREG) {
261                this.extractEntryTo (ar, pathname);
262            }
263        }
264    }
265
266    const(ubyte)[] readData (string fname)
267    {
268        import core.sys.posix.sys.stat;
269        import std.path;
270        archive_entry *en;
271
272        auto ar = openArchive ();
273        scope(exit) archive_read_free (ar);
274
275        auto fnameAbs = absolutePath (fname, "/");
276        while (archive_read_next_header (ar, &en) == ARCHIVE_OK) {
277            auto pathname = fromStringz (archive_entry_pathname (en));
278
279            if (pathMatches (fname, to!string (pathname))) {
280                auto filetype = archive_entry_filetype (en);
281
282                if (filetype == S_IFDIR) {
283                    /* we don't extract directories explicitly */
284                    throw new Exception (format ("Path %s is a directory and can not be extracted.", fname));
285                }
286
287                /* check if we are dealing with a symlink */
288                if (filetype == S_IFLNK) {
289                    string linkTarget = to!string (fromStringz (archive_entry_symlink (en)));
290                    if (linkTarget is null)
291                        throw new Exception (format ("Unable to read destination of symbolic link for %s.", fname));
292
293                    if (!isAbsolute (linkTarget))
294                        linkTarget = absolutePath (linkTarget, dirName (fnameAbs));
295
296                    return this.readData (buildNormalizedPath (linkTarget));
297                }
298
299                if (filetype != S_IFREG) {
300                    // we really don't want to extract special files from a tarball - usually, those shouldn't
301                    // be present anyway.
302                    // This should probably be an error, but return nothing for now.
303                    return null;
304                    }
305
306                return this.readEntry (ar);
307                    } else {
308                archive_read_data_skip (ar);
309            }
310        }
311
312        throw new Exception (format ("File %s was not found in the archive.", fname));
313    }
314
315    string[] extractFilesByRegex (Regex!char re, string destdir)
316    {
317        import std.path;
318        archive_entry *en;
319        auto matches = appender!(string[]);
320
321        auto ar = openArchive ();
322        scope(exit) archive_read_free (ar);
323
324        while (archive_read_next_header (ar, &en) == ARCHIVE_OK) {
325            auto pathname = fromStringz (archive_entry_pathname (en));
326
327            auto m = matchFirst (pathname, re);
328            if (!m.empty) {
329                auto fdest = buildPath (destdir, baseName (pathname));
330                this.extractEntryTo (ar, fdest);
331                matches ~= fdest;
332                    } else {
333                archive_read_data_skip (ar);
334            }
335        }
336
337        return matches.data;
338    }
339
340    string[] readContents ()
341    {
342        import std.conv : to;
343        archive_entry *en;
344
345        auto ar = openArchive ();
346        scope (exit) archive_read_free (ar);
347
348        auto contents = appender!(string[]);
349        while (archive_read_next_header (ar, &en) == ARCHIVE_OK) {
350            auto pathname = fromStringz (archive_entry_pathname (en));
351
352            // ignore directories
353            if (pathname.endsWith ("/"))
354                continue;
355
356            auto path = buildNormalizedPath ("/", to!string (pathname));
357            contents ~= path;
358        }
359
360        return contents.data;
361    }
362
363    /**
364     * Returns a generator to iterate over the contents of this tarball.
365     */
366    auto read ()
367    {
368        import core.sys.posix.sys.stat;
369        import std.path;
370
371        auto gen = new Generator!ArchiveEntry (
372        {
373            archive_entry *en;
374
375            auto ar = openArchive ();
376            scope (exit) archive_read_free (ar);
377
378            while (archive_read_next_header (ar, &en) == ARCHIVE_OK) {
379                auto pathname = fromStringz (archive_entry_pathname (en));
380
381                // ignore directories
382                if (pathname.endsWith ("/"))
383                    continue;
384
385                auto path = std.path.buildNormalizedPath ("/", to!string (pathname));
386
387                ArchiveEntry aentry;
388                aentry.fname = path;
389                aentry.data = null;
390
391                auto filetype = archive_entry_filetype (en);
392                /* check if we are dealing with a symlink */
393                if (filetype == S_IFLNK) {
394                    auto linkTarget = to!string (fromStringz (archive_entry_symlink (en)));
395                    if (linkTarget is null)
396                        throw new Exception (format ("Unable to read destination of symbolic link for %s.", path));
397
398                    // we cheat here and set the link target as data
399                    // TODO: Proper handling of symlinks, e.g. by adding a filetype property to ArchiveEntry.
400                    aentry.data = cast(ubyte[]) linkTarget;
401                    yield (aentry);
402                    continue;
403                }
404
405                if (filetype != S_IFREG) {
406                    yield (aentry);
407                    continue;
408                }
409
410                aentry.data = this.readEntry (ar);
411                yield (aentry);
412            }
413        });
414
415        return gen;
416    }
417}
418
419/**
420 * Save data to a compressed file.
421 *
422 * Params:
423 *      data = The data to save.
424 *      fname = The filename the data should be saved to.
425 *      atype = The archive type (GZ or XZ).
426 */
427void compressAndSave (ubyte[] data, const string fname, ArchiveType atype)
428{
429    auto ar = archive_write_new ();
430    scope (exit) archive_write_free (ar);
431
432    archive_write_set_format_raw (ar);
433    if (atype == ArchiveType.GZIP) {
434        archive_write_add_filter_gzip (ar);
435        archive_write_set_filter_option (ar, "gzip", "timestamp", null);
436    } else {
437        archive_write_add_filter_xz (ar);
438    }
439
440    auto ret = archive_write_open_filename (ar, fname.toStringz);
441    if (ret != ARCHIVE_OK)
442        throw new Exception (format ("Unable to open file '%s': %s", fname, getArchiveErrorMessage (ar)));
443
444    archive_entry *entry;
445    entry = archive_entry_new ();
446    scope (exit) archive_entry_free (entry);
447
448    archive_entry_set_filetype (entry, AE_IFREG);
449    archive_entry_set_size (entry, ubyte.sizeof * data.length);
450    archive_write_header (ar, entry);
451
452    archive_write_data (ar, cast(void*) data, ubyte.sizeof * data.length);
453    archive_write_close (ar);
454}
455
456final class ArchiveCompressor
457{
458
459private:
460    string archiveFname;
461    archive *ar;
462    bool closed;
463
464public:
465
466    this (ArchiveType type)
467    {
468        ar = archive_write_new ();
469
470        if (type == ArchiveType.GZIP) {
471            archive_write_add_filter_gzip (ar);
472            archive_write_set_filter_option (ar, "gzip", "timestamp", null);
473        } else {
474            archive_write_add_filter_xz (ar);
475        }
476
477        archive_write_set_format_pax_restricted (ar);
478        closed = true;
479    }
480
481    ~this ()
482    {
483        close ();
484        archive_write_free (ar);
485    }
486
487    void open (string fname)
488    {
489        archiveFname = fname;
490        auto ret = archive_write_open_filename (ar, fname.toStringz);
491        if (ret != ARCHIVE_OK)
492            throw new Exception (format ("Unable to open file '%s'", fname, getArchiveErrorMessage (ar)));
493        closed = false;
494    }
495
496    bool isOpen ()
497    {
498        return !closed;
499    }
500
501    void close ()
502    {
503        if (closed)
504            return;
505        archive_write_close (ar);
506        closed = true;
507    }
508
509    void addFile (string fname, string dest = null)
510    in {
511        if (!std.file.exists (fname)) {
512            logError ("File %s does not exist!", fname);
513            assert (0);
514        }
515    }
516    body
517    {
518        import std.conv: octal;
519        import std.path: baseName;
520        import core.sys.posix.sys.stat;
521
522        immutable BUFFER_SIZE = 8192;
523        archive_entry *entry;
524        stat_t st;
525        ubyte[BUFFER_SIZE] buff;
526
527        if (dest is null)
528            dest = baseName (fname);
529
530        lstat (fname.toStringz, &st);
531        entry = archive_entry_new ();
532        scope (exit) archive_entry_free (entry);
533        archive_entry_set_pathname (entry, toStringz (dest));
534
535        archive_entry_set_size (entry, st.st_size);
536        archive_entry_set_filetype (entry, S_IFREG);
537        archive_entry_set_perm (entry, octal!755);
538        archive_entry_set_mtime (entry, st.st_mtime, 0);
539
540        synchronized {
541            archive_write_header (ar, entry);
542
543            auto f = File (fname, "r");
544            while (!f.eof) {
545                auto data = f.rawRead (buff);
546                archive_write_data (ar, cast(void*) data, ubyte.sizeof * data.length);
547            }
548        }
549    }
550
551}
552
553version (unittest) {
554    version (GNU) {
555        extern(C) char *mkdtemp (char *) nothrow @nogc;
556    } else {
557        import core.sys.posix.stdlib : mkdtemp;
558    }
559}
560
561unittest
562{
563    writeln ("TEST: ", "Compressed empty file");
564
565    ubyte[] emptyGz = [
566       0x1f, 0x8b, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00,
567       0x00, 0x03, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x00,
568       0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
569       0x00, 0x00,
570    ];
571    assert (decompressData (emptyGz) == "");
572
573    writeln ("TEST: ", "Extracting a tarball");
574
575    import std.file : tempDir;
576    import std.path : buildPath;
577    import asgen.utils : getTestSamplesDir;
578
579    auto archive = buildPath (getTestSamplesDir (), "test.tar.xz");
580    assert (std.file.exists (archive));
581    auto ar = new ArchiveDecompressor ();
582
583    auto tmpdir = buildPath (tempDir, "asgenXXXXXX");
584    auto ctmpdir = new char[](tmpdir.length + 1);
585    ctmpdir[0 .. tmpdir.length] = tmpdir[];
586    ctmpdir[$ - 1] = '\0';
587
588    tmpdir = to!string(mkdtemp (ctmpdir.ptr));
589    scope(exit) std.file.rmdirRecurse (tmpdir);
590
591    ar.open (archive);
592    ar.extractArchive (tmpdir);
593
594    auto path = buildPath (tmpdir, "b", "a");
595    assert (std.file.exists (path));
596    assert (std.file.readText (path).chomp == "hello");
597}
Note: See TracBrowser for help on using the repository browser.