Add modern FTP search providers for 1.2.4

This commit is contained in:
Mario Fetka
2026-07-15 03:55:27 +02:00
parent 04bbab10df
commit daa4649536
17 changed files with 361 additions and 47 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20)
project(libprozilla VERSION 1.2.3 LANGUAGES C)
project(libprozilla VERSION 1.2.4 LANGUAGES C)
include(CTest)
+1 -1
View File
@@ -65,4 +65,4 @@ prozilla-dev:
Homepage
--------
prozilla.genesys.ro
https://prozilla.disconnected-by-peer.at
+1 -1
View File
@@ -6,7 +6,7 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: libprozilla 1.2.3\n"
"Project-Id-Version: libprozilla 1.2.4\n"
"Report-Msgid-Bugs-To: prozilla-dev@disconnected-by-peer.at\n"
"POT-Creation-Date: 2026-07-14 19:48+0200\n"
"PO-Revision-Date: 2026-07-14 19:48+0200\n"
+1
View File
@@ -10,6 +10,7 @@ target_include_directories(prozilla
PRIVATE "${PROJECT_BINARY_DIR}/generated")
target_compile_definitions(prozilla PRIVATE HAVE_CONFIG_H=1 _GNU_SOURCE LOCALEDIR="${LOCALEDIR}")
target_compile_options(prozilla PRIVATE $<$<COMPILE_LANG_AND_ID:C,GNU,Clang>:-Wall;-Wextra>)
target_compile_options(prozilla PRIVATE $<$<COMPILE_LANG_AND_ID:C,GNU>:-Wno-clobbered>)
target_link_libraries(prozilla PUBLIC Threads::Threads)
target_link_libraries(prozilla PRIVATE CURL::libcurl)
if(Intl_FOUND)
+1 -3
View File
@@ -199,9 +199,7 @@ int select_fd(int fd, struct timeval *timeout, int writep)
int krecv(int sock, char *buffer, int size, int flags,
struct timeval *timeout)
{
int ret, arglen;
arglen = sizeof(int);
int ret;
assert(size >= 0);
+8 -6
View File
@@ -160,7 +160,7 @@ uerr_t connection_retr_fsize_not_known(connection_t * connection,
char *read_buffer,
int read_buffer_size)
{
off_t bytes_read;
int bytes_read;
connection_change_status(connection, DOWNLOADING);
gettimeofday(&connection->time_begin, NULL);
@@ -172,7 +172,8 @@ uerr_t connection_retr_fsize_not_known(connection_t * connection,
&connection->xfer_timeout);
if (bytes_read > 0)
{
if (write_data_with_lock(connection, read_buffer, sizeof(char), bytes_read) < bytes_read)
if (write_data_with_lock(connection, read_buffer, sizeof(char),
(size_t)bytes_read) < (size_t)bytes_read)
{
proz_debug(_("write failed"));
connection_show_message(connection,
@@ -224,7 +225,7 @@ uerr_t connection_retr_fsize_not_known(connection_t * connection,
uerr_t connection_retr_fsize_known(connection_t * connection,
char *read_buffer, int read_buffer_size)
{
off_t bytes_read;
int bytes_read;
off_t bytes_to_get;
pthread_mutex_lock(&connection->access_mutex);
@@ -266,7 +267,8 @@ uerr_t connection_retr_fsize_known(connection_t * connection,
if (bytes_read > 0)
{
if (write_data_with_lock(connection, read_buffer, sizeof(char), bytes_read) < bytes_read)
if (write_data_with_lock(connection, read_buffer, sizeof(char),
(size_t)bytes_read) < (size_t)bytes_read)
{
proz_debug(_("write failed"));
connection_show_message(connection,
@@ -329,7 +331,7 @@ dl_status proz_connection_get_status(connection_t * connection)
/*This will return a textual representation of the status of a conenction. */
char *proz_connection_get_status_string(connection_t * connection)
const char *proz_connection_get_status_string(connection_t * connection)
{
dl_status status;
@@ -337,7 +339,7 @@ char *proz_connection_get_status_string(connection_t * connection)
status = connection->status;
pthread_mutex_unlock(connection->status_change_mutex);
switch (connection->status)
switch (status)
{
case IDLE:
return(_("Idle"));
-2
View File
@@ -625,7 +625,6 @@ uerr_t ftp_login(connection_t * connection, const char *username,
{
uerr_t err = FTPERR;
int ret_code = 220;
boolean logged_in = FALSE;
while (1)
{
@@ -688,7 +687,6 @@ uerr_t ftp_login(connection_t * connection, const char *username,
case 231: /* Fallthrough. */
case 202:
logged_in = TRUE;
if (!ftp_use_proxy(connection))
return FTPOK; /* Logged in succesfully. */
+2 -3
View File
@@ -188,6 +188,7 @@ uerr_t ftp_parse(ftpparse *fp, char *buf, int len)
if (*buf == 'l')
fp->filetype = SYMBOLIC_LINK;
ptr = cp = strdup(buf);
(void)ptr;
for (i = 0; i < 4; i++)
{
@@ -235,9 +236,7 @@ uerr_t ftp_parse(ftpparse *fp, char *buf, int len)
time_t parse_time(const char * str)
{
char * p;
struct tm tm;
time_t t;
const char *p;
if (!str)
return 0;
+283 -5
View File
@@ -26,13 +26,18 @@
#include "ftpsearch.h"
urlinfo *prepare_lycos_url(ftps_request_t * request, char *ftps_loc,
urlinfo *prepare_lycos_url(ftps_request_t * request, const char *ftps_loc,
int num_req_mirrors);
urlinfo *prepare_filesearching_url(ftps_request_t * request, char *ftps_loc,
urlinfo *prepare_filesearching_url(ftps_request_t * request, const char *ftps_loc,
int num_req_mirrors);
urlinfo *prepare_archie_url(ftps_request_t * request, const char *ftps_loc,
int num_req_mirrors);
urlinfo *prepare_mamont_url(ftps_request_t * request, const char *ftps_loc);
uerr_t parse_lycos_html_mirror_list(ftps_request_t * request, char *p);
uerr_t parse_filesearching_html_mirror_list(ftps_request_t * request, char *p);
uerr_t parse_archie_json_mirror_list(ftps_request_t * request, char *p);
uerr_t parse_mamont_html_mirror_list(ftps_request_t * request, char *p);
uerr_t get_mirror_info(connection_t * connection, char **ret_buf);
@@ -52,7 +57,7 @@ ftp_mirror_t *reprocess_mirror_list(ftp_mirror_t * mirrors,
ftps_request_t * proz_ftps_request_init(
urlinfo * requested_url, off_t file_size,
char *ftps_loc,
const char *ftps_loc,
ftpsearch_server_type_t server_type,
int num_req_mirrors)
{
@@ -92,6 +97,20 @@ ftps_request_t * proz_ftps_request_init(
request->connection = proz_connection_init(url, 0);
break;
case ARCHIE_JSON:
url = prepare_archie_url(request, ftps_loc, num_req_mirrors);
if (url == 0)
proz_die("Bad URL specification");
request->connection = proz_connection_init(url, 0);
break;
case MAMONT_HTML:
url = prepare_mamont_url(request, ftps_loc);
if (url == 0)
proz_die("Bad URL specification");
request->connection = proz_connection_init(url, 0);
break;
default:
proz_debug("Unsupported FTP search server type");
proz_die("Unsupported FTP search server type");
@@ -101,7 +120,78 @@ ftps_request_t * proz_ftps_request_init(
}
urlinfo *prepare_lycos_url(ftps_request_t * request, char *ftps_loc,
static char *url_encode_component(const char *input)
{
static const char hex[] = "0123456789ABCDEF";
size_t input_len = strlen(input);
char *encoded = kmalloc(input_len * 3 + 1);
char *out = encoded;
while (*input)
{
unsigned char ch = (unsigned char)*input++;
if (isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~')
*out++ = (char)ch;
else
{
*out++ = '%';
*out++ = hex[ch >> 4];
*out++ = hex[ch & 15];
}
}
*out = '\0';
return encoded;
}
urlinfo *prepare_archie_url(ftps_request_t * request, const char *ftps_loc,
int num_req_mirrors)
{
urlinfo *url = kmalloc(sizeof(*url));
char *encoded_name = url_encode_component(request->file_name);
size_t length = strlen(ftps_loc) + strlen(encoded_name) + 80;
char *request_url = kmalloc(length);
uerr_t err;
snprintf(request_url, length,
"%s?q=%s&type=exact&database=anonftp&limit=%d",
ftps_loc, encoded_name, num_req_mirrors);
proz_debug("ftpsearch url= %s\n", request_url);
err = proz_parse_url(request_url, url, 0);
kfree(encoded_name);
kfree(request_url);
if (err != URLOK)
{
kfree(url);
return 0;
}
return url;
}
urlinfo *prepare_mamont_url(ftps_request_t * request, const char *ftps_loc)
{
urlinfo *url = kmalloc(sizeof(*url));
char *encoded_name = url_encode_component(request->file_name);
size_t length = strlen(ftps_loc) + strlen(encoded_name) + 16;
char *request_url = kmalloc(length);
uerr_t err;
snprintf(request_url, length, "%s?st=%s", ftps_loc, encoded_name);
proz_debug("ftpsearch url= %s\n", request_url);
err = proz_parse_url(request_url, url, 0);
kfree(encoded_name);
kfree(request_url);
if (err != URLOK)
{
kfree(url);
return 0;
}
return url;
}
urlinfo *prepare_lycos_url(ftps_request_t * request, const char *ftps_loc,
int num_req_mirrors)
{
urlinfo *url;
@@ -141,7 +231,7 @@ urlinfo *prepare_lycos_url(ftps_request_t * request, char *ftps_loc,
urlinfo *prepare_filesearching_url(ftps_request_t * request, char *ftps_loc,
urlinfo *prepare_filesearching_url(ftps_request_t * request, const char *ftps_loc,
int num_req_mirrors)
{
urlinfo *url;
@@ -193,6 +283,12 @@ uerr_t parse_html_mirror_list(ftps_request_t * request, char *p)
return parse_filesearching_html_mirror_list(request, p);
break;
case ARCHIE_JSON:
return parse_archie_json_mirror_list(request, p);
case MAMONT_HTML:
return parse_mamont_html_mirror_list(request, p);
default:
proz_debug("Unsupported FTP search server type");
proz_die("Unsupported FTP search server type");
@@ -200,6 +296,188 @@ uerr_t parse_html_mirror_list(ftps_request_t * request, char *p)
return MIRPARSEFAIL;
}
static const char *json_url_value(const char *cursor, char *output,
size_t output_size)
{
const char *key = strstr(cursor, "\"URL\"");
size_t used = 0;
if (key == NULL)
return NULL;
cursor = key + 5;
while (isspace((unsigned char)*cursor))
cursor++;
if (*cursor++ != ':')
return json_url_value(cursor, output, output_size);
while (isspace((unsigned char)*cursor))
cursor++;
if (*cursor++ != '\"')
return json_url_value(cursor, output, output_size);
while (*cursor && *cursor != '\"')
{
unsigned char ch = (unsigned char)*cursor++;
if (ch == '\\')
{
ch = (unsigned char)*cursor++;
if (ch == 'n') ch = '\n';
else if (ch == 'r') ch = '\r';
else if (ch == 't') ch = '\t';
else if (ch == 'b') ch = '\b';
else if (ch == 'f') ch = '\f';
else if (ch == 'u')
{
/* FTP URLs are ASCII; skip a JSON Unicode escape safely. */
for (int i = 0; i < 4 && isxdigit((unsigned char)*cursor); i++)
cursor++;
ch = '?';
}
}
if (used + 1 < output_size)
output[used++] = (char)ch;
}
output[used] = '\0';
return *cursor == '\"' ? cursor + 1 : cursor;
}
uerr_t parse_archie_json_mirror_list(ftps_request_t * request, char *p)
{
ftp_mirror_t *mirrors;
const char *cursor = p;
int capacity = 1;
int count = 0;
char url_string[4096];
while ((cursor = strstr(cursor, "\"URL\"")) != NULL)
{
capacity++;
cursor += 5;
}
mirrors = kmalloc(sizeof(*mirrors) * (size_t)capacity);
cursor = p;
while ((cursor = json_url_value(cursor, url_string,
sizeof(url_string))) != NULL)
{
urlinfo parsed;
if (proz_parse_url(url_string, &parsed, 0) != URLOK)
continue;
if (parsed.proto != URLFTP && parsed.proto != URLFTPS &&
parsed.proto != URLSFTP)
{
proz_free_url(&parsed, FALSE);
continue;
}
memset(&mirrors[count], 0, sizeof(mirrors[count]));
mirrors[count].server_name = kstrdup(parsed.host);
mirrors[count].paths = kmalloc(sizeof(mirror_path_t));
mirrors[count].num_paths = 1;
mirrors[count].paths[0].path = kstrdup(parsed.dir ? parsed.dir : "");
mirrors[count].paths[0].valid = TRUE;
mirrors[count].file_name = kstrdup(parsed.file);
mirrors[count].full_name = kstrdup(url_string);
count++;
proz_free_url(&parsed, FALSE);
}
if (request->requested_url->proto == URLFTP ||
request->requested_url->proto == URLFTPS ||
request->requested_url->proto == URLSFTP)
{
urlinfo *original = request->requested_url;
memset(&mirrors[count], 0, sizeof(mirrors[count]));
mirrors[count].server_name = kstrdup(original->host);
mirrors[count].paths = kmalloc(sizeof(mirror_path_t));
mirrors[count].num_paths = 1;
mirrors[count].paths[0].path = kstrdup(original->dir ? original->dir : "");
mirrors[count].paths[0].valid = TRUE;
mirrors[count].file_name = kstrdup(original->file);
mirrors[count].full_name = str_url(original, 1);
count++;
}
request->num_mirrors = count;
request->mirrors = reprocess_mirror_list(mirrors, &request->num_mirrors);
return MIRINFOK;
}
uerr_t parse_mamont_html_mirror_list(ftps_request_t * request, char *p)
{
static const char marker[] = "href=\"ftp://";
ftp_mirror_t *mirrors;
const char *cursor = p;
int capacity = 1;
int count = 0;
while ((cursor = strstr(cursor, marker)) != NULL)
{
capacity++;
cursor += sizeof(marker) - 1;
}
mirrors = kmalloc(sizeof(*mirrors) * (size_t)capacity);
cursor = p;
while ((cursor = strstr(cursor, marker)) != NULL)
{
urlinfo parsed;
const char *start = cursor + 6;
const char *end = strchr(start, '\"');
size_t length;
char *url_string;
if (end == NULL)
break;
cursor = end + 1;
length = (size_t)(end - start);
if (length == 0 || length >= 4096)
continue;
url_string = kmalloc(length + 1);
memcpy(url_string, start, length);
url_string[length] = '\0';
if (proz_parse_url(url_string, &parsed, 0) != URLOK ||
parsed.proto != URLFTP)
{
kfree(url_string);
continue;
}
memset(&mirrors[count], 0, sizeof(mirrors[count]));
mirrors[count].server_name = kstrdup(parsed.host);
mirrors[count].paths = kmalloc(sizeof(mirror_path_t));
mirrors[count].num_paths = 1;
mirrors[count].paths[0].path = kstrdup(parsed.dir ? parsed.dir : "");
mirrors[count].paths[0].valid = TRUE;
mirrors[count].file_name = kstrdup(parsed.file);
mirrors[count].full_name = url_string;
count++;
proz_free_url(&parsed, FALSE);
}
if (request->requested_url->proto == URLFTP ||
request->requested_url->proto == URLFTPS ||
request->requested_url->proto == URLSFTP)
{
urlinfo *original = request->requested_url;
memset(&mirrors[count], 0, sizeof(mirrors[count]));
mirrors[count].server_name = kstrdup(original->host);
mirrors[count].paths = kmalloc(sizeof(mirror_path_t));
mirrors[count].num_paths = 1;
mirrors[count].paths[0].path = kstrdup(original->dir ? original->dir : "");
mirrors[count].paths[0].valid = TRUE;
mirrors[count].file_name = kstrdup(original->file);
mirrors[count].full_name = str_url(original, 1);
count++;
}
request->num_mirrors = count;
request->mirrors = reprocess_mirror_list(mirrors, &request->num_mirrors);
return MIRINFOK;
}
uerr_t parse_lycos_html_mirror_list(ftps_request_t * request, char *p)
{
struct ftp_mirror **pmirrors = &request->mirrors;
+2 -3
View File
@@ -360,7 +360,7 @@ uerr_t http_fetch_headers(connection_t * connection, http_stat_t * hs,
char *command)
{
uerr_t err;
int num_written, hcount, statcode, all_length;
int num_written, hcount, statcode;
off_t contlen, contrange;
char *hdr, *type, *all_headers;
const char *error;
@@ -375,14 +375,13 @@ uerr_t http_fetch_headers(connection_t * connection, http_stat_t * hs,
num_written = ksend(connection->data_sock, command, strlen(command), 0,
&connection->xfer_timeout);
if (num_written != strlen(command))
if (num_written < 0 || (size_t)num_written != strlen(command))
{
proz_debug(_("Failed writing HTTP request"));
return WRITEERR;
}
all_headers = NULL;
all_length = 0;
contlen = contrange = -1;
statcode = -1;
type = NULL;
+1 -1
View File
@@ -227,7 +227,7 @@ int proz_log_read_logfile(logfile * lf, download_t * download,
lf->url = kmalloc(lf->url_len + 1);
if (fread(lf->url, 1, lf->url_len, fp) != lf->url_len)
if (fread(lf->url, 1, (size_t)lf->url_len, fp) != (size_t)lf->url_len)
{
fclose(fp);
return -1;
+2 -2
View File
@@ -54,8 +54,8 @@ int proz_init(int argc, char **argv)
/* Gettext stuff */
setlocale(LC_ALL, "");
bindtextdomain(PACKAGE, LOCALEDIR);
textdomain(PACKAGE);
(void)bindtextdomain(PACKAGE, LOCALEDIR);
(void)textdomain(PACKAGE);
memset(&libprozrtinfo, 0, sizeof(libprozrtinfo));
libprozrtinfo.argc = argc;
+1 -2
View File
@@ -316,7 +316,7 @@ int close_sock(int *sock)
/*Returns a string representation of a prozilla errror */
char *proz_strerror(uerr_t error)
const char *proz_strerror(uerr_t error)
{
switch (error)
{
@@ -568,4 +568,3 @@ void cleanup_httpsocks(connection_t * connection)
}
}
+4 -4
View File
@@ -349,7 +349,7 @@ typedef enum {
} ftp_mirror_stat_t;
typedef enum {
LYCOS, FILESEARCH_RU
LYCOS, FILESEARCH_RU, ARCHIE_JSON, MAMONT_HTML
} ftpsearch_server_type_t;
@@ -487,7 +487,7 @@ void proz_debug_delete_log();
connection_t * proz_connection_init(urlinfo * url, pthread_mutex_t * mutex);
void proz_connection_set_url(connection_t * connection, urlinfo *url);
char *proz_connection_get_status_string(connection_t * connection);
const char *proz_connection_get_status_string(connection_t * connection);
off_t proz_connection_get_total_bytes_got(connection_t * connection);
void proz_get_url_info_loop(connection_t * connection, pthread_t *thread);
off_t proz_connection_get_total_remote_bytes_got(connection_t *
@@ -538,12 +538,12 @@ int proz_log_read_logfile(logfile * lf, download_t * download,
boolean load_con_info);
int proz_log_delete_logfile(download_t * download);
int proz_log_logfile_exists(download_t * download);
char *proz_strerror(uerr_t error);
const char *proz_strerror(uerr_t error);
/*Ftpsearch releated */
ftps_request_t * proz_ftps_request_init(
urlinfo * requested_url, off_t file_size,
char *ftps_loc,
const char *ftps_loc,
ftpsearch_server_type_t server_type,
int num_req_mirrors);
void proz_get_complete_mirror_list(ftps_request_t * request);
+9 -13
View File
@@ -134,7 +134,7 @@ enum {
#define ARRAY_SIZE(array) (sizeof(array) / sizeof(*(array)))
const static unsigned char urlchr_table[256] = {
static const unsigned char urlchr_table[256] = {
U, U, U, U, U, U, U, U, /* NUL SOH STX ETX EOT ENQ ACK BEL */
U, U, U, U, U, U, U, U, /* BS HT LF VT FF CR SO SI */
U, U, U, U, U, U, U, U, /* DLE DC1 DC2 DC3 DC4 NAK SYN ETB */
@@ -303,7 +303,7 @@ char *encode_string(const char *s)
URLUNKNOWN if not. */
uerr_t urlproto(const char *url)
{
int i;
size_t i;
for (i = 0; i < ARRAY_SIZE(sup_protos); i++)
if (!strncasecmp(url, sup_protos[i].name, strlen(sup_protos[i].name)))
@@ -613,7 +613,8 @@ static uerr_t parse_uname(const char *url, char **user, char **passwd)
char *str_url(const urlinfo * u, int hide)
{
char *res, *host, *user, *passwd, *proto_name, *dir, *file;
int i, l, ln, lu, lh, lp, lf, ld;
size_t i;
int l, ln, lu, lh, lp, lf, ld;
unsigned short proto_default_port;
/* Look for the protocol name. */
@@ -725,7 +726,8 @@ char *str_url(const urlinfo * u, int hide)
form. */
uerr_t proz_parse_url(const char *url, urlinfo * u, int strict)
{
int i, l, abs_ftp;
size_t i;
int l, abs_ftp;
int recognizable; /* Recognizable URL is the one where
the protocol name was explicitly
named, i.e. it wasn't deduced from
@@ -757,9 +759,9 @@ uerr_t proz_parse_url(const char *url, urlinfo * u, int strict)
them for now). */
if (recognizable)
l += skip_uname(url + l);
for (i = l; url[i] && url[i] != ':' && url[i] != '/'; i++)
for (i = (size_t)l; url[i] && url[i] != ':' && url[i] != '/'; i++)
;
if (i == l)
if (i == (size_t)l)
return URLBADHOST;
/* Get the hostname. */
u->host = strdupdelim(url + l, url + i);
@@ -791,7 +793,7 @@ uerr_t proz_parse_url(const char *url, urlinfo * u, int strict)
u->proto = type = URLHTTP;
if (!u->port)
{
int ind;
size_t ind;
for (ind = 0; ind < ARRAY_SIZE(sup_protos); ind++)
if (sup_protos[ind].ind == type)
break;
@@ -937,12 +939,6 @@ urlpos *add_url(urlpos * l, const char *url, const char *file)
}
/*This will copy a url structure to another */
void url_cpy(urlinfo * src, urlinfo * dest)
{
}
/* Find the last occurrence of character C in the range [b, e), or
NULL, if none are present. This is almost completely equivalent to
{ *e = '\0'; return strrchr(b); }, except that it doesn't change
+4
View File
@@ -1,3 +1,7 @@
add_executable(url_parse_test url_parse_test.c)
target_link_libraries(url_parse_test PRIVATE prozilla)
add_test(NAME url_parse COMMAND url_parse_test)
add_executable(ftpsearch_parse_test ftpsearch_parse_test.c)
target_link_libraries(ftpsearch_parse_test PRIVATE prozilla)
add_test(NAME ftpsearch_parse COMMAND ftpsearch_parse_test)
+40
View File
@@ -0,0 +1,40 @@
#include <assert.h>
#include <string.h>
#include "prozilla.h"
uerr_t parse_archie_json_mirror_list(ftps_request_t *request, char *data);
uerr_t parse_mamont_html_mirror_list(ftps_request_t *request, char *data);
static void init_request(ftps_request_t *request, urlinfo *requested_url)
{
memset(request, 0, sizeof(*request));
memset(requested_url, 0, sizeof(*requested_url));
requested_url->proto = URLHTTP;
request->requested_url = requested_url;
}
int main(void)
{
ftps_request_t request;
urlinfo requested_url;
char archie_json[] =
"{\"hits\":2,\"results\":["
"{\"URL\":\"ftp://ftp.example.org/pub/file.zip\"},"
"{\"URL\":\"ftps://secure.example.org/releases/file.zip\"}]}";
char mamont_html[] =
"<a href=\"ftp://mirror.example.net/dist/file.zip\">file.zip</a>";
init_request(&request, &requested_url);
assert(parse_archie_json_mirror_list(&request, archie_json) == MIRINFOK);
assert(request.num_mirrors == 2);
assert(strcmp(request.mirrors[0].server_name, "ftp.example.org") == 0);
assert(strcmp(request.mirrors[1].server_name, "secure.example.org") == 0);
init_request(&request, &requested_url);
assert(parse_mamont_html_mirror_list(&request, mamont_html) == MIRINFOK);
assert(request.num_mirrors == 1);
assert(strcmp(request.mirrors[0].server_name, "mirror.example.net") == 0);
assert(strcmp(request.mirrors[0].file_name, "file.zip") == 0);
return 0;
}