summaryrefslogtreecommitdiffstats
path: root/src/client.S
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:42:22 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:44:20 -0300
commitc3b525537e944064869a2cd6495733388144ce64 (patch)
treea30fa16568ffe470876f670e6bd75873c201df1a /src/client.S
parentb7b08a7d29d84de187dd8a55e0c94de66b07a572 (diff)
downloadasm-c2a632111bdee923e7cb6a1eb9f2ab352b8d5c70.tar.gz
Surface the faixa a 404 now carriesHEADv0.2.0main
A CEP that fails is almost never a random number. It is someone who knows the city and guessed the last digits, and the guess is usually the -000 that 957 municipalities never received. The API answers those 404s with a faixa object naming the municipality that owns the band, which turns "not found" from a dead end into something the caller can act on - and the caller cannot reach it through this library, because the 404 path never looked at the body at all. cepis_error is where a 404 already reports itself, so faixa goes there, as inline storage rather than a pointer: the struct is documented as owning nothing and declared on the caller's stack, and that stays true. It costs sizeof(cepis_error), 272 bytes to 384, so 0.2.0 needs a recompile. The parser needed one new primitive. cepis__json_field could only pull scalars out of a flat object; splitting it over cepis__json_find, which returns a pointer to a value of any shape, reaches one level down without building a document tree - the same depth cepis__geo_parse already needs for geo. Absence is the case that matters: no faixa, an empty one, or one that is not an object all leave the error byte-for-byte what it was before. cep_ini and cep_fim are deliberately not presented as a range. They are the lowest and highest CEP that exist, the band is sparse - Areal's 118 CEPs are scattered over ~4900 numbers - and ceps is the only honest count.
Diffstat (limited to 'src/client.S')
-rw-r--r--src/client.S279
1 files changed, 224 insertions, 55 deletions
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"