summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile11
-rw-r--r--README.md64
-rw-r--r--include/cepis.h68
-rw-r--r--src/client.S279
-rw-r--r--src/defs.inc14
-rwxr-xr-xtests/server.py43
-rw-r--r--tests/test_cepis.c111
7 files changed, 518 insertions, 72 deletions
diff --git a/Makefile b/Makefile
index c484804..e114da9 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
index 25fbff6..0efefdb 100644
--- a/README.md
+++ b/README.md
@@ -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();