diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:25:48 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:25:48 -0300 |
| commit | e2c6ae13af335b311da2429c903032124f926df2 (patch) | |
| tree | 73776fab7f3e52124a217e2768968fc9a8a76c34 /src | |
| download | c-996c5d18fbf2ed4d3aaaf391ba4b7c2fcd2c010b.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.c | 191 | ||||
| -rw-r--r-- | src/client.c | 822 | ||||
| -rw-r--r-- | src/csv.c | 168 | ||||
| -rw-r--r-- | src/csv.h | 45 | ||||
| -rw-r--r-- | src/internal.h | 74 | ||||
| -rw-r--r-- | src/json.c | 550 | ||||
| -rw-r--r-- | src/json.h | 82 | ||||
| -rw-r--r-- | src/model.c | 246 | ||||
| -rw-r--r-- | src/util.c | 78 |
9 files changed, 2256 insertions, 0 deletions
diff --git a/src/cli.c b/src/cli.c new file mode 100644 index 0000000..e4c9bd4 --- /dev/null +++ b/src/cli.c @@ -0,0 +1,191 @@ +/** + * @file cli.c + * @brief Minimal command-line front end: `cepis 01001000`. + * + * It exists for one concrete case -- quickly confirming a local instance + * answers before pointing code at it: + * + * cepis --base-url http://localhost:8080 01001000 + * + * A single CEP goes through the unit lookup, which carries `geo`; several go + * through the batch, which is one request instead of N and has no `geo`. + * + * It is not the library's main interface; for that, include `cepis.h`. + */ +#include "cepis.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static void print_escaped(const char *text) +{ + size_t i; + + putchar('"'); + for (i = 0; text[i] != '\0'; i++) { + unsigned char c = (unsigned char)text[i]; + switch (c) { + case '"': + fputs("\\\"", stdout); + break; + case '\\': + fputs("\\\\", stdout); + break; + case '\n': + fputs("\\n", stdout); + break; + case '\r': + fputs("\\r", stdout); + break; + case '\t': + fputs("\\t", stdout); + break; + default: + if (c < 0x20) + printf("\\u%04x", c); + else + putchar((int)c); + } + } + putchar('"'); +} + +static void print_pair(const char *key, const char *value, bool last) +{ + printf(" \"%s\": ", key); + print_escaped(value); + printf("%s\n", last ? "" : ","); +} + +static void print_address(const cepis_address_t *address) +{ + if (address == NULL) { + printf(" null"); + return; + } + printf(" {\n"); + print_pair("cep", address->cep, false); + print_pair("logradouro", address->logradouro, false); + print_pair("complemento", address->complemento, false); + print_pair("unidade", address->unidade, false); + print_pair("bairro", address->bairro, false); + print_pair("localidade", address->localidade, false); + print_pair("uf", address->uf, false); + print_pair("estado", address->estado, false); + print_pair("regiao", address->regiao, false); + print_pair("ibge", address->ibge, false); + print_pair("gia", address->gia, false); + print_pair("ddd", address->ddd, false); + print_pair("siafi", address->siafi, address->geo == NULL); + if (address->geo != NULL) { + printf(" \"geo\": {\"lat\": %.6f, \"lon\": %.6f, " + "\"raio_m\": %ld, \"precisao\": ", + address->geo->lat, address->geo->lon, + address->geo->raio_m); + print_escaped(address->geo->precisao); + printf("}\n"); + } + printf(" }"); +} + +static int usage(int code) +{ + FILE *stream = code == 0 ? stdout : stderr; + + fprintf(stream, + "usage: cepis [--base-url URL] [--timeout SECONDS] CEP...\n" + "\n" + " --base-url URL API URL (default: %s)\n" + " --timeout SECONDS per-socket timeout (default: 10)\n" + " --version print the version and exit\n", + CEPIS_DEFAULT_BASE_URL); + return code; +} + +int main(int argc, char **argv) +{ + cepis_config_t config; + cepis_client_t *client = NULL; + cepis_error_t err; + const char **ceps; + size_t count = 0; + int status = 0; + int i; + + cepis_config_init(&config); + ceps = calloc((size_t)argc, sizeof(*ceps)); + if (ceps == NULL) + return 1; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0) { + free(ceps); + return usage(0); + } + if (strcmp(argv[i], "--version") == 0) { + printf("cepis %s\n", CEPIS_VERSION); + free(ceps); + return 0; + } + if (strcmp(argv[i], "--base-url") == 0 && i + 1 < argc) { + config.base_url = argv[++i]; + continue; + } + if (strcmp(argv[i], "--timeout") == 0 && i + 1 < argc) { + config.timeout = strtod(argv[++i], NULL); + continue; + } + if (argv[i][0] == '-') { + free(ceps); + return usage(2); + } + ceps[count++] = argv[i]; + } + + if (count == 0) { + free(ceps); + return usage(2); + } + + if (cepis_client_new(&config, &client, &err) != CEPIS_OK) { + fprintf(stderr, "error: %s\n", err.message); + free(ceps); + return 1; + } + + if (count == 1) { + cepis_address_t *address = NULL; + if (cepis_lookup(client, ceps[0], &address, &err) != CEPIS_OK) { + fprintf(stderr, "error: %s: %s\n", + cepis_status_str(err.status), err.message); + status = 1; + } else { + print_address(address); + printf("\n"); + cepis_address_free(address); + } + } else { + cepis_address_list_t list = {NULL, 0}; + if (cepis_lookup_many(client, ceps, count, &list, &err) != + CEPIS_OK) { + fprintf(stderr, "error: %s: %s\n", + cepis_status_str(err.status), err.message); + status = 1; + } else { + size_t index; + printf("[\n"); + for (index = 0; index < list.count; index++) { + print_address(list.items[index]); + printf("%s\n", + index + 1 < list.count ? "," : ""); + } + printf("]\n"); + cepis_address_list_free(&list); + } + } + + cepis_client_free(client); + free(ceps); + return status; +} 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; +} diff --git a/src/csv.c b/src/csv.c new file mode 100644 index 0000000..ea9859b --- /dev/null +++ b/src/csv.c @@ -0,0 +1,168 @@ +#include "csv.h" + +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> + +struct csv_reader { + const char *p; + const char *end; +}; + +typedef struct { + char *data; + size_t len; + size_t cap; +} csv_buf_t; + +csv_reader_t *csv_reader_new(const char *data, size_t len) +{ + csv_reader_t *reader; + + if (data == NULL) + return NULL; + reader = calloc(1, sizeof(*reader)); + if (reader == NULL) + return NULL; + reader->p = data; + reader->end = data + len; + if (len >= 3 && memcmp(data, "\xEF\xBB\xBF", 3) == 0) + reader->p += 3; + return reader; +} + +void csv_reader_free(csv_reader_t *reader) +{ + free(reader); +} + +void csv_row_free(csv_row_t *row) +{ + size_t i; + + if (row == NULL) + return; + for (i = 0; i < row->count; i++) + free(row->fields[i]); + free(row->fields); + row->fields = NULL; + row->count = 0; +} + +static bool buf_push(csv_buf_t *buf, char c) +{ + if (buf->len + 2 > buf->cap) { + size_t cap = buf->cap ? buf->cap * 2 : 32; + char *grown = realloc(buf->data, cap); + if (grown == NULL) + return false; + buf->data = grown; + buf->cap = cap; + } + buf->data[buf->len++] = c; + buf->data[buf->len] = '\0'; + return true; +} + +static bool row_push(csv_row_t *row, size_t *cap, char *field) +{ + if (row->count + 1 > *cap) { + size_t next = *cap ? *cap * 2 : 16; + char **grown = realloc(row->fields, next * sizeof(*grown)); + if (grown == NULL) + return false; + row->fields = grown; + *cap = next; + } + row->fields[row->count++] = field; + return true; +} + +static char *parse_field(csv_reader_t *reader) +{ + csv_buf_t field = {NULL, 0, 0}; + bool quoted = false; + + if (!buf_push(&field, '\0')) + return NULL; + field.len = 0; + field.data[0] = '\0'; + + if (reader->p < reader->end && *reader->p == '"') { + quoted = true; + reader->p++; + } + + while (reader->p < reader->end) { + char c = *reader->p; + + if (quoted) { + reader->p++; + if (c != '"') { + if (!buf_push(&field, c)) + goto fail; + continue; + } + if (reader->p < reader->end && *reader->p == '"') { + reader->p++; + if (!buf_push(&field, '"')) + goto fail; + continue; + } + quoted = false; + continue; + } + if (c == ',' || c == '\n' || c == '\r') + break; + reader->p++; + if (!buf_push(&field, c)) + goto fail; + } + + if (quoted) + goto fail; + return field.data; + +fail: + free(field.data); + return NULL; +} + +int csv_reader_next(csv_reader_t *reader, csv_row_t *out) +{ + csv_row_t row = {NULL, 0}; + size_t cap = 0; + + if (reader == NULL || out == NULL) + return -1; + if (reader->p >= reader->end) + return 0; + + for (;;) { + char *field = parse_field(reader); + + if (field == NULL) { + csv_row_free(&row); + return -1; + } + if (!row_push(&row, &cap, field)) { + free(field); + csv_row_free(&row); + return -1; + } + if (reader->p >= reader->end) + break; + if (*reader->p == ',') { + reader->p++; + continue; + } + if (*reader->p == '\r') + reader->p++; + if (reader->p < reader->end && *reader->p == '\n') + reader->p++; + break; + } + + *out = row; + return 1; +} diff --git a/src/csv.h b/src/csv.h new file mode 100644 index 0000000..90a9d83 --- /dev/null +++ b/src/csv.h @@ -0,0 +1,45 @@ +/** + * @file csv.h + * @brief Minimal RFC 4180 CSV reader, internal to this library. + * + * The batch endpoint answers `text/csv`, and its values may contain commas and + * are then double-quoted with `""` escaping. Splitting on commas would corrupt + * exactly the street names most likely to matter, so this reads the real + * grammar: quoted fields, embedded commas, embedded newlines, CRLF or LF line + * endings, and a leading UTF-8 BOM. + */ +#ifndef CEPIS_CSV_H +#define CEPIS_CSV_H + +#include <stddef.h> + +/** One parsed record. Owns its fields. */ +typedef struct csv_row { + char **fields; + size_t count; +} csv_row_t; + +typedef struct csv_reader csv_reader_t; + +/** + * @brief Start reading @p len bytes of CSV. + * @return A reader borrowing @p data, or `NULL` on allocation failure. @p data + * must outlive the reader. + */ +csv_reader_t *csv_reader_new(const char *data, size_t len); + +/** + * @brief Read the next record into @p out. + * @return 1 when a record was read, 0 at end of input, -1 on a malformed + * record or allocation failure. @p out is only valid after 1, and the + * caller releases it with ::csv_row_free. + */ +int csv_reader_next(csv_reader_t *reader, csv_row_t *out); + +/** @brief Release a record's fields and zero it. `NULL` is ignored. */ +void csv_row_free(csv_row_t *row); + +/** @brief Release a reader. `NULL` is ignored. */ +void csv_reader_free(csv_reader_t *reader); + +#endif diff --git a/src/internal.h b/src/internal.h new file mode 100644 index 0000000..57487c2 --- /dev/null +++ b/src/internal.h @@ -0,0 +1,74 @@ +/** + * @file internal.h + * @brief Helpers shared between the translation units of this library. + * + * Nothing here is part of the public ABI. + */ +#ifndef CEPIS_INTERNAL_H +#define CEPIS_INTERNAL_H + +#include <stdarg.h> +#include <stdbool.h> +#include <stddef.h> + +#include "cepis.h" +#include "csv.h" +#include "json.h" + +/** A growable, always NUL-terminated byte buffer. */ +typedef struct cepis_buf { + char *data; + size_t len; + size_t cap; +} cepis_buf_t; + +/** @brief Put @p buf into the empty state. Allocates nothing. */ +void cepis_buf_init(cepis_buf_t *buf); + +/** @brief Append @p len bytes. @return `false` on allocation failure. */ +bool cepis_buf_append(cepis_buf_t *buf, const char *data, size_t len); + +/** @brief Append a NUL-terminated string. */ +bool cepis_buf_append_str(cepis_buf_t *buf, const char *text); + +/** @brief Release the buffer and return it to the empty state. */ +void cepis_buf_free(cepis_buf_t *buf); + +/** @brief `strdup`, tolerating a `NULL` argument by copying `""`. */ +char *cepis_strdup(const char *text); + +/** + * @brief Fill @p err, tolerating `NULL`. + * @return @p status, so callers can `return cepis_set_error(...)`. + */ +cepis_status_t cepis_set_error(cepis_error_t *err, cepis_status_t status, + long http_status, double retry_after, + const char *fmt, ...); + +/** + * @brief Build an address from a parsed JSON object. + * @param object The decoded body. + * @param raw The body as it arrived, copied into + * ::cepis_address_t::raw. May be `NULL`. + * @param raw_len Length of @p raw. + * @return An owned address, or `NULL` on allocation failure. + * + * Unknown members are ignored but preserved in `raw`, so a field the API grows + * cannot break this client. A malformed `geo` block yields `geo == NULL` rather + * than failing the whole address: the rest of the response is still useful. + */ +cepis_address_t *cepis_address_from_json(const json_value_t *object, + const char *raw, size_t raw_len); + +/** + * @brief Build an address from one CSV record and the header record. + * @return An owned address, or `NULL` on allocation failure. The result never + * carries a coordinate: the CSV has no `geo` column. + */ +cepis_address_t *cepis_address_from_csv(const csv_row_t *header, + const csv_row_t *record); + +/** @brief Build a health report from a parsed JSON object. */ +cepis_health_t *cepis_health_from_json(const json_value_t *object); + +#endif diff --git a/src/json.c b/src/json.c new file mode 100644 index 0000000..ee1382c --- /dev/null +++ b/src/json.c @@ -0,0 +1,550 @@ +#include "json.h" + +#include <errno.h> +#include <stdlib.h> +#include <string.h> + +typedef struct { + const char *p; + const char *end; + int depth; +} json_parser_t; + +static json_value_t *parse_value(json_parser_t *ps); + +static json_value_t *value_new(json_type_t type) +{ + json_value_t *v = calloc(1, sizeof(*v)); + if (v != NULL) + v->type = type; + return v; +} + +void json_free(json_value_t *value) +{ + size_t i; + + if (value == NULL) + return; + switch (value->type) { + case JSON_STRING: + free(value->as.string); + break; + case JSON_ARRAY: + for (i = 0; i < value->as.array.count; i++) + json_free(value->as.array.items[i]); + free(value->as.array.items); + break; + case JSON_OBJECT: + for (i = 0; i < value->as.object.count; i++) { + free(value->as.object.keys[i]); + json_free(value->as.object.values[i]); + } + free(value->as.object.keys); + free(value->as.object.values); + break; + default: + break; + } + free(value); +} + +static void skip_ws(json_parser_t *ps) +{ + while (ps->p < ps->end) { + char c = *ps->p; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') + return; + ps->p++; + } +} + +static bool at(const json_parser_t *ps, char c) +{ + return ps->p < ps->end && *ps->p == c; +} + +static bool literal(json_parser_t *ps, const char *word) +{ + size_t n = strlen(word); + + if ((size_t)(ps->end - ps->p) < n) + return false; + if (memcmp(ps->p, word, n) != 0) + return false; + ps->p += n; + return true; +} + +static int hex_digit(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + return -1; +} + +static bool read_hex4(json_parser_t *ps, unsigned *out) +{ + unsigned value = 0; + int i; + + if (ps->end - ps->p < 4) + return false; + for (i = 0; i < 4; i++) { + int digit = hex_digit(ps->p[i]); + if (digit < 0) + return false; + value = (value << 4) | (unsigned)digit; + } + ps->p += 4; + *out = value; + return true; +} + +static size_t utf8_encode(unsigned cp, char *out) +{ + if (cp < 0x80) { + out[0] = (char)cp; + return 1; + } + if (cp < 0x800) { + out[0] = (char)(0xC0 | (cp >> 6)); + out[1] = (char)(0x80 | (cp & 0x3F)); + return 2; + } + if (cp < 0x10000) { + out[0] = (char)(0xE0 | (cp >> 12)); + out[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); + out[2] = (char)(0x80 | (cp & 0x3F)); + return 3; + } + out[0] = (char)(0xF0 | (cp >> 18)); + out[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); + out[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); + out[3] = (char)(0x80 | (cp & 0x3F)); + return 4; +} + +typedef struct { + char *data; + size_t len; + size_t cap; +} strbuf_t; + +static bool strbuf_push(strbuf_t *sb, const char *data, size_t n) +{ + if (sb->len + n + 1 > sb->cap) { + size_t cap = sb->cap ? sb->cap : 32; + char *grown; + while (sb->len + n + 1 > cap) + cap *= 2; + grown = realloc(sb->data, cap); + if (grown == NULL) + return false; + sb->data = grown; + sb->cap = cap; + } + memcpy(sb->data + sb->len, data, n); + sb->len += n; + sb->data[sb->len] = '\0'; + return true; +} + +static bool parse_escape(json_parser_t *ps, strbuf_t *sb) +{ + char encoded[4]; + unsigned cp; + char c; + + if (ps->p >= ps->end) + return false; + c = *ps->p++; + switch (c) { + case '"': + return strbuf_push(sb, "\"", 1); + case '\\': + return strbuf_push(sb, "\\", 1); + case '/': + return strbuf_push(sb, "/", 1); + case 'b': + return strbuf_push(sb, "\b", 1); + case 'f': + return strbuf_push(sb, "\f", 1); + case 'n': + return strbuf_push(sb, "\n", 1); + case 'r': + return strbuf_push(sb, "\r", 1); + case 't': + return strbuf_push(sb, "\t", 1); + case 'u': + break; + default: + return false; + } + + if (!read_hex4(ps, &cp)) + return false; + if (cp >= 0xD800 && cp <= 0xDBFF) { + unsigned low; + const char *save = ps->p; + if (ps->end - ps->p >= 2 && ps->p[0] == '\\' && + ps->p[1] == 'u') { + ps->p += 2; + if (!read_hex4(ps, &low)) + return false; + if (low >= 0xDC00 && low <= 0xDFFF) { + cp = 0x10000 + ((cp - 0xD800) << 10) + + (low - 0xDC00); + } else { + ps->p = save; + cp = 0xFFFD; + } + } else { + cp = 0xFFFD; + } + } else if (cp >= 0xDC00 && cp <= 0xDFFF) { + cp = 0xFFFD; + } + return strbuf_push(sb, encoded, utf8_encode(cp, encoded)); +} + +static char *parse_raw_string(json_parser_t *ps) +{ + strbuf_t sb = {NULL, 0, 0}; + + if (!at(ps, '"')) + return NULL; + ps->p++; + if (!strbuf_push(&sb, "", 0)) + return NULL; + + while (ps->p < ps->end) { + char c = *ps->p; + if (c == '"') { + ps->p++; + return sb.data; + } + if (c == '\\') { + ps->p++; + if (!parse_escape(ps, &sb)) + break; + continue; + } + if ((unsigned char)c < 0x20) + break; + ps->p++; + if (!strbuf_push(&sb, &c, 1)) + break; + } + free(sb.data); + return NULL; +} + +static json_value_t *parse_string(json_parser_t *ps) +{ + char *text = parse_raw_string(ps); + json_value_t *v; + + if (text == NULL) + return NULL; + v = value_new(JSON_STRING); + if (v == NULL) { + free(text); + return NULL; + } + v->as.string = text; + return v; +} + +static json_value_t *parse_number(json_parser_t *ps) +{ + const char *start = ps->p; + json_value_t *v; + char *stop = NULL; + char token[64]; + size_t n; + double number; + + if (at(ps, '-')) + ps->p++; + if (ps->p >= ps->end) + return NULL; + if (*ps->p == '0') + ps->p++; + else if (*ps->p >= '1' && *ps->p <= '9') + while (ps->p < ps->end && *ps->p >= '0' && *ps->p <= '9') + ps->p++; + else + return NULL; + if (at(ps, '.')) { + ps->p++; + if (ps->p >= ps->end || *ps->p < '0' || *ps->p > '9') + return NULL; + while (ps->p < ps->end && *ps->p >= '0' && *ps->p <= '9') + ps->p++; + } + if (at(ps, 'e') || at(ps, 'E')) { + ps->p++; + if (at(ps, '+') || at(ps, '-')) + ps->p++; + if (ps->p >= ps->end || *ps->p < '0' || *ps->p > '9') + return NULL; + while (ps->p < ps->end && *ps->p >= '0' && *ps->p <= '9') + ps->p++; + } + + n = (size_t)(ps->p - start); + if (n == 0 || n >= sizeof(token)) + return NULL; + memcpy(token, start, n); + token[n] = '\0'; + + errno = 0; + number = strtod(token, &stop); + if (stop != token + n) + return NULL; + + v = value_new(JSON_NUMBER); + if (v == NULL) + return NULL; + v->as.number = number; + return v; +} + +static bool grow_slots(void ***slots, size_t count, size_t *cap, size_t size) +{ + void **grown; + size_t next; + + if (count < *cap) + return true; + next = *cap ? *cap * 2 : 8; + grown = realloc(*slots, next * size); + if (grown == NULL) + return false; + memset((char *)grown + count * size, 0, (next - count) * size); + *slots = grown; + *cap = next; + return true; +} + +static json_value_t *parse_array(json_parser_t *ps) +{ + json_value_t *v = value_new(JSON_ARRAY); + size_t cap = 0; + + if (v == NULL) + return NULL; + ps->p++; + skip_ws(ps); + if (at(ps, ']')) { + ps->p++; + return v; + } + for (;;) { + json_value_t *item; + if (!grow_slots((void ***)&v->as.array.items, v->as.array.count, + &cap, sizeof(json_value_t *))) + break; + item = parse_value(ps); + if (item == NULL) + break; + v->as.array.items[v->as.array.count++] = item; + skip_ws(ps); + if (at(ps, ',')) { + ps->p++; + continue; + } + if (at(ps, ']')) { + ps->p++; + return v; + } + break; + } + json_free(v); + return NULL; +} + +static json_value_t *parse_object(json_parser_t *ps) +{ + json_value_t *v = value_new(JSON_OBJECT); + size_t cap_keys = 0; + size_t cap_values = 0; + + if (v == NULL) + return NULL; + ps->p++; + skip_ws(ps); + if (at(ps, '}')) { + ps->p++; + return v; + } + for (;;) { + char *key; + json_value_t *item; + + skip_ws(ps); + if (!grow_slots((void ***)&v->as.object.keys, + v->as.object.count, &cap_keys, sizeof(char *))) + break; + if (!grow_slots((void ***)&v->as.object.values, + v->as.object.count, &cap_values, + sizeof(json_value_t *))) + break; + key = parse_raw_string(ps); + if (key == NULL) + break; + skip_ws(ps); + if (!at(ps, ':')) { + free(key); + break; + } + ps->p++; + item = parse_value(ps); + if (item == NULL) { + free(key); + break; + } + v->as.object.keys[v->as.object.count] = key; + v->as.object.values[v->as.object.count] = item; + v->as.object.count++; + + skip_ws(ps); + if (at(ps, ',')) { + ps->p++; + continue; + } + if (at(ps, '}')) { + ps->p++; + return v; + } + break; + } + json_free(v); + return NULL; +} + +static json_value_t *parse_value(json_parser_t *ps) +{ + json_value_t *v; + + if (ps->depth >= JSON_MAX_DEPTH) + return NULL; + skip_ws(ps); + if (ps->p >= ps->end) + return NULL; + + switch (*ps->p) { + case '"': + return parse_string(ps); + case '{': + ps->depth++; + v = parse_object(ps); + ps->depth--; + return v; + case '[': + ps->depth++; + v = parse_array(ps); + ps->depth--; + return v; + case 't': + if (!literal(ps, "true")) + return NULL; + v = value_new(JSON_BOOL); + if (v != NULL) + v->as.boolean = true; + return v; + case 'f': + if (!literal(ps, "false")) + return NULL; + v = value_new(JSON_BOOL); + if (v != NULL) + v->as.boolean = false; + return v; + case 'n': + if (!literal(ps, "null")) + return NULL; + return value_new(JSON_NULL); + default: + return parse_number(ps); + } +} + +json_value_t *json_parse(const char *text, size_t len) +{ + json_parser_t ps; + json_value_t *root; + + if (text == NULL) + return NULL; + ps.p = text; + ps.end = text + len; + ps.depth = 0; + + if (len >= 3 && memcmp(text, "\xEF\xBB\xBF", 3) == 0) + ps.p += 3; + + root = parse_value(&ps); + if (root == NULL) + return NULL; + skip_ws(&ps); + if (ps.p != ps.end) { + json_free(root); + return NULL; + } + return root; +} + +const json_value_t *json_object_get(const json_value_t *object, const char *key) +{ + size_t i; + + if (object == NULL || object->type != JSON_OBJECT || key == NULL) + return NULL; + for (i = 0; i < object->as.object.count; i++) + if (strcmp(object->as.object.keys[i], key) == 0) + return object->as.object.values[i]; + return NULL; +} + +const char *json_as_string(const json_value_t *value) +{ + if (value == NULL || value->type != JSON_STRING) + return NULL; + return value->as.string; +} + +bool json_as_double(const json_value_t *value, double *out) +{ + char *stop = NULL; + double parsed; + + if (value == NULL) + return false; + if (value->type == JSON_NUMBER) { + *out = value->as.number; + return true; + } + if (value->type != JSON_STRING || value->as.string[0] == '\0') + return false; + errno = 0; + parsed = strtod(value->as.string, &stop); + if (stop == NULL || *stop != '\0' || errno == ERANGE) + return false; + *out = parsed; + return true; +} + +bool json_as_llong(const json_value_t *value, long long *out) +{ + double parsed; + + if (!json_as_double(value, &parsed)) + return false; + *out = (long long)parsed; + return true; +} diff --git a/src/json.h b/src/json.h new file mode 100644 index 0000000..4d41714 --- /dev/null +++ b/src/json.h @@ -0,0 +1,82 @@ +/** + * @file json.h + * @brief Minimal JSON reader, internal to this library. + * + * Read-only, allocation-per-node, no streaming and no writer. It is here + * because C ships no JSON parser and the shapes this client has to read are + * small and fixed. It handles the whole grammar it is fed: UTF-8 throughout, + * `\uXXXX` escapes with surrogate pairs, and the full number syntax. + */ +#ifndef CEPIS_JSON_H +#define CEPIS_JSON_H + +#include <stdbool.h> +#include <stddef.h> + +/** Deepest nesting accepted, so a hostile body cannot exhaust the stack. */ +#define JSON_MAX_DEPTH 64 + +/** Kind of a ::json_value_t. */ +typedef enum json_type { + JSON_NULL, + JSON_BOOL, + JSON_NUMBER, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT +} json_type_t; + +typedef struct json_value json_value_t; + +/** A parsed JSON value. Owns everything it points at. */ +struct json_value { + json_type_t type; + union { + bool boolean; + double number; + /** NUL-terminated UTF-8, escapes already decoded. */ + char *string; + struct { + json_value_t **items; + size_t count; + } array; + struct { + char **keys; + json_value_t **values; + size_t count; + } object; + } as; +}; + +/** + * @brief Parse @p len bytes of JSON. + * @return The root value, owned by the caller, or `NULL` when @p text is not + * one complete JSON value followed only by whitespace. + */ +json_value_t *json_parse(const char *text, size_t len); + +/** @brief Release a value and everything below it. `NULL` is ignored. */ +void json_free(json_value_t *value); + +/** + * @brief Look a key up in an object. + * @return The member, borrowed from @p object, or `NULL` when @p object is not + * an object or has no such key. The first match wins. + */ +const json_value_t *json_object_get(const json_value_t *object, + const char *key); + +/** @brief The string a value holds, or `NULL` when it is not a string. */ +const char *json_as_string(const json_value_t *value); + +/** + * @brief Read a value as a number, accepting a numeric string. + * @return `false` when the value is absent or cannot be read as a number, in + * which case @p out is untouched. + */ +bool json_as_double(const json_value_t *value, double *out); + +/** @brief Like ::json_as_double, truncating towards zero. */ +bool json_as_llong(const json_value_t *value, long long *out); + +#endif diff --git a/src/model.c b/src/model.c new file mode 100644 index 0000000..75ed0d4 --- /dev/null +++ b/src/model.c @@ -0,0 +1,246 @@ +#include "internal.h" + +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +typedef struct { + const char *name; + size_t offset; +} address_field_t; + +static const address_field_t ADDRESS_FIELDS[] = { + {"cep", offsetof(cepis_address_t, cep)}, + {"logradouro", offsetof(cepis_address_t, logradouro)}, + {"complemento", offsetof(cepis_address_t, complemento)}, + {"unidade", offsetof(cepis_address_t, unidade)}, + {"bairro", offsetof(cepis_address_t, bairro)}, + {"localidade", offsetof(cepis_address_t, localidade)}, + {"uf", offsetof(cepis_address_t, uf)}, + {"estado", offsetof(cepis_address_t, estado)}, + {"regiao", offsetof(cepis_address_t, regiao)}, + {"ibge", offsetof(cepis_address_t, ibge)}, + {"gia", offsetof(cepis_address_t, gia)}, + {"ddd", offsetof(cepis_address_t, ddd)}, + {"siafi", offsetof(cepis_address_t, siafi)}, +}; + +#define ADDRESS_FIELD_COUNT (sizeof(ADDRESS_FIELDS) / sizeof(ADDRESS_FIELDS[0])) + +static char **field_slot(cepis_address_t *address, size_t index) +{ + return (char **)((char *)address + ADDRESS_FIELDS[index].offset); +} + +static cepis_address_t *address_new(void) +{ + cepis_address_t *address = calloc(1, sizeof(*address)); + size_t i; + + if (address == NULL) + return NULL; + for (i = 0; i < ADDRESS_FIELD_COUNT; i++) { + char **slot = field_slot(address, i); + *slot = cepis_strdup(""); + if (*slot == NULL) { + cepis_address_free(address); + return NULL; + } + } + return address; +} + +static bool set_field(cepis_address_t *address, size_t index, const char *value) +{ + char **slot = field_slot(address, index); + char *copy = cepis_strdup(value); + + if (copy == NULL) + return false; + free(*slot); + *slot = copy; + return true; +} + +void cepis_address_free(cepis_address_t *address) +{ + size_t i; + + if (address == NULL) + return; + for (i = 0; i < ADDRESS_FIELD_COUNT; i++) + free(*field_slot(address, i)); + if (address->geo != NULL) { + free(address->geo->precisao); + free(address->geo); + } + free(address->raw); + free(address); +} + +void cepis_address_list_free(cepis_address_list_t *list) +{ + size_t i; + + if (list == NULL) + return; + for (i = 0; i < list->count; i++) + cepis_address_free(list->items[i]); + free(list->items); + list->items = NULL; + list->count = 0; +} + +void cepis_health_free(cepis_health_t *health) +{ + if (health == NULL) + return; + free(health->status); + free(health->dne); + free(health->base_gerada_em); + free(health); +} + +bool cepis_geo_is_precise(const cepis_geo_t *geo) +{ + return geo != NULL && geo->precisao != NULL && + strcmp(geo->precisao, "logradouro") == 0; +} + +bool cepis_health_ok(const cepis_health_t *health) +{ + return health != NULL && health->status != NULL && + strcmp(health->status, "ok") == 0; +} + +static cepis_geo_t *geo_from_json(const json_value_t *block) +{ + const char *precisao; + cepis_geo_t *geo; + double lat; + double lon; + long long raio; + + if (block == NULL || block->type != JSON_OBJECT) + return NULL; + if (!json_as_double(json_object_get(block, "lat"), &lat)) + return NULL; + if (!json_as_double(json_object_get(block, "lon"), &lon)) + return NULL; + if (!json_as_llong(json_object_get(block, "raio_m"), &raio)) + return NULL; + precisao = json_as_string(json_object_get(block, "precisao")); + if (precisao == NULL) + return NULL; + + geo = calloc(1, sizeof(*geo)); + if (geo == NULL) + return NULL; + geo->lat = lat; + geo->lon = lon; + geo->raio_m = (long)raio; + geo->precisao = cepis_strdup(precisao); + if (geo->precisao == NULL) { + free(geo); + return NULL; + } + return geo; +} + +cepis_address_t *cepis_address_from_json(const json_value_t *object, + const char *raw, size_t raw_len) +{ + cepis_address_t *address; + size_t i; + + if (object == NULL || object->type != JSON_OBJECT) + return NULL; + address = address_new(); + if (address == NULL) + return NULL; + + for (i = 0; i < ADDRESS_FIELD_COUNT; i++) { + const char *value = json_as_string( + json_object_get(object, ADDRESS_FIELDS[i].name)); + if (value == NULL) + continue; + if (!set_field(address, i, value)) { + cepis_address_free(address); + return NULL; + } + } + + address->geo = geo_from_json(json_object_get(object, "geo")); + + if (raw != NULL) { + address->raw = malloc(raw_len + 1); + if (address->raw == NULL) { + cepis_address_free(address); + return NULL; + } + memcpy(address->raw, raw, raw_len); + address->raw[raw_len] = '\0'; + } + return address; +} + +cepis_address_t *cepis_address_from_csv(const csv_row_t *header, + const csv_row_t *record) +{ + cepis_address_t *address; + size_t column; + + if (header == NULL || record == NULL) + return NULL; + address = address_new(); + if (address == NULL) + return NULL; + + for (column = 0; column < header->count; column++) { + size_t i; + if (column >= record->count) + break; + for (i = 0; i < ADDRESS_FIELD_COUNT; i++) { + if (strcmp(header->fields[column], + ADDRESS_FIELDS[i].name) != 0) + continue; + if (!set_field(address, i, record->fields[column])) { + cepis_address_free(address); + return NULL; + } + break; + } + } + return address; +} + +cepis_health_t *cepis_health_from_json(const json_value_t *object) +{ + cepis_health_t *health; + const char *text; + + if (object == NULL || object->type != JSON_OBJECT) + return NULL; + health = calloc(1, sizeof(*health)); + if (health == NULL) + return NULL; + + text = json_as_string(json_object_get(object, "status")); + health->status = cepis_strdup(text); + text = json_as_string(json_object_get(object, "dne")); + health->dne = cepis_strdup(text); + text = json_as_string(json_object_get(object, "base_gerada_em")); + health->base_gerada_em = cepis_strdup(text); + + if (health->status == NULL || health->dne == NULL || + health->base_gerada_em == NULL) { + cepis_health_free(health); + return NULL; + } + + if (!json_as_llong(json_object_get(object, "ceps"), &health->ceps)) + health->ceps = 0; + if (!json_as_llong(json_object_get(object, "geo"), &health->geo)) + health->geo = 0; + return health; +} diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..a2c6c18 --- /dev/null +++ b/src/util.c @@ -0,0 +1,78 @@ +#include "internal.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +void cepis_buf_init(cepis_buf_t *buf) +{ + buf->data = NULL; + buf->len = 0; + buf->cap = 0; +} + +bool cepis_buf_append(cepis_buf_t *buf, const char *data, size_t len) +{ + if (buf->len + len + 1 > buf->cap) { + size_t cap = buf->cap ? buf->cap : 256; + char *grown; + while (buf->len + len + 1 > cap) { + if (cap > (size_t)-1 / 2) + return false; + cap *= 2; + } + grown = realloc(buf->data, cap); + if (grown == NULL) + return false; + buf->data = grown; + buf->cap = cap; + } + if (len > 0) + memcpy(buf->data + buf->len, data, len); + buf->len += len; + buf->data[buf->len] = '\0'; + return true; +} + +bool cepis_buf_append_str(cepis_buf_t *buf, const char *text) +{ + return cepis_buf_append(buf, text, strlen(text)); +} + +void cepis_buf_free(cepis_buf_t *buf) +{ + free(buf->data); + cepis_buf_init(buf); +} + +char *cepis_strdup(const char *text) +{ + size_t len; + char *copy; + + if (text == NULL) + text = ""; + len = strlen(text); + copy = malloc(len + 1); + if (copy == NULL) + return NULL; + memcpy(copy, text, len + 1); + return copy; +} + +cepis_status_t cepis_set_error(cepis_error_t *err, cepis_status_t status, + long http_status, double retry_after, + const char *fmt, ...) +{ + va_list args; + + if (err == NULL) + return status; + err->status = status; + err->http_status = http_status; + err->retry_after = retry_after; + va_start(args, fmt); + vsnprintf(err->message, sizeof(err->message), fmt, args); + va_end(args); + return status; +} |