about summary refs log tree commit diff stats
path: root/src/common.c
blob: 3a1b9cc4b912aee01f6f2c8c229f8c8a8d6f5179 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
void create_jid_from_null_returns_null(void **state);
void create_jid_from_empty_string_returns_null(void **state);
void create_jid_from_full_returns_full(void **state);
void create_jid_from_full_returns_bare(void **state);
void create_jid_from_full_returns_resourcepart(void **state);
void create_jid_from_full_returns_localpart(void **state);
void create_jid_from_full_returns_domainpart(void **state);
void create_jid_from_full_nolocal_returns_full(void **state);
void create_jid_from_full_nolocal_returns_bare(void **state);
void create_jid_from_full_nolocal_returns_resourcepart(void **state);
void create_jid_from_full_nolocal_returns_domainpart(void **state);
void create_jid_from_full_nolocal_returns_null_localpart(void **state);
void create_jid_from_bare_returns_null_full(void **state);
void create_jid_from_bare_returns_null_resource(void **state);
void create_jid_from_bare_returns_bare(void **state);
void create_jid_from_bare_returns_localpart(void **state);
void create_jid_from_bare_returns_domainpart(void **state);
void create_room_jid_returns_room(void **state);
void create_room_jid_returns_nick(void **state);
void create_with_slash_in_resource(void **state);
void create_with_at_in_resource(void **state);
void create_with_at_and_slash_in_resource(void **state);
void create_full_with_trailing_slash(void **state);
void returns_fulljid_when_exists(void **state);
void returns_barejid_when_fulljid_not_exists(void **state);
46' href='#n446'>446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
/*
 * common.c
 * vim: expandtab:ts=4:sts=4:sw=4
 *
 * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
 * Copyright (C) 2019 - 2022 Michael Vetter <jubalh@iodoru.org>
 *
 * This file is part of Profanity.
 *
 * Profanity is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Profanity 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give permission to
 * link the code of portions of this program with the OpenSSL library under
 * certain conditions as described in each individual source file, and
 * distribute linked combinations including the two.
 *
 * You must obey the GNU General Public License in all respects for all of the
 * code used other than OpenSSL. If you modify file(s) with this exception, you
 * may extend this exception to your version of the file(s), but you are not
 * obligated to do so. If you do not wish to do so, delete this exception
 * statement from your version. If you delete this exception statement from all
 * source files in the program, then also delete it here.
 *
 */

#include "config.h"

#include <errno.h>
#include <sys/select.h>
#include <assert.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <curl/curl.h>
#include <curl/easy.h>
#include <glib.h>
#include <gio/gio.h>

#ifdef HAVE_NCURSESW_NCURSES_H
#include <ncursesw/ncurses.h>
#elif HAVE_NCURSES_H
#include <ncurses.h>
#elif HAVE_CURSES_H
#include <curses.h>
#endif

#include "log.h"
#include "common.h"

struct curl_data_t
{
    char* buffer;
    size_t size;
};

static size_t _data_callback(void* ptr, size_t size, size_t nmemb, void* data);

gboolean
create_dir(const char* name)
{
    if (g_mkdir_with_parents(name, S_IRWXU) != 0) {
        log_error("Failed to create directory at '%s' with error '%s'", name, strerror(errno));
        return FALSE;
    }
    return TRUE;
}

gboolean
copy_file(const char* const sourcepath, const char* const targetpath, const gboolean overwrite_existing)
{
    GFile* source = g_file_new_for_path(sourcepath);
    GFile* dest = g_file_new_for_path(targetpath);
    GError* error = NULL;
    GFileCopyFlags flags = overwrite_existing ? G_FILE_COPY_OVERWRITE : G_FILE_COPY_NONE;
    gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, &error);
    if (error != NULL)
        g_error_free(error);
    g_object_unref(source);
    g_object_unref(dest);
    return success;
}

