diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:56:51 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:56:59 -0300 |
| commit | b7b08a7d29d84de187dd8a55e0c94de66b07a572 (patch) | |
| tree | 7e55661d3eca3cf3b16aada6e87eabee68802fb9 | |
| download | asm-0.1.0.tar.gz | |
Initial importv0.1.0
| -rw-r--r-- | .gitignore | 8 | ||||
| -rw-r--r-- | LICENSE | 15 | ||||
| -rw-r--r-- | Makefile | 121 | ||||
| -rw-r--r-- | README.md | 250 | ||||
| -rw-r--r-- | examples/demo.c | 83 | ||||
| -rw-r--r-- | include/cepis.h | 334 | ||||
| -rw-r--r-- | src/abi.inc | 58 | ||||
| -rw-r--r-- | src/cep.S | 53 | ||||
| -rw-r--r-- | src/client.S | 879 | ||||
| -rw-r--r-- | src/csv.S | 297 | ||||
| -rw-r--r-- | src/defs.inc | 79 | ||||
| -rw-r--r-- | src/http.S | 759 | ||||
| -rw-r--r-- | src/json.S | 482 | ||||
| -rw-r--r-- | src/models.S | 716 | ||||
| -rw-r--r-- | src/util.S | 190 | ||||
| -rw-r--r-- | tests/abi_probe.S | 102 | ||||
| -rwxr-xr-x | tests/run.sh | 32 | ||||
| -rwxr-xr-x | tests/server.py | 222 | ||||
| -rw-r--r-- | tests/test_cepis.c | 721 | ||||
| -rwxr-xr-x | tools/style.sh | 43 |
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__/ @@ -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" |