summaryrefslogtreecommitdiffstats
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
downloadasm-b7b08a7d29d84de187dd8a55e0c94de66b07a572.tar.gz
Initial importv0.1.0
-rw-r--r--.gitignore8
-rw-r--r--LICENSE15
-rw-r--r--Makefile121
-rw-r--r--README.md250
-rw-r--r--examples/demo.c83
-rw-r--r--include/cepis.h334
-rw-r--r--src/abi.inc58
-rw-r--r--src/cep.S53
-rw-r--r--src/client.S879
-rw-r--r--src/csv.S297
-rw-r--r--src/defs.inc79
-rw-r--r--src/http.S759
-rw-r--r--src/json.S482
-rw-r--r--src/models.S716
-rw-r--r--src/util.S190
-rw-r--r--tests/abi_probe.S102
-rwxr-xr-xtests/run.sh32
-rwxr-xr-xtests/server.py222
-rw-r--r--tests/test_cepis.c721
-rwxr-xr-xtools/style.sh43
20 files changed, 5444 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fb68e79
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+build/
+*.o
+*.a
+*.so
+*.so.*
+core
+vgcore.*
+__pycache__/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5e9104d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2026 Murilo Ijanc <[email protected]>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c484804
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,121 @@
+.POSIX:
+.SUFFIXES:
+
+VERSION = 0.1.0
+PREFIX = /usr/local
+LIBDIR = $(PREFIX)/lib
+INCDIR = $(PREFIX)/include
+
+AS = gcc
+CC = gcc
+ASFLAGS = -Isrc -Wa,--fatal-warnings
+CFLAGS = -std=c11 -O2 -Wall -Wextra -Iinclude
+LDLIBS = -lcurl
+
+BUILD = build
+SRC = src/util.S src/cep.S src/json.S src/csv.S src/models.S \
+ src/http.S src/client.S
+OBJ = $(BUILD)/util.o $(BUILD)/cep.o $(BUILD)/json.o $(BUILD)/csv.o \
+ $(BUILD)/models.o $(BUILD)/http.o $(BUILD)/client.o
+LIB = $(BUILD)/libcepis.a
+SO = $(BUILD)/libcepis.so.$(VERSION)
+DRIVER = $(BUILD)/test_cepis
+DEMO = $(BUILD)/cepis-demo
+
+.PHONY: help test lint build check clean format install valgrind asan demo
+
+help:
+ @echo 'cepis $(VERSION) - x86-64 assembly client for https://cep.is'
+ @echo
+ @echo 'targets:'
+ @echo ' build assemble the static and shared libraries'
+ @echo ' test run the suite against a local HTTP server'
+ @echo ' valgrind run the same suite under valgrind'
+ @echo ' asan run the same suite under AddressSanitizer'
+ @echo ' lint read-only checks: assemble with warnings fatal,'
+ @echo ' compile the header and driver with -Wall -Wextra,'
+ @echo ' reject tabs/trailing whitespace/over-long lines'
+ @echo ' check lint, then test, then build'
+ @echo ' demo build the example command-line client'
+ @echo ' clean remove $(BUILD)'
+ @echo ' install install the library and header under $(PREFIX)'
+
+build: $(LIB) $(SO)
+
+$(BUILD)/util.o: src/util.S src/abi.inc src/defs.inc
+ @mkdir -p $(BUILD)
+ $(AS) $(ASFLAGS) -c -o $@ src/util.S
+
+$(BUILD)/cep.o: src/cep.S src/abi.inc src/defs.inc
+ @mkdir -p $(BUILD)
+ $(AS) $(ASFLAGS) -c -o $@ src/cep.S
+
+$(BUILD)/json.o: src/json.S src/abi.inc src/defs.inc
+ @mkdir -p $(BUILD)
+ $(AS) $(ASFLAGS) -c -o $@ src/json.S
+
+$(BUILD)/csv.o: src/csv.S src/abi.inc src/defs.inc
+ @mkdir -p $(BUILD)
+ $(AS) $(ASFLAGS) -c -o $@ src/csv.S
+
+$(BUILD)/models.o: src/models.S src/abi.inc src/defs.inc
+ @mkdir -p $(BUILD)
+ $(AS) $(ASFLAGS) -c -o $@ src/models.S
+
+$(BUILD)/http.o: src/http.S src/abi.inc src/defs.inc
+ @mkdir -p $(BUILD)
+ $(AS) $(ASFLAGS) -c -o $@ src/http.S
+
+$(BUILD)/client.o: src/client.S src/abi.inc src/defs.inc
+ @mkdir -p $(BUILD)
+ $(AS) $(ASFLAGS) -c -o $@ src/client.S
+
+$(LIB): $(OBJ)
+ @mkdir -p $(BUILD)
+ ar rcs $@ $(OBJ)
+
+$(SO): $(OBJ)
+ $(CC) -shared -Wl,-soname,libcepis.so.0 -o $@ $(OBJ) $(LDLIBS)
+
+$(DRIVER): tests/test_cepis.c tests/abi_probe.S $(LIB) include/cepis.h
+ $(CC) $(CFLAGS) -o $@ tests/test_cepis.c tests/abi_probe.S $(LIB) \
+ $(LDLIBS)
+
+$(BUILD)/test_cepis_asan: tests/test_cepis.c tests/abi_probe.S $(LIB) include/cepis.h
+ $(CC) $(CFLAGS) -g -fsanitize=address,undefined -fno-omit-frame-pointer \
+ -o $@ tests/test_cepis.c tests/abi_probe.S $(LIB) $(LDLIBS)
+
+$(DEMO): examples/demo.c $(LIB) include/cepis.h
+ $(CC) $(CFLAGS) -o $@ examples/demo.c $(LIB) $(LDLIBS)
+
+demo: $(DEMO)
+
+test: $(DRIVER)
+ sh tests/run.sh $(DRIVER)
+
+asan: $(BUILD)/test_cepis_asan
+ sh tests/run.sh $(BUILD)/test_cepis_asan
+
+valgrind: $(DRIVER)
+ sh tests/run.sh $(DRIVER) valgrind --error-exitcode=9 --leak-check=full \
+ --show-leak-kinds=definite,indirect --errors-for-leak-kinds=definite
+
+lint: $(OBJ) $(DRIVER)
+ $(CC) $(CFLAGS) -fsyntax-only -x c include/cepis.h
+ sh tools/style.sh
+
+check: lint test build
+
+format:
+ @echo 'no formatter for assembly; tools/style.sh is the style check'
+
+clean:
+ rm -rf $(BUILD)
+
+install: build
+ mkdir -p $(DESTDIR)$(LIBDIR) $(DESTDIR)$(INCDIR)
+ cp $(LIB) $(DESTDIR)$(LIBDIR)/libcepis.a
+ cp $(SO) $(DESTDIR)$(LIBDIR)/libcepis.so.$(VERSION)
+ ln -sf libcepis.so.$(VERSION) $(DESTDIR)$(LIBDIR)/libcepis.so.0
+ ln -sf libcepis.so.0 $(DESTDIR)$(LIBDIR)/libcepis.so
+ cp include/cepis.h $(DESTDIR)$(INCDIR)/cepis.h
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).
diff --git a/examples/demo.c b/examples/demo.c
new file mode 100644
index 0000000..d3109a6
--- /dev/null
+++ b/examples/demo.c
@@ -0,0 +1,83 @@
+/*
+ * Small command-line client, mostly useful to confirm quickly that an
+ * instance answers:
+ *
+ * cepis-demo 01001000
+ * cepis-demo --base-url http://localhost:8080 01001000 20040020
+ * cepis-demo --health
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "cepis.h"
+
+static void print_address(const cepis_address *a)
+{
+ char masked[10];
+
+ cepis_address_formatted_cep(a, masked);
+ printf("%s %s, %s - %s/%s\n", masked, a->logradouro, a->bairro,
+ a->localidade, a->uf);
+ if (a->geo != NULL)
+ printf(" geo %.6f,%.6f raio %ldm (%s)\n", a->geo->lat,
+ a->geo->lon, a->geo->raio_m, a->geo->precisao);
+}
+
+int main(int argc, char **argv)
+{
+ cepis_config cfg;
+ cepis_client *client = NULL;
+ cepis_error err;
+ int want_health = 0;
+ int i;
+ int rc = 0;
+
+ memset(&cfg, 0, sizeof cfg);
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--base-url") == 0 && i + 1 < argc)
+ cfg.base_url = argv[++i];
+ else if (strcmp(argv[i], "--health") == 0)
+ want_health = 1;
+ else
+ break;
+ }
+
+ if (cepis_client_new(&cfg, &client) != CEPIS_OK) {
+ fprintf(stderr, "cannot build a client\n");
+ return 1;
+ }
+
+ if (want_health) {
+ struct cepis_health *health = NULL;
+ cepis_status st = cepis_health(client, &health, &err);
+
+ if (st != CEPIS_OK) {
+ fprintf(stderr, "health: %s\n", err.message);
+ rc = 1;
+ } else {
+ printf("status %s dne %s ceps %lld geo %lld gerada %s\n",
+ health->status, health->dne, health->ceps, health->geo,
+ health->base_gerada_em);
+ cepis_health_free(health);
+ }
+ }
+
+ for (; i < argc; i++) {
+ cepis_address *address = NULL;
+ cepis_status st = cepis_lookup(client, argv[i], &address, &err);
+
+ if (st != CEPIS_OK) {
+ fprintf(stderr, "%s: %s (%s)\n", argv[i], err.message,
+ cepis_strerror(st));
+ rc = 1;
+ continue;
+ }
+ print_address(address);
+ cepis_address_free(address);
+ }
+
+ cepis_client_free(client);
+ cepis_global_cleanup();
+ return rc;
+}
diff --git a/include/cepis.h b/include/cepis.h
new file mode 100644
index 0000000..ea8d378
--- /dev/null
+++ b/include/cepis.h
@@ -0,0 +1,334 @@
+/*
+ * cepis - client for the public CEP API at https://cep.is
+ *
+ * Copyright (c) 2026 Murilo Ijanc <[email protected]>
+ * SPDX-License-Identifier: ISC
+ *
+ * The whole implementation is hand-written x86-64 assembly (GNU as, AT&T
+ * syntax); this header is the only C in the library. Every entry point below
+ * follows the System V AMD64 ABI.
+ */
+
+#ifndef CEPIS_H
+#define CEPIS_H
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define CEPIS_VERSION "0.1.0"
+
+/** Largest number of CEPs the API accepts in one /csv request. */
+#define CEPIS_MAX_BATCH 500
+
+/** Size of cepis_error::message, including the terminating NUL. */
+#define CEPIS_MESSAGE_MAX 256
+
+/**
+ * Outcome of a call. Split by the decision the caller has to make, not by the
+ * HTTP status that caused it: CEPIS_ERR_INVALID_CEP and CEPIS_ERR_NOT_FOUND
+ * are problems with the request and retrying is pointless, while
+ * CEPIS_ERR_RATE_LIMITED, CEPIS_ERR_SERVER and CEPIS_ERR_NETWORK are problems
+ * with transport that may succeed later. The client has already retried those
+ * three internally before returning them.
+ */
+typedef enum {
+ CEPIS_OK = 0,
+ /** The CEP is not 8 digits. Detected locally; no request was made. */
+ CEPIS_ERR_INVALID_CEP = 1,
+ /** Well-formed, but absent from the DNE database (HTTP 404). */
+ CEPIS_ERR_NOT_FOUND = 2,
+ /** HTTP 429 on every attempt. See cepis_error::retry_after. */
+ CEPIS_ERR_RATE_LIMITED = 3,
+ /** HTTP 5xx on every attempt. See cepis_error::http_status. */
+ CEPIS_ERR_SERVER = 4,
+ /** The request never completed: DNS, connection, timeout, TLS. */
+ CEPIS_ERR_NETWORK = 5,
+ /** A success status came back with a body that cannot be parsed. */
+ CEPIS_ERR_INVALID_RESPONSE = 6,
+ /** An allocation failed. */
+ CEPIS_ERR_NOMEM = 7,
+ /** A required out-parameter was NULL, or a configuration value was
+ * nonsensical (a negative max_attempts). */
+ CEPIS_ERR_USAGE = 8
+} cepis_status;
+
+/**
+ * The CEP's coordinate, when the database has one.
+ *
+ * The point is the median of the coordinates of that CEP's addresses, not the
+ * mean: one badly geocoded address 40 km away shifts a mean and does not move
+ * a median.
+ *
+ * Always check `precisao` before using lat/lon for distance:
+ *
+ * - "logradouro": the point is a stretch of street, `raio_m` around
+ * 100-150 m. Good for maps and for distance.
+ * - "localidade": the CEP *is* a whole municipality and the point is its
+ * centroid, `raio_m` from 7 km to 40 km. Computing shipping from this is
+ * wrong by tens of kilometres.
+ *
+ * `raio_m` is the radius containing 90% of that CEP's addresses. It exists
+ * precisely so that error cannot pass unnoticed.
+ */
+typedef struct cepis_geo {
+ double lat;
+ double lon;
+ long raio_m;
+ /** "logradouro" or "localidade". Never NULL. */
+ char *precisao;
+} cepis_geo;
+
+/**
+ * A CEP and everything the database knows about it.
+ *
+ * Field names are the API's own, in Portuguese, so that what you read in the
+ * JSON at https://cep.is/01001000 is exactly what you access here.
+ *
+ * Every char* field is non-NULL. A field the database lacks for that CEP is
+ * the empty string, never NULL, matching what the API returns.
+ *
+ * `geo` is the exception: NULL when there is no coordinate (about 43% of
+ * CEPs), because the API omits the whole object in that case rather than
+ * sending null. It is also always NULL for addresses from
+ * cepis_lookup_many() - see that function.
+ *
+ * `raw` holds the response body exactly as it arrived, so a field the API
+ * grows before this library is updated is not lost. It is NULL for addresses
+ * produced by cepis_lookup_many().
+ *
+ * Free with cepis_address_free().
+ */
+typedef struct cepis_address {
+ char *cep;
+ char *logradouro;
+ char *complemento;
+ char *unidade;
+ char *bairro;
+ char *localidade;
+ char *uf;
+ char *estado;
+ char *regiao;
+ char *ibge;
+ char *gia;
+ char *ddd;
+ char *siafi;
+ cepis_geo *geo;
+ char *raw;
+} cepis_address;
+
+/**
+ * The response of GET /health.
+ *
+ * This one is a bare struct tag, not a typedef, because cepis_health() is the
+ * function that fills it - the same split POSIX makes between struct stat and
+ * stat(). Write struct cepis_health.
+ *
+ * Good for two things: checking that the API answers, and knowing which
+ * version of the database is live. The DNE database changes every two weeks;
+ * `dne` identifies it. An API reporting itself degraded is not an error - that
+ * arrives in `status`.
+ *
+ * Free with cepis_health_free().
+ */
+struct cepis_health {
+ /** "ok" when the API reports itself healthy. Never NULL. */
+ char *status;
+ char *dne;
+ long long ceps;
+ long long geo;
+ char *base_gerada_em;
+ /** The response body as it arrived. Never NULL. */
+ char *raw;
+};
+
+/**
+ * Detail about a failure. Optional: pass NULL to any call that takes one.
+ *
+ * Caller-allocated, no ownership: declare one on the stack and reuse it. It is
+ * fully overwritten at the start of every call that receives it.
+ */
+typedef struct cepis_error {
+ /** Same value the call returned. */
+ int status;
+ /** The HTTP status for CEPIS_ERR_SERVER and CEPIS_ERR_RATE_LIMITED,
+ * else 0. */
+ int http_status;
+ /** Seconds the server asked for via Retry-After, or 0 if it did not say.
+ * Only meaningful for CEPIS_ERR_RATE_LIMITED. */
+ double retry_after;
+ /** Human-readable description, NUL-terminated, empty on success. */
+ char message[CEPIS_MESSAGE_MAX];
+} cepis_error;
+
+/**
+ * Client configuration. Zero-initialise it and set only what you need: every
+ * field left at 0 (or NULL) takes its default.
+ */
+typedef struct cepis_config {
+ /** Where the API lives. Default "https://cep.is". A trailing slash is
+ * stripped. This is the only field that usually changes between
+ * environments - point it at a local instance to test. */
+ const char *base_url;
+ /** Seconds allowed for one attempt, connect and transfer together.
+ * Default 10.0. libcurl has no per-socket-operation timeout, so unlike
+ * the Python client this is a per-attempt cap; `total_timeout` caps the
+ * whole call including retries and waits. */
+ double timeout;
+ /** Attempts in total, counting the first. Default 3; 1 disables retrying.
+ * Negative is rejected with CEPIS_ERR_USAGE. */
+ int max_attempts;
+ /** Base of the exponential wait, in seconds. Default 0.5. The actual wait
+ * is drawn from [0, base * 2^(n-1)] - full jitter. Without the
+ * randomness, N clients that fail together come back together. */
+ double backoff_base;
+ /** Cap on each individual wait, in seconds. Default 8.0. It applies to a
+ * server-sent Retry-After too, so a misconfigured proxy answering
+ * "Retry-After: 3600" cannot park the caller for an hour. */
+ double backoff_max;
+ /** Cap on the whole call, attempts and waits included, in seconds.
+ * 0 leaves max_attempts as the only bound. */
+ double total_timeout;
+ /** Overrides the User-Agent. Default
+ * "cepis-asm/0.1.0 (+https://cep.is)". */
+ const char *user_agent;
+} cepis_config;
+
+/** Opaque client. Create with cepis_client_new(), destroy with
+ * cepis_client_free(). The configuration is immutable and no call keeps state
+ * between requests, so one instance is safe to share across threads. */
+typedef struct cepis_client cepis_client;
+
+/** Library version, "0.1.0". Static storage; do not free. */
+const char *cepis_version(void);
+
+/** One-line description of a cepis_status. Static storage; do not free. */
+const char *cepis_strerror(int status);
+
+/**
+ * Build a client. `cfg` may be NULL for an all-defaults client.
+ *
+ * On CEPIS_OK, *out owns a client the caller must release with
+ * cepis_client_free(). On failure *out is NULL.
+ */
+cepis_status cepis_client_new(const cepis_config *cfg, cepis_client **out);
+
+/** Release a client. NULL is accepted. */
+void cepis_client_free(cepis_client *client);
+
+/**
+ * The process-wide default client, created on first use with all defaults.
+ * This is what the lookup calls use when passed a NULL client. Returns NULL
+ * only if it could not be allocated. Owned by the library; do not free it -
+ * cepis_global_cleanup() releases it.
+ */
+cepis_client *cepis_default_client(void);
+
+/**
+ * Initialise libcurl's global state. Optional: the first request does it
+ * anyway. Call it once from the main thread before starting other threads if
+ * your program is multi-threaded, since the lazy path races.
+ */
+void cepis_global_init(void);
+
+/**
+ * Release libcurl's global state and the default client. Call once at exit,
+ * after the last cepis call and with no other thread in the library. Not
+ * required for correctness, but it makes leak checkers quiet.
+ */
+void cepis_global_cleanup(void);
+
+/**
+ * Return the CEP's 8 digits, accepting the mask.
+ *
+ * "01001-000", "01001000" and " 01001-000 " all become "01001000"; the
+ * separators '-', '.' and whitespace are ignored wherever they appear.
+ *
+ * `out` must have room for 9 bytes and receives a NUL-terminated string, or
+ * "" on failure. Returns CEPIS_ERR_INVALID_CEP unless exactly 8 digits
+ * remain, or CEPIS_ERR_USAGE if `out` is NULL.
+ */
+cepis_status cepis_normalize_cep(const char *value, char *out);
+
+/**
+ * Look up a single CEP. `cep` may be masked or unmasked.
+ *
+ * `client` may be NULL to use the default client. `err` may be NULL.
+ *
+ * On CEPIS_OK, *out owns an address the caller must release with
+ * cepis_address_free(). On failure *out is NULL.
+ *
+ * The CEP is validated locally first: a 7-digit CEP returns
+ * CEPIS_ERR_INVALID_CEP without a request being made. A 400 from the API is
+ * also CEPIS_ERR_INVALID_CEP, carrying the API's own message. A 404 is
+ * CEPIS_ERR_NOT_FOUND. Neither is retried; 429, 5xx and network failures are.
+ */
+cepis_status cepis_lookup(cepis_client *client, const char *cep,
+ cepis_address **out, cepis_error *err);
+
+/**
+ * Look up many CEPs, up to CEPIS_MAX_BATCH per request. Do not loop over
+ * cepis_lookup(): 500 calls pay 500 TLS handshakes, the batch pays one.
+ *
+ * On CEPIS_OK, *out owns an array of exactly `n` pointers, in the same order
+ * as `ceps`. A CEP that does not exist, or that is malformed, is NULL at its
+ * position - it does not vanish from the array and it is not an error. If you
+ * need to know which one was malformed, validate first with
+ * cepis_normalize_cep(). Release the array with
+ * cepis_address_list_free(arr, n).
+ *
+ * Input longer than CEPIS_MAX_BATCH is chunked automatically.
+ *
+ * WARNING: the batch carries no coordinate. The endpoint behind it is CSV and
+ * the CSV has no geo column, so every address returned here has geo == NULL,
+ * including for CEPs that do have one. Need geo? Use cepis_lookup(), one at a
+ * time. `raw` is NULL here for the same reason.
+ *
+ * `n` may be 0, in which case *out is a valid empty array.
+ */
+cepis_status cepis_lookup_many(cepis_client *client, const char *const *ceps,
+ size_t n, cepis_address ***out,
+ cepis_error *err);
+
+/**
+ * Query GET /health.
+ *
+ * On CEPIS_OK, *out owns a cepis_health the caller must release with
+ * cepis_health_free(). An API reporting itself degraded (HTTP 503 with a body)
+ * is still CEPIS_OK - the diagnosis arrives in cepis_health::status, which is
+ * exactly what you want when the thing you are doing is checking health.
+ * Network failures still fail.
+ */
+cepis_status cepis_health(cepis_client *client, struct cepis_health **out,
+ cepis_error *err);
+
+/** Release an address. NULL is accepted. */
+void cepis_address_free(cepis_address *address);
+
+/** Release an array of `n` addresses and the array itself, skipping NULL
+ * entries. NULL is accepted. */
+void cepis_address_list_free(cepis_address **addresses, size_t n);
+
+/** Release a cepis_health. NULL is accepted. */
+void cepis_health_free(struct cepis_health *health);
+
+/**
+ * Write the CEP with its mask, "01001-000", into `out`, which must have room
+ * for 10 bytes. The API always returns it unmasked; this is presentation.
+ */
+void cepis_address_formatted_cep(const cepis_address *address, char *out);
+
+/**
+ * Non-zero when the coordinate points at a street rather than a municipality,
+ * i.e. strcmp(geo->precisao, "logradouro") == 0. This is the check to make
+ * before using the point to measure distance. NULL returns 0.
+ */
+int cepis_geo_is_precise(const cepis_geo *geo);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CEPIS_H */
diff --git a/src/abi.inc b/src/abi.inc
new file mode 100644
index 0000000..490c4b1
--- /dev/null
+++ b/src/abi.inc
@@ -0,0 +1,58 @@
+#ifndef CEPIS_ABI_INC
+#define CEPIS_ABI_INC
+
+.macro FUNC name
+ .text
+ .globl \name
+ .hidden \name
+ .type \name, @function
+ .p2align 4
+\name:
+.endm
+
+.macro PUBFUNC name
+ .text
+ .globl \name
+ .type \name, @function
+ .p2align 4
+\name:
+.endm
+
+.macro ENDFUNC name
+ .size \name, . - \name
+.endm
+
+.macro PROLOGUE n=8
+.if (\n % 16) != 8
+ .error "PROLOGUE frame size must be congruent to 8 modulo 16"
+.endif
+ push %rbp
+ mov %rsp, %rbp
+ push %rbx
+ push %r12
+ push %r13
+ push %r14
+ push %r15
+ sub $\n, %rsp
+.endm
+
+.macro EPILOGUE
+ lea -40(%rbp), %rsp
+ pop %r15
+ pop %r14
+ pop %r13
+ pop %r12
+ pop %rbx
+ pop %rbp
+.endm
+
+#define LOCAL0 -48
+#define LOCAL1 -56
+#define LOCAL2 -64
+#define LOCAL3 -72
+#define LOCAL4 -80
+#define LOCAL5 -88
+#define LOCAL6 -96
+#define LOCAL7 -104
+
+#endif
diff --git a/src/cep.S b/src/cep.S
new file mode 100644
index 0000000..8b7661d
--- /dev/null
+++ b/src/cep.S
@@ -0,0 +1,53 @@
+#include "abi.inc"
+#include "defs.inc"
+
+ .text
+
+PUBFUNC cepis_normalize_cep
+ test %rsi, %rsi
+ jz .Lnorm_usage
+ movb $0, (%rsi)
+ test %rdi, %rdi
+ jz .Lnorm_bad
+ xor %ecx, %ecx
+.Lnorm_loop:
+ movzbl (%rdi), %eax
+ inc %rdi
+ test %al, %al
+ jz .Lnorm_done
+ cmp $'-', %al
+ je .Lnorm_loop
+ cmp $'.', %al
+ je .Lnorm_loop
+ cmp $' ', %al
+ je .Lnorm_loop
+ cmp $9, %al
+ jb .Lnorm_digit
+ cmp $13, %al
+ jbe .Lnorm_loop
+.Lnorm_digit:
+ cmp $'0', %al
+ jb .Lnorm_bad
+ cmp $'9', %al
+ ja .Lnorm_bad
+ cmp $8, %rcx
+ jae .Lnorm_bad
+ mov %al, (%rsi, %rcx, 1)
+ inc %rcx
+ jmp .Lnorm_loop
+.Lnorm_done:
+ cmp $8, %rcx
+ jne .Lnorm_bad
+ movb $0, 8(%rsi)
+ xor %eax, %eax
+ ret
+.Lnorm_bad:
+ movb $0, (%rsi)
+ mov $CEPIS_ERR_INVALID_CEP, %eax
+ ret
+.Lnorm_usage:
+ mov $CEPIS_ERR_USAGE, %eax
+ ret
+ENDFUNC cepis_normalize_cep
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/src/client.S b/src/client.S
new file mode 100644
index 0000000..469f0f2
--- /dev/null
+++ b/src/client.S
@@ -0,0 +1,879 @@
+#include "abi.inc"
+#include "defs.inc"
+
+#define LK_URL -72
+#define LK_BODY -96
+#define LK_TMP -112
+#define LK_STATUS -120
+#define LK_MSG -128
+
+#define HL_URL -72
+#define HL_BODY -96
+#define HL_STATUS -104
+
+#define LM_IDX -48
+#define LM_CHUNK -56
+#define LM_SENT -64
+#define LM_MAP -72
+#define LM_ROWS -80
+#define LM_NROWS -88
+#define LM_STATUS -96
+#define LM_TMP -112
+#define LM_URL -136
+#define LM_BODY -160
+#define LM_J -168
+#define LM_K -176
+
+#define JF_KEY -48
+#define JF_VAL -56
+#define JF_OUT -64
+
+ .text
+
+PUBFUNC cepis_version
+ lea .Ls_version(%rip), %rax
+ ret
+ENDFUNC cepis_version
+
+PUBFUNC cepis_strerror
+ lea cepis__errnames(%rip), %rax
+ cmp $0, %edi
+ jl .Lse_unknown
+ cmp $8, %edi
+ jg .Lse_unknown
+ movslq %edi, %rdi
+ mov (%rax, %rdi, 8), %rax
+ ret
+.Lse_unknown:
+ lea .Ls_unknown(%rip), %rax
+ ret
+ENDFUNC cepis_strerror
+
+FUNC cepis__err_reset
+ test %rdi, %rdi
+ jz .Ler_out
+ movl $0, E_STATUS(%rdi)
+ movl $0, E_HTTP_STATUS(%rdi)
+ movq $0, E_RETRY_AFTER(%rdi)
+ movb $0, E_MESSAGE(%rdi)
+.Ler_out:
+ ret
+ENDFUNC cepis__err_reset
+
+FUNC cepis__err_set
+ PROLOGUE 8
+ test %rdi, %rdi
+ jz .Les_out
+ mov %esi, E_STATUS(%rdi)
+ mov %rdx, %r10
+ lea E_MESSAGE(%rdi), %rdi
+ mov $E_MSGLEN, %esi
+ mov %r10, %rdx
+ xor %eax, %eax
+ call snprintf@PLT
+.Les_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis__err_set
+
+FUNC cepis__json_field
+ PROLOGUE 40
+ mov %rsi, %r15
+ movq $0, JF_KEY(%rbp)
+ movq $0, JF_VAL(%rbp)
+ movq $0, JF_OUT(%rbp)
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $'{', (%r12)
+ jne .Ljf_out
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $'}', (%r12)
+ je .Ljf_out
+.Ljf_pair:
+ cmpb $'"', (%r12)
+ jne .Ljf_out
+ mov %r12, %rdi
+ lea JF_KEY(%rbp), %rsi
+ call cepis__js_string
+ test %rax, %rax
+ jz .Ljf_out
+ mov %rax, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $':', (%r12)
+ jne .Ljf_out
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpq $0, JF_OUT(%rbp)
+ jne .Ljf_skip
+ mov JF_KEY(%rbp), %rdi
+ mov %r15, %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jnz .Ljf_skip
+ movzbl (%r12), %eax
+ cmp $'{', %al
+ je .Ljf_skip
+ cmp $'[', %al
+ je .Ljf_skip
+ mov %r12, %rdi
+ lea JF_VAL(%rbp), %rsi
+ call cepis__js_scalar_text
+ test %rax, %rax
+ jz .Ljf_out
+ mov %rax, %r12
+ mov JF_VAL(%rbp), %rax
+ mov %rax, JF_OUT(%rbp)
+ jmp .Ljf_next
+.Ljf_skip:
+ mov %r12, %rdi
+ call cepis__js_skip
+ test %rax, %rax
+ jz .Ljf_out
+ mov %rax, %r12
+.Ljf_next:
+ mov JF_KEY(%rbp), %rdi
+ call free@PLT
+ movq $0, JF_KEY(%rbp)
+ mov %r12, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $',', (%r12)
+ jne .Ljf_out
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ jmp .Ljf_pair
+.Ljf_out:
+ mov JF_KEY(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Ljf_ret
+ call free@PLT
+.Ljf_ret:
+ mov JF_OUT(%rbp), %rax
+ EPILOGUE
+ ret
+ENDFUNC cepis__json_field
+
+PUBFUNC cepis_client_new
+ PROLOGUE 40
+ mov %rsi, %r14
+ test %r14, %r14
+ jz .Lcn_usage
+ movq $0, (%r14)
+ mov %rdi, %r15
+ test %r15, %r15
+ jz .Lcn_alloc
+ mov C_MAX_ATTEMPTS(%r15), %eax
+ cmp $0, %eax
+ jl .Lcn_usage
+.Lcn_alloc:
+ mov $1, %rdi
+ mov $CL_SIZE, %rsi
+ call calloc@PLT
+ test %rax, %rax
+ jz .Lcn_nomem
+ mov %rax, %rbx
+ lea .Ls_default_url(%rip), %r12
+ test %r15, %r15
+ jz .Lcn_url
+ mov C_BASE_URL(%r15), %rax
+ test %rax, %rax
+ jz .Lcn_url
+ mov %rax, %r12
+.Lcn_url:
+ mov %r12, %rdi
+ call cepis__strdup
+ test %rax, %rax
+ jz .Lcn_nomem_free
+ mov %rax, CL_BASE_URL(%rbx)
+ mov %rax, %rdi
+ call strlen@PLT
+ mov CL_BASE_URL(%rbx), %rdx
+.Lcn_strip:
+ test %rax, %rax
+ jz .Lcn_ua
+ cmpb $'/', -1(%rdx, %rax, 1)
+ jne .Lcn_ua
+ dec %rax
+ movb $0, (%rdx, %rax, 1)
+ jmp .Lcn_strip
+.Lcn_ua:
+ lea .Ls_default_ua(%rip), %r12
+ test %r15, %r15
+ jz .Lcn_ua_dup
+ mov C_USER_AGENT(%r15), %rax
+ test %rax, %rax
+ jz .Lcn_ua_dup
+ mov %rax, %r12
+.Lcn_ua_dup:
+ mov %r12, %rdi
+ call cepis__strdup
+ test %rax, %rax
+ jz .Lcn_nomem_free
+ mov %rax, CL_USER_AGENT(%rbx)
+ movsd .Ld_timeout(%rip), %xmm0
+ movl $3, %eax
+ mov %eax, CL_MAX_ATTEMPTS(%rbx)
+ movsd %xmm0, CL_TIMEOUT(%rbx)
+ movsd .Ld_backoff_base(%rip), %xmm0
+ movsd %xmm0, CL_BACKOFF_BASE(%rbx)
+ movsd .Ld_backoff_max(%rip), %xmm0
+ movsd %xmm0, CL_BACKOFF_MAX(%rbx)
+ xorpd %xmm0, %xmm0
+ movsd %xmm0, CL_TOTAL_TIMEOUT(%rbx)
+ test %r15, %r15
+ jz .Lcn_seed
+ movsd C_TIMEOUT(%r15), %xmm0
+ xorpd %xmm1, %xmm1
+ comisd %xmm1, %xmm0
+ jbe .Lcn_attempts
+ movsd %xmm0, CL_TIMEOUT(%rbx)
+.Lcn_attempts:
+ mov C_MAX_ATTEMPTS(%r15), %eax
+ test %eax, %eax
+ jz .Lcn_bbase
+ mov %eax, CL_MAX_ATTEMPTS(%rbx)
+.Lcn_bbase:
+ movsd C_BACKOFF_BASE(%r15), %xmm0
+ xorpd %xmm1, %xmm1
+ comisd %xmm1, %xmm0
+ jbe .Lcn_bmax
+ movsd %xmm0, CL_BACKOFF_BASE(%rbx)
+.Lcn_bmax:
+ movsd C_BACKOFF_MAX(%r15), %xmm0
+ xorpd %xmm1, %xmm1
+ comisd %xmm1, %xmm0
+ jbe .Lcn_total
+ movsd %xmm0, CL_BACKOFF_MAX(%rbx)
+.Lcn_total:
+ movsd C_TOTAL_TIMEOUT(%r15), %xmm0
+ xorpd %xmm1, %xmm1
+ comisd %xmm1, %xmm0
+ jbe .Lcn_seed
+ movsd %xmm0, CL_TOTAL_TIMEOUT(%rbx)
+.Lcn_seed:
+ xor %edi, %edi
+ lea LOCAL1(%rbp), %rsi
+ call clock_gettime@PLT
+ mov LOCAL1(%rbp), %rax
+ mov LOCAL0(%rbp), %rdx
+ imul $1000000007, %rax, %rax
+ xor %rdx, %rax
+ xor %rbx, %rax
+ mov $1, %rdx
+ lock xadd %rdx, cepis__seed_counter(%rip)
+ imul $1103515245, %rdx, %rdx
+ xor %rdx, %rax
+ test %rax, %rax
+ jnz .Lcn_store_seed
+ movabs $0x9E3779B97F4A7C15, %rax
+.Lcn_store_seed:
+ mov %rax, CL_RNG(%rbx)
+ mov %rbx, (%r14)
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lcn_nomem_free:
+ mov %rbx, %rdi
+ call cepis_client_free
+.Lcn_nomem:
+ mov $CEPIS_ERR_NOMEM, %eax
+ EPILOGUE
+ ret
+.Lcn_usage:
+ mov $CEPIS_ERR_USAGE, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis_client_new
+
+PUBFUNC cepis_client_free
+ PROLOGUE 8
+ test %rdi, %rdi
+ jz .Lcf_out
+ mov %rdi, %rbx
+ mov CL_BASE_URL(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lcf_ua
+ call free@PLT
+.Lcf_ua:
+ mov CL_USER_AGENT(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lcf_self
+ call free@PLT
+.Lcf_self:
+ mov %rbx, %rdi
+ call free@PLT
+.Lcf_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis_client_free
+
+PUBFUNC cepis_default_client
+ PROLOGUE 24
+ mov cepis__default_client(%rip), %rax
+ test %rax, %rax
+ jnz .Ldc_out
+ xor %edi, %edi
+ lea LOCAL0(%rbp), %rsi
+ call cepis_client_new
+ test %eax, %eax
+ jnz .Ldc_fail
+ mov LOCAL0(%rbp), %rbx
+ xor %eax, %eax
+ lock cmpxchgq %rbx, cepis__default_client(%rip)
+ jz .Ldc_won
+ mov %rax, %r12
+ mov %rbx, %rdi
+ call cepis_client_free
+ mov %r12, %rax
+ EPILOGUE
+ ret
+.Ldc_won:
+ mov %rbx, %rax
+.Ldc_out:
+ EPILOGUE
+ ret
+.Ldc_fail:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis_default_client
+
+FUNC cepis__default_client_destroy
+ PROLOGUE 8
+ xor %eax, %eax
+ xchg %rax, cepis__default_client(%rip)
+ test %rax, %rax
+ jz .Ldd_out
+ mov %rax, %rdi
+ call cepis_client_free
+.Ldd_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis__default_client_destroy
+
+FUNC cepis__resolve
+ test %rdi, %rdi
+ jz .Lrc_default
+ mov %rdi, %rax
+ ret
+.Lrc_default:
+ jmp cepis_default_client
+ENDFUNC cepis__resolve
+
+PUBFUNC cepis_lookup
+ PROLOGUE 88
+ mov %rdi, %rbx
+ mov %rsi, %r13
+ mov %rdx, %r14
+ mov %rcx, %r15
+ test %r14, %r14
+ jz .Llk_usage
+ movq $0, (%r14)
+ mov %r15, %rdi
+ call cepis__err_reset
+ mov %rbx, %rdi
+ call cepis__resolve
+ test %rax, %rax
+ jz .Llk_nomem_bare
+ mov %rax, %rbx
+ movq $0, LK_URL(%rbp)
+ movq $0, LK_URL+8(%rbp)
+ movq $0, LK_URL+16(%rbp)
+ movq $0, LK_BODY(%rbp)
+ movq $0, LK_BODY+8(%rbp)
+ movq $0, LK_BODY+16(%rbp)
+ mov %r13, %rdi
+ lea LK_TMP(%rbp), %rsi
+ call cepis_normalize_cep
+ test %eax, %eax
+ jnz .Llk_invalid_input
+ lea LK_URL(%rbp), %rdi
+ mov CL_BASE_URL(%rbx), %rsi
+ call cepis__buf_appendz
+ test %eax, %eax
+ jnz .Llk_nomem
+ lea LK_URL(%rbp), %rdi
+ mov $'/', %esi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Llk_nomem
+ lea LK_URL(%rbp), %rdi
+ lea LK_TMP(%rbp), %rsi
+ call cepis__buf_appendz
+ test %eax, %eax
+ jnz .Llk_nomem
+ mov %rbx, %rdi
+ mov LK_URL(%rbp), %rsi
+ xor %edx, %edx
+ lea LK_STATUS(%rbp), %rcx
+ lea LK_BODY(%rbp), %r8
+ mov %r15, %r9
+ call cepis__http_get
+ test %eax, %eax
+ jnz .Llk_httperr
+ mov LK_STATUS(%rbp), %eax
+ cmp $404, %eax
+ je .Llk_notfound
+ cmp $400, %eax
+ je .Llk_badcep
+ cmp $200, %eax
+ jne .Llk_badresp
+ mov LK_BODY(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llk_badresp
+ mov %r14, %rsi
+ call cepis__address_from_json
+ mov %eax, %r12d
+ test %eax, %eax
+ jz .Llk_done
+ cmp $CEPIS_ERR_NOMEM, %eax
+ je .Llk_nomem
+ mov %r15, %rdi
+ mov $CEPIS_ERR_INVALID_RESPONSE, %esi
+ lea .Lfmt_badjson(%rip), %rdx
+ mov LK_URL(%rbp), %rcx
+ xor %r8d, %r8d
+ call cepis__err_set
+ mov $CEPIS_ERR_INVALID_RESPONSE, %r12d
+ jmp .Llk_ret
+.Llk_done:
+ xor %r12d, %r12d
+.Llk_ret:
+ lea LK_URL(%rbp), %rdi
+ call cepis__buf_free
+ lea LK_BODY(%rbp), %rdi
+ call cepis__buf_free
+ mov %r12d, %eax
+ EPILOGUE
+ ret
+.Llk_httperr:
+ mov %eax, %r12d
+ jmp .Llk_ret
+.Llk_notfound:
+ mov %r15, %rdi
+ mov $CEPIS_ERR_NOT_FOUND, %esi
+ lea .Lfmt_notfound(%rip), %rdx
+ lea LK_TMP(%rbp), %rcx
+ xor %r8d, %r8d
+ call cepis__err_set
+ mov $CEPIS_ERR_NOT_FOUND, %r12d
+ jmp .Llk_ret
+.Llk_badcep:
+ movq $0, LK_MSG(%rbp)
+ mov LK_BODY(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llk_badcep_msg
+ lea .Ls_mensagem(%rip), %rsi
+ call cepis__json_field
+ mov %rax, LK_MSG(%rbp)
+.Llk_badcep_msg:
+ mov LK_MSG(%rbp), %r8
+ test %r8, %r8
+ jnz .Llk_badcep_emit
+ lea .Ls_rejected(%rip), %r8
+.Llk_badcep_emit:
+ mov %r15, %rdi
+ mov $CEPIS_ERR_INVALID_CEP, %esi
+ lea .Lfmt_badcep(%rip), %rdx
+ lea LK_TMP(%rbp), %rcx
+ call cepis__err_set
+ mov LK_MSG(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llk_badcep_done
+ call free@PLT
+.Llk_badcep_done:
+ mov $CEPIS_ERR_INVALID_CEP, %r12d
+ jmp .Llk_ret
+.Llk_badresp:
+ mov %r15, %rdi
+ mov $CEPIS_ERR_INVALID_RESPONSE, %esi
+ lea .Lfmt_status(%rip), %rdx
+ mov LK_STATUS(%rbp), %ecx
+ mov LK_URL(%rbp), %r8
+ call cepis__err_set
+ mov $CEPIS_ERR_INVALID_RESPONSE, %r12d
+ jmp .Llk_ret
+.Llk_invalid_input:
+ mov %r15, %rdi
+ mov $CEPIS_ERR_INVALID_CEP, %esi
+ lea .Lfmt_notdigits(%rip), %rdx
+ mov %r13, %rcx
+ test %rcx, %rcx
+ jnz .Llk_invalid_emit
+ lea .Ls_nullstr(%rip), %rcx
+.Llk_invalid_emit:
+ xor %r8d, %r8d
+ call cepis__err_set
+ mov $CEPIS_ERR_INVALID_CEP, %r12d
+ jmp .Llk_ret
+.Llk_nomem:
+ mov $CEPIS_ERR_NOMEM, %r12d
+ jmp .Llk_ret
+.Llk_nomem_bare:
+ mov $CEPIS_ERR_NOMEM, %eax
+ EPILOGUE
+ ret
+.Llk_usage:
+ mov $CEPIS_ERR_USAGE, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis_lookup
+
+PUBFUNC cepis_health
+ PROLOGUE 72
+ mov %rdi, %rbx
+ mov %rsi, %r14
+ mov %rdx, %r15
+ test %r14, %r14
+ jz .Lhl_usage
+ movq $0, (%r14)
+ mov %r15, %rdi
+ call cepis__err_reset
+ mov %rbx, %rdi
+ call cepis__resolve
+ test %rax, %rax
+ jz .Lhl_nomem_bare
+ mov %rax, %rbx
+ movq $0, HL_URL(%rbp)
+ movq $0, HL_URL+8(%rbp)
+ movq $0, HL_URL+16(%rbp)
+ movq $0, HL_BODY(%rbp)
+ movq $0, HL_BODY+8(%rbp)
+ movq $0, HL_BODY+16(%rbp)
+ lea HL_URL(%rbp), %rdi
+ mov CL_BASE_URL(%rbx), %rsi
+ call cepis__buf_appendz
+ test %eax, %eax
+ jnz .Lhl_nomem
+ lea HL_URL(%rbp), %rdi
+ lea .Ls_health_path(%rip), %rsi
+ call cepis__buf_appendz
+ test %eax, %eax
+ jnz .Lhl_nomem
+ mov %rbx, %rdi
+ mov HL_URL(%rbp), %rsi
+ mov $1, %edx
+ lea HL_STATUS(%rbp), %rcx
+ lea HL_BODY(%rbp), %r8
+ mov %r15, %r9
+ call cepis__http_get
+ test %eax, %eax
+ jnz .Lhl_httperr
+ mov HL_BODY(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Lhl_badresp
+ mov %r14, %rsi
+ call cepis__health_from_json
+ mov %eax, %r12d
+ test %eax, %eax
+ jz .Lhl_ret
+ cmp $CEPIS_ERR_NOMEM, %eax
+ je .Lhl_nomem
+ jmp .Lhl_badresp
+.Lhl_ret:
+ lea HL_URL(%rbp), %rdi
+ call cepis__buf_free
+ lea HL_BODY(%rbp), %rdi
+ call cepis__buf_free
+ mov %r12d, %eax
+ EPILOGUE
+ ret
+.Lhl_httperr:
+ mov %eax, %r12d
+ jmp .Lhl_ret
+.Lhl_badresp:
+ mov %r15, %rdi
+ mov $CEPIS_ERR_INVALID_RESPONSE, %esi
+ lea .Lfmt_badjson(%rip), %rdx
+ mov HL_URL(%rbp), %rcx
+ xor %r8d, %r8d
+ call cepis__err_set
+ mov $CEPIS_ERR_INVALID_RESPONSE, %r12d
+ jmp .Lhl_ret
+.Lhl_nomem:
+ mov $CEPIS_ERR_NOMEM, %r12d
+ jmp .Lhl_ret
+.Lhl_nomem_bare:
+ mov $CEPIS_ERR_NOMEM, %eax
+ EPILOGUE
+ ret
+.Lhl_usage:
+ mov $CEPIS_ERR_USAGE, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis_health
+
+PUBFUNC cepis_lookup_many
+ PROLOGUE 136
+ mov %rdi, %rbx
+ mov %rsi, %r12
+ mov %rdx, %r13
+ mov %rcx, %r14
+ mov %r8, %r15
+ test %r14, %r14
+ jz .Llm_usage
+ movq $0, (%r14)
+ mov %r15, %rdi
+ call cepis__err_reset
+ test %r13, %r13
+ jz .Llm_check_ptr
+ test %r12, %r12
+ jz .Llm_usage
+.Llm_check_ptr:
+ mov %rbx, %rdi
+ call cepis__resolve
+ test %rax, %rax
+ jz .Llm_nomem_bare
+ mov %rax, %rbx
+ mov %r13, %rdi
+ test %rdi, %rdi
+ jnz .Llm_alloc
+ mov $1, %rdi
+.Llm_alloc:
+ mov $8, %rsi
+ call calloc@PLT
+ test %rax, %rax
+ jz .Llm_nomem_bare
+ mov %rax, (%r14)
+ movq $0, LM_MAP(%rbp)
+ movq $0, LM_ROWS(%rbp)
+ movq $0, LM_URL(%rbp)
+ movq $0, LM_URL+8(%rbp)
+ movq $0, LM_URL+16(%rbp)
+ movq $0, LM_BODY(%rbp)
+ movq $0, LM_BODY+8(%rbp)
+ movq $0, LM_BODY+16(%rbp)
+ movq $0, LM_IDX(%rbp)
+.Llm_chunk:
+ mov LM_IDX(%rbp), %rax
+ cmp %r13, %rax
+ jae .Llm_done
+ mov %r13, %rcx
+ sub %rax, %rcx
+ cmp $CEPIS_MAX_BATCH, %rcx
+ jbe .Llm_chunk_set
+ mov $CEPIS_MAX_BATCH, %rcx
+.Llm_chunk_set:
+ mov %rcx, LM_CHUNK(%rbp)
+ mov %rcx, %rdi
+ mov $8, %rsi
+ call calloc@PLT
+ test %rax, %rax
+ jz .Llm_nomem
+ mov %rax, LM_MAP(%rbp)
+ lea LM_URL(%rbp), %rdi
+ call cepis__buf_reset
+ lea LM_URL(%rbp), %rdi
+ mov CL_BASE_URL(%rbx), %rsi
+ call cepis__buf_appendz
+ test %eax, %eax
+ jnz .Llm_nomem
+ lea LM_URL(%rbp), %rdi
+ lea .Ls_csv_path(%rip), %rsi
+ call cepis__buf_appendz
+ test %eax, %eax
+ jnz .Llm_nomem
+ movq $0, LM_SENT(%rbp)
+ movq $0, LM_J(%rbp)
+.Llm_build:
+ mov LM_J(%rbp), %rax
+ cmp LM_CHUNK(%rbp), %rax
+ jae .Llm_request
+ add LM_IDX(%rbp), %rax
+ mov (%r12, %rax, 8), %rdi
+ lea LM_TMP(%rbp), %rsi
+ call cepis_normalize_cep
+ test %eax, %eax
+ jnz .Llm_build_next
+ cmpq $0, LM_SENT(%rbp)
+ je .Llm_build_first
+ lea LM_URL(%rbp), %rdi
+ mov $',', %esi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Llm_nomem
+.Llm_build_first:
+ lea LM_URL(%rbp), %rdi
+ lea LM_TMP(%rbp), %rsi
+ call cepis__buf_appendz
+ test %eax, %eax
+ jnz .Llm_nomem
+ mov LM_J(%rbp), %rax
+ add LM_IDX(%rbp), %rax
+ mov LM_MAP(%rbp), %rcx
+ mov LM_SENT(%rbp), %rdx
+ mov %rax, (%rcx, %rdx, 8)
+ incq LM_SENT(%rbp)
+.Llm_build_next:
+ incq LM_J(%rbp)
+ jmp .Llm_build
+.Llm_request:
+ cmpq $0, LM_SENT(%rbp)
+ je .Llm_next_chunk
+ lea LM_BODY(%rbp), %rdi
+ call cepis__buf_reset
+ mov %rbx, %rdi
+ mov LM_URL(%rbp), %rsi
+ xor %edx, %edx
+ lea LM_STATUS(%rbp), %rcx
+ lea LM_BODY(%rbp), %r8
+ mov %r15, %r9
+ call cepis__http_get
+ test %eax, %eax
+ jnz .Llm_fail
+ cmpl $200, LM_STATUS(%rbp)
+ jne .Llm_badstatus
+ mov LM_BODY(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llm_badstatus
+ lea LM_ROWS(%rbp), %rsi
+ lea LM_NROWS(%rbp), %rdx
+ call cepis__csv_addresses
+ test %eax, %eax
+ jnz .Llm_fail
+ movq $0, LM_K(%rbp)
+.Llm_assign:
+ mov LM_K(%rbp), %rax
+ cmp LM_SENT(%rbp), %rax
+ jae .Llm_drop_extra
+ cmp LM_NROWS(%rbp), %rax
+ jae .Llm_drop_extra
+ mov LM_ROWS(%rbp), %rcx
+ mov (%rcx, %rax, 8), %rdx
+ movq $0, (%rcx, %rax, 8)
+ mov LM_MAP(%rbp), %rcx
+ mov (%rcx, %rax, 8), %rcx
+ mov (%r14), %rsi
+ mov %rdx, (%rsi, %rcx, 8)
+ incq LM_K(%rbp)
+ jmp .Llm_assign
+.Llm_drop_extra:
+ mov LM_ROWS(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llm_free_map
+ mov LM_NROWS(%rbp), %rsi
+ call cepis_address_list_free
+ movq $0, LM_ROWS(%rbp)
+ movq $0, LM_NROWS(%rbp)
+.Llm_free_map:
+ mov LM_MAP(%rbp), %rdi
+ call free@PLT
+ movq $0, LM_MAP(%rbp)
+.Llm_next_chunk:
+ mov LM_MAP(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llm_advance
+ call free@PLT
+ movq $0, LM_MAP(%rbp)
+.Llm_advance:
+ mov LM_CHUNK(%rbp), %rax
+ add %rax, LM_IDX(%rbp)
+ jmp .Llm_chunk
+.Llm_done:
+ lea LM_URL(%rbp), %rdi
+ call cepis__buf_free
+ lea LM_BODY(%rbp), %rdi
+ call cepis__buf_free
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Llm_badstatus:
+ mov %r15, %rdi
+ mov $CEPIS_ERR_INVALID_RESPONSE, %esi
+ lea .Lfmt_status(%rip), %rdx
+ mov LM_STATUS(%rbp), %ecx
+ mov LM_URL(%rbp), %r8
+ call cepis__err_set
+ mov $CEPIS_ERR_INVALID_RESPONSE, %eax
+.Llm_fail:
+ mov %eax, %r12d
+ mov LM_ROWS(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llm_fail_map
+ mov LM_NROWS(%rbp), %rsi
+ call cepis_address_list_free
+ movq $0, LM_ROWS(%rbp)
+.Llm_fail_map:
+ mov LM_MAP(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Llm_fail_bufs
+ call free@PLT
+ movq $0, LM_MAP(%rbp)
+.Llm_fail_bufs:
+ lea LM_URL(%rbp), %rdi
+ call cepis__buf_free
+ lea LM_BODY(%rbp), %rdi
+ call cepis__buf_free
+ mov (%r14), %rdi
+ mov %r13, %rsi
+ call cepis_address_list_free
+ movq $0, (%r14)
+ mov %r12d, %eax
+ EPILOGUE
+ ret
+.Llm_nomem:
+ mov $CEPIS_ERR_NOMEM, %eax
+ jmp .Llm_fail
+.Llm_nomem_bare:
+ mov $CEPIS_ERR_NOMEM, %eax
+ EPILOGUE
+ ret
+.Llm_usage:
+ mov $CEPIS_ERR_USAGE, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis_lookup_many
+
+ .section .rodata
+.Ls_version: .asciz "0.1.0"
+.Ls_default_url: .asciz "https://cep.is"
+.Ls_default_ua: .asciz "cepis-asm/0.1.0 (+https://cep.is)"
+.Ls_health_path: .asciz "/health"
+.Ls_csv_path: .asciz "/csv?ceps="
+.Ls_mensagem: .asciz "mensagem"
+.Ls_rejected: .asciz "rejected by the API"
+.Ls_nullstr: .asciz "(null)"
+.Ls_unknown: .asciz "unknown error"
+.Lfmt_notfound: .asciz "CEP %s does not exist in the DNE database"
+.Lfmt_badcep: .asciz "CEP %s rejected: %s"
+.Lfmt_notdigits: .asciz "CEP must be 8 digits, got \"%s\""
+.Lfmt_badjson: .asciz "unparseable response from %s"
+.Lfmt_status: .asciz "unexpected status %d from %s"
+
+.Le_ok: .asciz "ok"
+.Le_invalid: .asciz "invalid CEP"
+.Le_notfound: .asciz "CEP not found"
+.Le_rate: .asciz "rate limited"
+.Le_server: .asciz "server error"
+.Le_network: .asciz "network error"
+.Le_badresp: .asciz "invalid response"
+.Le_nomem: .asciz "out of memory"
+.Le_usage: .asciz "invalid argument"
+
+ .section .data.rel.ro,"aw",@progbits
+ .p2align 3
+cepis__errnames:
+ .quad .Le_ok, .Le_invalid, .Le_notfound, .Le_rate, .Le_server
+ .quad .Le_network, .Le_badresp, .Le_nomem, .Le_usage
+
+ .section .rodata
+ .p2align 3
+.Ld_timeout: .double 10.0
+.Ld_backoff_base: .double 0.5
+.Ld_backoff_max: .double 8.0
+
+ .data
+ .p2align 3
+ .globl cepis__default_client
+ .hidden cepis__default_client
+cepis__default_client:
+ .quad 0
+cepis__seed_counter:
+ .quad 0
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/src/csv.S b/src/csv.S
new file mode 100644
index 0000000..9c16a8e
--- /dev/null
+++ b/src/csv.S
@@ -0,0 +1,297 @@
+#include "abi.inc"
+#include "defs.inc"
+
+#define CBUF -72
+
+#define CA_ROWS -72
+#define CA_STATE -80
+#define CA_NF -88
+#define CA_FIELDS -344
+#define CA_MAP -472
+
+ .text
+
+FUNC cepis__csv_row
+ PROLOGUE 56
+ mov %rdi, %rbx
+ mov %rsi, %r14
+ mov %rdx, %r15
+ movl $0, (%r15)
+ mov (%rbx), %r12
+ cmpb $0, (%r12)
+ je .Lrow_eof
+ xor %r13d, %r13d
+.Lfield:
+ cmpb $'"', (%r12)
+ je .Lquoted
+ mov %r12, %rax
+.Lbare_scan:
+ movzbl (%rax), %ecx
+ test %cl, %cl
+ jz .Lbare_end
+ cmp $',', %cl
+ je .Lbare_end
+ cmp $10, %cl
+ je .Lbare_end
+ cmp $13, %cl
+ je .Lbare_end
+ inc %rax
+ jmp .Lbare_scan
+.Lbare_end:
+ mov %rax, LOCAL0(%rbp)
+ mov %rax, %rsi
+ sub %r12, %rsi
+ mov %r12, %rdi
+ call cepis__strndup
+ test %rax, %rax
+ jz .Lrow_err
+ mov LOCAL0(%rbp), %r12
+ jmp .Lstore
+.Lquoted:
+ movq $0, CBUF(%rbp)
+ movq $0, CBUF+8(%rbp)
+ movq $0, CBUF+16(%rbp)
+ inc %r12
+.Lq_loop:
+ movzbl (%r12), %eax
+ test %al, %al
+ jz .Lq_done
+ cmp $'"', %al
+ je .Lq_quote
+ lea CBUF(%rbp), %rdi
+ movzbl (%r12), %esi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lq_err
+ inc %r12
+ jmp .Lq_loop
+.Lq_quote:
+ cmpb $'"', 1(%r12)
+ jne .Lq_close
+ lea CBUF(%rbp), %rdi
+ mov $'"', %esi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lq_err
+ add $2, %r12
+ jmp .Lq_loop
+.Lq_close:
+ inc %r12
+.Lq_skip:
+ movzbl (%r12), %eax
+ test %al, %al
+ jz .Lq_done
+ cmp $',', %al
+ je .Lq_done
+ cmp $10, %al
+ je .Lq_done
+ cmp $13, %al
+ je .Lq_done
+ inc %r12
+ jmp .Lq_skip
+.Lq_done:
+ cmpq $0, CBUF(%rbp)
+ jne .Lq_take
+ call cepis__empty
+ test %rax, %rax
+ jz .Lrow_err
+ jmp .Lstore
+.Lq_take:
+ lea CBUF(%rbp), %rdi
+ call cepis__buf_take
+.Lstore:
+ cmp $32, %r13
+ jae .Ldrop
+ mov %rax, (%r14, %r13, 8)
+ inc %r13
+ jmp .Lsep
+.Ldrop:
+ mov %rax, %rdi
+ call free@PLT
+.Lsep:
+ movzbl (%r12), %eax
+ cmp $',', %al
+ jne .Lrow_end
+ inc %r12
+ jmp .Lfield
+.Lrow_end:
+ cmpb $13, (%r12)
+ jne .Lrow_nl
+ inc %r12
+.Lrow_nl:
+ cmpb $10, (%r12)
+ jne .Lrow_fin
+ inc %r12
+.Lrow_fin:
+ mov %r12, (%rbx)
+ mov %r13d, (%r15)
+ mov $1, %eax
+ EPILOGUE
+ ret
+.Lrow_eof:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lq_err:
+ lea CBUF(%rbp), %rdi
+ call cepis__buf_free
+.Lrow_err:
+ xor %r12d, %r12d
+.Lrow_err_loop:
+ cmp %r13, %r12
+ jae .Lrow_err_out
+ mov (%r14, %r12, 8), %rdi
+ call free@PLT
+ inc %r12
+ jmp .Lrow_err_loop
+.Lrow_err_out:
+ movl $0, (%r15)
+ mov $-1, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__csv_row
+
+FUNC cepis__csv_addresses
+ PROLOGUE 440
+ mov %rsi, %r14
+ mov %rdx, %r15
+ movq $0, (%r14)
+ movq $0, (%r15)
+ movq $0, CA_ROWS(%rbp)
+ movq $0, CA_ROWS+8(%rbp)
+ movq $0, CA_ROWS+16(%rbp)
+ cmpb $0xEF, (%rdi)
+ jne .Lca_nobom
+ cmpb $0xBB, 1(%rdi)
+ jne .Lca_nobom
+ cmpb $0xBF, 2(%rdi)
+ jne .Lca_nobom
+ add $3, %rdi
+.Lca_nobom:
+ mov %rdi, CA_STATE(%rbp)
+ lea CA_STATE(%rbp), %rdi
+ lea CA_FIELDS(%rbp), %rsi
+ lea CA_NF(%rbp), %rdx
+ call cepis__csv_row
+ cmp $1, %eax
+ je .Lca_header
+ cmp $0, %eax
+ je .Lca_invalid
+ jmp .Lca_nomem
+.Lca_header:
+ xor %r12d, %r12d
+.Lca_hdr_loop:
+ movslq CA_NF(%rbp), %rax
+ cmp %rax, %r12
+ jae .Lca_rows
+ mov CA_FIELDS(%rbp, %r12, 8), %rdi
+ call cepis__addr_field_offset
+ lea CA_MAP(%rbp), %rcx
+ mov %eax, (%rcx, %r12, 4)
+ mov CA_FIELDS(%rbp, %r12, 8), %rdi
+ call free@PLT
+ inc %r12
+ jmp .Lca_hdr_loop
+.Lca_rows:
+ lea CA_STATE(%rbp), %rdi
+ lea CA_FIELDS(%rbp), %rsi
+ lea CA_NF(%rbp), %rdx
+ call cepis__csv_row
+ cmp $1, %eax
+ je .Lca_row
+ cmp $0, %eax
+ je .Lca_done
+ jmp .Lca_nomem
+.Lca_row:
+ cmpl $1, CA_NF(%rbp)
+ jne .Lca_build
+ mov CA_FIELDS(%rbp), %rdi
+ cmpb $0, (%rdi)
+ jne .Lca_build
+ call free@PLT
+ jmp .Lca_rows
+.Lca_build:
+ call cepis__address_new
+ test %rax, %rax
+ jz .Lca_nomem_fields
+ mov %rax, %rbx
+ xor %r12d, %r12d
+.Lca_assign:
+ movslq CA_NF(%rbp), %rax
+ cmp %rax, %r12
+ jae .Lca_check
+ lea CA_MAP(%rbp), %rcx
+ movslq (%rcx, %r12, 4), %r13
+ cmp $0, %r13
+ jl .Lca_assign_drop
+ mov %rbx, %rdi
+ mov %r13, %rsi
+ mov CA_FIELDS(%rbp, %r12, 8), %rdx
+ call cepis__addr_set
+ jmp .Lca_assign_next
+.Lca_assign_drop:
+ mov CA_FIELDS(%rbp, %r12, 8), %rdi
+ call free@PLT
+.Lca_assign_next:
+ inc %r12
+ jmp .Lca_assign
+.Lca_check:
+ mov A_LOCALIDADE(%rbx), %rdi
+ cmpb $0, (%rdi)
+ jne .Lca_push
+ mov %rbx, %rdi
+ call cepis_address_free
+ xor %ebx, %ebx
+.Lca_push:
+ mov %rbx, LOCAL0(%rbp)
+ lea CA_ROWS(%rbp), %rdi
+ lea LOCAL0(%rbp), %rsi
+ mov $8, %rdx
+ call cepis__buf_append
+ test %eax, %eax
+ jnz .Lca_nomem_row
+ jmp .Lca_rows
+.Lca_done:
+ mov CA_ROWS+8(%rbp), %rax
+ shr $3, %rax
+ mov %rax, (%r15)
+ lea CA_ROWS(%rbp), %rdi
+ call cepis__buf_take
+ mov %rax, (%r14)
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lca_nomem_row:
+ mov %rbx, %rdi
+ call cepis_address_free
+ jmp .Lca_nomem
+.Lca_nomem_fields:
+ xor %r12d, %r12d
+.Lca_nmf_loop:
+ movslq CA_NF(%rbp), %rax
+ cmp %rax, %r12
+ jae .Lca_nomem
+ mov CA_FIELDS(%rbp, %r12, 8), %rdi
+ call free@PLT
+ inc %r12
+ jmp .Lca_nmf_loop
+.Lca_nomem:
+ mov $CEPIS_ERR_NOMEM, %r13d
+ jmp .Lca_cleanup
+.Lca_invalid:
+ mov $CEPIS_ERR_INVALID_RESPONSE, %r13d
+.Lca_cleanup:
+ mov CA_ROWS(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Lca_cleanup_done
+ mov CA_ROWS+8(%rbp), %rsi
+ shr $3, %rsi
+ call cepis_address_list_free
+ movq $0, CA_ROWS(%rbp)
+.Lca_cleanup_done:
+ mov %r13d, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__csv_addresses
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/src/defs.inc b/src/defs.inc
new file mode 100644
index 0000000..4a64e0e
--- /dev/null
+++ b/src/defs.inc
@@ -0,0 +1,79 @@
+#ifndef CEPIS_DEFS_INC
+#define CEPIS_DEFS_INC
+
+#define CEPIS_OK 0
+#define CEPIS_ERR_INVALID_CEP 1
+#define CEPIS_ERR_NOT_FOUND 2
+#define CEPIS_ERR_RATE_LIMITED 3
+#define CEPIS_ERR_SERVER 4
+#define CEPIS_ERR_NETWORK 5
+#define CEPIS_ERR_INVALID_RESPONSE 6
+#define CEPIS_ERR_NOMEM 7
+#define CEPIS_ERR_USAGE 8
+
+#define GEO_LAT 0
+#define GEO_LON 8
+#define GEO_RAIO_M 16
+#define GEO_PRECISAO 24
+#define GEO_SIZE 32
+
+#define A_CEP 0
+#define A_LOGRADOURO 8
+#define A_COMPLEMENTO 16
+#define A_UNIDADE 24
+#define A_BAIRRO 32
+#define A_LOCALIDADE 40
+#define A_UF 48
+#define A_ESTADO 56
+#define A_REGIAO 64
+#define A_IBGE 72
+#define A_GIA 80
+#define A_DDD 88
+#define A_SIAFI 96
+#define A_GEO 104
+#define A_RAW 112
+#define A_SIZE 120
+#define A_NFIELDS 13
+
+#define H_STATUS 0
+#define H_DNE 8
+#define H_CEPS 16
+#define H_GEO 24
+#define H_BASE_GERADA 32
+#define H_RAW 40
+#define H_SIZE 48
+
+#define E_STATUS 0
+#define E_HTTP_STATUS 4
+#define E_RETRY_AFTER 8
+#define E_MESSAGE 16
+#define E_MSGLEN 256
+#define E_SIZE 272
+
+#define C_BASE_URL 0
+#define C_TIMEOUT 8
+#define C_MAX_ATTEMPTS 16
+#define C_BACKOFF_BASE 24
+#define C_BACKOFF_MAX 32
+#define C_TOTAL_TIMEOUT 40
+#define C_USER_AGENT 48
+#define C_SIZE 56
+
+#define CL_BASE_URL 0
+#define CL_USER_AGENT 8
+#define CL_TIMEOUT 16
+#define CL_BACKOFF_BASE 24
+#define CL_BACKOFF_MAX 32
+#define CL_TOTAL_TIMEOUT 40
+#define CL_MAX_ATTEMPTS 48
+#define CL_RNG 56
+#define CL_SIZE 64
+
+#define B_DATA 0
+#define B_LEN 8
+#define B_CAP 16
+#define B_SIZE 24
+
+#define CEPIS_MAX_BATCH 500
+
+#endif
diff --git a/src/http.S b/src/http.S
new file mode 100644
index 0000000..9ff6a53
--- /dev/null
+++ b/src/http.S
@@ -0,0 +1,759 @@
+#include "abi.inc"
+#include "defs.inc"
+
+#define CURLOPT_URL 10002
+#define CURLOPT_WRITEFUNCTION 20011
+#define CURLOPT_WRITEDATA 10001
+#define CURLOPT_HEADERFUNCTION 20079
+#define CURLOPT_HEADERDATA 10029
+#define CURLOPT_USERAGENT 10018
+#define CURLOPT_HTTPHEADER 10023
+#define CURLOPT_TIMEOUT_MS 155
+#define CURLOPT_CONNECTTIMEOUT_MS 156
+#define CURLOPT_ACCEPT_ENCODING 10102
+#define CURLOPT_NOSIGNAL 99
+#define CURLOPT_ERRORBUFFER 10010
+#define CURLOPT_FOLLOWLOCATION 52
+#define CURLINFO_RESPONSE_CODE 2097154
+#define CURL_GLOBAL_DEFAULT 3
+#define CURL_ERRSZ 256
+
+#define HC_VALUE 0
+#define HC_HAVE 128
+#define HC_SIZE 136
+
+#define HG_HDR -184
+#define HG_ERRBUF -440
+#define HG_HANDLE -448
+#define HG_SLIST -456
+#define HG_DEADLINE -464
+#define HG_HASDL -472
+#define HG_RA -480
+#define HG_HASRA -488
+#define HG_LASTST -496
+#define HG_CODE -504
+#define HG_ATTEMPT -512
+#define HG_ACC503 -520
+#define HG_OUTST -528
+#define HG_CURLRC -536
+#define HG_WAIT -544
+#define HG_TMPL -552
+
+#define RA_TM -104
+#define RA_END -112
+#define RA_DAY -120
+#define RA_MON -128
+#define RA_YEAR -136
+#define RA_HH -144
+#define RA_MI -152
+#define RA_SS -160
+
+.macro COPT_LONG opt, val
+ mov HG_HANDLE(%rbp), %rdi
+ mov $\opt, %esi
+ mov $\val, %rdx
+ xor %eax, %eax
+ call curl_easy_setopt@PLT
+.endm
+
+.macro COPT_REG opt, reg
+ mov HG_HANDLE(%rbp), %rdi
+ mov $\opt, %esi
+ mov \reg, %rdx
+ xor %eax, %eax
+ call curl_easy_setopt@PLT
+.endm
+
+ .text
+
+PUBFUNC cepis_global_init
+ PROLOGUE 8
+ xor %eax, %eax
+ mov $1, %ecx
+ lock cmpxchgl %ecx, cepis__curl_state(%rip)
+ jne .Lonce_out
+ mov $CURL_GLOBAL_DEFAULT, %edi
+ call curl_global_init@PLT
+ movl $2, cepis__curl_state(%rip)
+.Lonce_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis_global_init
+
+PUBFUNC cepis_global_cleanup
+ PROLOGUE 8
+ call cepis__default_client_destroy
+ cmpl $0, cepis__curl_state(%rip)
+ je .Lgc_out
+ movl $0, cepis__curl_state(%rip)
+ call curl_global_cleanup@PLT
+.Lgc_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis_global_cleanup
+
+FUNC cepis__mono
+ PROLOGUE 24
+ mov $1, %edi
+ lea LOCAL1(%rbp), %rsi
+ call clock_gettime@PLT
+ cvtsi2sdq LOCAL1(%rbp), %xmm0
+ cvtsi2sdq LOCAL0(%rbp), %xmm1
+ mulsd .Lnano(%rip), %xmm1
+ addsd %xmm1, %xmm0
+ EPILOGUE
+ ret
+ENDFUNC cepis__mono
+
+FUNC cepis__sleep
+ PROLOGUE 40
+ xorpd %xmm1, %xmm1
+ comisd %xmm1, %xmm0
+ jbe .Lsleep_out
+ cvttsd2siq %xmm0, %rax
+ mov %rax, LOCAL1(%rbp)
+ cvtsi2sdq %rax, %xmm1
+ subsd %xmm1, %xmm0
+ mulsd .Lgiga(%rip), %xmm0
+ cvttsd2siq %xmm0, %rax
+ cmp $0, %rax
+ jge .Lsleep_store
+ xor %eax, %eax
+.Lsleep_store:
+ mov %rax, LOCAL0(%rbp)
+.Lsleep_call:
+ lea LOCAL1(%rbp), %rdi
+ lea LOCAL3(%rbp), %rsi
+ call nanosleep@PLT
+ test %eax, %eax
+ jz .Lsleep_out
+ call __errno_location@PLT
+ cmpl $4, (%rax)
+ jne .Lsleep_out
+ mov LOCAL3(%rbp), %rax
+ mov %rax, LOCAL1(%rbp)
+ mov LOCAL2(%rbp), %rax
+ mov %rax, LOCAL0(%rbp)
+ jmp .Lsleep_call
+.Lsleep_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis__sleep
+
+FUNC cepis__rand01
+ mov CL_RNG(%rdi), %rax
+.Lrand_loop:
+ mov %rax, %r8
+ mov %r8, %rdx
+ shl $13, %rdx
+ xor %rdx, %r8
+ mov %r8, %rdx
+ shr $7, %rdx
+ xor %rdx, %r8
+ mov %r8, %rdx
+ shl $17, %rdx
+ xor %rdx, %r8
+ lock cmpxchgq %r8, CL_RNG(%rdi)
+ jne .Lrand_loop
+ mov %r8, %rax
+ shr $11, %rax
+ cvtsi2sdq %rax, %xmm0
+ mulsd .Ltwo53(%rip), %xmm0
+ ret
+ENDFUNC cepis__rand01
+
+FUNC cepis__backoff
+ PROLOGUE 24
+ mov %rdi, %rbx
+ test %edx, %edx
+ jz .Lbo_calc
+ movsd CL_BACKOFF_MAX(%rbx), %xmm1
+ minsd %xmm1, %xmm0
+ xorpd %xmm1, %xmm1
+ maxsd %xmm1, %xmm0
+ EPILOGUE
+ ret
+.Lbo_calc:
+ movsd CL_BACKOFF_BASE(%rbx), %xmm0
+ movsd CL_BACKOFF_MAX(%rbx), %xmm1
+ mov %esi, %ecx
+ dec %ecx
+.Lbo_loop:
+ test %ecx, %ecx
+ jle .Lbo_cap
+ comisd %xmm1, %xmm0
+ jae .Lbo_cap
+ addsd %xmm0, %xmm0
+ dec %ecx
+ jmp .Lbo_loop
+.Lbo_cap:
+ minsd %xmm1, %xmm0
+ movsd %xmm0, LOCAL0(%rbp)
+ mov %rbx, %rdi
+ call cepis__rand01
+ mulsd LOCAL0(%rbp), %xmm0
+ EPILOGUE
+ ret
+ENDFUNC cepis__backoff
+
+FUNC cepis__read_uint
+ xor %ecx, %ecx
+ xor %r8d, %r8d
+ movl $0, (%rsi)
+.Lru_loop:
+ cmp %edx, %r8d
+ jae .Lru_end
+ movzbl (%rdi), %eax
+ cmp $'0', %al
+ jb .Lru_end
+ cmp $'9', %al
+ ja .Lru_end
+ sub $'0', %eax
+ imul $10, %ecx, %ecx
+ add %eax, %ecx
+ inc %rdi
+ inc %r8d
+ jmp .Lru_loop
+.Lru_end:
+ test %r8d, %r8d
+ jz .Lru_bad
+ mov %ecx, (%rsi)
+ mov %rdi, %rax
+ ret
+.Lru_bad:
+ xor %eax, %eax
+ ret
+ENDFUNC cepis__read_uint
+
+FUNC cepis__skip_blanks
+.Lsb_loop:
+ movzbl (%rdi), %eax
+ cmp $' ', %al
+ je .Lsb_adv
+ cmp $9, %al
+ je .Lsb_adv
+ mov %rdi, %rax
+ ret
+.Lsb_adv:
+ inc %rdi
+ jmp .Lsb_loop
+ENDFUNC cepis__skip_blanks
+
+FUNC cepis__retry_after
+ PROLOGUE 120
+ mov %rsi, %r14
+ call cepis__skip_blanks
+ mov %rax, %rbx
+ cmpb $0, (%rbx)
+ je .Lra_bad
+ mov %rbx, %rdi
+ lea RA_END(%rbp), %rsi
+ call strtod@PLT
+ mov RA_END(%rbp), %rax
+ cmp %rbx, %rax
+ je .Lra_date
+ movsd %xmm0, LOCAL0(%rbp)
+ mov %rax, %rdi
+ call cepis__skip_blanks
+ movzbl (%rax), %ecx
+ test %cl, %cl
+ jz .Lra_numeric
+ cmp $13, %cl
+ je .Lra_numeric
+ cmp $10, %cl
+ je .Lra_numeric
+ jmp .Lra_date
+.Lra_numeric:
+ movsd LOCAL0(%rbp), %xmm0
+ xorpd %xmm1, %xmm1
+ maxsd %xmm1, %xmm0
+ movsd %xmm0, (%r14)
+ mov $1, %eax
+ EPILOGUE
+ ret
+.Lra_date:
+ mov %rbx, %r12
+.Lra_findnum:
+ movzbl (%r12), %eax
+ test %al, %al
+ jz .Lra_bad
+ cmp $'0', %al
+ jb .Lra_findnum_adv
+ cmp $'9', %al
+ jbe .Lra_day
+.Lra_findnum_adv:
+ inc %r12
+ jmp .Lra_findnum
+.Lra_day:
+ mov %r12, %rdi
+ lea RA_DAY(%rbp), %rsi
+ mov $2, %edx
+ call cepis__read_uint
+ test %rax, %rax
+ jz .Lra_bad
+ mov %rax, %rdi
+ call cepis__skip_blanks
+ mov %rax, %r12
+ xor %r13d, %r13d
+.Lra_mon_loop:
+ cmp $12, %r13d
+ jae .Lra_bad
+ lea cepis__months(%rip), %rax
+ mov (%rax, %r13, 8), %rsi
+ mov %r12, %rdi
+ mov $3, %rdx
+ call strncasecmp@PLT
+ test %eax, %eax
+ jz .Lra_mon_hit
+ inc %r13d
+ jmp .Lra_mon_loop
+.Lra_mon_hit:
+ mov %r13d, RA_MON(%rbp)
+ lea 3(%r12), %rdi
+ call cepis__skip_blanks
+ mov %rax, %rdi
+ lea RA_YEAR(%rbp), %rsi
+ mov $4, %edx
+ call cepis__read_uint
+ test %rax, %rax
+ jz .Lra_bad
+ mov %rax, %rdi
+ call cepis__skip_blanks
+ mov %rax, %rdi
+ lea RA_HH(%rbp), %rsi
+ mov $2, %edx
+ call cepis__read_uint
+ test %rax, %rax
+ jz .Lra_bad
+ cmpb $':', (%rax)
+ jne .Lra_bad
+ lea 1(%rax), %rdi
+ lea RA_MI(%rbp), %rsi
+ mov $2, %edx
+ call cepis__read_uint
+ test %rax, %rax
+ jz .Lra_bad
+ cmpb $':', (%rax)
+ jne .Lra_bad
+ lea 1(%rax), %rdi
+ lea RA_SS(%rbp), %rsi
+ mov $2, %edx
+ call cepis__read_uint
+ test %rax, %rax
+ jz .Lra_bad
+ lea RA_TM(%rbp), %rdi
+ xor %esi, %esi
+ mov $56, %rdx
+ call memset@PLT
+ mov RA_SS(%rbp), %eax
+ mov %eax, RA_TM+0(%rbp)
+ mov RA_MI(%rbp), %eax
+ mov %eax, RA_TM+4(%rbp)
+ mov RA_HH(%rbp), %eax
+ mov %eax, RA_TM+8(%rbp)
+ mov RA_DAY(%rbp), %eax
+ mov %eax, RA_TM+12(%rbp)
+ mov RA_MON(%rbp), %eax
+ mov %eax, RA_TM+16(%rbp)
+ mov RA_YEAR(%rbp), %eax
+ sub $1900, %eax
+ mov %eax, RA_TM+20(%rbp)
+ movl $-1, RA_TM+32(%rbp)
+ lea RA_TM(%rbp), %rdi
+ call timegm@PLT
+ cmp $-1, %rax
+ je .Lra_bad
+ mov %rax, %rbx
+ xor %edi, %edi
+ call time@PLT
+ sub %rax, %rbx
+ cmp $0, %rbx
+ jge .Lra_diff
+ xor %ebx, %ebx
+.Lra_diff:
+ cvtsi2sdq %rbx, %xmm0
+ movsd %xmm0, (%r14)
+ mov $1, %eax
+ EPILOGUE
+ ret
+.Lra_bad:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__retry_after
+
+FUNC cepis__write_cb
+ PROLOGUE 24
+ mov %rdi, %r12
+ mov %rsi, %rax
+ imul %rdx, %rax
+ mov %rax, %r13
+ mov %rcx, %rdi
+ mov %r12, %rsi
+ mov %r13, %rdx
+ call cepis__buf_append
+ test %eax, %eax
+ jnz .Lwcb_fail
+ mov %r13, %rax
+ EPILOGUE
+ ret
+.Lwcb_fail:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__write_cb
+
+FUNC cepis__header_cb
+ PROLOGUE 24
+ mov %rdi, %r12
+ mov %rsi, %rax
+ imul %rdx, %rax
+ mov %rax, %r13
+ mov %rcx, %rbx
+ cmp $12, %r13
+ jb .Lhcb_out
+ mov %r12, %rdi
+ lea .Lhdr_retry_after(%rip), %rsi
+ mov $12, %rdx
+ call strncasecmp@PLT
+ test %eax, %eax
+ jnz .Lhcb_out
+ lea 12(%r12), %r14
+ mov %r12, %r15
+ add %r13, %r15
+.Lhcb_lead:
+ cmp %r15, %r14
+ jae .Lhcb_copy
+ movzbl (%r14), %eax
+ cmp $' ', %al
+ je .Lhcb_lead_adv
+ cmp $9, %al
+ jne .Lhcb_trail
+.Lhcb_lead_adv:
+ inc %r14
+ jmp .Lhcb_lead
+.Lhcb_trail:
+ cmp %r15, %r14
+ jae .Lhcb_copy
+ movzbl -1(%r15), %eax
+ cmp $' ', %al
+ je .Lhcb_trail_adv
+ cmp $9, %al
+ je .Lhcb_trail_adv
+ cmp $13, %al
+ je .Lhcb_trail_adv
+ cmp $10, %al
+ je .Lhcb_trail_adv
+ jmp .Lhcb_copy
+.Lhcb_trail_adv:
+ dec %r15
+ jmp .Lhcb_trail
+.Lhcb_copy:
+ mov %r15, %rdx
+ sub %r14, %rdx
+ cmp $127, %rdx
+ jbe .Lhcb_do
+ mov $127, %rdx
+.Lhcb_do:
+ mov %rdx, LOCAL0(%rbp)
+ mov %rbx, %rdi
+ mov %r14, %rsi
+ call memcpy@PLT
+ mov LOCAL0(%rbp), %rdx
+ movb $0, (%rbx, %rdx, 1)
+ movl $1, HC_HAVE(%rbx)
+.Lhcb_out:
+ mov %r13, %rax
+ EPILOGUE
+ ret
+ENDFUNC cepis__header_cb
+
+FUNC cepis__http_get
+ PROLOGUE 520
+ mov %rdi, %rbx
+ mov %rsi, %r12
+ mov %edx, %eax
+ mov %eax, HG_ACC503(%rbp)
+ mov %rcx, HG_OUTST(%rbp)
+ mov %r8, %r13
+ mov %r9, %r14
+ movl $0, (%rcx)
+ movl $0, HG_LASTST(%rbp)
+ movl $0, HG_HASRA(%rbp)
+ movl $0, HG_CURLRC(%rbp)
+ movq $0, HG_RA(%rbp)
+ movb $0, HG_ERRBUF(%rbp)
+ call cepis_global_init
+ movsd CL_TOTAL_TIMEOUT(%rbx), %xmm0
+ xorpd %xmm1, %xmm1
+ comisd %xmm1, %xmm0
+ jbe .Lhg_nodl
+ call cepis__mono
+ addsd CL_TOTAL_TIMEOUT(%rbx), %xmm0
+ movsd %xmm0, HG_DEADLINE(%rbp)
+ movl $1, HG_HASDL(%rbp)
+ jmp .Lhg_start
+.Lhg_nodl:
+ movl $0, HG_HASDL(%rbp)
+.Lhg_start:
+ movl $1, HG_ATTEMPT(%rbp)
+.Lhg_attempt:
+ cmpl $0, HG_HASDL(%rbp)
+ je .Lhg_go
+ call cepis__mono
+ comisd HG_DEADLINE(%rbp), %xmm0
+ jae .Lhg_exhausted
+.Lhg_go:
+ mov %r13, %rdi
+ call cepis__buf_reset
+ lea HG_HDR(%rbp), %rdi
+ movl $0, HC_HAVE(%rdi)
+ movb $0, HC_VALUE(%rdi)
+ movb $0, HG_ERRBUF(%rbp)
+ movq $0, HG_SLIST(%rbp)
+ call curl_easy_init@PLT
+ test %rax, %rax
+ jz .Lhg_nomem
+ mov %rax, HG_HANDLE(%rbp)
+ xor %edi, %edi
+ lea .Lhdr_accept(%rip), %rsi
+ call curl_slist_append@PLT
+ mov %rax, HG_SLIST(%rbp)
+ COPT_REG CURLOPT_URL, %r12
+ lea cepis__write_cb(%rip), %rax
+ COPT_REG CURLOPT_WRITEFUNCTION, %rax
+ COPT_REG CURLOPT_WRITEDATA, %r13
+ lea cepis__header_cb(%rip), %rax
+ COPT_REG CURLOPT_HEADERFUNCTION, %rax
+ lea HG_HDR(%rbp), %rax
+ COPT_REG CURLOPT_HEADERDATA, %rax
+ mov CL_USER_AGENT(%rbx), %rax
+ COPT_REG CURLOPT_USERAGENT, %rax
+ mov HG_SLIST(%rbp), %rax
+ COPT_REG CURLOPT_HTTPHEADER, %rax
+ lea .Lidentity(%rip), %rax
+ COPT_REG CURLOPT_ACCEPT_ENCODING, %rax
+ lea HG_ERRBUF(%rbp), %rax
+ COPT_REG CURLOPT_ERRORBUFFER, %rax
+ COPT_LONG CURLOPT_NOSIGNAL, 1
+ COPT_LONG CURLOPT_FOLLOWLOCATION, 0
+ movsd CL_TIMEOUT(%rbx), %xmm0
+ mulsd .Lthousand(%rip), %xmm0
+ cvttsd2siq %xmm0, %rax
+ cmp $1, %rax
+ jge .Lhg_tmo
+ mov $1, %rax
+.Lhg_tmo:
+ mov %rax, HG_TMPL(%rbp)
+ COPT_REG CURLOPT_TIMEOUT_MS, %rax
+ mov HG_TMPL(%rbp), %rax
+ COPT_REG CURLOPT_CONNECTTIMEOUT_MS, %rax
+ mov HG_HANDLE(%rbp), %rdi
+ call curl_easy_perform@PLT
+ mov %eax, HG_CURLRC(%rbp)
+ test %eax, %eax
+ jnz .Lhg_network
+ mov HG_HANDLE(%rbp), %rdi
+ mov $CURLINFO_RESPONSE_CODE, %esi
+ lea HG_CODE(%rbp), %rdx
+ xor %eax, %eax
+ call curl_easy_getinfo@PLT
+ mov HG_CODE(%rbp), %rax
+ mov %eax, %r15d
+ cmp $200, %r15d
+ je .Lhg_accept
+ cmpl $0, HG_ACC503(%rbp)
+ je .Lhg_notaccept
+ cmp $503, %r15d
+ je .Lhg_accept
+.Lhg_notaccept:
+ cmp $429, %r15d
+ je .Lhg_retriable
+ cmp $500, %r15d
+ je .Lhg_retriable
+ cmp $502, %r15d
+ je .Lhg_retriable
+ cmp $503, %r15d
+ je .Lhg_retriable
+ cmp $504, %r15d
+ je .Lhg_retriable
+.Lhg_accept:
+ mov HG_OUTST(%rbp), %rax
+ mov %r15d, (%rax)
+ lea HG_HANDLE(%rbp), %rdi
+ lea HG_SLIST(%rbp), %rsi
+ call cepis__curl_release
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lhg_retriable:
+ mov %r15d, HG_LASTST(%rbp)
+ movl $0, HG_HASRA(%rbp)
+ lea HG_HDR(%rbp), %rax
+ cmpl $0, HC_HAVE(%rax)
+ je .Lhg_after
+ lea HG_HDR(%rbp), %rdi
+ lea HG_RA(%rbp), %rsi
+ call cepis__retry_after
+ mov %eax, HG_HASRA(%rbp)
+ jmp .Lhg_after
+.Lhg_network:
+ movl $0, HG_LASTST(%rbp)
+ movl $0, HG_HASRA(%rbp)
+.Lhg_after:
+ lea HG_HANDLE(%rbp), %rdi
+ lea HG_SLIST(%rbp), %rsi
+ call cepis__curl_release
+ mov HG_ATTEMPT(%rbp), %eax
+ cmp CL_MAX_ATTEMPTS(%rbx), %eax
+ jge .Lhg_exhausted
+ mov %rbx, %rdi
+ mov HG_ATTEMPT(%rbp), %esi
+ movsd HG_RA(%rbp), %xmm0
+ mov HG_HASRA(%rbp), %edx
+ call cepis__backoff
+ movsd %xmm0, HG_WAIT(%rbp)
+ cmpl $0, HG_HASDL(%rbp)
+ je .Lhg_sleep
+ call cepis__mono
+ movsd HG_DEADLINE(%rbp), %xmm1
+ subsd %xmm0, %xmm1
+ xorpd %xmm2, %xmm2
+ comisd %xmm2, %xmm1
+ jbe .Lhg_exhausted
+ movsd HG_WAIT(%rbp), %xmm0
+ minsd %xmm1, %xmm0
+ movsd %xmm0, HG_WAIT(%rbp)
+.Lhg_sleep:
+ movsd HG_WAIT(%rbp), %xmm0
+ call cepis__sleep
+ incl HG_ATTEMPT(%rbp)
+ jmp .Lhg_attempt
+.Lhg_exhausted:
+ mov CL_MAX_ATTEMPTS(%rbx), %r15d
+ cmpl $429, HG_LASTST(%rbp)
+ je .Lhg_rate
+ cmpl $0, HG_LASTST(%rbp)
+ jne .Lhg_server
+ test %r14, %r14
+ jz .Lhg_ret_network
+ mov HG_CURLRC(%rbp), %edi
+ call curl_easy_strerror@PLT
+ mov %rax, %r9
+ cmpb $0, HG_ERRBUF(%rbp)
+ je .Lhg_net_msg
+ lea HG_ERRBUF(%rbp), %r9
+.Lhg_net_msg:
+ lea E_MESSAGE(%r14), %rdi
+ mov $E_MSGLEN, %esi
+ lea .Lfmt_network(%rip), %rdx
+ mov %r12, %rcx
+ mov %r15d, %r8d
+ xor %eax, %eax
+ call snprintf@PLT
+ movl $CEPIS_ERR_NETWORK, E_STATUS(%r14)
+.Lhg_ret_network:
+ mov $CEPIS_ERR_NETWORK, %eax
+ EPILOGUE
+ ret
+.Lhg_server:
+ test %r14, %r14
+ jz .Lhg_ret_server
+ mov HG_LASTST(%rbp), %eax
+ mov %eax, E_HTTP_STATUS(%r14)
+ movl $CEPIS_ERR_SERVER, E_STATUS(%r14)
+ lea E_MESSAGE(%r14), %rdi
+ mov $E_MSGLEN, %esi
+ lea .Lfmt_server(%rip), %rdx
+ mov HG_LASTST(%rbp), %ecx
+ mov %r12, %r8
+ mov %r15d, %r9d
+ xor %eax, %eax
+ call snprintf@PLT
+.Lhg_ret_server:
+ mov $CEPIS_ERR_SERVER, %eax
+ EPILOGUE
+ ret
+.Lhg_rate:
+ test %r14, %r14
+ jz .Lhg_ret_rate
+ movl $429, E_HTTP_STATUS(%r14)
+ movl $CEPIS_ERR_RATE_LIMITED, E_STATUS(%r14)
+ cmpl $0, HG_HASRA(%rbp)
+ je .Lhg_rate_msg
+ movsd HG_RA(%rbp), %xmm0
+ movsd %xmm0, E_RETRY_AFTER(%r14)
+.Lhg_rate_msg:
+ lea E_MESSAGE(%r14), %rdi
+ mov $E_MSGLEN, %esi
+ lea .Lfmt_rate(%rip), %rdx
+ mov %r12, %rcx
+ mov %r15d, %r8d
+ xor %eax, %eax
+ call snprintf@PLT
+.Lhg_ret_rate:
+ mov $CEPIS_ERR_RATE_LIMITED, %eax
+ EPILOGUE
+ ret
+.Lhg_nomem:
+ mov $CEPIS_ERR_NOMEM, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__http_get
+
+FUNC cepis__curl_release
+ PROLOGUE 8
+ mov %rdi, %rbx
+ mov %rsi, %r12
+ mov (%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lrel_slist
+ call curl_easy_cleanup@PLT
+ movq $0, (%rbx)
+.Lrel_slist:
+ mov (%r12), %rdi
+ test %rdi, %rdi
+ jz .Lrel_out
+ call curl_slist_free_all@PLT
+ movq $0, (%r12)
+.Lrel_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis__curl_release
+
+ .section .rodata
+ .p2align 3
+.Lnano: .double 1e-9
+.Lgiga: .double 1e9
+.Ltwo53: .double 1.1102230246251565e-16
+.Lthousand: .double 1000.0
+.Lhdr_retry_after: .asciz "retry-after:"
+.Lhdr_accept: .asciz "Accept: application/json, text/csv;q=0.9, */*;q=0.1"
+.Lidentity: .asciz "identity"
+.Lfmt_network: .asciz "could not reach %s after %d attempt(s): %s"
+.Lfmt_server: .asciz "the API answered %d at %s after %d attempt(s)"
+.Lfmt_rate: .asciz "rate limit reached at %s after %d attempt(s)"
+.Ls_jan: .asciz "Jan"
+.Ls_feb: .asciz "Feb"
+.Ls_mar: .asciz "Mar"
+.Ls_apr: .asciz "Apr"
+.Ls_may: .asciz "May"
+.Ls_jun: .asciz "Jun"
+.Ls_jul: .asciz "Jul"
+.Ls_aug: .asciz "Aug"
+.Ls_sep: .asciz "Sep"
+.Ls_oct: .asciz "Oct"
+.Ls_nov: .asciz "Nov"
+.Ls_dec: .asciz "Dec"
+ .section .data.rel.ro,"aw",@progbits
+ .p2align 3
+cepis__months:
+ .quad .Ls_jan, .Ls_feb, .Ls_mar, .Ls_apr, .Ls_may, .Ls_jun
+ .quad .Ls_jul, .Ls_aug, .Ls_sep, .Ls_oct, .Ls_nov, .Ls_dec
+
+ .data
+ .p2align 2
+cepis__curl_state:
+ .long 0
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/src/json.S b/src/json.S
new file mode 100644
index 0000000..58c5077
--- /dev/null
+++ b/src/json.S
@@ -0,0 +1,482 @@
+#include "abi.inc"
+#include "defs.inc"
+
+#define JBUF -72
+
+ .text
+
+FUNC cepis__js_ws
+.Lws_loop:
+ movzbl (%rdi), %eax
+ cmp $' ', %al
+ je .Lws_adv
+ cmp $9, %al
+ jb .Lws_end
+ cmp $13, %al
+ jbe .Lws_adv
+.Lws_end:
+ mov %rdi, %rax
+ ret
+.Lws_adv:
+ inc %rdi
+ jmp .Lws_loop
+ENDFUNC cepis__js_ws
+
+FUNC cepis__js_hex4
+ xor %ecx, %ecx
+ mov $4, %r8d
+.Lhex_loop:
+ movzbl (%rdi), %eax
+ inc %rdi
+ cmp $'0', %al
+ jb .Lhex_bad
+ cmp $'9', %al
+ jbe .Lhex_digit
+ or $0x20, %al
+ cmp $'a', %al
+ jb .Lhex_bad
+ cmp $'f', %al
+ ja .Lhex_bad
+ sub $('a' - 10), %eax
+ jmp .Lhex_acc
+.Lhex_digit:
+ sub $'0', %eax
+.Lhex_acc:
+ shl $4, %ecx
+ or %eax, %ecx
+ dec %r8d
+ jnz .Lhex_loop
+ mov %ecx, %eax
+ ret
+.Lhex_bad:
+ mov $-1, %eax
+ ret
+ENDFUNC cepis__js_hex4
+
+FUNC cepis__js_raw_string
+ cmpb $'"', (%rdi)
+ jne .Lraw_bad
+ inc %rdi
+.Lraw_loop:
+ movzbl (%rdi), %eax
+ test %al, %al
+ jz .Lraw_bad
+ cmp $'\\', %al
+ je .Lraw_esc
+ cmp $'"', %al
+ je .Lraw_close
+ inc %rdi
+ jmp .Lraw_loop
+.Lraw_esc:
+ cmpb $0, 1(%rdi)
+ je .Lraw_bad
+ add $2, %rdi
+ jmp .Lraw_loop
+.Lraw_close:
+ lea 1(%rdi), %rax
+ ret
+.Lraw_bad:
+ xor %eax, %eax
+ ret
+ENDFUNC cepis__js_raw_string
+
+FUNC cepis__js_skip
+ PROLOGUE 8
+ call cepis__js_ws
+ mov %rax, %rbx
+ movzbl (%rbx), %eax
+ test %al, %al
+ jz .Lskip_bad
+ cmp $'"', %al
+ je .Lskip_str
+ cmp $'{', %al
+ je .Lskip_nest
+ cmp $'[', %al
+ je .Lskip_nest
+ jmp .Lskip_token
+.Lskip_str:
+ mov %rbx, %rdi
+ call cepis__js_raw_string
+ EPILOGUE
+ ret
+.Lskip_token:
+ mov %rbx, %r12
+.Lskip_token_loop:
+ movzbl (%r12), %eax
+ cmp $'0', %al
+ jb .Lskip_token_sym
+ cmp $'9', %al
+ jbe .Lskip_token_adv
+ or $0x20, %al
+ cmp $'a', %al
+ jb .Lskip_token_end
+ cmp $'z', %al
+ jbe .Lskip_token_adv
+ jmp .Lskip_token_end
+.Lskip_token_sym:
+ cmp $'+', %al
+ je .Lskip_token_adv
+ cmp $'-', %al
+ je .Lskip_token_adv
+ cmp $'.', %al
+ je .Lskip_token_adv
+ jmp .Lskip_token_end
+.Lskip_token_adv:
+ inc %r12
+ jmp .Lskip_token_loop
+.Lskip_token_end:
+ cmp %rbx, %r12
+ je .Lskip_bad
+ mov %r12, %rax
+ EPILOGUE
+ ret
+.Lskip_nest:
+ xor %r13d, %r13d
+.Lskip_nest_loop:
+ movzbl (%rbx), %eax
+ test %al, %al
+ jz .Lskip_bad
+ cmp $'"', %al
+ je .Lskip_nest_str
+ cmp $'{', %al
+ je .Lskip_nest_open
+ cmp $'[', %al
+ je .Lskip_nest_open
+ cmp $'}', %al
+ je .Lskip_nest_close
+ cmp $']', %al
+ je .Lskip_nest_close
+ inc %rbx
+ jmp .Lskip_nest_loop
+.Lskip_nest_str:
+ mov %rbx, %rdi
+ call cepis__js_raw_string
+ test %rax, %rax
+ jz .Lskip_bad
+ mov %rax, %rbx
+ jmp .Lskip_nest_loop
+.Lskip_nest_open:
+ inc %r13d
+ inc %rbx
+ jmp .Lskip_nest_loop
+.Lskip_nest_close:
+ dec %r13d
+ inc %rbx
+ test %r13d, %r13d
+ jz .Lskip_nest_done
+ jmp .Lskip_nest_loop
+.Lskip_nest_done:
+ mov %rbx, %rax
+ EPILOGUE
+ ret
+.Lskip_bad:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__js_skip
+
+FUNC cepis__js_string
+ PROLOGUE 40
+ mov %rsi, %r14
+ movq $0, JBUF(%rbp)
+ movq $0, JBUF+8(%rbp)
+ movq $0, JBUF+16(%rbp)
+ movq $0, (%r14)
+ cmpb $'"', (%rdi)
+ jne .Lstr_fail
+ lea 1(%rdi), %rbx
+.Lstr_loop:
+ movzbl (%rbx), %eax
+ test %al, %al
+ jz .Lstr_fail
+ cmp $'"', %al
+ je .Lstr_done
+ cmp $'\\', %al
+ je .Lstr_esc
+ lea JBUF(%rbp), %rdi
+ movzbl (%rbx), %esi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+ inc %rbx
+ jmp .Lstr_loop
+.Lstr_esc:
+ movzbl 1(%rbx), %eax
+ test %al, %al
+ jz .Lstr_fail
+ cmp $'u', %al
+ je .Lstr_uesc
+ mov %eax, %r12d
+ cmp $'"', %al
+ je .Lstr_emit
+ cmp $'\\', %al
+ je .Lstr_emit
+ cmp $'/', %al
+ je .Lstr_emit
+ cmp $'b', %al
+ je .Lstr_bs
+ cmp $'f', %al
+ je .Lstr_ff
+ cmp $'n', %al
+ je .Lstr_nl
+ cmp $'r', %al
+ je .Lstr_cr
+ cmp $'t', %al
+ je .Lstr_tab
+ jmp .Lstr_fail
+.Lstr_bs:
+ mov $8, %r12d
+ jmp .Lstr_emit
+.Lstr_ff:
+ mov $12, %r12d
+ jmp .Lstr_emit
+.Lstr_nl:
+ mov $10, %r12d
+ jmp .Lstr_emit
+.Lstr_cr:
+ mov $13, %r12d
+ jmp .Lstr_emit
+.Lstr_tab:
+ mov $9, %r12d
+.Lstr_emit:
+ lea JBUF(%rbp), %rdi
+ mov %r12d, %esi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+ add $2, %rbx
+ jmp .Lstr_loop
+.Lstr_uesc:
+ lea 2(%rbx), %rdi
+ call cepis__js_hex4
+ test %eax, %eax
+ js .Lstr_fail
+ mov %eax, %r12d
+ add $6, %rbx
+ cmp $0xD800, %r12d
+ jb .Lstr_encode
+ cmp $0xDBFF, %r12d
+ ja .Lstr_low
+ cmpb $'\\', (%rbx)
+ jne .Lstr_repl
+ cmpb $'u', 1(%rbx)
+ jne .Lstr_repl
+ lea 2(%rbx), %rdi
+ call cepis__js_hex4
+ test %eax, %eax
+ js .Lstr_repl
+ mov %eax, %r13d
+ cmp $0xDC00, %r13d
+ jb .Lstr_repl
+ cmp $0xDFFF, %r13d
+ ja .Lstr_repl
+ add $6, %rbx
+ sub $0xD800, %r12d
+ shl $10, %r12d
+ sub $0xDC00, %r13d
+ add %r13d, %r12d
+ add $0x10000, %r12d
+ jmp .Lstr_encode
+.Lstr_low:
+ cmp $0xDFFF, %r12d
+ ja .Lstr_encode
+.Lstr_repl:
+ mov $0xFFFD, %r12d
+.Lstr_encode:
+ cmp $0x80, %r12d
+ jb .Lstr_enc1
+ cmp $0x800, %r12d
+ jb .Lstr_enc2
+ cmp $0x10000, %r12d
+ jb .Lstr_enc3
+ mov %r12d, %esi
+ shr $18, %esi
+ or $0xF0, %esi
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+ mov %r12d, %esi
+ shr $12, %esi
+ and $0x3F, %esi
+ or $0x80, %esi
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+ jmp .Lstr_enc_tail2
+.Lstr_enc3:
+ mov %r12d, %esi
+ shr $12, %esi
+ or $0xE0, %esi
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+.Lstr_enc_tail2:
+ mov %r12d, %esi
+ shr $6, %esi
+ and $0x3F, %esi
+ or $0x80, %esi
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+ jmp .Lstr_enc_tail1
+.Lstr_enc2:
+ mov %r12d, %esi
+ shr $6, %esi
+ or $0xC0, %esi
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+.Lstr_enc_tail1:
+ mov %r12d, %esi
+ and $0x3F, %esi
+ or $0x80, %esi
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+ jmp .Lstr_loop
+.Lstr_enc1:
+ mov %r12d, %esi
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_appendc
+ test %eax, %eax
+ jnz .Lstr_fail
+ jmp .Lstr_loop
+.Lstr_done:
+ inc %rbx
+ cmpq $0, JBUF(%rbp)
+ jne .Lstr_take
+ call cepis__empty
+ test %rax, %rax
+ jz .Lstr_fail
+ mov %rax, (%r14)
+ mov %rbx, %rax
+ EPILOGUE
+ ret
+.Lstr_take:
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_take
+ mov %rax, (%r14)
+ mov %rbx, %rax
+ EPILOGUE
+ ret
+.Lstr_fail:
+ lea JBUF(%rbp), %rdi
+ call cepis__buf_free
+ movq $0, (%r14)
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__js_string
+
+FUNC cepis__js_scalar_text
+ PROLOGUE 24
+ mov %rsi, %r14
+ movq $0, (%r14)
+ call cepis__js_ws
+ mov %rax, %rbx
+ movzbl (%rbx), %eax
+ cmp $'"', %al
+ je .Lsc_string
+ cmp $'{', %al
+ je .Lsc_fail
+ cmp $'[', %al
+ je .Lsc_fail
+ mov %rbx, %rdi
+ call cepis__js_skip
+ test %rax, %rax
+ jz .Lsc_fail
+ mov %rax, %r12
+ mov %r12, %r13
+ sub %rbx, %r13
+ cmp $4, %r13
+ jne .Lsc_copy
+ mov %rbx, %rdi
+ lea .Lsc_null(%rip), %rsi
+ mov $4, %rdx
+ call memcmp@PLT
+ test %eax, %eax
+ jnz .Lsc_copy
+ call cepis__empty
+ test %rax, %rax
+ jz .Lsc_fail
+ mov %rax, (%r14)
+ mov %r12, %rax
+ EPILOGUE
+ ret
+.Lsc_copy:
+ mov %rbx, %rdi
+ mov %r13, %rsi
+ call cepis__strndup
+ test %rax, %rax
+ jz .Lsc_fail
+ mov %rax, (%r14)
+ mov %r12, %rax
+ EPILOGUE
+ ret
+.Lsc_string:
+ mov %rbx, %rdi
+ mov %r14, %rsi
+ call cepis__js_string
+ EPILOGUE
+ ret
+.Lsc_fail:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__js_scalar_text
+
+FUNC cepis__js_number
+ PROLOGUE 40
+ mov %rsi, %r14
+ call cepis__js_ws
+ mov %rax, %rbx
+ cmpb $'"', (%rbx)
+ je .Lnum_quoted
+ mov %rbx, %rdi
+ lea LOCAL0(%rbp), %rsi
+ call strtod@PLT
+ mov LOCAL0(%rbp), %rax
+ cmp %rbx, %rax
+ je .Lnum_fail
+ movsd %xmm0, (%r14)
+ EPILOGUE
+ ret
+.Lnum_quoted:
+ mov %rbx, %rdi
+ lea LOCAL1(%rbp), %rsi
+ call cepis__js_string
+ test %rax, %rax
+ jz .Lnum_fail
+ mov %rax, %r12
+ mov LOCAL1(%rbp), %rdi
+ lea LOCAL0(%rbp), %rsi
+ call strtod@PLT
+ movsd %xmm0, LOCAL2(%rbp)
+ mov LOCAL1(%rbp), %rbx
+ mov LOCAL0(%rbp), %r13
+ mov %rbx, %rdi
+ call free@PLT
+ cmp %rbx, %r13
+ je .Lnum_fail
+ movsd LOCAL2(%rbp), %xmm0
+ movsd %xmm0, (%r14)
+ mov %r12, %rax
+ EPILOGUE
+ ret
+.Lnum_fail:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__js_number
+
+ .section .rodata
+.Lsc_null:
+ .ascii "null"
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/src/models.S b/src/models.S
new file mode 100644
index 0000000..77218f4
--- /dev/null
+++ b/src/models.S
@@ -0,0 +1,716 @@
+#include "abi.inc"
+#include "defs.inc"
+
+ .text
+
+FUNC cepis__addr_field_offset
+ PROLOGUE 24
+ mov %rdi, %rbx
+ lea cepis__addr_fields(%rip), %r12
+.Lfo_loop:
+ mov (%r12), %rsi
+ test %rsi, %rsi
+ jz .Lfo_none
+ mov %rbx, %rdi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lfo_hit
+ add $16, %r12
+ jmp .Lfo_loop
+.Lfo_hit:
+ mov 8(%r12), %rax
+ EPILOGUE
+ ret
+.Lfo_none:
+ mov $-1, %rax
+ EPILOGUE
+ ret
+ENDFUNC cepis__addr_field_offset
+
+FUNC cepis__addr_set
+ PROLOGUE 24
+ mov %rdi, %rbx
+ mov %rsi, %r12
+ mov %rdx, %r13
+ mov (%rbx, %r12, 1), %rdi
+ test %rdi, %rdi
+ jz .Lset_store
+ call free@PLT
+.Lset_store:
+ mov %r13, (%rbx, %r12, 1)
+ EPILOGUE
+ ret
+ENDFUNC cepis__addr_set
+
+FUNC cepis__address_new
+ PROLOGUE 24
+ mov $1, %rdi
+ mov $A_SIZE, %rsi
+ call calloc@PLT
+ test %rax, %rax
+ jz .Lanew_out
+ mov %rax, %rbx
+ xor %r12d, %r12d
+.Lanew_loop:
+ cmp $(A_NFIELDS * 8), %r12
+ jae .Lanew_done
+ call cepis__empty
+ test %rax, %rax
+ jz .Lanew_fail
+ mov %rax, (%rbx, %r12, 1)
+ add $8, %r12
+ jmp .Lanew_loop
+.Lanew_done:
+ mov %rbx, %rax
+.Lanew_out:
+ EPILOGUE
+ ret
+.Lanew_fail:
+ mov %rbx, %rdi
+ call cepis_address_free
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__address_new
+
+PUBFUNC cepis_address_free
+ PROLOGUE 24
+ test %rdi, %rdi
+ jz .Lafree_out
+ mov %rdi, %rbx
+ xor %r12d, %r12d
+.Lafree_loop:
+ cmp $(A_NFIELDS * 8), %r12
+ jae .Lafree_geo
+ mov (%rbx, %r12, 1), %rdi
+ test %rdi, %rdi
+ jz .Lafree_next
+ call free@PLT
+.Lafree_next:
+ add $8, %r12
+ jmp .Lafree_loop
+.Lafree_geo:
+ mov A_GEO(%rbx), %r13
+ test %r13, %r13
+ jz .Lafree_raw
+ mov GEO_PRECISAO(%r13), %rdi
+ test %rdi, %rdi
+ jz .Lafree_geo_self
+ call free@PLT
+.Lafree_geo_self:
+ mov %r13, %rdi
+ call free@PLT
+.Lafree_raw:
+ mov A_RAW(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lafree_self
+ call free@PLT
+.Lafree_self:
+ mov %rbx, %rdi
+ call free@PLT
+.Lafree_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis_address_free
+
+PUBFUNC cepis_address_list_free
+ PROLOGUE 24
+ test %rdi, %rdi
+ jz .Llfree_out
+ mov %rdi, %rbx
+ mov %rsi, %r12
+ xor %r13d, %r13d
+.Llfree_loop:
+ cmp %r12, %r13
+ jae .Llfree_self
+ mov (%rbx, %r13, 8), %rdi
+ call cepis_address_free
+ inc %r13
+ jmp .Llfree_loop
+.Llfree_self:
+ mov %rbx, %rdi
+ call free@PLT
+.Llfree_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis_address_list_free
+
+PUBFUNC cepis_health_free
+ PROLOGUE 24
+ test %rdi, %rdi
+ jz .Lhfree_out
+ mov %rdi, %rbx
+ mov H_STATUS(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lhfree_1
+ call free@PLT
+.Lhfree_1:
+ mov H_DNE(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lhfree_2
+ call free@PLT
+.Lhfree_2:
+ mov H_BASE_GERADA(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lhfree_3
+ call free@PLT
+.Lhfree_3:
+ mov H_RAW(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lhfree_self
+ call free@PLT
+.Lhfree_self:
+ mov %rbx, %rdi
+ call free@PLT
+.Lhfree_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis_health_free
+
+PUBFUNC cepis_geo_is_precise
+ xor %eax, %eax
+ test %rdi, %rdi
+ jz .Lprec_out
+ mov GEO_PRECISAO(%rdi), %rdi
+ test %rdi, %rdi
+ jz .Lprec_out
+ lea .Ls_logradouro_val(%rip), %rsi
+ jmp cepis__streq_tail
+.Lprec_out:
+ ret
+ENDFUNC cepis_geo_is_precise
+
+FUNC cepis__streq_tail
+ PROLOGUE 8
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lseq_yes
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lseq_yes:
+ mov $1, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__streq_tail
+
+PUBFUNC cepis_address_formatted_cep
+ test %rsi, %rsi
+ jz .Lfmt_out
+ movb $0, (%rsi)
+ test %rdi, %rdi
+ jz .Lfmt_out
+ mov A_CEP(%rdi), %rdi
+ test %rdi, %rdi
+ jz .Lfmt_out
+ xor %ecx, %ecx
+ xor %edx, %edx
+.Lfmt_loop:
+ cmp $9, %rcx
+ jae .Lfmt_end
+ movzbl (%rdi, %rcx, 1), %eax
+ test %al, %al
+ jz .Lfmt_end
+ cmp $5, %rcx
+ jne .Lfmt_put
+ movb $'-', (%rsi, %rdx, 1)
+ inc %rdx
+.Lfmt_put:
+ mov %al, (%rsi, %rdx, 1)
+ inc %rdx
+ inc %rcx
+ jmp .Lfmt_loop
+.Lfmt_end:
+ movb $0, (%rsi, %rdx, 1)
+.Lfmt_out:
+ ret
+ENDFUNC cepis_address_formatted_cep
+
+FUNC cepis__geo_parse
+ PROLOGUE 88
+ mov %rsi, %r14
+ movq $0, (%r14)
+ movq $0, LOCAL3(%rbp)
+ xor %r15d, %r15d
+ cmpb $'{', (%rdi)
+ jne .Lgeo_fail
+ lea 1(%rdi), %rdi
+ call cepis__js_ws
+ mov %rax, %rbx
+ cmpb $'}', (%rbx)
+ je .Lgeo_finish
+.Lgeo_pair:
+ cmpb $'"', (%rbx)
+ jne .Lgeo_fail
+ mov %rbx, %rdi
+ lea LOCAL4(%rbp), %rsi
+ call cepis__js_string
+ test %rax, %rax
+ jz .Lgeo_fail
+ mov %rax, %rdi
+ call cepis__js_ws
+ mov %rax, %rbx
+ cmpb $':', (%rbx)
+ jne .Lgeo_fail_key
+ inc %rbx
+ mov LOCAL4(%rbp), %rdi
+ lea .Ls_lat(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lgeo_lat
+ mov LOCAL4(%rbp), %rdi
+ lea .Ls_lon(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lgeo_lon
+ mov LOCAL4(%rbp), %rdi
+ lea .Ls_raio_m(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lgeo_raio
+ mov LOCAL4(%rbp), %rdi
+ lea .Ls_precisao(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lgeo_prec
+ mov %rbx, %rdi
+ call cepis__js_skip
+ jmp .Lgeo_after
+.Lgeo_lat:
+ mov %rbx, %rdi
+ lea LOCAL0(%rbp), %rsi
+ call cepis__js_number
+ or $1, %r15d
+ jmp .Lgeo_after
+.Lgeo_lon:
+ mov %rbx, %rdi
+ lea LOCAL1(%rbp), %rsi
+ call cepis__js_number
+ or $2, %r15d
+ jmp .Lgeo_after
+.Lgeo_raio:
+ mov %rbx, %rdi
+ lea LOCAL2(%rbp), %rsi
+ call cepis__js_number
+ or $4, %r15d
+ jmp .Lgeo_after
+.Lgeo_prec:
+ mov LOCAL3(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Lgeo_prec_go
+ call free@PLT
+ movq $0, LOCAL3(%rbp)
+.Lgeo_prec_go:
+ mov %rbx, %rdi
+ lea LOCAL3(%rbp), %rsi
+ call cepis__js_scalar_text
+ or $8, %r15d
+.Lgeo_after:
+ mov %rax, %r12
+ mov LOCAL4(%rbp), %rdi
+ call free@PLT
+ movq $0, LOCAL4(%rbp)
+ test %r12, %r12
+ jz .Lgeo_fail
+ mov %r12, %rdi
+ call cepis__js_ws
+ mov %rax, %rbx
+ cmpb $',', (%rbx)
+ je .Lgeo_comma
+ cmpb $'}', (%rbx)
+ je .Lgeo_finish
+ jmp .Lgeo_fail
+.Lgeo_comma:
+ lea 1(%rbx), %rdi
+ call cepis__js_ws
+ mov %rax, %rbx
+ jmp .Lgeo_pair
+.Lgeo_finish:
+ cmp $15, %r15d
+ jne .Lgeo_fail
+ cmpq $0, LOCAL3(%rbp)
+ je .Lgeo_fail
+ mov $1, %rdi
+ mov $GEO_SIZE, %rsi
+ call calloc@PLT
+ test %rax, %rax
+ jz .Lgeo_fail
+ mov %rax, %rbx
+ movsd LOCAL0(%rbp), %xmm0
+ movsd %xmm0, GEO_LAT(%rbx)
+ movsd LOCAL1(%rbp), %xmm0
+ movsd %xmm0, GEO_LON(%rbx)
+ cvttsd2si LOCAL2(%rbp), %rax
+ mov %rax, GEO_RAIO_M(%rbx)
+ mov LOCAL3(%rbp), %rax
+ mov %rax, GEO_PRECISAO(%rbx)
+ movq $0, LOCAL3(%rbp)
+ mov %rbx, (%r14)
+ EPILOGUE
+ ret
+.Lgeo_fail_key:
+ mov LOCAL4(%rbp), %rdi
+ call free@PLT
+.Lgeo_fail:
+ mov LOCAL3(%rbp), %rdi
+ test %rdi, %rdi
+ jz .Lgeo_fail_out
+ call free@PLT
+.Lgeo_fail_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis__geo_parse
+
+FUNC cepis__address_from_json
+ PROLOGUE 40
+ mov %rsi, %r14
+ movq $0, (%r14)
+ mov %rdi, %r15
+ call cepis__address_new
+ test %rax, %rax
+ jz .Lafj_nomem
+ mov %rax, %rbx
+ mov %r15, %rdi
+ call cepis__strdup
+ test %rax, %rax
+ jz .Lafj_nomem_free
+ mov %rax, A_RAW(%rbx)
+ mov %r15, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $'{', (%r12)
+ jne .Lafj_invalid
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $'}', (%r12)
+ je .Lafj_ok
+.Lafj_pair:
+ cmpb $'"', (%r12)
+ jne .Lafj_invalid
+ mov %r12, %rdi
+ lea LOCAL0(%rbp), %rsi
+ call cepis__js_string
+ test %rax, %rax
+ jz .Lafj_invalid
+ mov %rax, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $':', (%r12)
+ jne .Lafj_invalid_key
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ mov LOCAL0(%rbp), %rdi
+ lea .Ls_geo(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lafj_geo
+ mov LOCAL0(%rbp), %rdi
+ call cepis__addr_field_offset
+ cmp $0, %rax
+ jl .Lafj_unknown
+ mov %rax, %r13
+ movzbl (%r12), %eax
+ cmp $'{', %al
+ je .Lafj_unknown
+ cmp $'[', %al
+ je .Lafj_unknown
+ mov %r12, %rdi
+ lea LOCAL1(%rbp), %rsi
+ call cepis__js_scalar_text
+ test %rax, %rax
+ jz .Lafj_invalid_key
+ mov %rax, %r12
+ mov %rbx, %rdi
+ mov %r13, %rsi
+ mov LOCAL1(%rbp), %rdx
+ call cepis__addr_set
+ jmp .Lafj_next
+.Lafj_unknown:
+ mov %r12, %rdi
+ call cepis__js_skip
+ test %rax, %rax
+ jz .Lafj_invalid_key
+ mov %rax, %r12
+ jmp .Lafj_next
+.Lafj_geo:
+ mov %r12, %rdi
+ call cepis__js_skip
+ test %rax, %rax
+ jz .Lafj_invalid_key
+ mov %rax, %r13
+ cmpb $'{', (%r12)
+ jne .Lafj_geo_done
+ mov %r12, %rdi
+ lea LOCAL2(%rbp), %rsi
+ call cepis__geo_parse
+ mov LOCAL2(%rbp), %rax
+ test %rax, %rax
+ jz .Lafj_geo_done
+ mov %rax, A_GEO(%rbx)
+.Lafj_geo_done:
+ mov %r13, %r12
+.Lafj_next:
+ mov LOCAL0(%rbp), %rdi
+ call free@PLT
+ movq $0, LOCAL0(%rbp)
+ mov %r12, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $',', (%r12)
+ je .Lafj_comma
+ cmpb $'}', (%r12)
+ je .Lafj_ok
+ jmp .Lafj_invalid
+.Lafj_comma:
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ jmp .Lafj_pair
+.Lafj_ok:
+ mov %rbx, (%r14)
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lafj_invalid_key:
+ mov LOCAL0(%rbp), %rdi
+ call free@PLT
+.Lafj_invalid:
+ mov %rbx, %rdi
+ call cepis_address_free
+ mov $CEPIS_ERR_INVALID_RESPONSE, %eax
+ EPILOGUE
+ ret
+.Lafj_nomem_free:
+ mov %rbx, %rdi
+ call cepis_address_free
+.Lafj_nomem:
+ mov $CEPIS_ERR_NOMEM, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__address_from_json
+
+FUNC cepis__health_from_json
+ PROLOGUE 40
+ mov %rsi, %r14
+ movq $0, (%r14)
+ mov %rdi, %r15
+ mov $1, %rdi
+ mov $H_SIZE, %rsi
+ call calloc@PLT
+ test %rax, %rax
+ jz .Lhfj_nomem
+ mov %rax, %rbx
+ call cepis__empty
+ test %rax, %rax
+ jz .Lhfj_nomem_free
+ mov %rax, H_STATUS(%rbx)
+ call cepis__empty
+ test %rax, %rax
+ jz .Lhfj_nomem_free
+ mov %rax, H_DNE(%rbx)
+ call cepis__empty
+ test %rax, %rax
+ jz .Lhfj_nomem_free
+ mov %rax, H_BASE_GERADA(%rbx)
+ mov %r15, %rdi
+ call cepis__strdup
+ test %rax, %rax
+ jz .Lhfj_nomem_free
+ mov %rax, H_RAW(%rbx)
+ mov %r15, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $'{', (%r12)
+ jne .Lhfj_invalid
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $'}', (%r12)
+ je .Lhfj_ok
+.Lhfj_pair:
+ cmpb $'"', (%r12)
+ jne .Lhfj_invalid
+ mov %r12, %rdi
+ lea LOCAL0(%rbp), %rsi
+ call cepis__js_string
+ test %rax, %rax
+ jz .Lhfj_invalid
+ mov %rax, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $':', (%r12)
+ jne .Lhfj_invalid_key
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ mov LOCAL0(%rbp), %rdi
+ lea .Ls_status(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lhfj_status
+ mov LOCAL0(%rbp), %rdi
+ lea .Ls_dne(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lhfj_dne
+ mov LOCAL0(%rbp), %rdi
+ lea .Ls_base_gerada(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lhfj_base
+ mov LOCAL0(%rbp), %rdi
+ lea .Ls_ceps(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lhfj_ceps
+ mov LOCAL0(%rbp), %rdi
+ lea .Ls_geo(%rip), %rsi
+ call strcmp@PLT
+ test %eax, %eax
+ jz .Lhfj_geo
+ mov %r12, %rdi
+ call cepis__js_skip
+ test %rax, %rax
+ jz .Lhfj_invalid_key
+ mov %rax, %r12
+ jmp .Lhfj_next
+.Lhfj_status:
+ mov $H_STATUS, %r13
+ jmp .Lhfj_text
+.Lhfj_dne:
+ mov $H_DNE, %r13
+ jmp .Lhfj_text
+.Lhfj_base:
+ mov $H_BASE_GERADA, %r13
+.Lhfj_text:
+ movzbl (%r12), %eax
+ cmp $'{', %al
+ je .Lhfj_skipval
+ cmp $'[', %al
+ je .Lhfj_skipval
+ mov %r12, %rdi
+ lea LOCAL1(%rbp), %rsi
+ call cepis__js_scalar_text
+ test %rax, %rax
+ jz .Lhfj_invalid_key
+ mov %rax, %r12
+ mov (%rbx, %r13, 1), %rdi
+ call free@PLT
+ mov LOCAL1(%rbp), %rax
+ mov %rax, (%rbx, %r13, 1)
+ jmp .Lhfj_next
+.Lhfj_skipval:
+ mov %r12, %rdi
+ call cepis__js_skip
+ test %rax, %rax
+ jz .Lhfj_invalid_key
+ mov %rax, %r12
+ jmp .Lhfj_next
+.Lhfj_ceps:
+ mov $H_CEPS, %r13
+ jmp .Lhfj_int
+.Lhfj_geo:
+ mov $H_GEO, %r13
+.Lhfj_int:
+ movzbl (%r12), %eax
+ cmp $'{', %al
+ je .Lhfj_skipval
+ cmp $'[', %al
+ je .Lhfj_skipval
+ mov %r12, %rdi
+ lea LOCAL1(%rbp), %rsi
+ call cepis__js_number
+ test %rax, %rax
+ jz .Lhfj_skipval
+ mov %rax, %r12
+ cvttsd2si LOCAL1(%rbp), %rax
+ mov %rax, (%rbx, %r13, 1)
+.Lhfj_next:
+ mov LOCAL0(%rbp), %rdi
+ call free@PLT
+ movq $0, LOCAL0(%rbp)
+ mov %r12, %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ cmpb $',', (%r12)
+ je .Lhfj_comma
+ cmpb $'}', (%r12)
+ je .Lhfj_ok
+ jmp .Lhfj_invalid
+.Lhfj_comma:
+ lea 1(%r12), %rdi
+ call cepis__js_ws
+ mov %rax, %r12
+ jmp .Lhfj_pair
+.Lhfj_ok:
+ mov %rbx, (%r14)
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lhfj_invalid_key:
+ mov LOCAL0(%rbp), %rdi
+ call free@PLT
+.Lhfj_invalid:
+ mov %rbx, %rdi
+ call cepis_health_free
+ mov $CEPIS_ERR_INVALID_RESPONSE, %eax
+ EPILOGUE
+ ret
+.Lhfj_nomem_free:
+ mov %rbx, %rdi
+ call cepis_health_free
+.Lhfj_nomem:
+ mov $CEPIS_ERR_NOMEM, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__health_from_json
+
+ .section .rodata
+
+.Ls_cep: .asciz "cep"
+.Ls_logradouro: .asciz "logradouro"
+.Ls_complemento: .asciz "complemento"
+.Ls_unidade: .asciz "unidade"
+.Ls_bairro: .asciz "bairro"
+.Ls_localidade: .asciz "localidade"
+.Ls_uf: .asciz "uf"
+.Ls_estado: .asciz "estado"
+.Ls_regiao: .asciz "regiao"
+.Ls_ibge: .asciz "ibge"
+.Ls_gia: .asciz "gia"
+.Ls_ddd: .asciz "ddd"
+.Ls_siafi: .asciz "siafi"
+.Ls_geo: .asciz "geo"
+.Ls_lat: .asciz "lat"
+.Ls_lon: .asciz "lon"
+.Ls_raio_m: .asciz "raio_m"
+.Ls_precisao: .asciz "precisao"
+.Ls_status: .asciz "status"
+.Ls_dne: .asciz "dne"
+.Ls_ceps: .asciz "ceps"
+.Ls_base_gerada: .asciz "base_gerada_em"
+.Ls_logradouro_val: .asciz "logradouro"
+
+ .section .data.rel.ro,"aw",@progbits
+ .p2align 3
+ .globl cepis__addr_fields
+ .hidden cepis__addr_fields
+cepis__addr_fields:
+ .quad .Ls_cep, A_CEP
+ .quad .Ls_logradouro, A_LOGRADOURO
+ .quad .Ls_complemento, A_COMPLEMENTO
+ .quad .Ls_unidade, A_UNIDADE
+ .quad .Ls_bairro, A_BAIRRO
+ .quad .Ls_localidade, A_LOCALIDADE
+ .quad .Ls_uf, A_UF
+ .quad .Ls_estado, A_ESTADO
+ .quad .Ls_regiao, A_REGIAO
+ .quad .Ls_ibge, A_IBGE
+ .quad .Ls_gia, A_GIA
+ .quad .Ls_ddd, A_DDD
+ .quad .Ls_siafi, A_SIAFI
+ .quad 0, 0
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/src/util.S b/src/util.S
new file mode 100644
index 0000000..447f66c
--- /dev/null
+++ b/src/util.S
@@ -0,0 +1,190 @@
+#include "abi.inc"
+#include "defs.inc"
+
+ .text
+
+FUNC cepis__strndup
+ PROLOGUE 24
+ mov %rdi, %r12
+ mov %rsi, %r13
+ lea 1(%r13), %rdi
+ call malloc@PLT
+ test %rax, %rax
+ jz .Lstrndup_out
+ mov %rax, %rbx
+ test %r13, %r13
+ jz .Lstrndup_term
+ mov %rbx, %rdi
+ mov %r12, %rsi
+ mov %r13, %rdx
+ call memcpy@PLT
+.Lstrndup_term:
+ movb $0, (%rbx, %r13, 1)
+ mov %rbx, %rax
+.Lstrndup_out:
+ EPILOGUE
+ ret
+ENDFUNC cepis__strndup
+
+FUNC cepis__strdup
+ PROLOGUE 8
+ test %rdi, %rdi
+ jz .Lstrdup_null
+ mov %rdi, %rbx
+ call strlen@PLT
+ mov %rbx, %rdi
+ mov %rax, %rsi
+ call cepis__strndup
+ EPILOGUE
+ ret
+.Lstrdup_null:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__strdup
+
+FUNC cepis__empty
+ PROLOGUE 8
+ lea .Lempty_str(%rip), %rdi
+ xor %esi, %esi
+ call cepis__strndup
+ EPILOGUE
+ ret
+ENDFUNC cepis__empty
+
+FUNC cepis__buf_reserve
+ PROLOGUE 24
+ mov %rdi, %rbx
+ mov B_LEN(%rbx), %rax
+ add %rsi, %rax
+ add $1, %rax
+ cmp B_CAP(%rbx), %rax
+ jbe .Lreserve_ok
+ mov %rax, %r12
+ mov B_CAP(%rbx), %r13
+ test %r13, %r13
+ jnz .Lreserve_grow
+ mov $64, %r13
+.Lreserve_grow:
+ cmp %r12, %r13
+ jae .Lreserve_alloc
+ shl $1, %r13
+ jmp .Lreserve_grow
+.Lreserve_alloc:
+ mov B_DATA(%rbx), %rdi
+ mov %r13, %rsi
+ call realloc@PLT
+ test %rax, %rax
+ jz .Lreserve_fail
+ mov %rax, B_DATA(%rbx)
+ mov %r13, B_CAP(%rbx)
+.Lreserve_ok:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lreserve_fail:
+ mov $-1, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__buf_reserve
+
+FUNC cepis__buf_append
+ PROLOGUE 24
+ mov %rdi, %rbx
+ mov %rsi, %r12
+ mov %rdx, %r13
+ mov %rbx, %rdi
+ mov %r13, %rsi
+ call cepis__buf_reserve
+ test %eax, %eax
+ jnz .Lappend_fail
+ test %r13, %r13
+ jz .Lappend_term
+ mov B_DATA(%rbx), %rdi
+ add B_LEN(%rbx), %rdi
+ mov %r12, %rsi
+ mov %r13, %rdx
+ call memcpy@PLT
+.Lappend_term:
+ add %r13, B_LEN(%rbx)
+ mov B_DATA(%rbx), %rax
+ mov B_LEN(%rbx), %rcx
+ movb $0, (%rax, %rcx, 1)
+ xor %eax, %eax
+ EPILOGUE
+ ret
+.Lappend_fail:
+ mov $-1, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__buf_append
+
+FUNC cepis__buf_appendc
+ PROLOGUE 24
+ mov %esi, %eax
+ movb %al, LOCAL0(%rbp)
+ lea LOCAL0(%rbp), %rsi
+ mov $1, %rdx
+ call cepis__buf_append
+ EPILOGUE
+ ret
+ENDFUNC cepis__buf_appendc
+
+FUNC cepis__buf_appendz
+ PROLOGUE 24
+ test %rsi, %rsi
+ jz .Lappendz_ok
+ mov %rdi, %rbx
+ mov %rsi, %r12
+ mov %rsi, %rdi
+ call strlen@PLT
+ mov %rbx, %rdi
+ mov %r12, %rsi
+ mov %rax, %rdx
+ call cepis__buf_append
+ EPILOGUE
+ ret
+.Lappendz_ok:
+ xor %eax, %eax
+ EPILOGUE
+ ret
+ENDFUNC cepis__buf_appendz
+
+FUNC cepis__buf_reset
+ movq $0, B_LEN(%rdi)
+ mov B_DATA(%rdi), %rax
+ test %rax, %rax
+ jz .Lreset_out
+ movb $0, (%rax)
+.Lreset_out:
+ ret
+ENDFUNC cepis__buf_reset
+
+FUNC cepis__buf_free
+ PROLOGUE 8
+ mov %rdi, %rbx
+ mov B_DATA(%rbx), %rdi
+ test %rdi, %rdi
+ jz .Lbfree_zero
+ call free@PLT
+.Lbfree_zero:
+ movq $0, B_DATA(%rbx)
+ movq $0, B_LEN(%rbx)
+ movq $0, B_CAP(%rbx)
+ EPILOGUE
+ ret
+ENDFUNC cepis__buf_free
+
+FUNC cepis__buf_take
+ mov B_DATA(%rdi), %rax
+ movq $0, B_DATA(%rdi)
+ movq $0, B_LEN(%rdi)
+ movq $0, B_CAP(%rdi)
+ ret
+ENDFUNC cepis__buf_take
+
+ .section .rodata
+.Lempty_str:
+ .byte 0
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/tests/abi_probe.S b/tests/abi_probe.S
new file mode 100644
index 0000000..0e07ec4
--- /dev/null
+++ b/tests/abi_probe.S
@@ -0,0 +1,102 @@
+/*
+ * cepis_abi_probe(fn, a0, a1, a2, a3)
+ *
+ * Calls fn(a0, a1, a2, a3) with every callee-saved register holding a
+ * distinctive sentinel and returns a bitmask of the ones it came back
+ * clobbered, plus bits for a moved stack pointer and a left-over direction
+ * flag. 0 means the callee honoured the System V AMD64 ABI.
+ *
+ * Not part of the library: this file is only linked into the test driver.
+ */
+
+ .text
+ .globl cepis_abi_probe
+ .type cepis_abi_probe, @function
+ .p2align 4
+cepis_abi_probe:
+ push %rbp
+ mov %rsp, %rbp
+ push %rbx
+ push %r12
+ push %r13
+ push %r14
+ push %r15
+ sub $8, %rsp
+ mov %rbp, cepis__probe_bp(%rip)
+ mov %rsp, cepis__probe_sp(%rip)
+ mov %rdi, %rax
+ mov %rsi, %rdi
+ mov %rdx, %rsi
+ mov %rcx, %rdx
+ mov %r8, %rcx
+ movabs $0x1111111111111111, %rbx
+ movabs $0x2222222222222222, %r12
+ movabs $0x3333333333333333, %r13
+ movabs $0x4444444444444444, %r14
+ movabs $0x5555555555555555, %r15
+ movabs $0x6666666666666666, %rbp
+ call *%rax
+ xor %r8d, %r8d
+ movabs $0x1111111111111111, %r9
+ cmp %r9, %rbx
+ je .Lp_r12
+ or $1, %r8d
+.Lp_r12:
+ movabs $0x2222222222222222, %r9
+ cmp %r9, %r12
+ je .Lp_r13
+ or $2, %r8d
+.Lp_r13:
+ movabs $0x3333333333333333, %r9
+ cmp %r9, %r13
+ je .Lp_r14
+ or $4, %r8d
+.Lp_r14:
+ movabs $0x4444444444444444, %r9
+ cmp %r9, %r14
+ je .Lp_r15
+ or $8, %r8d
+.Lp_r15:
+ movabs $0x5555555555555555, %r9
+ cmp %r9, %r15
+ je .Lp_rbp
+ or $16, %r8d
+.Lp_rbp:
+ movabs $0x6666666666666666, %r9
+ cmp %r9, %rbp
+ je .Lp_sp
+ or $32, %r8d
+.Lp_sp:
+ mov cepis__probe_sp(%rip), %r9
+ cmp %r9, %rsp
+ je .Lp_df
+ or $64, %r8d
+ mov %r9, %rsp
+.Lp_df:
+ pushfq
+ pop %r9
+ test $0x400, %r9d
+ jz .Lp_done
+ or $128, %r8d
+ cld
+.Lp_done:
+ mov cepis__probe_bp(%rip), %rbp
+ mov %r8d, %eax
+ lea -40(%rbp), %rsp
+ pop %r15
+ pop %r14
+ pop %r13
+ pop %r12
+ pop %rbx
+ pop %rbp
+ ret
+ .size cepis_abi_probe, . - cepis_abi_probe
+
+ .data
+ .p2align 3
+cepis__probe_bp:
+ .quad 0
+cepis__probe_sp:
+ .quad 0
+
+ .section .note.GNU-stack,"",@progbits
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100755
index 0000000..52e50d2
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+# Bring the local API stand-in up, run the driver against it, tear it down.
+# Exits non-zero when anything fails. $1 is the driver, the rest is a prefix
+# to run it under (valgrind, gdb, ...).
+
+set -eu
+
+here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+driver=$1
+shift
+
+fifo=$(mktemp -u)
+mkfifo "$fifo"
+
+python3 "$here/server.py" 0 >"$fifo" &
+server=$!
+
+cleanup() {
+ kill "$server" 2>/dev/null || true
+ wait "$server" 2>/dev/null || true
+ rm -f "$fifo"
+}
+trap cleanup EXIT INT TERM
+
+port=$(head -n 1 "$fifo")
+if [ -z "$port" ]; then
+ echo "server did not start" >&2
+ exit 1
+fi
+
+echo "server on 127.0.0.1:$port"
+"$@" "$driver" "http://127.0.0.1:$port"
diff --git a/tests/server.py b/tests/server.py
new file mode 100755
index 0000000..c77d7f6
--- /dev/null
+++ b/tests/server.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+"""Local stand-in for the cep.is API, used by the test driver.
+
+Behaviour is selected by the first path segment, so a test points the client
+at a different ``base_url`` to get a different server: ``/fail2`` fails twice
+then answers, ``/always503`` never answers, and so on. Every prefix counts the
+requests it saw; ``/_stats/<prefix>/health`` reports that count in the ``ceps``
+field so the C driver can assert on it without speaking HTTP itself.
+"""
+
+import json
+import sys
+import threading
+from datetime import datetime, timedelta, timezone
+from email.utils import format_datetime
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from urllib.parse import parse_qs, urlparse
+
+COLUMNS = [
+ "cep", "logradouro", "complemento", "unidade", "bairro", "localidade",
+ "uf", "estado", "regiao", "ibge", "gia", "ddd", "siafi",
+]
+
+DB = {
+ "01001000": {
+ "cep": "01001000", "logradouro": "Praça da Sé",
+ "complemento": "lado ímpar", "unidade": "", "bairro": "Sé",
+ "localidade": "São Paulo", "uf": "SP", "estado": "São Paulo",
+ "regiao": "Sudeste", "ibge": "3550308", "gia": "1004", "ddd": "11",
+ "siafi": "7107",
+ "geo": {"lat": -23.567202, "lon": -46.649659, "raio_m": 1084,
+ "precisao": "logradouro"},
+ },
+ "20040020": {
+ "cep": "20040020", "logradouro": "Avenida Rio Branco",
+ "complemento": "", "unidade": "", "bairro": "Centro",
+ "localidade": "Rio de Janeiro", "uf": "RJ",
+ "estado": "Rio de Janeiro", "regiao": "Sudeste", "ibge": "3304557",
+ "gia": "", "ddd": "21", "siafi": "6001",
+ },
+ "69900000": {
+ "cep": "69900000", "logradouro": "", "complemento": "", "unidade": "",
+ "bairro": "", "localidade": "Rio Branco", "uf": "AC",
+ "estado": "Acre", "regiao": "Norte", "ibge": "1200401", "gia": "",
+ "ddd": "68", "siafi": "0605",
+ "geo": {"lat": -9.97499, "lon": -67.8243, "raio_m": 18420,
+ "precisao": "localidade"},
+ },
+ "12345678": {
+ "cep": "12345678", "logradouro": 'Rua A, B "quoted"',
+ "complemento": "até 100", "unidade": "", "bairro": "Vila, Nova",
+ "localidade": "Campinas", "uf": "SP", "estado": "São Paulo",
+ "regiao": "Sudeste", "ibge": "3509502", "gia": "2446", "ddd": "19",
+ "siafi": "6291",
+ },
+}
+
+for _n in range(600):
+ _c = "%08d" % (70000000 + _n)
+ DB[_c] = {
+ "cep": _c, "logradouro": "Rua %d" % _n, "complemento": "",
+ "unidade": "", "bairro": "Centro", "localidade": "Cidade %d" % _n,
+ "uf": "SP", "estado": "São Paulo", "regiao": "Sudeste",
+ "ibge": "3550308", "gia": "", "ddd": "11", "siafi": "7107",
+ }
+
+HITS = {}
+LOCK = threading.Lock()
+
+
+def bump(prefix):
+ with LOCK:
+ HITS[prefix] = HITS.get(prefix, 0) + 1
+ return HITS[prefix]
+
+
+def csv_escape(value):
+ if any(c in value for c in ',"\r\n'):
+ return '"' + value.replace('"', '""') + '"'
+ return value
+
+
+def csv_row(cep):
+ entry = DB.get(cep)
+ if entry is None:
+ return ",".join([cep] + [""] * (len(COLUMNS) - 1))
+ return ",".join(csv_escape(entry.get(c, "")) for c in COLUMNS)
+
+
+class Handler(BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def log_message(self, fmt, *args):
+ pass
+
+ def send(self, code, body, ctype="application/json; charset=utf-8",
+ headers=None):
+ raw = body.encode("utf-8") if isinstance(body, str) else body
+ self.send_response(code)
+ self.send_header("Content-Type", ctype)
+ self.send_header("Content-Length", str(len(raw)))
+ for key, value in (headers or {}).items():
+ self.send_header(key, value)
+ self.end_headers()
+ self.wfile.write(raw)
+
+ def do_GET(self):
+ parsed = urlparse(self.path)
+ parts = [p for p in parsed.path.split("/") if p]
+
+ if parts and parts[0] == "_stats":
+ name = parts[1] if len(parts) > 1 else ""
+ with LOCK:
+ count = HITS.get(name, 0)
+ self.send(200, json.dumps({
+ "status": "ok", "dne": "test", "ceps": count, "geo": 0,
+ "base_gerada_em": "2026-07-24T03:48:28Z",
+ }))
+ return
+
+ prefix = ""
+ if parts and not parts[0].isdigit() and parts[0] not in ("csv", "health"):
+ prefix = parts.pop(0)
+ count = bump(prefix)
+
+ if prefix == "always503":
+ self.send(503, '{"erro":"interno","mensagem":"falha interna"}')
+ return
+ if prefix == "always429":
+ self.send(429, '{"erro":"limite","mensagem":"devagar"}',
+ headers={"Retry-After": "1"})
+ return
+ if prefix == "date429":
+ when = datetime.now(timezone.utc) + timedelta(seconds=2)
+ self.send(429, '{"erro":"limite","mensagem":"devagar"}',
+ headers={"Retry-After": format_datetime(when,
+ usegmt=True)})
+ return
+ if prefix == "hugeretry":
+ self.send(429, '{"erro":"limite","mensagem":"devagar"}',
+ headers={"Retry-After": "3600"})
+ return
+ if prefix == "fail2" and count <= 2:
+ self.send(503, '{"erro":"interno","mensagem":"falha interna"}')
+ return
+ if prefix == "flaky" and count == 1:
+ self.send(500, '{"erro":"interno","mensagem":"falha interna"}')
+ return
+ if prefix == "always400":
+ self.send(400, '{"erro":"cep_invalido",'
+ '"mensagem":"CEP deve ter 8 dígitos"}')
+ return
+ if prefix == "badjson":
+ self.send(200, "<html>captive portal</html>", ctype="text/html")
+ return
+ if prefix == "badgeo":
+ entry = dict(DB["01001000"])
+ entry["geo"] = {"lat": "nao-e-numero", "precisao": "logradouro"}
+ entry["campo_novo"] = {"nested": [1, 2, 3]}
+ self.send(200, json.dumps(entry, ensure_ascii=False))
+ return
+ if prefix == "surrogate":
+ entry = dict(DB["01001000"])
+ entry["logradouro"] = "Rua \U0001F600 Feliz"
+ self.send(200, json.dumps(entry, ensure_ascii=True))
+ return
+ if prefix == "escaped":
+ self.send(200, json.dumps(DB["01001000"], ensure_ascii=True))
+ return
+
+ if parts and parts[0] == "health":
+ body = {"status": "ok", "dne": "26071", "ceps": 1596168,
+ "geo": 894489, "base_gerada_em": "2026-07-24T03:48:28Z"}
+ if prefix == "degraded":
+ body["status"] = "degraded"
+ self.send(503, json.dumps(body))
+ return
+ self.send(200, json.dumps(body))
+ return
+
+ if parts and parts[0] == "csv":
+ query = parse_qs(parsed.query)
+ ceps = [c for c in query.get("ceps", [""])[0].split(",") if c]
+ if len(ceps) > 500:
+ self.send(400, '{"erro":"limite","mensagem":"máximo 500"}')
+ return
+ if prefix == "short":
+ ceps = ceps[:1]
+ rows = [",".join(COLUMNS)] + [csv_row(c) for c in ceps]
+ body = "\r\n".join(rows) + "\r\n"
+ if prefix == "bom":
+ body = "\ufeff" + body
+ self.send(200, body, ctype="text/csv; charset=utf-8")
+ return
+
+ if not parts:
+ self.send(404, '{"erro":"cep_nao_encontrado",'
+ '"mensagem":"CEP não existe na base do DNE"}')
+ return
+
+ cep = parts[0]
+ if not (len(cep) == 8 and cep.isdigit()):
+ self.send(400, '{"erro":"cep_invalido",'
+ '"mensagem":"CEP deve ter 8 dígitos"}')
+ return
+ entry = DB.get(cep)
+ if entry is None:
+ self.send(404, '{"erro":"cep_nao_encontrado",'
+ '"mensagem":"CEP não existe na base do DNE"}')
+ return
+ self.send(200, json.dumps(entry, ensure_ascii=False))
+
+
+def main():
+ port = int(sys.argv[1]) if len(sys.argv) > 1 else 0
+ server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
+ print(server.server_address[1], flush=True)
+ server.serve_forever()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_cepis.c b/tests/test_cepis.c
new file mode 100644
index 0000000..3dd6dc6
--- /dev/null
+++ b/tests/test_cepis.c
@@ -0,0 +1,721 @@
+/*
+ * Test driver. Every case runs against the local server in tests/server.py,
+ * reached through base_url; nothing here mocks the HTTP layer.
+ *
+ * Usage: test_cepis http://127.0.0.1:<port>
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "cepis.h"
+
+_Static_assert(sizeof(cepis_geo) == 32, "cepis_geo layout");
+_Static_assert(offsetof(cepis_geo, precisao) == 24, "cepis_geo layout");
+_Static_assert(sizeof(cepis_address) == 120, "cepis_address layout");
+_Static_assert(offsetof(cepis_address, siafi) == 96, "cepis_address layout");
+_Static_assert(offsetof(cepis_address, geo) == 104, "cepis_address layout");
+_Static_assert(offsetof(cepis_address, raw) == 112, "cepis_address layout");
+_Static_assert(sizeof(struct cepis_health) == 48, "cepis_health layout");
+_Static_assert(offsetof(struct cepis_health, base_gerada_em) == 32,
+ "cepis_health layout");
+_Static_assert(sizeof(cepis_error) == 272, "cepis_error layout");
+_Static_assert(offsetof(cepis_error, retry_after) == 8, "cepis_error layout");
+_Static_assert(offsetof(cepis_error, message) == 16, "cepis_error layout");
+_Static_assert(sizeof(cepis_config) == 56, "cepis_config layout");
+_Static_assert(offsetof(cepis_config, max_attempts) == 16,
+ "cepis_config layout");
+_Static_assert(offsetof(cepis_config, user_agent) == 48, "cepis_config layout");
+
+static const char *base;
+static int failures;
+static int checks;
+static const char *current;
+
+#define CHECK(cond) \
+ do { \
+ checks++; \
+ if (!(cond)) { \
+ failures++; \
+ fprintf(stderr, " FAIL %s:%d [%s]: %s\n", __FILE__, __LINE__, \
+ current, #cond); \
+ } \
+ } while (0)
+
+#define CHECK_STR(got, want) \
+ do { \
+ checks++; \
+ const char *g_ = (got); \
+ if (g_ == NULL || strcmp(g_, (want)) != 0) { \
+ failures++; \
+ fprintf(stderr, " FAIL %s:%d [%s]: want \"%s\" got \"%s\"\n", \
+ __FILE__, __LINE__, current, (want), \
+ g_ ? g_ : "(null)"); \
+ } \
+ } while (0)
+
+static void begin(const char *name)
+{
+ current = name;
+ printf("- %s\n", name);
+}
+
+static char *urlf(const char *fmt, ...)
+{
+ static char buf[8][512];
+ static int slot;
+ char *out = buf[slot++ % 8];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(out, 512, fmt, ap);
+ va_end(ap);
+ return out;
+}
+
+static cepis_client *client_at(const char *url, int attempts, double backoff)
+{
+ cepis_config cfg;
+ cepis_client *client = NULL;
+
+ memset(&cfg, 0, sizeof cfg);
+ cfg.base_url = url;
+ cfg.max_attempts = attempts;
+ cfg.backoff_base = backoff;
+ cfg.backoff_max = backoff * 4.0;
+ cfg.timeout = 5.0;
+ if (cepis_client_new(&cfg, &client) != CEPIS_OK)
+ return NULL;
+ return client;
+}
+
+static long hits(const char *prefix)
+{
+ cepis_client *client;
+
+ client = client_at(urlf("%s/_stats/%s", base, prefix), 1, 0.001);
+ struct cepis_health *health = NULL;
+ long count = -1;
+
+ if (client == NULL)
+ return -1;
+ if (cepis_health(client, &health, NULL) == CEPIS_OK && health != NULL)
+ count = (long)health->ceps;
+ cepis_health_free(health);
+ cepis_client_free(client);
+ return count;
+}
+
+static double now_seconds(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
+}
+
+static void test_normalize(void)
+{
+ char out[16];
+
+ begin("normalize_cep");
+ CHECK(cepis_normalize_cep("01001-000", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep("01001000", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep(" 01001-000 ", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep("01.001-000", out) == CEPIS_OK);
+ CHECK_STR(out, "01001000");
+ CHECK(cepis_normalize_cep("0100100", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK_STR(out, "");
+ CHECK(cepis_normalize_cep("010010000", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep("0100100a", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep("", out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep(NULL, out) == CEPIS_ERR_INVALID_CEP);
+ CHECK(cepis_normalize_cep("01001000", NULL) == CEPIS_ERR_USAGE);
+}
+
+static void test_lookup_geo(void)
+{
+ cepis_client *client = client_at(base, 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+ char masked[16];
+
+ begin("lookup with geo");
+ CHECK(cepis_lookup(client, "01001-000", &address, &err) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK_STR(address->cep, "01001000");
+ CHECK_STR(address->logradouro, "Praça da Sé");
+ CHECK_STR(address->complemento, "lado ímpar");
+ CHECK_STR(address->unidade, "");
+ CHECK_STR(address->bairro, "Sé");
+ CHECK_STR(address->localidade, "São Paulo");
+ CHECK_STR(address->uf, "SP");
+ CHECK_STR(address->estado, "São Paulo");
+ CHECK_STR(address->regiao, "Sudeste");
+ CHECK_STR(address->ibge, "3550308");
+ CHECK_STR(address->gia, "1004");
+ CHECK_STR(address->ddd, "11");
+ CHECK_STR(address->siafi, "7107");
+ CHECK(address->raw != NULL);
+ CHECK(strstr(address->raw, "logradouro") != NULL);
+ CHECK(address->geo != NULL);
+ if (address->geo != NULL) {
+ CHECK(address->geo->lat < -23.5 && address->geo->lat > -23.6);
+ CHECK(address->geo->lon < -46.6 && address->geo->lon > -46.7);
+ CHECK(address->geo->raio_m == 1084);
+ CHECK_STR(address->geo->precisao, "logradouro");
+ CHECK(cepis_geo_is_precise(address->geo) == 1);
+ }
+ cepis_address_formatted_cep(address, masked);
+ CHECK_STR(masked, "01001-000");
+ }
+ cepis_address_free(address);
+
+ address = NULL;
+ CHECK(cepis_lookup(client, "69900000", &address, &err) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK(address->geo != NULL);
+ if (address->geo != NULL) {
+ CHECK_STR(address->geo->precisao, "localidade");
+ CHECK(cepis_geo_is_precise(address->geo) == 0);
+ CHECK(address->geo->raio_m == 18420);
+ }
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_lookup_no_geo(void)
+{
+ cepis_client *client = client_at(base, 3, 0.001);
+ cepis_address *address = NULL;
+
+ begin("lookup without geo");
+ CHECK(cepis_lookup(client, "20040020", &address, NULL) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK(address->geo == NULL);
+ CHECK_STR(address->localidade, "Rio de Janeiro");
+ CHECK_STR(address->gia, "");
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_escaped_json(void)
+{
+ cepis_client *client = client_at(urlf("%s/escaped", base), 1, 0.001);
+ cepis_address *address = NULL;
+
+ begin("\\u escapes decoded to UTF-8");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK_STR(address->logradouro, "Praça da Sé");
+ CHECK_STR(address->localidade, "São Paulo");
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_surrogate_pair(void)
+{
+ cepis_client *client = client_at(urlf("%s/surrogate", base), 1, 0.001);
+ cepis_address *address = NULL;
+
+ begin("a surrogate pair decodes to one UTF-8 sequence");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ if (address != NULL)
+ CHECK_STR(address->logradouro, "Rua \xf0\x9f\x98\x80 Feliz");
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_csv_bom(void)
+{
+ const char *ceps[] = {"01001000", "20040020"};
+ cepis_client *client = client_at(urlf("%s/bom", base), 1, 0.001);
+ cepis_address **out = NULL;
+
+ begin("a UTF-8 BOM on the CSV is stripped");
+ CHECK(cepis_lookup_many(client, ceps, 2, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] != NULL);
+ if (out[0] != NULL)
+ CHECK_STR(out[0]->cep, "01001000");
+ }
+ cepis_address_list_free(out, 2);
+ cepis_client_free(client);
+}
+
+static void test_not_found_not_retried(void)
+{
+ cepis_client *client = client_at(urlf("%s/nf", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("404 is not retried");
+ CHECK(cepis_lookup(client, "99999999", &address, &err) ==
+ CEPIS_ERR_NOT_FOUND);
+ CHECK(address == NULL);
+ CHECK(err.status == CEPIS_ERR_NOT_FOUND);
+ CHECK(strstr(err.message, "99999999") != NULL);
+ CHECK(hits("nf") == 1);
+ cepis_client_free(client);
+}
+
+static void test_bad_cep_from_api(void)
+{
+ cepis_client *client = client_at(urlf("%s/always400", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("400 is not retried and carries the API message");
+ CHECK(cepis_lookup(client, "00000000", &address, &err) ==
+ CEPIS_ERR_INVALID_CEP);
+ CHECK(address == NULL);
+ CHECK(strstr(err.message, "8 d") != NULL);
+ CHECK(hits("always400") == 1);
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_local_validation_makes_no_request(void)
+{
+ cepis_client *client = client_at(urlf("%s/novalid", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("local validation makes no request");
+ CHECK(cepis_lookup(client, "123", &address, &err) == CEPIS_ERR_INVALID_CEP);
+ CHECK(address == NULL);
+ CHECK(strstr(err.message, "8 digits") != NULL);
+ CHECK(hits("novalid") == 0);
+ cepis_client_free(client);
+}
+
+static void test_retry_then_success(void)
+{
+ cepis_client *client = client_at(urlf("%s/fail2", base), 3, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("retry then success");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_OK);
+ CHECK(address != NULL);
+ CHECK(hits("fail2") == 3);
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_retry_exhausted(void)
+{
+ cepis_client *client = client_at(urlf("%s/always503", base), 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("retry exhausted on 5xx");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER);
+ CHECK(address == NULL);
+ CHECK(err.status == CEPIS_ERR_SERVER);
+ CHECK(err.http_status == 503);
+ CHECK(strstr(err.message, "503") != NULL);
+ CHECK(hits("always503") == 2);
+ cepis_client_free(client);
+}
+
+static void test_rate_limited(void)
+{
+ cepis_client *client = client_at(urlf("%s/always429", base), 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("429 exposes Retry-After in seconds");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_RATE_LIMITED);
+ CHECK(address == NULL);
+ CHECK(err.http_status == 429);
+ CHECK(err.retry_after > 0.5 && err.retry_after < 1.5);
+ CHECK(hits("always429") == 2);
+ cepis_client_free(client);
+}
+
+static void test_rate_limited_http_date(void)
+{
+ cepis_client *client = client_at(urlf("%s/date429", base), 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("429 with an HTTP-date Retry-After");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_RATE_LIMITED);
+ CHECK(err.retry_after > 0.0 && err.retry_after <= 3.0);
+ CHECK(hits("date429") == 2);
+ cepis_client_free(client);
+}
+
+static void test_retry_after_is_capped(void)
+{
+ cepis_client *client = client_at(urlf("%s/hugeretry", base), 2, 0.01);
+ cepis_address *address = NULL;
+ cepis_error err;
+ double started = now_seconds();
+
+ begin("a huge Retry-After is capped at backoff_max");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_RATE_LIMITED);
+ CHECK(now_seconds() - started < 5.0);
+ CHECK(err.retry_after > 3000.0);
+ cepis_client_free(client);
+}
+
+static void test_total_timeout(void)
+{
+ cepis_config cfg;
+ cepis_client *client = NULL;
+ cepis_address *address = NULL;
+ cepis_error err;
+ double started;
+
+ begin("total_timeout bounds the whole call");
+ memset(&cfg, 0, sizeof cfg);
+ cfg.base_url = urlf("%s/always503", base);
+ cfg.max_attempts = 50;
+ cfg.backoff_base = 0.05;
+ cfg.backoff_max = 0.05;
+ cfg.total_timeout = 0.4;
+ CHECK(cepis_client_new(&cfg, &client) == CEPIS_OK);
+ started = now_seconds();
+ CHECK(cepis_lookup(client, "01001000", &address, &err) == CEPIS_ERR_SERVER);
+ CHECK(now_seconds() - started < 4.0);
+ cepis_client_free(client);
+}
+
+static void test_invalid_response(void)
+{
+ cepis_client *client = client_at(urlf("%s/badjson", base), 1, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("a 200 with an unparseable body");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_INVALID_RESPONSE);
+ CHECK(address == NULL);
+ cepis_client_free(client);
+}
+
+static void test_bad_geo_kept_address(void)
+{
+ cepis_client *client = client_at(urlf("%s/badgeo", base), 1, 0.001);
+ cepis_address *address = NULL;
+
+ begin("a malformed geo drops the geo, not the address");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ if (address != NULL) {
+ CHECK(address->geo == NULL);
+ CHECK_STR(address->localidade, "São Paulo");
+ CHECK(strstr(address->raw, "campo_novo") != NULL);
+ }
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+static void test_network_error(void)
+{
+ cepis_client *client = client_at("http://127.0.0.1:1", 2, 0.001);
+ cepis_address *address = NULL;
+ cepis_error err;
+
+ begin("a refused connection is a network error");
+ CHECK(cepis_lookup(client, "01001000", &address, &err) ==
+ CEPIS_ERR_NETWORK);
+ CHECK(address == NULL);
+ CHECK(err.message[0] != '\0');
+ cepis_client_free(client);
+}
+
+static void test_health(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ struct cepis_health *health = NULL;
+
+ begin("health");
+ CHECK(cepis_health(client, &health, NULL) == CEPIS_OK);
+ if (health != NULL) {
+ CHECK_STR(health->status, "ok");
+ CHECK_STR(health->dne, "26071");
+ CHECK(health->ceps == 1596168);
+ CHECK(health->geo == 894489);
+ CHECK_STR(health->base_gerada_em, "2026-07-24T03:48:28Z");
+ CHECK(health->raw != NULL);
+ }
+ cepis_health_free(health);
+ cepis_client_free(client);
+}
+
+static void test_health_degraded(void)
+{
+ cepis_client *client = client_at(urlf("%s/degraded", base), 2, 0.001);
+ struct cepis_health *health = NULL;
+
+ begin("a degraded API is not an error");
+ CHECK(cepis_health(client, &health, NULL) == CEPIS_OK);
+ if (health != NULL)
+ CHECK_STR(health->status, "degraded");
+ CHECK(hits("degraded") == 1);
+ cepis_health_free(health);
+ cepis_client_free(client);
+}
+
+static void test_batch_order(void)
+{
+ const char *ceps[] = {"01001000", "99999999", "20040020"};
+ cepis_client *client = client_at(urlf("%s/batch", base), 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("batch keeps order and nulls the missing entry");
+ CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] == NULL);
+ CHECK(out[2] != NULL);
+ if (out[0] != NULL) {
+ CHECK_STR(out[0]->localidade, "São Paulo");
+ CHECK(out[0]->geo == NULL);
+ CHECK(out[0]->raw == NULL);
+ }
+ if (out[2] != NULL)
+ CHECK_STR(out[2]->localidade, "Rio de Janeiro");
+ }
+ CHECK(hits("batch") == 1);
+ cepis_address_list_free(out, 3);
+ cepis_client_free(client);
+}
+
+static void test_batch_quoting(void)
+{
+ const char *ceps[] = {"12345678"};
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("batch parses quoted CSV fields");
+ CHECK(cepis_lookup_many(client, ceps, 1, &out, NULL) == CEPIS_OK);
+ if (out != NULL && out[0] != NULL) {
+ CHECK_STR(out[0]->logradouro, "Rua A, B \"quoted\"");
+ CHECK_STR(out[0]->bairro, "Vila, Nova");
+ CHECK_STR(out[0]->complemento, "até 100");
+ }
+ cepis_address_list_free(out, 1);
+ cepis_client_free(client);
+}
+
+static void test_batch_malformed_entry(void)
+{
+ const char *ceps[] = {"01001000", "abc", "20040020"};
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("a malformed CEP in a batch is null, not an error");
+ CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] == NULL);
+ CHECK(out[2] != NULL);
+ }
+ cepis_address_list_free(out, 3);
+ cepis_client_free(client);
+}
+
+static void test_batch_chunking(void)
+{
+ cepis_client *client = client_at(urlf("%s/chunk", base), 2, 0.001);
+ cepis_address **out = NULL;
+ const char **ceps;
+ char *storage;
+ size_t i;
+ size_t n = 501;
+ int ok = 1;
+
+ begin("batches above 500 are chunked");
+ ceps = malloc(n * sizeof *ceps);
+ storage = malloc(n * 9);
+ for (i = 0; i < n; i++) {
+ snprintf(storage + i * 9, 9, "%08zu", (size_t)70000000 + i);
+ ceps[i] = storage + i * 9;
+ }
+ CHECK(cepis_lookup_many(client, ceps, n, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ for (i = 0; i < n; i++) {
+ if (out[i] == NULL || strcmp(out[i]->cep, ceps[i]) != 0) {
+ ok = 0;
+ break;
+ }
+ }
+ CHECK(ok);
+ }
+ CHECK(hits("chunk") == 2);
+ cepis_address_list_free(out, n);
+ free(storage);
+ free(ceps);
+ cepis_client_free(client);
+}
+
+static void test_batch_short_response(void)
+{
+ const char *ceps[] = {"01001000", "20040020", "12345678"};
+ cepis_client *client = client_at(urlf("%s/short", base), 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("a short batch response is padded, not shifted");
+ CHECK(cepis_lookup_many(client, ceps, 3, &out, NULL) == CEPIS_OK);
+ if (out != NULL) {
+ CHECK(out[0] != NULL);
+ CHECK(out[1] == NULL);
+ CHECK(out[2] == NULL);
+ if (out[0] != NULL)
+ CHECK_STR(out[0]->cep, "01001000");
+ }
+ cepis_address_list_free(out, 3);
+ cepis_client_free(client);
+}
+
+static void test_batch_empty(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address **out = NULL;
+
+ begin("an empty batch is a valid empty result");
+ CHECK(cepis_lookup_many(client, NULL, 0, &out, NULL) == CEPIS_OK);
+ CHECK(out != NULL);
+ cepis_address_list_free(out, 0);
+ cepis_client_free(client);
+}
+
+static void test_trailing_slash(void)
+{
+ cepis_client *client = client_at(urlf("%s/", base), 2, 0.001);
+ cepis_address *address = NULL;
+
+ begin("a trailing slash in base_url is stripped");
+ CHECK(cepis_lookup(client, "01001000", &address, NULL) == CEPIS_OK);
+ CHECK(address != NULL);
+ cepis_address_free(address);
+ cepis_client_free(client);
+}
+
+int cepis_abi_probe(void *fn, void *a0, void *a1, void *a2, void *a3);
+
+static void probe(const char *name, void *fn, void *a0, void *a1, void *a2,
+ void *a3)
+{
+ int mask = cepis_abi_probe(fn, a0, a1, a2, a3);
+
+ checks++;
+ if (mask != 0) {
+ failures++;
+ fprintf(stderr, " FAIL [%s]: %s violated the ABI, mask 0x%02x\n",
+ current, name, mask);
+ }
+}
+
+static void test_abi(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_address *address = NULL;
+ struct cepis_health *health = NULL;
+ cepis_address **many = NULL;
+ cepis_error err;
+ char out[16];
+
+ begin("callee-saved registers, stack pointer and DF survive every call");
+ probe("cepis_normalize_cep", (void *)cepis_normalize_cep,
+ (void *)"01001-000", out, NULL, NULL);
+ probe("cepis_strerror", (void *)cepis_strerror, (void *)(long)4, NULL,
+ NULL, NULL);
+ probe("cepis_lookup(invalid)", (void *)cepis_lookup, client, (void *)"1",
+ &address, &err);
+ probe("cepis_lookup", (void *)cepis_lookup, client, (void *)"01001000",
+ &address, &err);
+ probe("cepis_address_free", (void *)cepis_address_free, address, NULL,
+ NULL, NULL);
+ probe("cepis_health", (void *)cepis_health, client, &health, &err, NULL);
+ probe("cepis_health_free", (void *)cepis_health_free, health, NULL, NULL,
+ NULL);
+ probe("cepis_lookup_many", (void *)cepis_lookup_many, client, NULL, NULL,
+ &many);
+ cepis_address_list_free(many, 0);
+ probe("cepis_default_client", (void *)cepis_default_client, NULL, NULL,
+ NULL, NULL);
+ probe("cepis_client_free", (void *)cepis_client_free, client, NULL, NULL,
+ NULL);
+}
+
+static void test_usage(void)
+{
+ cepis_client *client = client_at(base, 2, 0.001);
+ cepis_config cfg;
+ cepis_client *bad = NULL;
+
+ begin("argument errors");
+ CHECK(cepis_lookup(client, "01001000", NULL, NULL) == CEPIS_ERR_USAGE);
+ CHECK(cepis_health(client, NULL, NULL) == CEPIS_ERR_USAGE);
+ CHECK(cepis_lookup_many(client, NULL, 0, NULL, NULL) == CEPIS_ERR_USAGE);
+ memset(&cfg, 0, sizeof cfg);
+ cfg.max_attempts = -1;
+ CHECK(cepis_client_new(&cfg, &bad) == CEPIS_ERR_USAGE);
+ CHECK(bad == NULL);
+ CHECK(strcmp(cepis_version(), CEPIS_VERSION) == 0);
+ CHECK(strcmp(cepis_strerror(CEPIS_ERR_NOT_FOUND), "CEP not found") == 0);
+ cepis_client_free(client);
+}
+
+int main(int argc, char **argv)
+{
+ if (argc < 2) {
+ fprintf(stderr, "usage: %s <base-url>\n", argv[0]);
+ return 2;
+ }
+ base = argv[1];
+ cepis_global_init();
+
+ test_normalize();
+ test_lookup_geo();
+ test_lookup_no_geo();
+ test_escaped_json();
+ test_surrogate_pair();
+ test_csv_bom();
+ test_not_found_not_retried();
+ test_bad_cep_from_api();
+ test_local_validation_makes_no_request();
+ test_retry_then_success();
+ test_retry_exhausted();
+ test_rate_limited();
+ test_rate_limited_http_date();
+ test_retry_after_is_capped();
+ test_total_timeout();
+ test_invalid_response();
+ test_bad_geo_kept_address();
+ test_network_error();
+ test_health();
+ test_health_degraded();
+ test_batch_order();
+ test_batch_quoting();
+ test_batch_malformed_entry();
+ test_batch_chunking();
+ test_batch_short_response();
+ test_batch_empty();
+ test_trailing_slash();
+ test_abi();
+ test_usage();
+
+ cepis_global_cleanup();
+
+ printf("\n%d checks, %d failures\n", checks, failures);
+ return failures == 0 ? 0 : 1;
+}
diff --git a/tools/style.sh b/tools/style.sh
new file mode 100755
index 0000000..08e40b1
--- /dev/null
+++ b/tools/style.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+# Read-only style check: no tabs, no trailing whitespace, nothing past 80
+# columns, and every .S file must declare a non-executable stack.
+
+set -eu
+
+here=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+status=0
+
+for file in "$here"/src/*.S "$here"/src/*.inc "$here"/include/*.h \
+ "$here"/tests/*.c "$here"/examples/*.c; do
+ [ -e "$file" ] || continue
+ name=${file#"$here"/}
+ if grep -n ' ' "$file" >/dev/null 2>&1; then
+ echo "$name: contains a tab" >&2
+ grep -n ' ' "$file" | head -5 >&2
+ status=1
+ fi
+ if grep -n '[ ]$' "$file" >/dev/null 2>&1; then
+ echo "$name: trailing whitespace" >&2
+ grep -n '[ ]$' "$file" | head -5 >&2
+ status=1
+ fi
+ if awk 'length > 80 { print FILENAME ":" FNR ": line over 80 columns"; f=1 }
+ END { exit f ? 1 : 0 }' "$file" >&2; then
+ :
+ else
+ status=1
+ fi
+done
+
+for file in "$here"/src/*.S; do
+ name=${file#"$here"/}
+ if ! grep -q 'note.GNU-stack' "$file"; then
+ echo "$name: missing .note.GNU-stack" >&2
+ status=1
+ fi
+done
+
+if [ "$status" -eq 0 ]; then
+ echo "style: ok"
+fi
+exit "$status"