char*
str_replace(const char* string, const char* substr,
            const char* replacement)
{
    char* tok = NULL;
    char* newstr = NULL;
    char* head = NULL;

    if (string == NULL)
        return NULL;

    if (substr == NULL || replacement == NULL || (strcmp(substr, "") == 0))
        return strdup(string);

    newstr = strdup(string);
    head = newstr;

    while ((tok = strstr(head, substr))) {
        char* oldstr = newstr;
        newstr = malloc(strlen(oldstr) - strlen(substr) + strlen(replacement) + 1);

        if (newstr == NULL) {
            free(oldstr);
            return NULL;
        }

        memcpy(newstr, oldstr, tok - oldstr);
        memcpy(newstr + (tok - oldstr), replacement, strlen(replacement));
        memcpy(newstr + (tok - oldstr) + strlen(replacement),
               tok + strlen(substr),
               strlen(oldstr) - strlen(substr) - (tok - oldstr));
        memset(newstr + strlen(oldstr) - strlen(substr) + strlen(replacement), 0, 1);

        head = newstr + (tok - oldstr) + strlen(replacement);
        free(oldstr);
    }

    return newstr;
}

gboolean
strtoi_range(char* str, int* saveptr, int min, int max, char** err_msg)
{
    char* ptr;
    int val;

    errno = 0;
    val = (int)strtol(str, &ptr, 0);
    if (errno != 0 || *str == '\0' || *ptr != '\0') {
        GString* err_str = g_string_new("");
        g_string_printf(err_str, "Could not convert \"%s\" to a number.", str);
        *err_msg = err_str->str;
        g_string_free(err_str, FALSE);
        return FALSE;
    } else if (val < min || val > max) {
        GString* err_str = g_string_new("");
        g_string_printf(err_str, "Value %s out of range. Must be in %d..%d.", str, min, max);
        *err_msg = err_str->str;
        g_string_free(err_str, FALSE);
        return FALSE;
    }

    *saveptr = val;

    return TRUE;
}

int
utf8_display_len(const char* const str)
{
    if (!str) {
        return 0;
    }

    int len = 0;
    gchar* curr = g_utf8_offset_to_pointer(str, 0);
    while (*curr != '\0') {
        gunichar curru = g_utf8_get_char(curr);
        if (g_unichar_iswide(curru)) {
            len += 2;
        } else {
            len++;
        }
        curr = g_utf8_next_char(curr);
    }

    return len;
}

char*
release_get_latest(void)
{
    char* url = "https://profanity-im.github.io/profanity_version.txt";

    CURL* handle = curl_easy_init();
    struct curl_data_t output;
    output.buffer = NULL;
    output.size = 0;

    curl_easy_setopt(handle, CURLOPT_URL, url);
    curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, _data_callback);
    curl_easy_setopt(handle, CURLOPT_TIMEOUT, 2);
    curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void*)&output);

    curl_easy_perform(handle);
    curl_easy_cleanup(handle);

    if (output.buffer) {
        output.buffer[output.size++] = '\0';
        return output.buffer;
    } else {
        return NULL;
    }
}

gboolean
release_is_new(char* found_version)
{
    int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch;

    int parse_curr = sscanf(PACKAGE_VERSION, "%d.%d.%d", &curr_maj, &curr_min,
                            &curr_patch);
    int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min,
                             &found_patch);

    if (parse_found == 3 && parse_curr == 3) {
        if (found_maj > curr_maj) {
            return TRUE;
        } else if (found_maj == curr_maj && found_min > curr_min) {
            return TRUE;
        } else if (found_maj == curr_maj && found_min == curr_min
                   && found_patch > curr_patch) {
            return TRUE;
        } else {
            return FALSE;
        }
    } else {
        return FALSE;
    }
}

static size_t
_data_callback(void* ptr, size_t size, size_t nmemb, void* data)
{
    size_t realsize = size * nmemb;
    struct curl_data_t* mem = (struct curl_data_t*)data;
    mem->buffer = realloc(mem->buffer, mem->size + realsize + 1);

    if (mem->buffer) {
        memcpy(&(mem->buffer[mem->size]), ptr, realsize);
        mem->size += realsize;
        mem->buffer[mem->size] = 0;
    }

    return realsize;
}

char*
get_file_or_linked(char* loc, char* basedir)
{
    char* true_loc = NULL;

    // check for symlink
    if (g_file_test(loc, G_FILE_TEST_IS_SYMLINK)) {
        true_loc = g_file_read_link(loc, NULL);

        // if relative, add basedir
        if (!g_str_has_prefix(true_loc, "/") && !g_str_has_prefix(true_loc, "~")) {
            GString* base_str = g_string_new(basedir);
            g_string_append(base_str, "/");
            g_string_append(base_str, true_loc);
            free(true_loc);
            true_loc = base_str->str;
            g_string_free(base_str, FALSE);
        }
        // use given location
    } else {
        true_loc = strdup(loc);
    }

    return true_loc;
}

