summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:25:48 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:25:48 -0300
commite2c6ae13af335b311da2429c903032124f926df2 (patch)
tree73776fab7f3e52124a217e2768968fc9a8a76c34 /src
downloadc-0.1.0.tar.gz
Initial importv0.1.0
Diffstat (limited to 'src')
-rw-r--r--src/cli.c191
-rw-r--r--src/client.c822
-rw-r--r--src/csv.c168
-rw-r--r--src/csv.h45
-rw-r--r--src/internal.h74
-rw-r--r--src/json.c550
-rw-r--r--src/json.h82
-rw-r--r--src/model.c246
-rw-r--r--src/util.c78
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;
+}