summaryrefslogtreecommitdiffstats
path: root/README.md
blob: 25fbff6670ffe246778d4e782740aaa31f675a92 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
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).