char*
strip_arg_quotes(const char* const input)
{
    char* unquoted = strdup(input);

    // Remove starting quote if it exists
    if (strchr(unquoted, '"')) {
        if (strchr(unquoted, ' ') + 1 == strchr(unquoted, '"')) {
            memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
        }
    }

    // Remove ending quote if it exists
    if (strchr(unquoted, '"')) {
        if (strchr(unquoted, '\0') - 1 == strchr(unquoted, '"')) {
            memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
        }
    }

    return unquoted;
}

gboolean
is_notify_enabled(void)
{
    gboolean notify_enabled = FALSE;

#ifdef HAVE_OSXNOTIFY
    notify_enabled = TRUE;
#endif
#ifdef HAVE_LIBNOTIFY
    notify_enabled = TRUE;
#endif
#ifdef PLATFORM_CYGWIN
    notify_enabled = TRUE;
#endif

    return notify_enabled;
}

GSList*
prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result)
{
    if (needle == NULL || haystack == NULL) {
        return *result;
    }

    gchar* haystack_curr = g_utf8_offset_to_pointer(haystack, offset);
    if (g_str_has_prefix(haystack_curr, needle)) {
        if (whole_word) {
            gunichar before = 0;
            gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, haystack_curr);
            if (haystack_before_ch) {
                before = g_utf8_get_char(haystack_before_ch);
            }

            gunichar after = 0;
            gchar* haystack_after_ch = haystack_curr + strlen(needle);
            if (haystack_after_ch[0] != '\0') {
                after = g_utf8_get_char(haystack_after_ch);
            }

            if (!g_unichar_isalnum(before) && !g_unichar_isalnum(after)) {
                *result = g_slist_append(*result, GINT_TO_POINTER(offset));
            }
        } else {
            *result = g_slist_append(*result, GINT_TO_POINTER(offset));
        }
    }

    offset++;
    if (g_strcmp0(g_utf8_offset_to_pointer(haystack, offset), "\0") != 0) {
        *result = prof_occurrences(needle, haystack, offset, whole_word, result);
    }

    return *result;
}

int
is_regular_file(const char* path)
{
    struct stat st;
    int ret = stat(path, &st);
    if (ret != 0) {
        perror(NULL);
        return 0;
    }
    return S_ISREG(st.st_mode);
}

int
is_dir(const char* path)
{
    struct stat st;
    int ret = stat(path, &st);
    if (ret != 0) {
        perror(NULL);
        return 0;
    }
    return S_ISDIR(st.st_mode);
}

void
get_file_paths_recursive(const char* path, GSList** contents)
{
    if (!is_dir(path)) {
        return;
    }

    GDir* directory = g_dir_open(path, 0, NULL);
    const gchar* entry = g_dir_read_name(directory);
    while (entry) {
        GString* full = g_string_new(path);
        if (!g_str_has_suffix(full->str, "/")) {
            g_string_append(full, "/");
        }
        g_string_append(full, entry);

        if (is_dir(full->str)) {
            get_file_paths_recursive(full->str, contents);
        } else if (is_regular_file(full->str)) {
            *contents = g_slist_append(*contents, full->str);
        }

        g_string_free(full, FALSE);
        entry = g_dir_read_name(directory);
    }
}

char*
get_random_string(int length)
{
    GRand* prng;
    char* rand;
    char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    int endrange = sizeof(alphabet) - 1;

    rand = calloc(length + 1, sizeof(char));

    prng = g_rand_new();

    for (int i = 0; i < length; i++) {
        rand[i] = alphabet[g_rand_int_range(prng, 0, endrange)];
    }
    g_rand_free(prng);

    return rand;
}

GSList*
get_mentions(gboolean whole_word, gboolean case_sensitive, const char* const message, const char* const nick)
{
    GSList* mentions = NULL;
    gchar* message_search = case_sensitive ? g_strdup(message) : g_utf8_strdown(message, -1);
    gchar* mynick_search = case_sensitive ? g_strdup(nick) : g_utf8_strdown(nick, -1);

    mentions = prof_occurrences(mynick_search, message_search, 0, whole_word, &mentions);

    g_free(message_search);
    g_free(mynick_search);

    return mentions;
}

