diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:56:51 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:56:59 -0300 |
| commit | b7b08a7d29d84de187dd8a55e0c94de66b07a572 (patch) | |
| tree | 7e55661d3eca3cf3b16aada6e87eabee68802fb9 /README.md | |
| download | asm-b7b08a7d29d84de187dd8a55e0c94de66b07a572.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..25fbff6 --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +# cepis + +Client for the [public CEP API at cep.is](https://cep.is), written in +hand-written x86-64 assembly (GNU `as`, AT&T syntax) with a C-callable API. +Official Correios data (DNE), refreshed every two weeks. + +```c +#include <cepis.h> + +cepis_address *a; + +if (cepis_lookup(NULL, "01001-000", &a, NULL) == CEPIS_OK) { + printf("%s %s %s\n", a->logradouro, a->localidade, a->uf); + /* Praça da Sé São Paulo SP */ + cepis_address_free(a); +} +``` + +## What it reaches + +TLS is not implemented here — the library links against **libcurl**, which is +the only dependency, and drives it through the C ABI. It therefore talks to +the real `https://cep.is` as well as to any plain-HTTP instance you point it +at. Marshalling, JSON and CSV parsing, retry, backoff and `Retry-After` +handling are all assembly. + +Linux on x86-64, glibc, GNU binutils. There is no portable-assembly story: the +System V AMD64 ABI, glibc's `timegm`/`strtod`, and `as` syntax are all assumed. + +## Installation + +```sh +make build +make install PREFIX=/usr/local +``` + +Link with `-lcepis -lcurl`. Requires libcurl's headers only for the build of +your own program (this library ships no curl types in its header). + +## Usage + +### A single lookup + +`cepis_lookup` accepts the CEP masked or unmasked. Passing `NULL` as the +client uses a process-wide default client; `err` is optional. + +```c +cepis_address *a = NULL; +cepis_error err; +char masked[10]; + +switch (cepis_lookup(NULL, "01310-100", &a, &err)) { +case CEPIS_OK: + a->logradouro; /* "Avenida Paulista" */ + a->localidade; /* "São Paulo" */ + a->uf; /* "SP" */ + a->ibge; /* "3550308" */ + a->cep; /* "01310100" (always unmasked) */ + cepis_address_formatted_cep(a, masked); /* "01310-100" */ + cepis_address_free(a); + break; +case CEPIS_ERR_NOT_FOUND: + break; +default: + fprintf(stderr, "%s\n", err.message); +} +``` + +Fields the database does not have for that CEP come back as `""`, never +`NULL`. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `NULL`: + +```c +if (a->geo != NULL) { + a->geo->lat; /* -23.567202 */ + a->geo->lon; /* -46.649659 */ + a->geo->raio_m; /* 1084 */ + a->geo->precisao; /* "logradouro" */ +} +``` + +**Check `precisao` before computing distance.** There are two kinds of point, +and the difference between them is tens of kilometres: + +| `precisao` | typical radius | what the point is | +| --- | --- | --- | +| `logradouro` | 110–140 m | the stretch of street — good for maps and distance | +| `localidade` | 7–40 km | the municipality centroid: the CEP *is* the whole city | + +`cepis_geo_is_precise(a->geo)` is the shorthand for the first case. + +### Many CEPs at once + +Do not loop over `cepis_lookup`. Use `cepis_lookup_many`, which resolves up to +500 CEPs per request: + +```c +const char *ceps[] = {"01001000", "99999999", "20040020"}; +cepis_address **out = NULL; +size_t i; + +if (cepis_lookup_many(NULL, ceps, 3, &out, NULL) == CEPIS_OK) { + for (i = 0; i < 3; i++) + printf("%s: %s\n", ceps[i], + out[i] ? out[i]->localidade : "not found"); + cepis_address_list_free(out, 3); +} +``` + +The array has **the same length and the same order** as the input. A CEP that +does not exist, or is malformed, is `NULL` at its position — it does not +vanish, and it is not an error. Arrays longer than 500 are chunked +automatically. + +> **Batches carry no coordinate.** The endpoint behind them is CSV, and the +> CSV has no `geo` column. Every address from `cepis_lookup_many` has +> `geo == NULL`, including for CEPs that do have one, and `raw == NULL`. Need +> `geo`? Use `cepis_lookup`, one at a time. + +### API health + +```c +struct cepis_health *h = NULL; + +if (cepis_health(NULL, &h, NULL) == CEPIS_OK) { + h->status; /* "ok" */ + h->dne; /* "26071" identifier of the fortnightly base */ + h->ceps; /* 1596168 */ + h->base_gerada_em; /* "2026-07-24T03:48:28Z" */ + cepis_health_free(h); +} +``` + +An API reporting itself degraded is not an error — that arrives in +`h->status`. + +## Memory + +Every `out` parameter transfers ownership to the caller and has exactly one +matching release function. All allocation goes through libc `malloc`, so the +library is safe to use alongside anything else that does. + +| you received | you must call | +| --- | --- | +| `cepis_address *` | `cepis_address_free` | +| `cepis_address **` from `cepis_lookup_many` | `cepis_address_list_free(arr, n)` | +| `struct cepis_health *` | `cepis_health_free` | +| `cepis_client *` | `cepis_client_free` | +| the default client, `cepis_strerror`, `cepis_version` | nothing | + +`cepis_error` is caller-allocated and owns nothing. On any non-`CEPIS_OK` +return, every out-pointer is `NULL`. `cepis_global_cleanup()` at exit releases +libcurl's global state and the default client. + +## Configuration + +Zero-initialise a `cepis_config`, set what you need, and reuse the client. It +keeps no state between calls and is safe to share across threads: + +```c +cepis_config cfg; +cepis_client *client = NULL; + +memset(&cfg, 0, sizeof cfg); +cfg.base_url = "http://localhost:8080"; /* point at another instance */ +cfg.timeout = 10.0; /* seconds per attempt */ +cfg.max_attempts = 3; /* counting the first; 1 disables retrying */ +cfg.backoff_base = 0.5; /* base of the exponential wait */ +cfg.backoff_max = 8.0; /* cap on each wait, Retry-After included */ +cfg.total_timeout = 0.0; /* cap on the whole call; 0 means none */ + +cepis_client_new(&cfg, &client); +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and full +jitter; 400 and 404 are never retried. `base_url` is the only field that +usually changes between environments. + +Call `cepis_global_init()` once from the main thread before starting other +threads: the lazy initialisation on first request is the one part that races. + +## Errors + +Every call returns a `cepis_status`. The split is by **the decision you have +to make**, not by the HTTP status that caused it: + +| status | when | does retrying help? | +| --- | --- | --- | +| `CEPIS_ERR_INVALID_CEP` | not 8 digits, or a 400 | no | +| `CEPIS_ERR_NOT_FOUND` | well-formed, not in the database | no | +| `CEPIS_ERR_RATE_LIMITED` | 429; see `err.retry_after` | yes, later | +| `CEPIS_ERR_SERVER` | 5xx; see `err.http_status` | yes, later | +| `CEPIS_ERR_NETWORK` | DNS, connection, timeout, TLS | yes, later | +| `CEPIS_ERR_INVALID_RESPONSE` | unintelligible response | probably not | +| `CEPIS_ERR_NOMEM` | an allocation failed | no | +| `CEPIS_ERR_USAGE` | a required out-parameter was `NULL` | no | + +Pass a `cepis_error` to get `message`, `http_status` and `retry_after`; +`cepis_strerror` names the status itself. + +`CEPIS_ERR_INVALID_RESPONSE` almost always means something between you and the +API is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an +HTML error page. Useful as a diagnosis: the problem is not in your code. + +## Limitations + +- `timeout` bounds one **attempt**, not one socket operation: libcurl has no + per-operation timeout. `total_timeout` bounds the whole call. +- `Retry-After` is parsed as a number of seconds and as an IMF-fixdate + (`Wed, 21 Oct 2026 07:28:00 GMT`). The two obsolete HTTP-date formats of RFC + 9110 (RFC 850 and `asctime`) are not parsed; such a header is ignored and + the usual jittered backoff applies. +- `cepis_lookup_many` validates each CEP locally and does not send the + malformed ones, so they cost no bandwidth. Their positions are `NULL` either + way. +- Each request uses a fresh libcurl handle, so connections are not reused + between calls. This is why the batch endpoint exists. +- The JSON reader accepts an object at the top level, tolerates and skips + unknown fields of any shape, and decodes `\uXXXX` including surrogate pairs. + It is not a general-purpose JSON parser and does not build a document tree — + `raw` is there for anything it does not model. + +## Development + +```sh +make # list the targets +make build # static and shared library into build/ +make test # the suite, against a local HTTP server on a random port +make asan # the same suite under AddressSanitizer +make valgrind # the same suite under valgrind +make lint # assemble with warnings fatal, compile the header, style check +make check # lint, then test, then build +make demo # build build/cepis-demo +``` + +`make test` starts `tests/server.py` (Python 3, standard library only), points +the driver at it through `base_url`, and tears it down. Nothing is mocked and +no test touches the network. + +```sh +build/cepis-demo --health 01001000 +build/cepis-demo --base-url http://localhost:8080 01001000 +``` + +## Licence + +ISC. See [LICENSE](LICENSE). |