diff options
| -rw-r--r-- | Makefile | 11 | ||||
| -rw-r--r-- | README.md | 64 | ||||
| -rw-r--r-- | include/cepis.h | 68 | ||||
| -rw-r--r-- | src/client.S | 279 | ||||
| -rw-r--r-- | src/defs.inc | 14 | ||||
| -rwxr-xr-x | tests/server.py | 43 | ||||
| -rw-r--r-- | tests/test_cepis.c | 111 |
7 files changed, 518 insertions, 72 deletions
@@ -1,7 +1,10 @@ .POSIX: .SUFFIXES: -VERSION = 0.1.0 +VERSION = 0.2.0 +# Bumped to 1 in 0.2.0: cepis_error grew a faixa field, so a 0.1.0 binary +# that declares one on the stack would be written past its end. +SOVERSION = 1 PREFIX = /usr/local LIBDIR = $(PREFIX)/lib INCDIR = $(PREFIX)/include @@ -75,7 +78,7 @@ $(LIB): $(OBJ) ar rcs $@ $(OBJ) $(SO): $(OBJ) - $(CC) -shared -Wl,-soname,libcepis.so.0 -o $@ $(OBJ) $(LDLIBS) + $(CC) -shared -Wl,-soname,libcepis.so.$(SOVERSION) -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) \ @@ -116,6 +119,6 @@ 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 + ln -sf libcepis.so.$(VERSION) $(DESTDIR)$(LIBDIR)/libcepis.so.$(SOVERSION) + ln -sf libcepis.so.$(SOVERSION) $(DESTDIR)$(LIBDIR)/libcepis.so cp include/cepis.h $(DESTDIR)$(INCDIR)/cepis.h @@ -37,6 +37,16 @@ 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). +> **0.2.0 changed `sizeof(cepis_error)`** — it grew a `faixa` member, from 272 +> to 384 bytes. The source API is unchanged, but a program built against +> 0.1.0 must be recompiled, not just relinked. While the version starts with +> a `0`, that is the promise: source compatibility, not binary. +> +> The soname went with it, `libcepis.so.0` to `libcepis.so.1`. Callers declare +> `cepis_error` on their own stack, so an old binary against the new library +> would have it written past its end — a silent corruption. The soname bump +> turns that into an honest "not found" from the loader instead. + ## Usage ### A single lookup @@ -60,7 +70,7 @@ case CEPIS_OK: cepis_address_free(a); break; case CEPIS_ERR_NOT_FOUND: - break; + break; /* err.faixa may name the municipality - see Errors */ default: fprintf(stderr, "%s\n", err.message); } @@ -201,6 +211,55 @@ to make**, not by the HTTP status that caused it: Pass a `cepis_error` to get `message`, `http_status` and `retry_after`; `cepis_strerror` names the status itself. +### Not found: which city was it? + +A CEP that fails is rarely a random number. It is usually someone who knows +the city and guessed the last digits, and the classic guess is the `-000` that +957 municipalities never received. So a 404 carries an optional `faixa` +naming the municipality that owns the band the number falls in, and +`cepis_lookup` copies it into `err.faixa`: + +```c +if (cepis_lookup(NULL, "25845-000", &a, &err) == CEPIS_ERR_NOT_FOUND) { + fprintf(stderr, "%s\n", err.message); + /* CEP 25845000 does not exist in the DNE database + (Areal/RJ has 118 CEPs, the lowest 25845001 and the highest + 25849899) */ + + if (err.faixa.present) { + err.faixa.localidade; /* "Areal" */ + err.faixa.uf; /* "RJ" */ + err.faixa.cep_ini; /* "25845001" */ + err.faixa.cep_fim; /* "25849899" */ + err.faixa.ceps; /* 118 */ + } +} +``` + +**It is still a 404.** `faixa` is context for a failure, never an address, and +the status stays `CEPIS_ERR_NOT_FOUND`. Everything in it is inline storage +inside the `cepis_error` you already own: nothing to free, nothing that can be +`NULL`. + +Three things about the numbers: + +- `cep_ini` and `cep_fim` are the **smallest and largest CEP that actually + exist** there, not the band the Correios publish. Areal's published band + starts at 25845000 — exactly the CEP that just failed — while `cep_ini` is + 25845001. +- **The band is sparse.** Those 118 CEPs are scattered over the ~4900 numbers + between the two endpoints. `ceps` is a count; the pair is a pair of + endpoints. Do not show it as a range of valid CEPs and do not iterate it. +- The API **omits** `faixa`, and never sends it as null, when the number falls + in a band no municipality owns. Then `present` is 0, the strings are `""`, + `ceps` is 0, and `message` is exactly what it has always been. + +`err.faixa` is reset by every call that receives the `cepis_error`, so a band +left over from a previous 404 cannot be read as this one's. The other keys the +API sends inside `faixa` — `estado`, `regiao`, `ibge` — are not modelled here; +`cepis_lookup_many` never fills `faixa` at all, since the CSV endpoint behind +it does not carry one. + `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. @@ -222,6 +281,9 @@ HTML error page. Useful as a diagnosis: the problem is not in your code. 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. +- It reaches exactly one level into a nested object, and only for the two the + API defines: `geo` on a 200 and `faixa` on a 404. Either one arriving as + something other than an object is dropped, not an error. ## Development diff --git a/include/cepis.h b/include/cepis.h index ea8d378..a00982a 100644 --- a/include/cepis.h +++ b/include/cepis.h @@ -18,7 +18,7 @@ extern "C" { #endif -#define CEPIS_VERSION "0.1.0" +#define CEPIS_VERSION "0.2.0" /** Largest number of CEPs the API accepts in one /csv request. */ #define CEPIS_MAX_BATCH 500 @@ -26,6 +26,12 @@ extern "C" { /** Size of cepis_error::message, including the terminating NUL. */ #define CEPIS_MESSAGE_MAX 256 +/** Size of cepis_faixa::localidade, including the terminating NUL. The + * longest municipality name in the IBGE list is 32 characters; this leaves + * room for it in UTF-8 twice over. A longer name is truncated, never + * overflowed. */ +#define CEPIS_FAIXA_LOCALIDADE_MAX 72 + /** * 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 @@ -145,10 +151,54 @@ struct cepis_health { }; /** + * The municipality a CEP that does not exist would have belonged to. + * + * A CEP that fails is rarely a random number: it is usually someone who knows + * the city and guessed the last digits, and the classic guess is the `-000` + * that 957 municipalities never received. So the API answers a 404 with the + * band the number falls in, when a municipality owns that band. This is + * context for an error and **not an address**: the lookup still failed, and + * cepis_lookup() still returns CEPIS_ERR_NOT_FOUND. + * + * Everything here is inline storage inside cepis_error - nothing to free, + * nothing that can be NULL. Read it only when `present` is non-zero; when it + * is zero the strings are `""` and `ceps` is 0. + * + * `cep_ini` and `cep_fim` are the **smallest and largest CEP that actually + * exist** in that municipality, not the band the Correios publish. For Areal + * the DNE's own band starts at 25845000 - exactly the CEP that just failed - + * while `cep_ini` is 25845001. + * + * The band is sparse. Areal's 118 CEPs are scattered over the ~4900 numbers + * between 25845001 and 25849899, so `ceps` is a count and the pair is a pair + * of endpoints. Never present the two as a range of valid CEPs, and never + * iterate it. + */ +typedef struct cepis_faixa { + /** Non-zero when the API named a municipality for this CEP. Zero means it + * did not - the field is omitted, never null, for a number in a band no + * municipality owns. */ + int present; + /** Municipality name, e.g. "Areal". Truncated at + * CEPIS_FAIXA_LOCALIDADE_MAX - 1 bytes. */ + char localidade[CEPIS_FAIXA_LOCALIDADE_MAX]; + /** Two-letter state, e.g. "RJ". */ + char uf[3]; + /** Smallest CEP that exists there, 8 digits unmasked. */ + char cep_ini[9]; + /** Largest CEP that exists there, 8 digits unmasked. */ + char cep_fim[9]; + /** How many CEPs exist there. Far smaller than cep_fim - cep_ini. */ + long long ceps; +} cepis_faixa; + +/** * 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. + * fully overwritten at the start of every call that receives it, `faixa` + * included, so a band left over from a previous 404 cannot be mistaken for + * this one's. */ typedef struct cepis_error { /** Same value the call returned. */ @@ -159,8 +209,12 @@ typedef struct cepis_error { /** 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. */ + /** Human-readable description, NUL-terminated, empty on success. On a 404 + * with a `faixa` it names the municipality too. */ char message[CEPIS_MESSAGE_MAX]; + /** Only ever filled by CEPIS_ERR_NOT_FOUND, and only when the API sent a + * band. See cepis_faixa. */ + cepis_faixa faixa; } cepis_error; /** @@ -192,7 +246,7 @@ typedef struct cepis_config { * 0 leaves max_attempts as the only bound. */ double total_timeout; /** Overrides the User-Agent. Default - * "cepis-asm/0.1.0 (+https://cep.is)". */ + * "cepis-asm/0.2.0 (+https://cep.is)". */ const char *user_agent; } cepis_config; @@ -201,7 +255,7 @@ typedef struct cepis_config { * 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. */ +/** Library version, "0.2.0". Static storage; do not free. */ const char *cepis_version(void); /** One-line description of a cepis_status. Static storage; do not free. */ @@ -263,7 +317,9 @@ cepis_status cepis_normalize_cep(const char *value, char *out); * 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_ERR_NOT_FOUND, and fills cepis_error::faixa when the API says which + * municipality owns that band. 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); diff --git a/src/client.S b/src/client.S index 469f0f2..5cc7c77 100644 --- a/src/client.S +++ b/src/client.S @@ -24,9 +24,10 @@ #define LM_J -168 #define LM_K -176 -#define JF_KEY -48 -#define JF_VAL -56 -#define JF_OUT -64 +#define FD_KEY -48 + +#define NF_NOTE -240 +#define NF_NOTELEN 192 .text @@ -56,10 +57,22 @@ FUNC cepis__err_reset movl $0, E_HTTP_STATUS(%rdi) movq $0, E_RETRY_AFTER(%rdi) movb $0, E_MESSAGE(%rdi) + jmp cepis__faixa_clear .Ler_out: ret ENDFUNC cepis__err_reset +/* Blank the faixa of the error at %rdi, which must not be NULL. Leaf. */ +FUNC cepis__faixa_clear + movl $0, E_FAIXA+F_PRESENT(%rdi) + movb $0, E_FAIXA+F_LOCALIDADE(%rdi) + movb $0, E_FAIXA+F_UF(%rdi) + movb $0, E_FAIXA+F_CEP_INI(%rdi) + movb $0, E_FAIXA+F_CEP_FIM(%rdi) + movq $0, E_FAIXA+F_CEPS(%rdi) + ret +ENDFUNC cepis__faixa_clear + FUNC cepis__err_set PROLOGUE 8 test %rdi, %rdi @@ -76,88 +89,237 @@ FUNC cepis__err_set 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) +/* + * cepis__json_find(json, key) - point at the value of `key` in the object + * starting at `json`, with leading whitespace already stepped over, or 0 when + * the object has no such key. The value is not copied and not validated: the + * caller decides what to do with whatever byte it finds there, which is how + * a nested object is reached without a document tree. + * + * Scanning stops at the first key that matches and at the closing brace, so + * calling it on an object embedded in a larger document is safe. + */ +FUNC cepis__json_find + PROLOGUE 24 + mov %rsi, %r13 + movq $0, FD_KEY(%rbp) call cepis__js_ws mov %rax, %r12 cmpb $'{', (%r12) - jne .Ljf_out + jne .Lfd_none lea 1(%r12), %rdi call cepis__js_ws mov %rax, %r12 cmpb $'}', (%r12) - je .Ljf_out -.Ljf_pair: + je .Lfd_none +.Lfd_pair: cmpb $'"', (%r12) - jne .Ljf_out + jne .Lfd_none mov %r12, %rdi - lea JF_KEY(%rbp), %rsi + lea FD_KEY(%rbp), %rsi call cepis__js_string test %rax, %rax - jz .Ljf_out + jz .Lfd_none mov %rax, %rdi call cepis__js_ws mov %rax, %r12 cmpb $':', (%r12) - jne .Ljf_out + jne .Lfd_none 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 + mov FD_KEY(%rbp), %rdi + mov %r13, %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: + jz .Lfd_hit mov %r12, %rdi call cepis__js_skip test %rax, %rax - jz .Ljf_out + jz .Lfd_none mov %rax, %r12 -.Ljf_next: - mov JF_KEY(%rbp), %rdi + mov FD_KEY(%rbp), %rdi call free@PLT - movq $0, JF_KEY(%rbp) + movq $0, FD_KEY(%rbp) mov %r12, %rdi call cepis__js_ws mov %rax, %r12 cmpb $',', (%r12) - jne .Ljf_out + jne .Lfd_none lea 1(%r12), %rdi call cepis__js_ws mov %rax, %r12 - jmp .Ljf_pair -.Ljf_out: - mov JF_KEY(%rbp), %rdi + jmp .Lfd_pair +.Lfd_hit: + mov FD_KEY(%rbp), %rdi + call free@PLT + movq $0, FD_KEY(%rbp) + mov %r12, %rax + EPILOGUE + ret +.Lfd_none: + mov FD_KEY(%rbp), %rdi test %rdi, %rdi - jz .Ljf_ret + jz .Lfd_ret call free@PLT -.Ljf_ret: - mov JF_OUT(%rbp), %rax +.Lfd_ret: + xor %eax, %eax + EPILOGUE + ret +ENDFUNC cepis__json_find + +/* + * cepis__json_field(json, key) - the scalar value of `key` as a fresh string + * the caller frees, or 0 when the key is absent or holds an object or array. + */ +FUNC cepis__json_field + PROLOGUE 24 + movq $0, LOCAL0(%rbp) + call cepis__json_find + test %rax, %rax + jz .Ljf_none + movzbl (%rax), %ecx + cmp $'{', %cl + je .Ljf_none + cmp $'[', %cl + je .Ljf_none + mov %rax, %rdi + lea LOCAL0(%rbp), %rsi + call cepis__js_scalar_text + mov LOCAL0(%rbp), %rax + EPILOGUE + ret +.Ljf_none: + xor %eax, %eax EPILOGUE ret ENDFUNC cepis__json_field +/* + * cepis__faixa_str(obj, key, dst, size) - copy the scalar `key` of `obj` into + * the fixed buffer `dst`, truncating rather than overflowing. `dst` is left + * untouched when the key is absent, so the caller's blank stands. + */ +FUNC cepis__faixa_str + PROLOGUE 8 + mov %rdx, %r12 + mov %rcx, %r13 + call cepis__json_field + test %rax, %rax + jz .Lfx_out + mov %rax, %rbx + mov %r12, %rdi + mov %r13, %rsi + lea .Ls_pct_s(%rip), %rdx + mov %rbx, %rcx + xor %eax, %eax + call snprintf@PLT + mov %rbx, %rdi + call free@PLT +.Lfx_out: + EPILOGUE + ret +ENDFUNC cepis__faixa_str + +/* + * cepis__err_notfound(err, cep, body) - fill `err` for a 404. `err` may be + * NULL and `body` may be NULL. + * + * The 404 body optionally carries a `faixa` object naming the municipality + * that owns the band the CEP falls in. It is context for the failure, not an + * address: the status stays CEPIS_ERR_NOT_FOUND either way. When the object + * is missing, empty, or not an object, the message is byte-for-byte the one + * this library has always produced. + */ +FUNC cepis__err_notfound + PROLOGUE 216 + test %rdi, %rdi + jz .Lnf_ret + mov %rdi, %rbx + mov %rsi, %r14 + mov %rdx, %r15 + movb $0, NF_NOTE(%rbp) + test %r15, %r15 + jz .Lnf_emit + mov %r15, %rdi + lea .Ls_faixa(%rip), %rsi + call cepis__json_find + test %rax, %rax + jz .Lnf_emit + mov %rax, %r12 + cmpb $'{', (%r12) + jne .Lnf_emit + mov %r12, %rdi + lea .Ls_k_localidade(%rip), %rsi + lea E_FAIXA+F_LOCALIDADE(%rbx), %rdx + mov $F_LOCALIDADE_MAX, %ecx + call cepis__faixa_str + mov %r12, %rdi + lea .Ls_k_uf(%rip), %rsi + lea E_FAIXA+F_UF(%rbx), %rdx + mov $F_UF_MAX, %ecx + call cepis__faixa_str + mov %r12, %rdi + lea .Ls_k_cep_ini(%rip), %rsi + lea E_FAIXA+F_CEP_INI(%rbx), %rdx + mov $F_CEP_MAX, %ecx + call cepis__faixa_str + mov %r12, %rdi + lea .Ls_k_cep_fim(%rip), %rsi + lea E_FAIXA+F_CEP_FIM(%rbx), %rdx + mov $F_CEP_MAX, %ecx + call cepis__faixa_str + mov %r12, %rdi + lea .Ls_k_ceps(%rip), %rsi + call cepis__json_find + test %rax, %rax + jz .Lnf_check + movzbl (%rax), %ecx + cmp $'{', %cl + je .Lnf_check + cmp $'[', %cl + je .Lnf_check + mov %rax, %rdi + lea LOCAL0(%rbp), %rsi + call cepis__js_number + test %rax, %rax + jz .Lnf_check + cvttsd2si LOCAL0(%rbp), %rax + mov %rax, E_FAIXA+F_CEPS(%rbx) +.Lnf_check: + cmpb $0, E_FAIXA+F_LOCALIDADE(%rbx) + je .Lnf_clear + movl $1, E_FAIXA+F_PRESENT(%rbx) + lea NF_NOTE(%rbp), %rdi + mov $NF_NOTELEN, %esi + lea .Lfmt_faixa(%rip), %rdx + lea E_FAIXA+F_LOCALIDADE(%rbx), %rcx + lea E_FAIXA+F_UF(%rbx), %r8 + mov E_FAIXA+F_CEPS(%rbx), %r9 + lea E_FAIXA+F_CEP_FIM(%rbx), %rax + push %rax + lea E_FAIXA+F_CEP_INI(%rbx), %rax + push %rax + xor %eax, %eax + call snprintf@PLT + add $16, %rsp + jmp .Lnf_emit +.Lnf_clear: + mov %rbx, %rdi + call cepis__faixa_clear + movb $0, NF_NOTE(%rbp) +.Lnf_emit: + mov %rbx, %rdi + mov $CEPIS_ERR_NOT_FOUND, %esi + lea .Lfmt_notfound(%rip), %rdx + mov %r14, %rcx + lea NF_NOTE(%rbp), %r8 + call cepis__err_set +.Lnf_ret: + EPILOGUE + ret +ENDFUNC cepis__err_notfound + PUBFUNC cepis_client_new PROLOGUE 40 mov %rsi, %r14 @@ -456,11 +618,9 @@ PUBFUNC cepis_lookup 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 + lea LK_TMP(%rbp), %rsi + mov LK_BODY(%rbp), %rdx + call cepis__err_notfound mov $CEPIS_ERR_NOT_FOUND, %r12d jmp .Llk_ret .Llk_badcep: @@ -830,16 +990,25 @@ PUBFUNC cepis_lookup_many ENDFUNC cepis_lookup_many .section .rodata -.Ls_version: .asciz "0.1.0" +.Ls_version: .asciz "0.2.0" .Ls_default_url: .asciz "https://cep.is" -.Ls_default_ua: .asciz "cepis-asm/0.1.0 (+https://cep.is)" +.Ls_default_ua: .asciz "cepis-asm/0.2.0 (+https://cep.is)" .Ls_health_path: .asciz "/health" .Ls_csv_path: .asciz "/csv?ceps=" .Ls_mensagem: .asciz "mensagem" +.Ls_faixa: .asciz "faixa" +.Ls_k_localidade: .asciz "localidade" +.Ls_k_uf: .asciz "uf" +.Ls_k_cep_ini: .asciz "cep_ini" +.Ls_k_cep_fim: .asciz "cep_fim" +.Ls_k_ceps: .asciz "ceps" +.Ls_pct_s: .asciz "%s" .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_notfound: .asciz "CEP %s does not exist in the DNE database%s" +.Lfmt_faixa: + .asciz " (%s/%s has %lld CEPs, the lowest %s and the highest %s)" .Lfmt_badcep: .asciz "CEP %s rejected: %s" .Lfmt_notdigits: .asciz "CEP must be 8 digits, got \"%s\"" .Lfmt_badjson: .asciz "unparseable response from %s" diff --git a/src/defs.inc b/src/defs.inc index 4a64e0e..b0a38bf 100644 --- a/src/defs.inc +++ b/src/defs.inc @@ -43,12 +43,24 @@ #define H_RAW 40 #define H_SIZE 48 +#define F_PRESENT 0 +#define F_LOCALIDADE 4 +#define F_LOCALIDADE_MAX 72 +#define F_UF 76 +#define F_UF_MAX 3 +#define F_CEP_INI 79 +#define F_CEP_FIM 88 +#define F_CEP_MAX 9 +#define F_CEPS 104 +#define F_SIZE 112 + #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 E_FAIXA 272 +#define E_SIZE 384 #define C_BASE_URL 0 #define C_TIMEOUT 8 diff --git a/tests/server.py b/tests/server.py index c77d7f6..09410b2 100755 --- a/tests/server.py +++ b/tests/server.py @@ -64,10 +64,33 @@ for _n in range(600): "ibge": "3550308", "gia": "", "ddd": "11", "siafi": "7107", } +# Bands the API knows about. A 404 for a CEP inside one carries the owning +# municipality in "faixa"; a 404 outside every band omits the key entirely. +# cep_ini/cep_fim inside the object are the extreme CEPs that exist, which is +# why they differ from the band bounds: 25845000 is in the band, is the DNE's +# own start for Areal, and does not exist. +FAIXAS = [ + ("25845000", "25849999", { + "localidade": "Areal", "uf": "RJ", "estado": "Rio de Janeiro", + "regiao": "Sudeste", "ibge": "3300225", "cep_ini": "25845001", + "cep_fim": "25849899", "ceps": 118, + }), +] + HITS = {} LOCK = threading.Lock() +def not_found(cep=None): + body = {"erro": "cep_nao_encontrado", + "mensagem": "CEP não existe na base do DNE"} + for low, high, faixa in FAIXAS: + if cep is not None and low <= cep <= high: + body["faixa"] = faixa + break + return json.dumps(body, ensure_ascii=False) + + def bump(prefix): with LOCK: HITS[prefix] = HITS.get(prefix, 0) + 1 @@ -167,6 +190,20 @@ class Handler(BaseHTTPRequestHandler): if prefix == "escaped": self.send(200, json.dumps(DB["01001000"], ensure_ascii=True)) return + if prefix == "faixavazia": + self.send(404, json.dumps({ + "erro": "cep_nao_encontrado", + "mensagem": "CEP não existe na base do DNE", + "faixa": {}, + }, ensure_ascii=False)) + return + if prefix == "faixatexto": + self.send(404, json.dumps({ + "erro": "cep_nao_encontrado", + "mensagem": "CEP não existe na base do DNE", + "faixa": "Areal", + }, ensure_ascii=False)) + return if parts and parts[0] == "health": body = {"status": "ok", "dne": "26071", "ceps": 1596168, @@ -194,8 +231,7 @@ class Handler(BaseHTTPRequestHandler): return if not parts: - self.send(404, '{"erro":"cep_nao_encontrado",' - '"mensagem":"CEP não existe na base do DNE"}') + self.send(404, not_found()) return cep = parts[0] @@ -205,8 +241,7 @@ class Handler(BaseHTTPRequestHandler): 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"}') + self.send(404, not_found(cep)) return self.send(200, json.dumps(entry, ensure_ascii=False)) diff --git a/tests/test_cepis.c b/tests/test_cepis.c index 3dd6dc6..0c3ec87 100644 --- a/tests/test_cepis.c +++ b/tests/test_cepis.c @@ -25,9 +25,16 @@ _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(sizeof(cepis_faixa) == 112, "cepis_faixa layout"); +_Static_assert(offsetof(cepis_faixa, localidade) == 4, "cepis_faixa layout"); +_Static_assert(offsetof(cepis_faixa, uf) == 76, "cepis_faixa layout"); +_Static_assert(offsetof(cepis_faixa, cep_ini) == 79, "cepis_faixa layout"); +_Static_assert(offsetof(cepis_faixa, cep_fim) == 88, "cepis_faixa layout"); +_Static_assert(offsetof(cepis_faixa, ceps) == 104, "cepis_faixa layout"); +_Static_assert(sizeof(cepis_error) == 384, "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(offsetof(cepis_error, faixa) == 272, "cepis_error layout"); _Static_assert(sizeof(cepis_config) == 56, "cepis_config layout"); _Static_assert(offsetof(cepis_config, max_attempts) == 16, "cepis_config layout"); @@ -266,10 +273,106 @@ static void test_not_found_not_retried(void) CHECK(address == NULL); CHECK(err.status == CEPIS_ERR_NOT_FOUND); CHECK(strstr(err.message, "99999999") != NULL); + /* No band owns 99999999, so the API omits "faixa" and the message is + exactly the one this library produced before the field existed. */ + CHECK(err.faixa.present == 0); + CHECK_STR(err.faixa.localidade, ""); + CHECK_STR(err.faixa.uf, ""); + CHECK_STR(err.faixa.cep_ini, ""); + CHECK_STR(err.faixa.cep_fim, ""); + CHECK(err.faixa.ceps == 0); + CHECK_STR(err.message, "CEP 99999999 does not exist in the DNE database"); CHECK(hits("nf") == 1); cepis_client_free(client); } +static void test_not_found_faixa(void) +{ + cepis_client *client = client_at(urlf("%s/nff", base), 3, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("404 carries the municipality that owns the band"); + CHECK(cepis_lookup(client, "25845-000", &address, &err) == + CEPIS_ERR_NOT_FOUND); + CHECK(address == NULL); + CHECK(err.status == CEPIS_ERR_NOT_FOUND); + CHECK(err.faixa.present == 1); + CHECK_STR(err.faixa.localidade, "Areal"); + CHECK_STR(err.faixa.uf, "RJ"); + /* cep_ini is 25845001, not the 25845000 that just failed: the band the + Correios publish starts one lower than the lowest CEP that exists. */ + CHECK_STR(err.faixa.cep_ini, "25845001"); + CHECK_STR(err.faixa.cep_fim, "25849899"); + CHECK(err.faixa.ceps == 118); + CHECK(strstr(err.message, "25845000") != NULL); + CHECK(strstr(err.message, "Areal") != NULL); + CHECK(strstr(err.message, "118") != NULL); + CHECK(hits("nff") == 1); + cepis_client_free(client); +} + +static void test_faixa_does_not_survive_reuse(void) +{ + cepis_client *found = client_at(urlf("%s/nfr1", base), 1, 0.001); + cepis_client *bare = client_at(urlf("%s/nfr2", base), 1, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("a reused cepis_error does not keep the previous faixa"); + CHECK(cepis_lookup(found, "25845000", &address, &err) == + CEPIS_ERR_NOT_FOUND); + CHECK(err.faixa.present == 1); + CHECK(cepis_lookup(bare, "99999999", &address, &err) == + CEPIS_ERR_NOT_FOUND); + CHECK(err.faixa.present == 0); + CHECK_STR(err.faixa.localidade, ""); + CHECK(err.faixa.ceps == 0); + /* Any other call clears it too, not just another 404. */ + CHECK(cepis_lookup(found, "25845000", &address, &err) == + CEPIS_ERR_NOT_FOUND); + CHECK(err.faixa.present == 1); + CHECK(cepis_lookup(bare, "123", &address, &err) == CEPIS_ERR_INVALID_CEP); + CHECK(err.faixa.present == 0); + CHECK_STR(err.faixa.localidade, ""); + cepis_client_free(found); + cepis_client_free(bare); +} + +static void test_faixa_degenerate(void) +{ + cepis_client *empty = client_at(urlf("%s/faixavazia", base), 1, 0.001); + cepis_client *text = client_at(urlf("%s/faixatexto", base), 1, 0.001); + cepis_address *address = NULL; + cepis_error err; + + begin("an empty or non-object faixa is ignored"); + CHECK(cepis_lookup(empty, "25845000", &address, &err) == + CEPIS_ERR_NOT_FOUND); + CHECK(err.faixa.present == 0); + CHECK_STR(err.message, "CEP 25845000 does not exist in the DNE database"); + CHECK(cepis_lookup(text, "25845000", &address, &err) == + CEPIS_ERR_NOT_FOUND); + CHECK(err.faixa.present == 0); + CHECK_STR(err.faixa.localidade, ""); + CHECK_STR(err.message, "CEP 25845000 does not exist in the DNE database"); + CHECK(address == NULL); + cepis_client_free(empty); + cepis_client_free(text); +} + +static void test_faixa_without_error_struct(void) +{ + cepis_client *client = client_at(urlf("%s/nfnull", base), 1, 0.001); + cepis_address *address = NULL; + + begin("a 404 with a faixa and no cepis_error is still just a 404"); + CHECK(cepis_lookup(client, "25845000", &address, NULL) == + CEPIS_ERR_NOT_FOUND); + CHECK(address == NULL); + cepis_client_free(client); +} + static void test_bad_cep_from_api(void) { cepis_client *client = client_at(urlf("%s/always400", base), 3, 0.001); @@ -640,6 +743,8 @@ static void test_abi(void) NULL, NULL); probe("cepis_lookup(invalid)", (void *)cepis_lookup, client, (void *)"1", &address, &err); + probe("cepis_lookup(404 with faixa)", (void *)cepis_lookup, client, + (void *)"25845000", &address, &err); probe("cepis_lookup", (void *)cepis_lookup, client, (void *)"01001000", &address, &err); probe("cepis_address_free", (void *)cepis_address_free, address, NULL, @@ -691,6 +796,10 @@ int main(int argc, char **argv) test_surrogate_pair(); test_csv_bom(); test_not_found_not_retried(); + test_not_found_faixa(); + test_faixa_does_not_survive_reuse(); + test_faixa_degenerate(); + test_faixa_without_error_struct(); test_bad_cep_from_api(); test_local_validation_makes_no_request(); test_retry_then_success(); |