gboolean
call_external(gchar** argv, gchar** std_out, gchar** std_err)
{
    GSpawnFlags flags = G_SPAWN_SEARCH_PATH;
    if (std_out == NULL)
        flags |= G_SPAWN_STDOUT_TO_DEV_NULL;
    if (std_err == NULL)
        flags |= G_SPAWN_STDERR_TO_DEV_NULL;

    gint exit_status;
    gboolean spawn_result;
    GError* spawn_error;
    spawn_result = g_spawn_sync(NULL, // Inherit the parent PWD.
                                argv,
                                NULL, // Inherit the parent environment.
                                flags,
                                NULL, NULL, // No func. before exec() in child.
                                std_out, std_err,
                                &exit_status, &spawn_error);

    if (!spawn_result
        || !g_spawn_check_exit_status(exit_status, &spawn_error)) {
        gchar* cmd = g_strjoinv(" ", argv);
        log_error("Spawning '%s' failed with '%s'.", cmd, spawn_error->message);
        g_free(cmd);
        g_error_free(spawn_error);
    }

    return spawn_result;
}

gchar**
format_call_external_argv(const char* template, const char* url, const char* filename)
{
    gchar** argv = g_strsplit(template, " ", 0);

    guint num_args = 0;
    while (argv[num_args]) {
        if (0 == g_strcmp0(argv[num_args], "%u") && url != NULL) {
            g_free(argv[num_args]);
            argv[num_args] = g_strdup(url);
        } else if (0 == g_strcmp0(argv[num_args], "%p") && filename != NULL) {
            g_free(argv[num_args]);
            argv[num_args] = strdup(filename);
        }
        num_args++;
    }

    return argv;
}

gchar*
_unique_filename(const char* filename)
{
    gchar* unique = g_strdup(filename);

    unsigned int i = 0;
    while (g_file_test(unique, G_FILE_TEST_EXISTS)) {
        g_free(unique);

        if (i > 1000) { // Give up after 1000 attempts.
            return NULL;
        }

        unique = g_strdup_printf("%s.%u", filename, i);
        if (!unique) {
            return NULL;
        }

        i++;
    }

    return unique;
}

bool
_has_directory_suffix(const char* path)
{
    return (g_str_has_suffix(path, ".")
            || g_str_has_suffix(path, "..")
            || g_str_has_suffix(path, G_DIR_SEPARATOR_S));
}

char*
_basename_from_url(const char* url)
{
    const char* default_name = "index";

    GFile* file = g_file_new_for_commandline_arg(url);
    char* basename = g_file_get_basename(file);

    if (_has_directory_suffix(basename)) {
        g_free(basename);
        basename = strdup(default_name);
    }

    g_object_unref(file);

    return basename;
}

gchar*
get_expanded_path(const char* path)
{
    GString* exp_path = g_string_new("");
    gchar* result;

    if (g_str_has_prefix(path, "file://")) {
        path += strlen("file://");
    }
    if (strlen(path) >= 2 && path[0] == '~' && path[1] == '/') {
        g_string_printf(exp_path, "%s/%s", getenv("HOME"), path + 2);
    } else {
        g_string_printf(exp_path, "%s", path);
    }

    result = exp_path->str;
    g_string_free(exp_path, FALSE);

    return result;
}

gchar*
unique_filename_from_url(const char* url, const char* path)
{
    gchar* realpath;

    // Default to './' as path when none has been provided.
    if (path == NULL) {
        realpath = g_strdup("./");
    } else {
        realpath = get_expanded_path(path);
    }

    // Resolves paths such as './../.' for path.
    GFile* target = g_file_new_for_commandline_arg(realpath);
    gchar* filename = NULL;

    if (_has_directory_suffix(realpath) || g_file_test(realpath, G_FILE_TEST_IS_DIR)) {
        // The target should be used as a directory. Assume that the basename
        // should be derived from the URL.
        char* basename = _basename_from_url(url);
        filename = g_build_filename(g_file_peek_path(target), basename, NULL);
        g_free(basename);
    } else {
        // Just use the target as filename.
        filename = g_build_filename(g_file_peek_path(target), NULL);
    }

    gchar* unique_filename = _unique_filename(filename);
    if (unique_filename == NULL) {
        g_free(filename);
        g_free(realpath);
        return NULL;
    }

    g_object_unref(target);
    g_free(filename);
    g_free(realpath);

    return unique_filename;
}