summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:56:51 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:56:59 -0300
commitb7b08a7d29d84de187dd8a55e0c94de66b07a572 (patch)
tree7e55661d3eca3cf3b16aada6e87eabee68802fb9 /README.md
downloadasm-b7b08a7d29d84de187dd8a55e0c94de66b07a572.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md250
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).