diff options
Diffstat (limited to 'src/client.c')
| -rw-r--r-- | src/client.c | 822 |
1 files changed, 822 insertions, 0 deletions
diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..58c55b3 --- /dev/null +++ b/src/client.c @@ -0,0 +1,822 @@ +#include "internal.h" + +#include <curl/curl.h> +#include <errno.h> +#include <math.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +#define CEPIS_ACCEPT_HEADER \ + "Accept: application/json, text/csv;q=0.9, */*;q=0.1" + +static const long RETRIABLE_STATUS[] = {429, 500, 502, 503, 504}; + +#define RETRIABLE_COUNT (sizeof(RETRIABLE_STATUS) / sizeof(RETRIABLE_STATUS[0])) + +struct cepis_client { + char *base_url; + char *user_agent; + double timeout; + double backoff_base; + double backoff_max; + double total_timeout; + int max_attempts; + uint64_t rng; + CURL *curl; + struct curl_slist *headers; +}; + +typedef struct { + long status; + double retry_after; + cepis_buf_t body; +} cepis_response_t; + +const char *cepis_status_str(cepis_status_t status) +{ + switch (status) { + case CEPIS_OK: + return "ok"; + case CEPIS_ERR_INVALID_CEP: + return "invalid CEP"; + case CEPIS_ERR_NOT_FOUND: + return "CEP not found"; + case CEPIS_ERR_RATE_LIMITED: + return "rate limited"; + case CEPIS_ERR_SERVER: + return "server error"; + case CEPIS_ERR_NETWORK: + return "network error"; + case CEPIS_ERR_INVALID_RESPONSE: + return "invalid response"; + case CEPIS_ERR_CONFIG: + return "invalid configuration"; + case CEPIS_ERR_NOMEM: + return "out of memory"; + } + return "unknown error"; +} + +cepis_status_t cepis_global_init(void) +{ + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) + return CEPIS_ERR_NETWORK; + return CEPIS_OK; +} + +void cepis_global_cleanup(void) +{ + curl_global_cleanup(); +} + +cepis_status_t cepis_normalize_cep(const char *value, + char out[CEPIS_CEP_LEN + 1]) +{ + char digits[CEPIS_CEP_LEN + 1]; + size_t n = 0; + size_t i; + + if (value == NULL || out == NULL) + return CEPIS_ERR_INVALID_CEP; + + for (i = 0; value[i] != '\0'; i++) { + char c = value[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || + c == '-' || c == '.') + continue; + if (c < '0' || c > '9') + return CEPIS_ERR_INVALID_CEP; + if (n >= CEPIS_CEP_LEN) + return CEPIS_ERR_INVALID_CEP; + digits[n++] = c; + } + if (n != CEPIS_CEP_LEN) + return CEPIS_ERR_INVALID_CEP; + + digits[CEPIS_CEP_LEN] = '\0'; + memcpy(out, digits, CEPIS_CEP_LEN + 1); + return CEPIS_OK; +} + +cepis_status_t cepis_format_cep(const char *cep, char out[CEPIS_CEP_LEN + 2]) +{ + char digits[CEPIS_CEP_LEN + 1]; + cepis_status_t status = cepis_normalize_cep(cep, digits); + + if (status != CEPIS_OK) + return status; + memcpy(out, digits, 5); + out[5] = '-'; + memcpy(out + 6, digits + 5, 3); + out[CEPIS_CEP_LEN + 1] = '\0'; + return CEPIS_OK; +} + +void cepis_config_init(cepis_config_t *config) +{ + if (config == NULL) + return; + config->base_url = CEPIS_DEFAULT_BASE_URL; + config->user_agent = NULL; + config->timeout = 10.0; + config->max_attempts = 3; + config->backoff_base = 0.5; + config->backoff_max = 8.0; + config->total_timeout = 0.0; +} + +static char *trim_base_url(const char *base_url) +{ + size_t len = strlen(base_url); + char *copy; + + while (len > 0 && base_url[len - 1] == '/') + len--; + copy = malloc(len + 1); + if (copy == NULL) + return NULL; + memcpy(copy, base_url, len); + copy[len] = '\0'; + return copy; +} + +cepis_status_t cepis_client_new(const cepis_config_t *config, + cepis_client_t **out, cepis_error_t *err) +{ + cepis_config_t defaults; + cepis_client_t *client; + + if (out == NULL) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "out must not be NULL"); + if (config == NULL) { + cepis_config_init(&defaults); + config = &defaults; + } + if (config->base_url == NULL || config->base_url[0] == '\0') + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "base_url must not be empty"); + if (config->max_attempts < 1) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "max_attempts must be >= 1"); + if (!(config->timeout > 0.0)) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "timeout must be > 0"); + if (config->backoff_base < 0.0 || config->backoff_max < 0.0) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "backoff values must be >= 0"); + if (config->total_timeout < 0.0) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "total_timeout must be >= 0"); + + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) + return cepis_set_error(err, CEPIS_ERR_NETWORK, 0, -1.0, + "could not initialise libcurl"); + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + curl_global_cleanup(); + return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, + "out of memory"); + } + + client->timeout = config->timeout; + client->max_attempts = config->max_attempts; + client->backoff_base = config->backoff_base; + client->backoff_max = config->backoff_max; + client->total_timeout = config->total_timeout; + client->rng = + (uint64_t)time(NULL) ^ ((uint64_t)(uintptr_t)client << 16); + if (client->rng == 0) + client->rng = 0x9E3779B97F4A7C15ULL; + + client->base_url = trim_base_url(config->base_url); + if (config->user_agent != NULL && config->user_agent[0] != '\0') { + client->user_agent = cepis_strdup(config->user_agent); + } else { + client->user_agent = cepis_strdup("cepis-c/" CEPIS_VERSION + " (+https://cep.is)"); + } + client->curl = curl_easy_init(); + client->headers = curl_slist_append(NULL, CEPIS_ACCEPT_HEADER); + + if (client->base_url == NULL || client->user_agent == NULL || + client->curl == NULL || client->headers == NULL) { + cepis_client_free(client); + return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, + "out of memory"); + } + + *out = client; + return CEPIS_OK; +} + +void cepis_client_free(cepis_client_t *client) +{ + if (client == NULL) + return; + if (client->curl != NULL) + curl_easy_cleanup(client->curl); + if (client->headers != NULL) + curl_slist_free_all(client->headers); + free(client->base_url); + free(client->user_agent); + free(client); + curl_global_cleanup(); +} + +const char *cepis_client_base_url(const cepis_client_t *client) +{ + return client == NULL ? NULL : client->base_url; +} + +const char *cepis_client_user_agent(const cepis_client_t *client) +{ + return client == NULL ? NULL : client->user_agent; +} + +static double monotonic_now(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0.0; + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +} + +static void sleep_seconds(double seconds) +{ + struct timespec request; + + if (seconds <= 0.0) + return; + if (seconds > 3600.0) + seconds = 3600.0; + request.tv_sec = (time_t)seconds; + request.tv_nsec = (long)((seconds - (double)request.tv_sec) * 1e9); + if (request.tv_nsec < 0) + request.tv_nsec = 0; + if (request.tv_nsec > 999999999L) + request.tv_nsec = 999999999L; + while (nanosleep(&request, &request) == -1 && errno == EINTR) + ; +} + +static double next_uniform(cepis_client_t *client) +{ + uint64_t x = client->rng; + + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + client->rng = x; + x *= 0x2545F4914F6CDD1DULL; + return (double)(x >> 11) / 9007199254740992.0; +} + +static double backoff_delay(cepis_client_t *client, int attempt, + double retry_after) +{ + double ceiling; + + if (retry_after >= 0.0) + return retry_after < client->backoff_max ? retry_after + : client->backoff_max; + ceiling = client->backoff_base * pow(2.0, (double)(attempt - 1)); + if (ceiling > client->backoff_max) + ceiling = client->backoff_max; + return next_uniform(client) * ceiling; +} + +static double parse_retry_after(const char *value, size_t len) +{ + char text[128]; + const char *formats[] = {"%a, %d %b %Y %H:%M:%S", + "%A, %d-%b-%y %H:%M:%S", + "%a %b %d %H:%M:%S %Y"}; + char *stop = NULL; + double seconds; + size_t i; + + while (len > 0 && (*value == ' ' || *value == '\t')) { + value++; + len--; + } + while (len > 0 && (value[len - 1] == ' ' || value[len - 1] == '\t' || + value[len - 1] == '\r' || value[len - 1] == '\n')) + len--; + if (len == 0 || len >= sizeof(text)) + return -1.0; + memcpy(text, value, len); + text[len] = '\0'; + + seconds = strtod(text, &stop); + if (stop != NULL && *stop == '\0' && stop != text) + return seconds < 0.0 ? 0.0 : seconds; + + for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) { + struct tm parsed; + time_t target; + + memset(&parsed, 0, sizeof(parsed)); + if (strptime(text, formats[i], &parsed) == NULL) + continue; + target = timegm(&parsed); + if (target == (time_t)-1) + continue; + seconds = difftime(target, time(NULL)); + return seconds < 0.0 ? 0.0 : seconds; + } + return -1.0; +} + +static size_t write_body(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + cepis_buf_t *body = userdata; + size_t total = size * nmemb; + + if (!cepis_buf_append(body, ptr, total)) + return 0; + return total; +} + +static size_t read_header(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + cepis_response_t *response = userdata; + size_t total = size * nmemb; + static const char name[] = "Retry-After:"; + size_t name_len = sizeof(name) - 1; + + if (total > name_len && strncasecmp(ptr, name, name_len) == 0) + response->retry_after = + parse_retry_after(ptr + name_len, total - name_len); + return total; +} + +static bool is_retriable(long status) +{ + size_t i; + + for (i = 0; i < RETRIABLE_COUNT; i++) + if (RETRIABLE_STATUS[i] == status) + return true; + return false; +} + +static bool is_accepted(long status, const long *accepted, size_t count) +{ + size_t i; + + for (i = 0; i < count; i++) + if (accepted[i] == status) + return true; + return false; +} + +static void apply_options(cepis_client_t *client, const char *url, + cepis_response_t *response, char *error_buffer, + double budget) +{ + CURL *curl = client->curl; + long low_speed_time = (long)ceil(client->timeout); + + if (low_speed_time < 1) + low_speed_time = 1; + + curl_easy_reset(curl); + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, client->user_agent); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, client->headers); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "identity"); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, + (long)(client->timeout * 1000.0)); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, low_speed_time); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response->body); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, read_header); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, response); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer); + if (budget > 0.0) { + long ms = (long)(budget * 1000.0); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, ms > 1 ? ms : 1L); + } +} + +static cepis_status_t client_request(cepis_client_t *client, const char *url, + const long *accepted, + size_t accepted_count, + cepis_response_t *response, + cepis_error_t *err) +{ + char error_buffer[CURL_ERROR_SIZE]; + double deadline = client->total_timeout > 0.0 + ? monotonic_now() + client->total_timeout + : 0.0; + long last_status = 0; + double last_retry_after = -1.0; + int attempt; + + error_buffer[0] = '\0'; + response->status = 0; + response->retry_after = -1.0; + cepis_buf_init(&response->body); + + for (attempt = 1; attempt <= client->max_attempts; attempt++) { + double budget = 0.0; + CURLcode code; + double wait; + + if (deadline > 0.0) { + budget = deadline - monotonic_now(); + if (budget <= 0.0) + break; + } + + response->body.len = 0; + if (response->body.data != NULL) + response->body.data[0] = '\0'; + response->retry_after = -1.0; + error_buffer[0] = '\0'; + + apply_options(client, url, response, error_buffer, budget); + code = curl_easy_perform(client->curl); + + if (code == CURLE_OK) { + long status = 0; + curl_easy_getinfo(client->curl, CURLINFO_RESPONSE_CODE, + &status); + if (is_accepted(status, accepted, accepted_count) || + !is_retriable(status)) { + response->status = status; + return CEPIS_OK; + } + last_status = status; + last_retry_after = response->retry_after; + } else { + last_status = 0; + last_retry_after = -1.0; + if (error_buffer[0] == '\0') + snprintf(error_buffer, sizeof(error_buffer), + "%s", curl_easy_strerror(code)); + } + + if (attempt == client->max_attempts) + break; + + wait = backoff_delay(client, attempt, last_retry_after); + if (deadline > 0.0) { + double remaining = deadline - monotonic_now(); + if (remaining <= 0.0) + break; + if (wait > remaining) + wait = remaining; + } + sleep_seconds(wait); + } + + cepis_buf_free(&response->body); + + if (last_status == 429) + return cepis_set_error(err, CEPIS_ERR_RATE_LIMITED, last_status, + last_retry_after, + "rate limit reached at %s (after %d " + "attempt(s))", + url, client->max_attempts); + if (last_status != 0) + return cepis_set_error(err, CEPIS_ERR_SERVER, last_status, -1.0, + "the API answered %ld at %s (after %d " + "attempt(s))", + last_status, url, client->max_attempts); + return cepis_set_error(err, CEPIS_ERR_NETWORK, 0, -1.0, + "could not reach %s (after %d attempt(s)): %s", + url, client->max_attempts, + error_buffer[0] != '\0' ? error_buffer + : "request aborted"); +} + +static void api_message(const cepis_buf_t *body, char *out, size_t out_size, + const char *fallback) +{ + json_value_t *root = json_parse(body->data, body->len); + const char *message = json_as_string(json_object_get(root, "mensagem")); + + snprintf(out, out_size, "%s", message != NULL ? message : fallback); + json_free(root); +} + +static cepis_status_t build_url(const cepis_client_t *client, const char *path, + cepis_buf_t *out) +{ + cepis_buf_init(out); + if (!cepis_buf_append_str(out, client->base_url) || + !cepis_buf_append_str(out, path)) { + cepis_buf_free(out); + return CEPIS_ERR_NOMEM; + } + return CEPIS_OK; +} + +cepis_status_t cepis_lookup(cepis_client_t *client, const char *cep, + cepis_address_t **out, cepis_error_t *err) +{ + char number[CEPIS_CEP_LEN + 1]; + char path[CEPIS_CEP_LEN + 2]; + char message[CEPIS_ERROR_MESSAGE_MAX]; + char fallback[CEPIS_ERROR_MESSAGE_MAX]; + static const long accepted[] = {200}; + cepis_response_t response; + cepis_address_t *address; + cepis_buf_t url; + json_value_t *root; + cepis_status_t status; + + if (client == NULL || out == NULL) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "client and out must not be NULL"); + if (cepis_normalize_cep(cep, number) != CEPIS_OK) + return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 0, -1.0, + "CEP must be 8 digits, got \"%s\"", + cep != NULL ? cep : "(null)"); + + snprintf(path, sizeof(path), "/%s", number); + status = build_url(client, path, &url); + if (status != CEPIS_OK) + return cepis_set_error(err, status, 0, -1.0, "out of memory"); + + status = client_request(client, url.data, accepted, 1, &response, err); + cepis_buf_free(&url); + if (status != CEPIS_OK) + return status; + + if (response.status == 404) { + cepis_buf_free(&response.body); + return cepis_set_error( + err, CEPIS_ERR_NOT_FOUND, 404, -1.0, + "CEP %s does not exist in the DNE database", number); + } + if (response.status == 400) { + snprintf(fallback, sizeof(fallback), "CEP %s rejected", number); + api_message(&response.body, message, sizeof(message), fallback); + cepis_buf_free(&response.body); + return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 400, -1.0, + "%s", message); + } + if (response.status != 200) { + long seen = response.status; + cepis_buf_free(&response.body); + return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, seen, + -1.0, "the API answered %ld", seen); + } + + root = json_parse(response.body.data, response.body.len); + if (root == NULL || root->type != JSON_OBJECT) { + json_free(root); + status = cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, 200, + -1.0, + "response is not a JSON object - " + "something between you and " + "the API is answering instead of it"); + cepis_buf_free(&response.body); + return status; + } + + address = cepis_address_from_json(root, response.body.data, + response.body.len); + json_free(root); + cepis_buf_free(&response.body); + if (address == NULL) + return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, + "out of memory"); + + *out = address; + return CEPIS_OK; +} + +cepis_status_t cepis_health(cepis_client_t *client, cepis_health_t **out, + cepis_error_t *err) +{ + static const long accepted[] = {200, 503}; + cepis_response_t response; + cepis_health_t *health; + cepis_buf_t url; + json_value_t *root; + cepis_status_t status; + + if (client == NULL || out == NULL) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "client and out must not be NULL"); + + status = build_url(client, "/health", &url); + if (status != CEPIS_OK) + return cepis_set_error(err, status, 0, -1.0, "out of memory"); + + status = client_request(client, url.data, accepted, 2, &response, err); + cepis_buf_free(&url); + if (status != CEPIS_OK) + return status; + + root = json_parse(response.body.data, response.body.len); + health = cepis_health_from_json(root); + json_free(root); + cepis_buf_free(&response.body); + + if (health == NULL) + return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, + response.status, -1.0, + "health response is not a JSON object"); + + *out = health; + return CEPIS_OK; +} + +static bool blank(const char *text) +{ + size_t i; + + if (text == NULL) + return true; + for (i = 0; text[i] != '\0'; i++) + if (text[i] != ' ' && text[i] != '\t' && text[i] != '\r' && + text[i] != '\n') + return false; + return true; +} + +static size_t column_index(const csv_row_t *header, const char *name) +{ + size_t i; + + for (i = 0; i < header->count; i++) + if (strcmp(header->fields[i], name) == 0) + return i; + return (size_t)-1; +} + +static cepis_status_t chunk_url(cepis_client_t *client, const char *const *ceps, + size_t count, cepis_buf_t *out) +{ + cepis_buf_t joined; + char *escaped; + size_t i; + + cepis_buf_init(&joined); + for (i = 0; i < count; i++) { + const char *value = ceps[i] != NULL ? ceps[i] : ""; + if ((i > 0 && !cepis_buf_append_str(&joined, ",")) || + !cepis_buf_append_str(&joined, value)) { + cepis_buf_free(&joined); + return CEPIS_ERR_NOMEM; + } + } + + escaped = curl_easy_escape(client->curl, joined.data ? joined.data : "", + (int)joined.len); + cepis_buf_free(&joined); + if (escaped == NULL) + return CEPIS_ERR_NOMEM; + + cepis_buf_init(out); + if (!cepis_buf_append_str(out, client->base_url) || + !cepis_buf_append_str(out, "/csv?ceps=") || + !cepis_buf_append_str(out, escaped)) { + cepis_buf_free(out); + curl_free(escaped); + return CEPIS_ERR_NOMEM; + } + curl_free(escaped); + return CEPIS_OK; +} + +static cepis_status_t fill_chunk(cepis_client_t *client, + const char *const *ceps, size_t count, + cepis_address_t **slots, cepis_error_t *err) +{ + static const long accepted[] = {200}; + char message[CEPIS_ERROR_MESSAGE_MAX]; + cepis_response_t response; + csv_reader_t *reader; + csv_row_t header; + cepis_buf_t url; + size_t localidade; + size_t index; + cepis_status_t status; + + status = chunk_url(client, ceps, count, &url); + if (status != CEPIS_OK) + return cepis_set_error(err, status, 0, -1.0, "out of memory"); + + status = client_request(client, url.data, accepted, 1, &response, err); + cepis_buf_free(&url); + if (status != CEPIS_OK) + return status; + + if (response.status == 400) { + api_message(&response.body, message, sizeof(message), + "the batch was rejected"); + cepis_buf_free(&response.body); + return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 400, -1.0, + "%s", message); + } + if (response.status != 200) { + long seen = response.status; + cepis_buf_free(&response.body); + return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, seen, + -1.0, "the API answered %ld", seen); + } + + reader = csv_reader_new(response.body.data ? response.body.data : "", + response.body.len); + if (reader == NULL) { + cepis_buf_free(&response.body); + return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, + "out of memory"); + } + + if (csv_reader_next(reader, &header) != 1) { + csv_reader_free(reader); + cepis_buf_free(&response.body); + return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, 200, + -1.0, + "the batch response has no header"); + } + + localidade = column_index(&header, "localidade"); + status = CEPIS_OK; + + for (index = 0; index < count; index++) { + csv_row_t record; + int read = csv_reader_next(reader, &record); + + if (read != 1) + break; + if (localidade != (size_t)-1 && localidade < record.count && + !blank(record.fields[localidade])) { + slots[index] = cepis_address_from_csv(&header, &record); + if (slots[index] == NULL) { + csv_row_free(&record); + status = + cepis_set_error(err, CEPIS_ERR_NOMEM, 0, + -1.0, "out of memory"); + break; + } + } + csv_row_free(&record); + } + + csv_row_free(&header); + csv_reader_free(reader); + cepis_buf_free(&response.body); + return status; +} + +cepis_status_t cepis_lookup_many(cepis_client_t *client, + const char *const *ceps, size_t count, + cepis_address_list_t *out, cepis_error_t *err) +{ + cepis_address_list_t list = {NULL, 0}; + size_t start; + + if (client == NULL || out == NULL) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "client and out must not be NULL"); + if (count == 0) { + out->items = NULL; + out->count = 0; + return CEPIS_OK; + } + if (ceps == NULL) + return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, + "ceps must not be NULL"); + + list.items = calloc(count, sizeof(*list.items)); + if (list.items == NULL) + return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, + "out of memory"); + list.count = count; + + for (start = 0; start < count; start += CEPIS_MAX_BATCH) { + size_t size = count - start; + cepis_status_t status; + + if (size > CEPIS_MAX_BATCH) + size = CEPIS_MAX_BATCH; + status = fill_chunk(client, ceps + start, size, + list.items + start, err); + if (status != CEPIS_OK) { + cepis_address_list_free(&list); + return status; + } + } + + *out = list; + return CEPIS_OK; +} |